Improve upload error handling and admin transcoding monitoring
Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / E2E Tests (push) Blocked by required conditions

This commit is contained in:
Enki 2025-08-27 17:15:30 -07:00
parent 613b962a87
commit 9c22093aca
5 changed files with 169 additions and 22 deletions

View File

@ -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');
}
} }
} }

View File

@ -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>

View File

@ -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>';
} }

View File

@ -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

View File

@ -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 / {