From 2e0a8c9d393403ebdfc6a5b31b6b8fed41ec989f Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 11 Mar 2026 03:14:06 +0100 Subject: [PATCH] feat: stats panel, abort persistence, doodstream error logging - Stats tab in recent panel (queue counts, sizes, speed, ETA, run time) - Aborted jobs persist across restart (saved as queued) - Doodstream: throttle support, better error messages with HTTP status - Recent panel tab switching (Files / Stats) Co-Authored-By: Claude Opus 4.6 --- lib/doodstream-upload.js | 35 ++++++++++++--- renderer/app.js | 92 +++++++++++++++++++++++++++++++++++++--- renderer/index.html | 57 +++++++++++++++++++------ renderer/styles.css | 59 ++++++++++++++++++++++++-- 4 files changed, 214 insertions(+), 29 deletions(-) diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js index aa908ac..010ad9d 100644 --- a/lib/doodstream-upload.js +++ b/lib/doodstream-upload.js @@ -227,16 +227,29 @@ class DoodstreamUploader { bodyStream.push(preambleBuffer); bytesSent += preambleBuffer.length; - // Pipe file + // Pipe file with throttle support fileStream.on('data', (chunk) => { if (signal && signal.aborted) { fileStream.destroy(); bodyStream.destroy(); return; } - bodyStream.push(chunk); - bytesSent += chunk.length; - if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize); + if (throttle) { + fileStream.pause(); + throttle.consume(chunk.length, signal).then(() => { + bodyStream.push(chunk); + bytesSent += chunk.length; + if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize); + fileStream.resume(); + }).catch(() => { + fileStream.destroy(); + bodyStream.destroy(); + }); + } else { + bodyStream.push(chunk); + bytesSent += chunk.length; + if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize); + } }); fileStream.on('end', () => { @@ -262,10 +275,16 @@ class DoodstreamUploader { headersTimeout: 60000 }); + const statusCode = uploadRes.statusCode; const resText = await uploadRes.body.text(); let payload; try { payload = JSON.parse(resText); } catch {} + if (statusCode >= 400) { + const msg = payload && payload.msg ? payload.msg : resText.slice(0, 200); + throw new Error(`Doodstream Upload HTTP ${statusCode}: ${msg}`); + } + if (!payload) { // Try to extract from HTML response const match = resText.match(/filecode['":\s]+['"]([a-zA-Z0-9]+)['"]/i); @@ -276,7 +295,11 @@ class DoodstreamUploader { file_code: match[1] }; } - throw new Error('Doodstream Upload: Keine gueltige Antwort erhalten'); + throw new Error(`Doodstream Upload: Keine gueltige Antwort (HTTP ${statusCode}, Body: ${resText.slice(0, 150)})`); + } + + if (payload.status && Number(payload.status) !== 200 && payload.msg) { + throw new Error(`Doodstream Upload: ${payload.msg}`); } // Parse result @@ -289,7 +312,7 @@ class DoodstreamUploader { } if (!item) { - throw new Error(`Doodstream Upload fehlgeschlagen: ${payload.msg || 'Unbekannter Fehler'}`); + throw new Error(`Doodstream Upload fehlgeschlagen: ${payload.msg || JSON.stringify(payload).slice(0, 150)}`); } const fileCode = item.filecode || item.file_code || ''; diff --git a/renderer/app.js b/renderer/app.js index 9ec9698..6aa481f 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -310,10 +310,10 @@ function restoreQueueStateFromConfig() { } function buildPersistedQueueState() { - const unfinishedJobs = queueJobs.filter(job => !['done', 'skipped', 'aborted'].includes(job.status)); + const persistableJobs = queueJobs.filter(job => !['done', 'skipped'].includes(job.status)); const selectedFileMap = new Map(selectedFiles.map(file => [file.path, file])); - for (const job of unfinishedJobs) { + for (const job of persistableJobs) { if (job.file && !selectedFileMap.has(job.file)) { selectedFileMap.set(job.file, { path: job.file, @@ -323,7 +323,7 @@ function buildPersistedQueueState() { } } - if (selectedFileMap.size === 0 && queueJobs.every(job => ['done', 'skipped', 'aborted'].includes(job.status))) { + if (selectedFileMap.size === 0 && queueJobs.every(job => ['done', 'skipped'].includes(job.status))) { return null; } @@ -335,9 +335,10 @@ function buildPersistedQueueState() { file: job.file, fileName: job.fileName, hoster: job.hoster, - status: job.status, + // Save aborted jobs as queued so they survive restart + status: job.status === 'aborted' ? 'queued' : job.status, bytesTotal: job.bytesTotal || 0, - error: job.error || null, + error: job.status === 'aborted' ? null : (job.error || null), result: job.result || null, maxAttempts: job.maxAttempts || 0 })) @@ -998,6 +999,7 @@ function handleProgress(data) { scheduleQueueRender(); updateQueueActionButtons(); updateStatusBar(); + updateStatsPanel(); persistQueueStateSoon(); } @@ -1035,7 +1037,7 @@ function handleBatchDone(summary) { renderQueueTable(); } - if (queueJobs.some((job) => !['done', 'skipped', 'aborted'].includes(job.status))) persistQueueStateSoon(true); + if (queueJobs.some((job) => !['done', 'skipped'].includes(job.status))) persistQueueStateSoon(true); else clearPersistedQueueStateSoon(); lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: lastUploadStats.totalBytes, elapsed: lastUploadStats.elapsed, activeJobs: 0 }; @@ -1051,6 +1053,21 @@ function handleStats(data) { activeJobs: data.activeJobs || 0 }; updateStatusBar(); + updateStatsPanel(); + + // Track run time + if (data.state === 'uploading' || data.state === 'stopping') { + if (!statsStartTime) { + statsStartTime = Date.now(); + statsRunTimer = setInterval(() => { + const el = document.getElementById('statRunTime'); + if (el) el.textContent = formatDuration(Math.round((Date.now() - statsStartTime) / 1000)); + }, 1000); + } + } else if (data.state === 'idle' && statsRunTimer) { + clearInterval(statsRunTimer); + statsRunTimer = null; + } } // --- Retry --- @@ -2220,5 +2237,68 @@ function showCopyToast(msg) { } } +// --- Recent panel tabs --- +document.querySelectorAll('.recent-tab').forEach(tab => { + tab.addEventListener('click', () => { + document.querySelectorAll('.recent-tab').forEach(t => t.classList.remove('active')); + document.querySelectorAll('.recent-tab-body').forEach(b => b.classList.remove('active')); + tab.classList.add('active'); + const panel = document.getElementById(tab.dataset.panel); + if (panel) panel.classList.add('active'); + const hint = document.getElementById('recentFilesHint'); + if (hint) hint.textContent = tab.dataset.panel === 'statsTab' ? 'Upload-Statistiken' : 'Zuletzt erzeugte Upload-Links'; + }); +}); + +// --- Stats panel update --- +let statsStartTime = 0; +let statsRunTimer = null; + +function formatBytes(bytes) { + if (bytes <= 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); + return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 2 : 0) + ' ' + units[i]; +} + +function formatDuration(seconds) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; +} + +function updateStatsPanel() { + const total = queueJobs.length; + const done = queueJobs.filter(j => j.status === 'done').length; + const inProgress = queueJobs.filter(j => ['uploading', 'getting-server', 'retrying'].includes(j.status)).length; + const errors = queueJobs.filter(j => j.status === 'error').length; + const remaining = total - done - errors; + + const totalSize = queueJobs.reduce((s, j) => s + (j.bytesTotal || 0), 0); + const remainingSize = queueJobs.filter(j => !['done', 'error', 'skipped'].includes(j.status)) + .reduce((s, j) => s + ((j.bytesTotal || 0) - (j.bytesUploaded || 0)), 0); + + const el = (id) => document.getElementById(id); + if (el('statQueueTotal')) el('statQueueTotal').textContent = total; + if (el('statQueueDone')) el('statQueueDone').textContent = done; + if (el('statQueueRemaining')) el('statQueueRemaining').textContent = remaining; + if (el('statQueueInProgress')) el('statQueueInProgress').textContent = inProgress; + if (el('statQueueError')) el('statQueueError').textContent = errors; + if (el('statSizeTotal')) el('statSizeTotal').textContent = formatBytes(totalSize); + if (el('statSizeRemaining')) el('statSizeRemaining').textContent = formatBytes(remainingSize); + + const speed = lastUploadStats.globalSpeedKbs || 0; + if (el('statSpeed')) el('statSpeed').textContent = speed > 0 ? formatBytes(speed * 1024) + '/s' : '0 B/s'; + if (el('statEta')) { + if (speed > 0 && remainingSize > 0) { + el('statEta').textContent = formatDuration(Math.round(remainingSize / (speed * 1024))); + } else { + el('statEta').textContent = '--:--'; + } + } + if (el('statSessionBytes')) el('statSessionBytes').textContent = formatBytes(lastUploadStats.totalBytes || 0); +} + // --- Start --- init(); diff --git a/renderer/index.html b/renderer/index.html index e97cdf3..1c0531c 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -96,21 +96,50 @@
-

Files

- Zuletzt erzeugte Upload-Links +
+ + +
+ Zuletzt erzeugte Upload-Links
-
- - - - - - - - - - -
DatumDateinameHostLink
+
+
+ + + + + + + + + + +
DatumDateinameHostLink
+
+
+
+
+
+

Files in queue (count)

+
total:0
+
done:0
+
remaining:0
+
in progress:0
+
error:0
+
+
+

File size in queue

+
total:0 B
+
remaining:0 B
+
+
+

Session

+
Upload speed:0 B/s
+
Remaining time:--:--
+
Run time:00:00:00
+
Uploaded (this run):0 B
+
+
diff --git a/renderer/styles.css b/renderer/styles.css index e4c9885..764f5ca 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -374,14 +374,67 @@ body { border-bottom: 1px solid var(--border); background: rgba(0, 0, 0, 0.12); } -.recent-files-header h3 { - font-size: 13px; - font-weight: 600; +.recent-tabs { + display: flex; + gap: 0; } +.recent-tab { + padding: 4px 14px; + font-size: 12px; + font-weight: 500; + background: transparent; + border: 1px solid var(--border); + border-bottom: none; + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s; +} +.recent-tab:first-child { border-radius: 4px 0 0 0; } +.recent-tab:last-child { border-radius: 0 4px 0 0; } +.recent-tab.active { + background: rgba(255,255,255,0.06); + color: var(--text); + border-bottom-color: transparent; +} +.recent-tab:hover:not(.active) { + background: rgba(255,255,255,0.03); + color: var(--text); +} +.recent-tab-body { display: none; flex: 1; min-height: 0; overflow: auto; } +.recent-tab-body.active { display: flex; flex-direction: column; } .recent-files-hint { font-size: 11px; color: var(--text-dim); } +.stats-grid { + display: flex; + gap: 32px; + padding: 12px 20px; + flex: 1; +} +.stats-col { + flex: 1; + min-width: 0; +} +.stats-col h4 { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.stats-row { + display: flex; + justify-content: space-between; + font-size: 12px; + padding: 2px 0; + color: var(--text-dim); +} +.stats-row span:last-child { + color: var(--text); + font-variant-numeric: tabular-nums; +} .recent-files-table-wrap { flex: 1; min-height: 0;