fix: session-based counters and hoster cancel context menu

- Done/Error counters now use sessionFilesData (survives removeFromQueueOnDone)
- Uploaded/Total bytes tracked via session accumulators (never decrease)
- Errors no longer shown in Files list (stay in queue for retry)
- Right-click context menu: "hoster abbrechen" cancels all jobs for a hoster

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-12 04:34:11 +01:00
parent 052bd940f1
commit 2c9726a33d
3 changed files with 70 additions and 19 deletions

View File

@ -20,6 +20,10 @@ let queueJobs = []; // { id, file, fileName, hoster, status, bytesUploaded, byte
let _jobIndexById = new Map(); // id -> job (O(1) lookup)
let _jobIndexByUploadId = new Map(); // uploadId -> job
let selectedJobIds = new Set();
let _sessionTotalBytes = 0; // Total bytes ever added to queue this session
let _sessionUploadedBytes = 0; // Bytes fully uploaded this session (done jobs)
let _sessionTrackedJobs = new Set(); // Job IDs already counted for totalBytes
let _sessionDoneJobs = new Set(); // Job IDs already counted for uploadedBytes
let queueSortState = { key: 'filename', direction: 'asc' };
// History state
@ -919,6 +923,24 @@ function showContextMenu(x, y) {
const startItem = menu.querySelector('[data-action="start-selected"]');
if (startItem) startItem.textContent = n > 1 ? `Ausgewählte starten (${n})` : 'Ausgewählte starten';
// Dynamic "cancel hoster" items
const cancelSep = menu.querySelector('.ctx-hoster-cancel-sep');
const cancelContainer = menu.querySelector('.ctx-hoster-cancel-items');
const activeHosters = [...new Set(queueJobs.filter(j => j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server' || j.status === 'preview').map(j => j.hoster))];
cancelContainer.innerHTML = '';
if (activeHosters.length > 0) {
cancelSep.style.display = '';
activeHosters.forEach(h => {
const item = document.createElement('div');
item.className = 'ctx-item ctx-item-danger';
item.dataset.action = `cancel-hoster:${h}`;
item.textContent = `${h} abbrechen`;
cancelContainer.appendChild(item);
});
} else {
cancelSep.style.display = 'none';
}
menu.style.display = 'block';
const menuX = Math.min(x, window.innerWidth - menu.offsetWidth - 5);
menu.style.left = menuX + 'px';
@ -1116,6 +1138,24 @@ async function handleContextAction(action) {
} else if (action === 'always-on-top') {
alwaysOnTopState = !alwaysOnTopState;
await window.api.setAlwaysOnTop(alwaysOnTopState);
} else if (action.startsWith('cancel-hoster:')) {
const hoster = action.replace('cancel-hoster:', '');
const jobIds = [];
for (const job of queueJobs) {
if (job.hoster === hoster && (job.status === 'uploading' || job.status === 'queued' || job.status === 'retrying' || job.status === 'getting-server' || job.status === 'preview')) {
jobIds.push(job.id);
// Mark queued/preview jobs as error immediately
if (job.status === 'queued' || job.status === 'preview') {
job.status = 'error';
job.error = 'Hoster abgebrochen';
}
}
}
// Cancel active uploads via IPC
if (jobIds.length > 0) await window.api.cancelSelectedJobs(jobIds);
renderQueueTable();
updateStatusBar();
updateQueueActionButtons();
} else if (action.startsWith('shutdown-')) {
const mode = action.replace('shutdown-', '');
await window.api.setShutdownAfterFinish(mode);
@ -1302,6 +1342,11 @@ function handleProgress(data) {
job.status = data.status;
job.bytesUploaded = data.bytesUploaded || 0;
job.bytesTotal = data.bytesTotal || job.bytesTotal;
// Track session total bytes (survives removeFromQueueOnDone)
if (job.bytesTotal > 0 && !_sessionTrackedJobs.has(job.id)) {
_sessionTotalBytes += job.bytesTotal;
_sessionTrackedJobs.add(job.id);
}
job.speedKbs = data.speedKbs || 0;
job.elapsed = data.elapsed || 0;
job.remaining = data.remaining || 0;
@ -1317,6 +1362,12 @@ function handleProgress(data) {
maybeAddSessionFile(job);
// Track session uploaded bytes (survives removeFromQueueOnDone)
if (job.status === 'done' && !_sessionDoneJobs.has(job.id)) {
_sessionUploadedBytes += job.bytesTotal || 0;
_sessionDoneJobs.add(job.id);
}
// Remove finished jobs from queue immediately if setting is enabled
if (job.status === 'done' && config.globalSettings && config.globalSettings.removeFromQueueOnDone) {
removeJobFromIndex(job);
@ -1541,21 +1592,6 @@ function maybeAddSessionFile(job) {
}
}
if (job.status === 'error') {
const errorText = `[Fehler] ${job.error || ''}`;
if (!sessionFilesData.some((row) => row.isError && row.filename === job.fileName && row.host === job.hoster && row.link === errorText)) {
sessionFilesData.push({
date: dt.text,
dateTs: dt.ts,
filename: job.fileName || '',
host: job.hoster || '',
link: errorText,
isError: true,
order: sessionFilesData.length
});
renderRecentUploadsPanel();
}
}
}
function applySummaryResults(summary) {
@ -1637,15 +1673,26 @@ function updateStatusBar() {
document.getElementById('sbState').textContent = stateText;
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
const uploadedSize = Math.max(0, stats.totalSize - stats.remainingSize);
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(stats.totalSize)}`;
// Session-based bytes: survive removeFromQueueOnDone
// Uploaded = done jobs (session) + in-progress bytes still in queue
let inProgressBytes = 0;
for (const job of queueJobs) {
if (job.status === 'uploading' || job.status === 'getting-server' || job.status === 'retrying') {
inProgressBytes += job.bytesUploaded || 0;
}
}
const uploadedSize = _sessionUploadedBytes + inProgressBytes;
const totalSize = Math.max(stats.totalSize, _sessionTotalBytes);
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
document.getElementById('sbConnections').textContent = `Connections: ${lastUploadStats.activeJobs || 0}`;
document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`;
document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`;
document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`;
document.getElementById('sbDoneCount').textContent = `Done: ${stats.done}`;
document.getElementById('sbErrorCount').textContent = `Error: ${stats.errors}`;
const sessionDone = sessionFilesData.filter(r => !r.isError).length;
const sessionErrors = sessionFilesData.filter(r => r.isError).length;
document.getElementById('sbDoneCount').textContent = `Done: ${sessionDone}`;
document.getElementById('sbErrorCount').textContent = `Error: ${sessionErrors}`;
}
// --- Health Check ---

View File

@ -265,6 +265,8 @@
<div class="ctx-separator"></div>
<div class="ctx-item" data-action="delete-selected">Entfernen</div>
<div class="ctx-item" data-action="delete-all">Alle entfernen</div>
<div class="ctx-separator ctx-hoster-cancel-sep" style="display:none"></div>
<div class="ctx-hoster-cancel-items"></div>
</div>
<div class="context-menu" id="recentContextMenu" style="display:none">

View File

@ -605,6 +605,8 @@ body {
position: relative;
}
.ctx-item:hover { background: rgba(102, 126, 234, 0.2); }
.ctx-item-danger { color: var(--danger); }
.ctx-item-danger:hover { background: rgba(231, 76, 60, 0.2); }
.ctx-separator { height: 1px; margin: 4px 8px; background: var(--border); }
.ctx-submenu { position: relative; }
.ctx-submenu-items {