feat: retry/start selected jobs while upload batch is running

Previously, 'Erneut versuchen' and 'Ausgewählte starten' did nothing
when a batch was already running (uploading=true). Failed jobs were
set to 'Wartet' but never actually uploaded because they couldn't be
added to the running batch.

New: upload-manager.addJobs() allows adding tasks to a running batch.
When a batch is active and user retries/starts jobs, they're injected
into the running batch via IPC 'add-jobs-to-batch'. The upload manager
starts processing them immediately using the existing semaphores.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-23 18:15:31 +01:00
parent a1a3e87de8
commit e1b03605fa
4 changed files with 50 additions and 1 deletions

View File

@ -135,6 +135,7 @@ class UploadManager extends EventEmitter {
const { signal } = this.abortController; const { signal } = this.abortController;
const batchId = `batch-${Date.now()}`; const batchId = `batch-${Date.now()}`;
const results = new Map(); // filePath -> { name, size, results: [] } const results = new Map(); // filePath -> { name, size, results: [] }
this._batchResults = results;
for (const task of tasks) { for (const task of tasks) {
const fileName = path.basename(task.file); const fileName = path.basename(task.file);
@ -690,6 +691,24 @@ class UploadManager extends EventEmitter {
return next; return next;
} }
addJobs(tasks) {
if (!this.running || !tasks || tasks.length === 0) return;
const { signal } = this.abortController;
const results = this._batchResults || new Map();
for (const task of tasks) {
const fileName = path.basename(task.file);
if (!results.has(task.file)) {
let size = 0;
try { size = fs.statSync(task.file).size; } catch {}
results.set(task.file, { name: fileName, size, results: [] });
}
}
// Start each new job — they'll acquire semaphores and run
for (const task of tasks) {
this._runJob(task, results, signal);
}
}
cancelJobs(jobIds) { cancelJobs(jobIds) {
for (const jobId of jobIds || []) { for (const jobId of jobIds || []) {
if (!jobId) continue; if (!jobId) continue;

13
main.js
View File

@ -778,6 +778,19 @@ ipcMain.handle('cancel-selected-jobs', (_event, jobIds) => {
return true; return true;
}); });
ipcMain.handle('add-jobs-to-batch', (_event, payload) => {
if (!uploadManager || !uploadManager.running) {
return { error: 'Kein Upload aktiv' };
}
const config = configStore.load();
const jobs = payload && Array.isArray(payload.jobs) ? payload.jobs : [];
const tasks = buildUploadTasksFromJobs(config, jobs);
if (tasks.length === 0) return { added: 0 };
uploadManager.addJobs(tasks);
debugLog(`add-jobs-to-batch: added ${tasks.length} tasks to running batch`);
return { added: tasks.length };
});
ipcMain.handle('finish-after-active', () => { ipcMain.handle('finish-after-active', () => {
if (uploadManager) { if (uploadManager) {
uploadManager.finishAfterActive(); uploadManager.finishAfterActive();

View File

@ -34,6 +34,7 @@ contextBridge.exposeInMainWorld('api', {
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload), startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
cancelUpload: () => ipcRenderer.invoke('cancel-upload'), cancelUpload: () => ipcRenderer.invoke('cancel-upload'),
cancelSelectedJobs: (jobIds) => ipcRenderer.invoke('cancel-selected-jobs', jobIds), cancelSelectedJobs: (jobIds) => ipcRenderer.invoke('cancel-selected-jobs', jobIds),
addJobsToBatch: (payload) => ipcRenderer.invoke('add-jobs-to-batch', payload),
finishAfterActive: () => ipcRenderer.invoke('finish-after-active'), finishAfterActive: () => ipcRenderer.invoke('finish-after-active'),
runHealthCheck: (payload) => ipcRenderer.invoke('run-health-check', payload), runHealthCheck: (payload) => ipcRenderer.invoke('run-health-check', payload),

View File

@ -1352,7 +1352,23 @@ function _markSkippedJobs(result) {
} }
async function startSelectedUpload() { async function startSelectedUpload() {
if (uploading) return; if (uploading) {
// Batch already running — add selected jobs to the running batch
const retryable = queueJobs.filter(j => selectedJobIds.has(j.id) && ['queued', 'error', 'aborted', 'skipped'].includes(j.status));
if (retryable.length > 0) {
retryable.forEach(j => {
j.status = 'queued'; j.error = null; j.result = null;
j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null;
});
renderQueueTable();
const result = await window.api.addJobsToBatch({
jobs: retryable.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster }))
});
_markSkippedJobs(result);
persistQueueStateSoon();
}
return;
}
uploading = true; // set immediately to prevent double-click race uploading = true; // set immediately to prevent double-click race
updateQueueActionButtons(); updateQueueActionButtons();