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>
let adminUser = null;
let currentAdminSection = 'overview';
let transcodingPollInterval = null;
// Admin authentication
async function checkAdminAuth() {
@ -645,6 +646,12 @@
}
async function adminLogout() {
// Stop any polling intervals
if (transcodingPollInterval) {
clearInterval(transcodingPollInterval);
transcodingPollInterval = null;
}
if (window.nostrAuth) {
await window.nostrAuth.logout();
adminUser = null;
@ -664,10 +671,21 @@
document.querySelectorAll('.admin-section').forEach(sec => sec.classList.remove('active'));
document.getElementById(section + '-section').classList.add('active');
// Clear existing polling intervals
if (transcodingPollInterval) {
clearInterval(transcodingPollInterval);
transcodingPollInterval = null;
}
// Load section data
switch (section) {
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 'files': loadFiles(); break;
case 'reports': loadReports(); break;
@ -1202,21 +1220,56 @@
function refreshLogs() { loadLogs(); }
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
async function loadTranscodingStats() {
try {
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();
// Update stats cards
document.getElementById('queue-length').textContent = data.stats.queue_length || 0;
document.getElementById('processing-jobs').textContent = data.stats.processing_jobs || 0;
document.getElementById('completed-today').textContent = data.stats.completed_today || 0;
document.getElementById('failed-jobs').textContent = data.stats.failed_jobs || 0;
document.getElementById('ffmpeg-status').textContent = data.stats.ffmpeg_status || 'Unknown';
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('success-rate').textContent = data.stats.success_rate ?
document.getElementById('queue-length').textContent = data.stats?.queue_length || 0;
document.getElementById('processing-jobs').textContent = data.stats?.processing_jobs || 0;
document.getElementById('completed-today').textContent = data.stats?.completed_today || 0;
document.getElementById('failed-jobs').textContent = data.stats?.failed_jobs || 0;
document.getElementById('ffmpeg-status').textContent = data.stats?.ffmpeg_status || 'Unknown';
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('success-rate').textContent = data.stats?.success_rate ?
`${data.stats.success_rate.toFixed(1)}%` : '--%';
// Update active jobs table
@ -1224,7 +1277,10 @@
} catch (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 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">
<p class="empty-state">No recent uploads</p>
</div>

View File

@ -14,6 +14,9 @@ class GatewayUI {
// Update service status every 30 seconds
setInterval(() => this.checkServiceStatus(), 30000);
// Refresh file list every 15 seconds to pick up completed transcoding jobs
setInterval(() => this.loadServerFiles(false), 15000);
}
initializeElements() {
@ -187,6 +190,9 @@ class GatewayUI {
// Show progress
this.showUploadProgress(file.name);
// Reset progress
this.updateProgress(0, 0, file.size);
try {
this.currentUpload = {
file: file,
@ -202,7 +208,7 @@ class GatewayUI {
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',
headers: headers,
body: formData,
@ -254,12 +260,85 @@ class GatewayUI {
this.uploadFilename.textContent = filename;
this.uploadProgress.classList.remove('hidden');
this.uploadArea.style.display = 'none';
// Start progress simulation (since we can't track real progress easily)
this.simulateProgress();
}
async uploadWithProgress(url, options) {
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() {
// Fallback for browsers that don't support progress events
let progress = 0;
const startTime = Date.now();
@ -316,9 +395,9 @@ class GatewayUI {
this.loadServerFiles();
}
async loadServerFiles() {
// Show loading state
if (this.uploadsList) {
async loadServerFiles(showLoading = true) {
// Show loading state only if explicitly requested
if (this.uploadsList && showLoading) {
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 \
unzip \
wget \
ffmpeg
ffmpeg \
build-essential \
gcc
# Install latest stable 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
mkdir -p bin
# Build binary
# Build binary with CGO enabled for SQLite support
export CGO_ENABLED=1
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" \
./cmd/gateway

View File

@ -64,7 +64,9 @@ apt-get install -y \
bc \
ca-certificates \
gnupg \
lsb-release
lsb-release \
build-essential \
gcc
# Create service user
echo "👤 Creating service user..."
@ -82,6 +84,8 @@ if [ "$SKIP_BUILD" = false ]; then
# Create bin directory if it doesn't exist
mkdir -p bin
# Build with CGO enabled for SQLite support
export CGO_ENABLED=1
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" \
./cmd/gateway
@ -216,7 +220,7 @@ server {
add_header X-Frame-Options SAMEORIGIN always;
add_header X-XSS-Protection "1; mode=block" 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
location / {