Improve upload error handling and admin transcoding monitoring
This commit is contained in:
parent
613b962a87
commit
9c22093aca
@ -561,6 +561,7 @@
|
|||||||
<script>
|
<script>
|
||||||
let adminUser = null;
|
let adminUser = null;
|
||||||
let currentAdminSection = 'overview';
|
let currentAdminSection = 'overview';
|
||||||
|
let transcodingPollInterval = null;
|
||||||
|
|
||||||
// Admin authentication
|
// Admin authentication
|
||||||
async function checkAdminAuth() {
|
async function checkAdminAuth() {
|
||||||
@ -645,6 +646,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function adminLogout() {
|
async function adminLogout() {
|
||||||
|
// Stop any polling intervals
|
||||||
|
if (transcodingPollInterval) {
|
||||||
|
clearInterval(transcodingPollInterval);
|
||||||
|
transcodingPollInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (window.nostrAuth) {
|
if (window.nostrAuth) {
|
||||||
await window.nostrAuth.logout();
|
await window.nostrAuth.logout();
|
||||||
adminUser = null;
|
adminUser = null;
|
||||||
@ -664,10 +671,21 @@
|
|||||||
document.querySelectorAll('.admin-section').forEach(sec => sec.classList.remove('active'));
|
document.querySelectorAll('.admin-section').forEach(sec => sec.classList.remove('active'));
|
||||||
document.getElementById(section + '-section').classList.add('active');
|
document.getElementById(section + '-section').classList.add('active');
|
||||||
|
|
||||||
|
// Clear existing polling intervals
|
||||||
|
if (transcodingPollInterval) {
|
||||||
|
clearInterval(transcodingPollInterval);
|
||||||
|
transcodingPollInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Load section data
|
// Load section data
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case 'overview': loadAdminStats(); break;
|
case 'overview': loadAdminStats(); break;
|
||||||
case 'transcoding': loadTranscodingStats(); loadTranscodingJobs(); break;
|
case 'transcoding':
|
||||||
|
loadTranscodingStats();
|
||||||
|
loadTranscodingJobs();
|
||||||
|
// Start polling for active jobs
|
||||||
|
startTranscodingPolling();
|
||||||
|
break;
|
||||||
case 'users': loadUsers(); break;
|
case 'users': loadUsers(); break;
|
||||||
case 'files': loadFiles(); break;
|
case 'files': loadFiles(); break;
|
||||||
case 'reports': loadReports(); break;
|
case 'reports': loadReports(); break;
|
||||||
@ -1202,21 +1220,56 @@
|
|||||||
function refreshLogs() { loadLogs(); }
|
function refreshLogs() { loadLogs(); }
|
||||||
function refreshTranscodingJobs() { loadTranscodingStats(); loadTranscodingJobs(); }
|
function refreshTranscodingJobs() { loadTranscodingStats(); loadTranscodingJobs(); }
|
||||||
|
|
||||||
|
function startTranscodingPolling() {
|
||||||
|
// Poll every 5 seconds for active jobs
|
||||||
|
transcodingPollInterval = setInterval(() => {
|
||||||
|
if (currentAdminSection === 'transcoding') {
|
||||||
|
loadTranscodingStats();
|
||||||
|
loadTranscodingJobs();
|
||||||
|
} else {
|
||||||
|
// Stop polling if we're not on the transcoding section
|
||||||
|
clearInterval(transcodingPollInterval);
|
||||||
|
transcodingPollInterval = null;
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
// Transcoding Management Functions
|
// Transcoding Management Functions
|
||||||
async function loadTranscodingStats() {
|
async function loadTranscodingStats() {
|
||||||
try {
|
try {
|
||||||
const response = await adminFetch('/api/admin/transcoding/stats');
|
const response = await adminFetch('/api/admin/transcoding/stats');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 503) {
|
||||||
|
// Transcoding not enabled
|
||||||
|
document.getElementById('queue-length').textContent = 'N/A';
|
||||||
|
document.getElementById('processing-jobs').textContent = 'N/A';
|
||||||
|
document.getElementById('completed-today').textContent = 'N/A';
|
||||||
|
document.getElementById('failed-jobs').textContent = 'N/A';
|
||||||
|
document.getElementById('ffmpeg-status').textContent = 'Disabled';
|
||||||
|
document.getElementById('transcode-storage').textContent = 'N/A';
|
||||||
|
document.getElementById('avg-processing-time').textContent = 'N/A';
|
||||||
|
document.getElementById('success-rate').textContent = 'N/A';
|
||||||
|
|
||||||
|
// Clear active jobs table
|
||||||
|
const tbody = document.getElementById('active-jobs-table');
|
||||||
|
tbody.innerHTML = '<tr><td colspan="8" class="no-data">Transcoding service disabled</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Update stats cards
|
// Update stats cards
|
||||||
document.getElementById('queue-length').textContent = data.stats.queue_length || 0;
|
document.getElementById('queue-length').textContent = data.stats?.queue_length || 0;
|
||||||
document.getElementById('processing-jobs').textContent = data.stats.processing_jobs || 0;
|
document.getElementById('processing-jobs').textContent = data.stats?.processing_jobs || 0;
|
||||||
document.getElementById('completed-today').textContent = data.stats.completed_today || 0;
|
document.getElementById('completed-today').textContent = data.stats?.completed_today || 0;
|
||||||
document.getElementById('failed-jobs').textContent = data.stats.failed_jobs || 0;
|
document.getElementById('failed-jobs').textContent = data.stats?.failed_jobs || 0;
|
||||||
document.getElementById('ffmpeg-status').textContent = data.stats.ffmpeg_status || 'Unknown';
|
document.getElementById('ffmpeg-status').textContent = data.stats?.ffmpeg_status || 'Unknown';
|
||||||
document.getElementById('transcode-storage').textContent = data.stats.transcoded_storage || '0 GB';
|
document.getElementById('transcode-storage').textContent = data.stats?.transcoded_storage || '0 GB';
|
||||||
document.getElementById('avg-processing-time').textContent = data.stats.avg_processing_time || '-- min';
|
document.getElementById('avg-processing-time').textContent = data.stats?.avg_processing_time || '-- min';
|
||||||
document.getElementById('success-rate').textContent = data.stats.success_rate ?
|
document.getElementById('success-rate').textContent = data.stats?.success_rate ?
|
||||||
`${data.stats.success_rate.toFixed(1)}%` : '--%';
|
`${data.stats.success_rate.toFixed(1)}%` : '--%';
|
||||||
|
|
||||||
// Update active jobs table
|
// Update active jobs table
|
||||||
@ -1224,7 +1277,10 @@
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load transcoding stats:', error);
|
console.error('Failed to load transcoding stats:', error);
|
||||||
showToast('Failed to load transcoding stats', 'error');
|
// Only show toast on first error, not during polling
|
||||||
|
if (!transcodingPollInterval) {
|
||||||
|
showToast('Failed to load transcoding stats: ' + error.message, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="recent-uploads" class="recent-uploads">
|
<div id="recent-uploads" class="recent-uploads">
|
||||||
<h3>Recent Uploads</h3>
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||||
|
<h3 style="margin: 0;">Recent Uploads</h3>
|
||||||
|
<button class="action-btn" onclick="gatewayUI.loadServerFiles(true)" style="font-size: 0.8rem; padding: 5px 10px;">
|
||||||
|
↻ Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div id="uploads-list" class="uploads-list">
|
<div id="uploads-list" class="uploads-list">
|
||||||
<p class="empty-state">No recent uploads</p>
|
<p class="empty-state">No recent uploads</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,6 +14,9 @@ class GatewayUI {
|
|||||||
|
|
||||||
// Update service status every 30 seconds
|
// Update service status every 30 seconds
|
||||||
setInterval(() => this.checkServiceStatus(), 30000);
|
setInterval(() => this.checkServiceStatus(), 30000);
|
||||||
|
|
||||||
|
// Refresh file list every 15 seconds to pick up completed transcoding jobs
|
||||||
|
setInterval(() => this.loadServerFiles(false), 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeElements() {
|
initializeElements() {
|
||||||
@ -187,6 +190,9 @@ class GatewayUI {
|
|||||||
// Show progress
|
// Show progress
|
||||||
this.showUploadProgress(file.name);
|
this.showUploadProgress(file.name);
|
||||||
|
|
||||||
|
// Reset progress
|
||||||
|
this.updateProgress(0, 0, file.size);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.currentUpload = {
|
this.currentUpload = {
|
||||||
file: file,
|
file: file,
|
||||||
@ -202,7 +208,7 @@ class GatewayUI {
|
|||||||
console.log('Upload without auth - nostrAuth:', !!window.nostrAuth, 'sessionToken:', !!window.nostrAuth?.sessionToken);
|
console.log('Upload without auth - nostrAuth:', !!window.nostrAuth, 'sessionToken:', !!window.nostrAuth?.sessionToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/upload', {
|
const response = await this.uploadWithProgress('/api/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: formData,
|
body: formData,
|
||||||
@ -254,12 +260,85 @@ class GatewayUI {
|
|||||||
this.uploadFilename.textContent = filename;
|
this.uploadFilename.textContent = filename;
|
||||||
this.uploadProgress.classList.remove('hidden');
|
this.uploadProgress.classList.remove('hidden');
|
||||||
this.uploadArea.style.display = 'none';
|
this.uploadArea.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Start progress simulation (since we can't track real progress easily)
|
async uploadWithProgress(url, options) {
|
||||||
this.simulateProgress();
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
// Track upload progress
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percentComplete = (e.loaded / e.total) * 100;
|
||||||
|
this.updateProgress(percentComplete, e.loaded, e.total);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle completion
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
resolve(response);
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Invalid response format'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
reject(new Error('Network error occurred during upload'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle timeout
|
||||||
|
xhr.addEventListener('timeout', () => {
|
||||||
|
reject(new Error('Upload timed out'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle abort
|
||||||
|
xhr.addEventListener('abort', () => {
|
||||||
|
reject(new Error('AbortError'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set timeout (10 minutes for large files)
|
||||||
|
xhr.timeout = 600000;
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
Object.entries(options.headers || {}).forEach(([key, value]) => {
|
||||||
|
xhr.setRequestHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle abort signal
|
||||||
|
if (options.signal) {
|
||||||
|
options.signal.addEventListener('abort', () => {
|
||||||
|
xhr.abort();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
xhr.open(options.method || 'GET', url);
|
||||||
|
xhr.send(options.body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(percent, loaded, total) {
|
||||||
|
if (!this.currentUpload) return;
|
||||||
|
|
||||||
|
const elapsed = (Date.now() - this.currentUpload.startTime) / 1000;
|
||||||
|
const speed = loaded / elapsed;
|
||||||
|
const remaining = total > loaded ? (total - loaded) / speed : 0;
|
||||||
|
|
||||||
|
this.progressFill.style.width = `${percent}%`;
|
||||||
|
this.progressPercent.textContent = `${Math.round(percent)}%`;
|
||||||
|
this.progressSpeed.textContent = this.formatBytes(speed) + '/s';
|
||||||
|
this.progressEta.textContent = this.formatTime(remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
simulateProgress() {
|
simulateProgress() {
|
||||||
|
// Fallback for browsers that don't support progress events
|
||||||
let progress = 0;
|
let progress = 0;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
@ -316,9 +395,9 @@ class GatewayUI {
|
|||||||
this.loadServerFiles();
|
this.loadServerFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadServerFiles() {
|
async loadServerFiles(showLoading = true) {
|
||||||
// Show loading state
|
// Show loading state only if explicitly requested
|
||||||
if (this.uploadsList) {
|
if (this.uploadsList && showLoading) {
|
||||||
this.uploadsList.innerHTML = '<div class="loading-state"><div class="spinner"></div>Loading files...</div>';
|
this.uploadsList.innerHTML = '<div class="loading-state"><div class="spinner"></div>Loading files...</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +178,9 @@ apt-get install -y \
|
|||||||
tree \
|
tree \
|
||||||
unzip \
|
unzip \
|
||||||
wget \
|
wget \
|
||||||
ffmpeg
|
ffmpeg \
|
||||||
|
build-essential \
|
||||||
|
gcc
|
||||||
|
|
||||||
# Install latest stable Go version
|
# Install latest stable Go version
|
||||||
echo "📦 Checking for latest Go version..."
|
echo "📦 Checking for latest Go version..."
|
||||||
@ -277,7 +279,8 @@ if [ "$SKIP_BUILD" = false ]; then
|
|||||||
# Create bin directory if it doesn't exist
|
# Create bin directory if it doesn't exist
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
|
|
||||||
# Build binary
|
# Build binary with CGO enabled for SQLite support
|
||||||
|
export CGO_ENABLED=1
|
||||||
go build -o bin/gateway \
|
go build -o bin/gateway \
|
||||||
-ldflags "-X main.version=$(git describe --tags --always 2>/dev/null || echo 'dev') -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) -s -w" \
|
-ldflags "-X main.version=$(git describe --tags --always 2>/dev/null || echo 'dev') -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) -s -w" \
|
||||||
./cmd/gateway
|
./cmd/gateway
|
||||||
|
@ -64,7 +64,9 @@ apt-get install -y \
|
|||||||
bc \
|
bc \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
gnupg \
|
gnupg \
|
||||||
lsb-release
|
lsb-release \
|
||||||
|
build-essential \
|
||||||
|
gcc
|
||||||
|
|
||||||
# Create service user
|
# Create service user
|
||||||
echo "👤 Creating service user..."
|
echo "👤 Creating service user..."
|
||||||
@ -82,6 +84,8 @@ if [ "$SKIP_BUILD" = false ]; then
|
|||||||
# Create bin directory if it doesn't exist
|
# Create bin directory if it doesn't exist
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
|
|
||||||
|
# Build with CGO enabled for SQLite support
|
||||||
|
export CGO_ENABLED=1
|
||||||
go build -o bin/gateway \
|
go build -o bin/gateway \
|
||||||
-ldflags "-X main.version=$(git describe --tags --always) -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) -s -w" \
|
-ldflags "-X main.version=$(git describe --tags --always) -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) -s -w" \
|
||||||
./cmd/gateway
|
./cmd/gateway
|
||||||
@ -216,7 +220,7 @@ server {
|
|||||||
add_header X-Frame-Options SAMEORIGIN always;
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; connect-src 'self' wss: ws:;" always;
|
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src 'self' data: https:; connect-src 'self' wss: ws:;" always;
|
||||||
|
|
||||||
# Main application
|
# Main application
|
||||||
location / {
|
location / {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user