Add complete device and kiosk backup

This commit is contained in:
Mikei386
2026-07-22 11:58:58 +02:00
parent 890a47a272
commit 22444ff5bd
13 changed files with 767 additions and 18 deletions
+92
View File
@@ -0,0 +1,92 @@
const BACKUP_KIND = 'phoenix-device-backup';
const BACKUP_SCHEMA_VERSION = 1;
function setStatus(element, message, isError = false) {
if (!element) return;
element.textContent = message;
element.style.color = isError ? '#ff6b6b' : '';
}
function backupFileName() {
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
return `phoenix-device-backup-${stamp}.json`;
}
function validateBackup(payload) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('Die Datei enthält kein gültiges JSON-Objekt.');
}
if (payload.kind !== BACKUP_KIND) {
throw new Error('Die Datei ist keine Phoenix-Gerätesicherung.');
}
if (Number(payload.schemaVersion) !== BACKUP_SCHEMA_VERSION) {
throw new Error(`Nicht unterstützte Backup-Version: ${payload.schemaVersion ?? '?'}.`);
}
if (!payload.globalConfig || !payload.rtaConfig) {
throw new Error('Gerätekonfiguration oder RTA-Konfiguration fehlt.');
}
return payload;
}
export function setupDeviceBackup({ exportButton, importInput, status, resolveApiBaseUrl }) {
if (!exportButton || !importInput || typeof resolveApiBaseUrl !== 'function') return;
exportButton.addEventListener('click', async () => {
const original = exportButton.textContent;
exportButton.disabled = true;
exportButton.textContent = 'Sichere…';
setStatus(status, 'Gerätekonfiguration wird gelesen…');
try {
const response = await fetch(`${resolveApiBaseUrl()}/api/v1/device-backup?_=${Date.now()}`, {
cache: 'no-store',
});
const payload = await response.json().catch(() => null);
if (!response.ok) throw new Error(payload?.error || `HTTP ${response.status}`);
validateBackup(payload);
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = backupFileName();
link.click();
URL.revokeObjectURL(link.href);
const kioskCount = Object.keys(payload.kioskBrowserStorage || {}).length;
setStatus(status, `Backup vollständig heruntergeladen (${kioskCount} Kiosk-Einstellungen).`);
} catch (error) {
console.error('Device backup export error:', error);
setStatus(status, `Backup fehlgeschlagen: ${error?.message || String(error)}`, true);
} finally {
exportButton.disabled = false;
exportButton.textContent = original;
}
});
importInput.addEventListener('change', async () => {
const file = importInput.files?.[0];
if (!file) return;
try {
const payload = validateBackup(JSON.parse(await file.text()));
const confirmed = globalThis.confirm?.(
'Gerätesicherung wiederherstellen?\n\nGlobale Geräteeinstellungen, RTA, Software-Presets, Layout-Slots und die Oberfläche des internen Pi-Displays werden ersetzt. Externe Browser bleiben unverändert.'
);
if (!confirmed) return;
setStatus(status, 'Backup wird geprüft und wiederhergestellt…');
const response = await fetch(`${resolveApiBaseUrl()}/api/v1/device-backup`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
cache: 'no-store',
body: JSON.stringify(payload),
});
const result = await response.json().catch(() => null);
if (!response.ok || result?.ok === false) {
throw new Error(result?.error || `HTTP ${response.status}`);
}
setStatus(status, 'Wiederherstellung abgeschlossen. Oberfläche wird neu geladen.');
setTimeout(() => globalThis.location?.reload?.(), 500);
} catch (error) {
console.error('Device backup restore error:', error);
setStatus(status, `Wiederherstellung fehlgeschlagen: ${error?.message || String(error)}`, true);
} finally {
importInput.value = '';
}
});
}