From 5ebeea3220ebf3d21fcc3fbbbe8a1751bc4f2c3d Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:53:46 +0200 Subject: [PATCH] fix: preserve managed backup operation status Bind list state and status updates to matching load generations and operation contexts so delayed settings-navigation responses cannot clear newer busy or error states or replace newer managed-key data. Keep create and delete post-success refreshes authoritative and single-shot. --- renderer/app.js | 66 ++++++++++++++++++++++------------ tests/startup-renderer.test.js | 50 ++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 25 deletions(-) diff --git a/renderer/app.js b/renderer/app.js index c22e11f..92cc3c5 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -65,6 +65,7 @@ let healthCheckRunning = false; let managedOnlineBackups = []; let managedOnlineBackupsAuthoritative = false; let managedOnlineBackupLoadGeneration = 0; +let onlineBackupStatusContextGeneration = 0; const managedOnlineBackupOperationQueues = new Map(); let _rLongTasks = 0, _rLongTaskMax = 0, _rFrameLast = 0, _rFrameWorst = 0, _rFrameCount = 0, _rFrameJank = 0, _rPerfLastLog = 0, _rPerfWindowStart = 0; @@ -2639,11 +2640,23 @@ function showImportQueuePersistenceError(error) { showCopyToast(`Import übernommen. Warteschlange konnte nicht vollständig gespeichert werden: ${error.message || error}`, 8000); } -function setOnlineBackupStatus(message, state = '') { +function beginOnlineBackupStatusContext() { + return ++onlineBackupStatusContextGeneration; +} + +function beginManagedOnlineBackupOperation() { + managedOnlineBackupLoadGeneration++; + return beginOnlineBackupStatusContext(); +} + +function setOnlineBackupStatus(message, state = '', statusContext = null) { + const context = statusContext === null ? beginOnlineBackupStatusContext() : statusContext; + if (context !== onlineBackupStatusContextGeneration) return false; const status = document.getElementById('onlineBackupStatus'); - if (!status) return; + if (!status) return false; status.textContent = String(message || '').replace(/MHU2-[A-Za-z0-9_-]{70}/gu, localizeUiText('Geschützter Schlüssel')); status.dataset.state = state; + return true; } function syncOnlineBackupRestoreButton(busy = false) { @@ -2724,23 +2737,26 @@ function renderManagedOnlineBackups() { list.replaceChildren(content); } -async function loadManagedOnlineBackups() { +async function loadManagedOnlineBackups({ statusContext = null } = {}) { const generation = ++managedOnlineBackupLoadGeneration; + const status = document.getElementById('onlineBackupStatus'); + const loaderStatusContext = statusContext === null ? onlineBackupStatusContextGeneration : statusContext; + const mayUpdateStatus = statusContext !== null || !status || (!status.textContent && !status.dataset.state); try { const result = await window.api.listManagedOnlineBackups(); if (generation !== managedOnlineBackupLoadGeneration) return false; if (!result?.ok || !Array.isArray(result.entries)) { invalidateManagedOnlineBackups(); - setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error'); + if (mayUpdateStatus) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error', loaderStatusContext); return false; } replaceManagedOnlineBackups(result.entries); - setOnlineBackupStatus('', ''); + if (mayUpdateStatus) setOnlineBackupStatus('', '', loaderStatusContext); return true; } catch { if (generation === managedOnlineBackupLoadGeneration) { invalidateManagedOnlineBackups(); - setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error'); + if (mayUpdateStatus) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error', loaderStatusContext); } return false; } @@ -2751,8 +2767,9 @@ function hasManagedOnlineBackup(id) { } function enqueueManagedOnlineBackupOperation(id, action, operation) { + const statusContext = beginManagedOnlineBackupOperation(); const previous = managedOnlineBackupOperationQueues.get(id)?.promise || Promise.resolve(); - const promise = previous.catch(() => {}).then(operation); + const promise = previous.catch(() => {}).then(() => operation(statusContext)); const state = { action, promise }; managedOnlineBackupOperationQueues.set(id, state); renderManagedOnlineBackups(); @@ -2766,16 +2783,16 @@ function enqueueManagedOnlineBackupOperation(id, action, operation) { async function copyManagedOnlineBackup(entry) { const id = entry.id; if (!hasManagedOnlineBackup(id) || managedOnlineBackupOperationQueues.has(id)) return; - await enqueueManagedOnlineBackupOperation(id, 'copy', async () => { + await enqueueManagedOnlineBackupOperation(id, 'copy', async (statusContext) => { try { const result = await window.api.copyManagedOnlineBackup(id); if (!result?.ok) { - setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error'); + setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error', statusContext); return; } showCopyToast('Online-Schlüssel kopiert'); } catch { - setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error'); + setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error', statusContext); } }); } @@ -2790,28 +2807,29 @@ async function deleteManagedOnlineBackup(entry) { }); if (!confirmed) return; if (!hasManagedOnlineBackup(id)) { - setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); + const statusContext = beginManagedOnlineBackupOperation(); + setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error', statusContext); return; } if (managedOnlineBackupOperationQueues.get(id)?.action === 'delete') { setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); return; } - await enqueueManagedOnlineBackupOperation(id, 'delete', async () => { + await enqueueManagedOnlineBackupOperation(id, 'delete', async (statusContext) => { if (!hasManagedOnlineBackup(id)) { - setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); + setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error', statusContext); return; } try { const result = await window.api.deleteManagedOnlineBackup(id); if (!result?.ok || result.removedId !== id) { - setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); + setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error', statusContext); return; } - if (!await loadManagedOnlineBackups()) return; - setOnlineBackupStatus('Schlüssel gelöscht', 'success'); + if (!await loadManagedOnlineBackups({ statusContext })) return; + setOnlineBackupStatus('Schlüssel gelöscht', 'success', statusContext); } catch { - setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); + setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error', statusContext); } }); } @@ -2824,24 +2842,26 @@ async function doOnlineBackupCreate() { openOnlineBackupView(); } catch { openOnlineBackupView(); - setOnlineBackupStatus('Nicht alle Einstellungen konnten gespeichert werden', 'error'); + const statusContext = beginManagedOnlineBackupOperation(); + setOnlineBackupStatus('Nicht alle Einstellungen konnten gespeichert werden', 'error', statusContext); doOnlineBackupCreate.busy = false; return; } + const statusContext = beginManagedOnlineBackupOperation(); const createButton = document.getElementById('createOnlineBackupBtn'); if (createButton) createButton.disabled = true; - setOnlineBackupStatus('Verschlüssele und speichere Einstellungen…', 'busy'); + setOnlineBackupStatus('Verschlüssele und speichere Einstellungen…', 'busy', statusContext); try { const result = await window.api.createManagedOnlineBackup(); if (!result?.ok) { - setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error'); + setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error', statusContext); return; } - if (!await loadManagedOnlineBackups()) return; - setOnlineBackupStatus('Neuer Schlüssel erstellt. Ältere Schlüssel bleiben gültig.', 'success'); + if (!await loadManagedOnlineBackups({ statusContext })) return; + setOnlineBackupStatus('Neuer Schlüssel erstellt. Ältere Schlüssel bleiben gültig.', 'success', statusContext); showCopyToast('Online-Schlüssel erstellt'); } catch { - setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error'); + setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error', statusContext); } finally { if (createButton?.isConnected) createButton.disabled = false; doOnlineBackupCreate.busy = false; diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index 1c48477..b9b1a81 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -81,6 +81,7 @@ const managedOnlineBackupListResponses = [ ]; let managedOnlineBackupListIndex = 0; let managedOnlineBackupCreateIndex = 0; +let pendingManagedOnlineBackupList = null; let pendingManagedOnlineBackupCopy = null; let pendingManagedOnlineBackupDelete = null; contextBridge.exposeInMainWorld('api', { @@ -91,11 +92,22 @@ contextBridge.exposeInMainWorld('api', { listManagedOnlineBackups() { managedOnlineBackupListIndex++; managedOnlineBackupProbeCalls.push(['list', managedOnlineBackupListIndex]); + if (managedOnlineBackupListIndex === 5) { + return new Promise(resolve => { pendingManagedOnlineBackupList = resolve; }); + } return Promise.resolve(managedOnlineBackupListResponses[managedOnlineBackupListIndex - 1]); }, + releaseManagedOnlineBackupList() { + const resolve = pendingManagedOnlineBackupList; + pendingManagedOnlineBackupList = null; + resolve({ ok: true, entries: [ + { id: 'XXXXXXXXXXXXXXXXXXXXXX', displayKey: 'MHU2-XCVB…9753', createdAt: '2026-08-28T12:00:00.000Z' } + ] }); + }, createManagedOnlineBackup() { managedOnlineBackupCreateIndex++; managedOnlineBackupProbeCalls.push(['create', managedOnlineBackupCreateIndex]); + if (managedOnlineBackupCreateIndex === 3) return Promise.resolve({ ok: false, error: 'Online-Sicherung konnte nicht erstellt werden' }); const entry = managedOnlineBackupCreateIndex === 1 ? { id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' } : { id: 'FFFFFFFFFFFFFFFFFFFFFF', displayKey: 'MHU2-FGHJ…8642', createdAt: '2026-08-27T12:00:00.000Z' }; @@ -140,7 +152,6 @@ contextBridge.exposeInMainWorld('api', { return new Promise(resolve => { resolveConfirmation = resolve; }); }; flushPendingSettingsSaves = async () => {}; - openOnlineBackupView = () => {}; await loadManagedOnlineBackups(); const initialKeys = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent); const maliciousEntry = { id: 'DDDDDDDDDDDDDDDDDDDDDD', displayKey: 'prefix MHU2-' + 'X'.repeat(70) + ' suffix', createdAt: '2026-08-24T12:00:00.000Z' }; @@ -180,6 +191,28 @@ contextBridge.exposeInMainWorld('api', { status: document.getElementById('onlineBackupStatus').textContent, statusState: document.getElementById('onlineBackupStatus').dataset.state }; + const navigation = document.createElement('button'); + navigation.dataset.settingsPage = 'backup'; + navigation.addEventListener('click', () => { loadManagedOnlineBackups(); }); + fixture.append(navigation); + replaceManagedOnlineBackups([ + { id: 'GGGGGGGGGGGGGGGGGGGGGG', displayKey: 'MHU2-GHJK…7531', createdAt: '2026-08-29T12:00:00.000Z' } + ]); + setOnlineBackupStatus('', ''); + const failedCreate = doOnlineBackupCreate(); + await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length === 5); + await failedCreate; + const statusBeforeStaleNavigationResponse = { + text: document.getElementById('onlineBackupStatus').textContent, + state: document.getElementById('onlineBackupStatus').dataset.state + }; + window.api.releaseManagedOnlineBackupList(); + await new Promise(resolve => setTimeout(resolve, 0)); + const staleNavigationResponse = { + keys: [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent), + status: document.getElementById('onlineBackupStatus').textContent, + statusState: document.getElementById('onlineBackupStatus').dataset.state + }; const calls = await window.api.getManagedOnlineBackupProbeCalls(); return { initialKeys, @@ -197,6 +230,8 @@ contextBridge.exposeInMainWorld('api', { createPostRefreshCount, failedCreatePostRefreshCount, refreshFailure, + statusBeforeStaleNavigationResponse, + staleNavigationResponse, confirmation, calls, secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) @@ -357,6 +392,15 @@ app.whenReady().then(async () => { status: 'Online backups could not be loaded', statusState: 'error' }); + assert.deepEqual(result.onlineBackupBehavior.statusBeforeStaleNavigationResponse, { + text: 'Online backup could not be created', + state: 'error' + }); + assert.deepEqual(result.onlineBackupBehavior.staleNavigationResponse, { + keys: ['MHU2-GHJK…7531'], + status: 'Online backup could not be created', + statusState: 'error' + }); assert.equal(result.onlineBackupBehavior.afterCreate.length, 3); assert.deepEqual(result.onlineBackupBehavior.confirmation, { title: 'Online-Backup löschen', @@ -372,7 +416,9 @@ app.whenReady().then(async () => { ['create', 1], ['list', 3], ['create', 2], - ['list', 4] + ['list', 4], + ['list', 5], + ['create', 3] ]); assert.equal(result.onlineBackupBehavior.secretInBody, false); assert.ok(Math.abs(result.onlineBackupLayout.german.createRight - result.onlineBackupLayout.german.contentRight) <= 1);