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:
+43
-23
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user