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.
This commit is contained in:
Sucukdeluxe
2026-08-22 11:53:46 +02:00
parent f2c6630866
commit 5ebeea3220
2 changed files with 91 additions and 25 deletions
+43 -23
View File
@@ -65,6 +65,7 @@ let healthCheckRunning = false;
let managedOnlineBackups = []; let managedOnlineBackups = [];
let managedOnlineBackupsAuthoritative = false; let managedOnlineBackupsAuthoritative = false;
let managedOnlineBackupLoadGeneration = 0; let managedOnlineBackupLoadGeneration = 0;
let onlineBackupStatusContextGeneration = 0;
const managedOnlineBackupOperationQueues = new Map(); const managedOnlineBackupOperationQueues = new Map();
let _rLongTasks = 0, _rLongTaskMax = 0, _rFrameLast = 0, _rFrameWorst = 0, _rFrameCount = 0, _rFrameJank = 0, _rPerfLastLog = 0, _rPerfWindowStart = 0; 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); 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'); 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.textContent = String(message || '').replace(/MHU2-[A-Za-z0-9_-]{70}/gu, localizeUiText('Geschützter Schlüssel'));
status.dataset.state = state; status.dataset.state = state;
return true;
} }
function syncOnlineBackupRestoreButton(busy = false) { function syncOnlineBackupRestoreButton(busy = false) {
@@ -2724,23 +2737,26 @@ function renderManagedOnlineBackups() {
list.replaceChildren(content); list.replaceChildren(content);
} }
async function loadManagedOnlineBackups() { async function loadManagedOnlineBackups({ statusContext = null } = {}) {
const generation = ++managedOnlineBackupLoadGeneration; 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 { try {
const result = await window.api.listManagedOnlineBackups(); const result = await window.api.listManagedOnlineBackups();
if (generation !== managedOnlineBackupLoadGeneration) return false; if (generation !== managedOnlineBackupLoadGeneration) return false;
if (!result?.ok || !Array.isArray(result.entries)) { if (!result?.ok || !Array.isArray(result.entries)) {
invalidateManagedOnlineBackups(); invalidateManagedOnlineBackups();
setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error'); if (mayUpdateStatus) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error', loaderStatusContext);
return false; return false;
} }
replaceManagedOnlineBackups(result.entries); replaceManagedOnlineBackups(result.entries);
setOnlineBackupStatus('', ''); if (mayUpdateStatus) setOnlineBackupStatus('', '', loaderStatusContext);
return true; return true;
} catch { } catch {
if (generation === managedOnlineBackupLoadGeneration) { if (generation === managedOnlineBackupLoadGeneration) {
invalidateManagedOnlineBackups(); invalidateManagedOnlineBackups();
setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error'); if (mayUpdateStatus) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error', loaderStatusContext);
} }
return false; return false;
} }
@@ -2751,8 +2767,9 @@ function hasManagedOnlineBackup(id) {
} }
function enqueueManagedOnlineBackupOperation(id, action, operation) { function enqueueManagedOnlineBackupOperation(id, action, operation) {
const statusContext = beginManagedOnlineBackupOperation();
const previous = managedOnlineBackupOperationQueues.get(id)?.promise || Promise.resolve(); 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 }; const state = { action, promise };
managedOnlineBackupOperationQueues.set(id, state); managedOnlineBackupOperationQueues.set(id, state);
renderManagedOnlineBackups(); renderManagedOnlineBackups();
@@ -2766,16 +2783,16 @@ function enqueueManagedOnlineBackupOperation(id, action, operation) {
async function copyManagedOnlineBackup(entry) { async function copyManagedOnlineBackup(entry) {
const id = entry.id; const id = entry.id;
if (!hasManagedOnlineBackup(id) || managedOnlineBackupOperationQueues.has(id)) return; if (!hasManagedOnlineBackup(id) || managedOnlineBackupOperationQueues.has(id)) return;
await enqueueManagedOnlineBackupOperation(id, 'copy', async () => { await enqueueManagedOnlineBackupOperation(id, 'copy', async (statusContext) => {
try { try {
const result = await window.api.copyManagedOnlineBackup(id); const result = await window.api.copyManagedOnlineBackup(id);
if (!result?.ok) { if (!result?.ok) {
setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error'); setOnlineBackupStatus('Online-Sicherung konnte nicht kopiert werden', 'error', statusContext);
return; return;
} }
showCopyToast('Online-Schlüssel kopiert'); showCopyToast('Online-Schlüssel kopiert');
} catch { } 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 (!confirmed) return;
if (!hasManagedOnlineBackup(id)) { 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; return;
} }
if (managedOnlineBackupOperationQueues.get(id)?.action === 'delete') { if (managedOnlineBackupOperationQueues.get(id)?.action === 'delete') {
setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error');
return; return;
} }
await enqueueManagedOnlineBackupOperation(id, 'delete', async () => { await enqueueManagedOnlineBackupOperation(id, 'delete', async (statusContext) => {
if (!hasManagedOnlineBackup(id)) { if (!hasManagedOnlineBackup(id)) {
setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error'); setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error', statusContext);
return; return;
} }
try { try {
const result = await window.api.deleteManagedOnlineBackup(id); const result = await window.api.deleteManagedOnlineBackup(id);
if (!result?.ok || result.removedId !== 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; return;
} }
if (!await loadManagedOnlineBackups()) return; if (!await loadManagedOnlineBackups({ statusContext })) return;
setOnlineBackupStatus('Schlüssel gelöscht', 'success'); setOnlineBackupStatus('Schlüssel gelöscht', 'success', statusContext);
} catch { } 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(); openOnlineBackupView();
} catch { } catch {
openOnlineBackupView(); openOnlineBackupView();
setOnlineBackupStatus('Nicht alle Einstellungen konnten gespeichert werden', 'error'); const statusContext = beginManagedOnlineBackupOperation();
setOnlineBackupStatus('Nicht alle Einstellungen konnten gespeichert werden', 'error', statusContext);
doOnlineBackupCreate.busy = false; doOnlineBackupCreate.busy = false;
return; return;
} }
const statusContext = beginManagedOnlineBackupOperation();
const createButton = document.getElementById('createOnlineBackupBtn'); const createButton = document.getElementById('createOnlineBackupBtn');
if (createButton) createButton.disabled = true; if (createButton) createButton.disabled = true;
setOnlineBackupStatus('Verschlüssele und speichere Einstellungen…', 'busy'); setOnlineBackupStatus('Verschlüssele und speichere Einstellungen…', 'busy', statusContext);
try { try {
const result = await window.api.createManagedOnlineBackup(); const result = await window.api.createManagedOnlineBackup();
if (!result?.ok) { if (!result?.ok) {
setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error'); setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error', statusContext);
return; return;
} }
if (!await loadManagedOnlineBackups()) return; if (!await loadManagedOnlineBackups({ statusContext })) return;
setOnlineBackupStatus('Neuer Schlüssel erstellt. Ältere Schlüssel bleiben gültig.', 'success'); setOnlineBackupStatus('Neuer Schlüssel erstellt. Ältere Schlüssel bleiben gültig.', 'success', statusContext);
showCopyToast('Online-Schlüssel erstellt'); showCopyToast('Online-Schlüssel erstellt');
} catch { } catch {
setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error'); setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error', statusContext);
} finally { } finally {
if (createButton?.isConnected) createButton.disabled = false; if (createButton?.isConnected) createButton.disabled = false;
doOnlineBackupCreate.busy = false; doOnlineBackupCreate.busy = false;
+48 -2
View File
@@ -81,6 +81,7 @@ const managedOnlineBackupListResponses = [
]; ];
let managedOnlineBackupListIndex = 0; let managedOnlineBackupListIndex = 0;
let managedOnlineBackupCreateIndex = 0; let managedOnlineBackupCreateIndex = 0;
let pendingManagedOnlineBackupList = null;
let pendingManagedOnlineBackupCopy = null; let pendingManagedOnlineBackupCopy = null;
let pendingManagedOnlineBackupDelete = null; let pendingManagedOnlineBackupDelete = null;
contextBridge.exposeInMainWorld('api', { contextBridge.exposeInMainWorld('api', {
@@ -91,11 +92,22 @@ contextBridge.exposeInMainWorld('api', {
listManagedOnlineBackups() { listManagedOnlineBackups() {
managedOnlineBackupListIndex++; managedOnlineBackupListIndex++;
managedOnlineBackupProbeCalls.push(['list', managedOnlineBackupListIndex]); managedOnlineBackupProbeCalls.push(['list', managedOnlineBackupListIndex]);
if (managedOnlineBackupListIndex === 5) {
return new Promise(resolve => { pendingManagedOnlineBackupList = resolve; });
}
return Promise.resolve(managedOnlineBackupListResponses[managedOnlineBackupListIndex - 1]); 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() { createManagedOnlineBackup() {
managedOnlineBackupCreateIndex++; managedOnlineBackupCreateIndex++;
managedOnlineBackupProbeCalls.push(['create', managedOnlineBackupCreateIndex]); managedOnlineBackupProbeCalls.push(['create', managedOnlineBackupCreateIndex]);
if (managedOnlineBackupCreateIndex === 3) return Promise.resolve({ ok: false, error: 'Online-Sicherung konnte nicht erstellt werden' });
const entry = managedOnlineBackupCreateIndex === 1 const entry = managedOnlineBackupCreateIndex === 1
? { id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' } ? { 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' }; : { 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; }); return new Promise(resolve => { resolveConfirmation = resolve; });
}; };
flushPendingSettingsSaves = async () => {}; flushPendingSettingsSaves = async () => {};
openOnlineBackupView = () => {};
await loadManagedOnlineBackups(); await loadManagedOnlineBackups();
const initialKeys = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent); 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' }; 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, status: document.getElementById('onlineBackupStatus').textContent,
statusState: document.getElementById('onlineBackupStatus').dataset.state 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(); const calls = await window.api.getManagedOnlineBackupProbeCalls();
return { return {
initialKeys, initialKeys,
@@ -197,6 +230,8 @@ contextBridge.exposeInMainWorld('api', {
createPostRefreshCount, createPostRefreshCount,
failedCreatePostRefreshCount, failedCreatePostRefreshCount,
refreshFailure, refreshFailure,
statusBeforeStaleNavigationResponse,
staleNavigationResponse,
confirmation, confirmation,
calls, calls,
secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) 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', status: 'Online backups could not be loaded',
statusState: 'error' 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.equal(result.onlineBackupBehavior.afterCreate.length, 3);
assert.deepEqual(result.onlineBackupBehavior.confirmation, { assert.deepEqual(result.onlineBackupBehavior.confirmation, {
title: 'Online-Backup löschen', title: 'Online-Backup löschen',
@@ -372,7 +416,9 @@ app.whenReady().then(async () => {
['create', 1], ['create', 1],
['list', 3], ['list', 3],
['create', 2], ['create', 2],
['list', 4] ['list', 4],
['list', 5],
['create', 3]
]); ]);
assert.equal(result.onlineBackupBehavior.secretInBody, false); assert.equal(result.onlineBackupBehavior.secretInBody, false);
assert.ok(Math.abs(result.onlineBackupLayout.german.createRight - result.onlineBackupLayout.german.contentRight) <= 1); assert.ok(Math.abs(result.onlineBackupLayout.german.createRight - result.onlineBackupLayout.german.contentRight) <= 1);