diff --git a/main.js b/main.js index 1d07fa4..aa846fb 100644 --- a/main.js +++ b/main.js @@ -132,6 +132,7 @@ const configStore = new ConfigStore(app); configStore.setPerfLog((m) => { try { logInfo(m); } catch {} }); let uploadManager = null; const uploadBatchMutationGates = new WeakMap(); +const uploadRecoveryStates = new WeakMap(); let lastSessionSummary = null; let startupRecoveryCoordinator = null; let sourceDeleteJournal = null; @@ -2092,7 +2093,17 @@ async function executeReservedUploadStart(payload, startLease) { startedAt: new Date().toISOString(), jobIds: tasks.map(task => task.jobId).filter(Boolean) }; - try { await configStore.saveUploadRecovery(recovery); } catch (error) { debugLog(`upload recovery state could not be saved: ${error.message}`); } + try { + await configStore.saveUploadRecovery(recovery); + } catch (error) { + debugLog(`upload recovery state could not be saved: ${error.message}`); + if (uploadManager === _thisManager) { + uploadManager = null; + globalThis._mhuUploadManagerRef = null; + } + return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' }; + } + uploadRecoveryStates.set(_thisManager, recovery); // Pre-resolve a fallback for every hoster that has one. Lets the upload // manager break out of the retry loop after a single generic failure and @@ -2436,6 +2447,21 @@ ipcMain.handle('add-jobs-to-batch', async (_event, payload) => { ); if (!auditedAdd.ok) return { error: getUploadAuditFailureMessage(getConfiguredLanguage()) }; } + const batchRecovery = uploadRecoveryStates.get(batchManager); + const newRecoveryJobIds = tasks.map(task => task.jobId).filter(Boolean); + if (batchRecovery && newRecoveryJobIds.length > 0) { + const nextRecovery = { + ...batchRecovery, + jobIds: Array.from(new Set([...(batchRecovery.jobIds || []), ...newRecoveryJobIds])) + }; + try { + await configStore.saveUploadRecovery(nextRecovery); + } catch (error) { + debugLog(`upload recovery state could not be extended: ${error.message}`); + return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' }; + } + Object.assign(batchRecovery, nextRecovery); + } persistRotation(pick); const sourceCleanupFingerprints = batchManager.sourceFileCleanup ? await batchManager.sourceFileCleanup.registerGroups(sourceCleanupGroups) diff --git a/renderer/i18n.js b/renderer/i18n.js index 2f7f9bd..4b54b77 100644 --- a/renderer/i18n.js +++ b/renderer/i18n.js @@ -578,9 +578,12 @@ ['Update wird installiert. Die Anwendung startet neu…', 'Installing update. The application is restarting…'], ['Updateprüfung fehlgeschlagen', 'Update check failed'], ['Upload läuft...', 'Uploading...'], + ['Upload konnte nicht gestartet werden', 'The upload could not be started'], + ['Upload wurde abgebrochen', 'The upload was canceled'], ['Upload-Log', 'Upload log'], ['Upload-Status', 'Upload status'], ['Upload-Übersicht', 'Upload overview'], + ['Upload-Wiederherstellung konnte nicht gespeichert werden', 'Upload recovery could not be saved'], ['Verlauf als CSV exportieren?\n\nOK = CSV\nAbbrechen = JSON', 'Export history as CSV?\n\nOK = CSV\nCancel = JSON'], ['Verlauf wirklich löschen?', 'Are you sure you want to delete the history?'], ['Verschlüssele und speichere Einstellungen…', 'Encrypting and saving settings…'], diff --git a/tests/upload-recovery.test.js b/tests/upload-recovery.test.js index 7cc983d..01d4738 100644 --- a/tests/upload-recovery.test.js +++ b/tests/upload-recovery.test.js @@ -88,6 +88,12 @@ test('main and renderer keep recovery evidence until final queue persistence suc assert.match(startFailure, /saveUploadRecovery\(terminalRecovery\)/); assert.match(startFailure, /requestUploadFinalization\(errorSummary, historyPersisted\)/); assert.match(startFailure, /if \(queuePersisted && terminalRecoveryPersisted\)[\s\S]*saveUploadRecovery\(null\)/); + const startHandler = mainSource.slice(mainSource.indexOf("ipcMain.handle('start-upload'"), mainSource.indexOf("ipcMain.handle('cancel-upload'")); + assert.match(startHandler, /await configStore\.saveUploadRecovery\(recovery\)/); + assert.match(startHandler, /catch \(error\)[\s\S]*return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' }/); + const addHandler = mainSource.slice(mainSource.indexOf("ipcMain.handle('add-jobs-to-batch'"), mainSource.indexOf("ipcMain.handle('finish-after-active'")); + assert.match(addHandler, /await configStore\.saveUploadRecovery\(nextRecovery\)/); + assert.ok(addHandler.indexOf('saveUploadRecovery(nextRecovery)') < addHandler.indexOf('batchManager.addJobs(tasks)')); assert.match(rendererSource, /window\.UploadRecovery\.getRecoveryOutcome/); assert.match(rendererSource, /data\.historyPersisted !== true/); assert.ok(indexSource.indexOf('../lib/upload-recovery.js') < indexSource.indexOf('app.js'));