feat: FritzBox-Sicherung herunterladen + maximale Anzahl je Box einstellbar

- Satellit: fritzbox.backup_get liefert eine vorhandene Sicherung (nur aus
  dem Sicherungsordner, bis 20 MB). Bei ARIA landet sie unter
  /shared/uploads/ und kann in den Chat.
- Zugangsdaten FritzBox: "Maximale Sicherungen" (1–100, leer = 10). Bei
  jeder neuen Sicherung fliegt die aelteste dieser Box raus; die Liste
  meldet die Zahl mit (Kopfzeile im 💾-Fenster).
- Diagnostic: ⬇ je Sicherung laedt die .export-Datei herunter.

Getestet: Satellit (Limit 2 → die zwei neuesten bleiben, Download, Pfad-
schutz), Diagnostic in headless Chromium (Download-Inhalt, Feld laden,
0 abgelehnt, 20 gespeichert).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 18:17:12 +01:00
co-authored by Claude Opus 5.5
parent 0a1e334169
commit c77526e92a
3 changed files with 65 additions and 8 deletions
+31 -4
View File
@@ -330,7 +330,9 @@
<div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;"><input id="dc-fb-user" placeholder="FritzBox-Benutzer (z.B. fritz1234)" style="flex:1;">
<button type="button" id="dc-fb-detect" class="btn secondary" onclick="dcDetectFritzUser()" title="Fragt die FritzBox, welche Benutzer es gibt (geht ohne Anmeldung)" style="padding:2px 8px;font-size:11px;white-space:nowrap;">Benutzer ermitteln</button></div>
<div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;"><input id="dc-fb-pass" type="password" placeholder="FritzBox-Passwort" style="flex:1;"><button type="button" class="btn secondary" onclick="dcEye('dc-fb-pass')" title="Anzeigen / verbergen" style="padding:2px 8px;font-size:12px;">👁</button></div>
<div style="display:flex;gap:4px;align-items:center;"><input id="dc-fb-backuppass" type="password" placeholder="Kennwort für Sicherungen (mind. 12 Zeichen, Aa, 1, !)" title="Damit verschlüsselt ARIA Sicherungen der Box. ARIA nutzt es, kennt es aber nicht — zum Wiederherstellen brauchst du es." style="flex:1;"><button type="button" class="btn secondary" onclick="dcEye('dc-fb-backuppass')" title="Anzeigen / verbergen" style="padding:2px 8px;font-size:12px;">👁</button></div>
<div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;"><input id="dc-fb-backuppass" type="password" placeholder="Kennwort für Sicherungen (mind. 12 Zeichen, Aa, 1, !)" title="Damit verschlüsselt ARIA Sicherungen der Box. ARIA nutzt es, kennt es aber nicht — zum Wiederherstellen brauchst du es." style="flex:1;"><button type="button" class="btn secondary" onclick="dcEye('dc-fb-backuppass')" title="Anzeigen / verbergen" style="padding:2px 8px;font-size:12px;">👁</button></div>
<div style="display:flex;gap:6px;align-items:center;"><label for="dc-fb-keep" style="font-size:11px;color:#8888AA;">Maximale Sicherungen</label>
<input id="dc-fb-keep" type="number" min="1" max="100" placeholder="10" title="So viele Sicherungen dieser Box bleiben auf dem Satelliten — bei jeder neuen fliegt die älteste raus" style="width:70px;"></div>
</div>
<div style="border:1px solid #1E1E2E;border-radius:6px;padding:10px;">
@@ -6351,11 +6353,12 @@
const _fbFmtDate = iso => { try { return new Date(iso).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }); } catch (_) { return iso; } };
const _fbPwSrc = { backupPass: 'Sicherungs-Kennwort', login: 'FritzBox-Kennwort', angegeben: 'eigenes Kennwort' };
async function openFbBackups(sat, ip, name) {
fbB = { sat, ip, name, list: [], busy: '', sf: null };
fbB = { sat, ip, name, list: [], busy: '', sf: null, keep: 0 };
_fbRender('Lade Sicherungen …');
const [r, sf] = await Promise.all([fbCmd('fritzbox.backups', {}), fbCmd('fritzbox.second_factor', {})]);
if (!fbB) return;
fbB.list = (r.ok && r.result && r.result.sicherungen) || [];
fbB.keep = (r.ok && r.result && r.result.behalten) || 0;
fbB.sf = sf.ok ? sf.result : { fehler: sf.error };
_fbRender(r.ok ? '' : '⚠ ' + (r.error || 'Fehler'));
}
@@ -6368,12 +6371,13 @@
<td style="padding:6px;color:#9a9ab5;">${escapeHtml(_fbPwSrc[b.kennwort] || b.kennwort || '')}</td>
<td style="padding:6px;white-space:nowrap;">
<button class="btn secondary" onclick="fbViewBackup(${i})" title="Inhalt ansehen" style="padding:2px 8px;font-size:11px;">ℹ</button>
<button class="btn secondary" onclick="fbDownloadBackup(${i})" title="Herunterladen (.export, verschlüsselt mit dem Sicherungs-Kennwort)" style="padding:2px 8px;font-size:11px;">⬇</button>
<button class="btn secondary" onclick="fbRestoreBackup(${i})" title="Diese Sicherung einspielen" style="padding:2px 8px;font-size:11px;">Wiederherstellen</button>
<button class="btn secondary" onclick="fbDeleteBackup(${i})" title="Löschen" style="padding:2px 8px;font-size:11px;color:#FF6E6E;">🗑</button></td></tr>`).join('');
_fbModal(`<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:8px;">
<h3 style="margin:0;">💾 Sicherungen: ${escapeHtml(fbB.name)} <span style="color:#8888AA;font-size:12px;">(${escapeHtml(fbB.ip)})</span></h3>
<button onclick="closeFbBackups()" style="background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">&times;</button></div>
<p style="color:#8888AA;font-size:11px;margin:0 0 10px;">Liegen auf dem Satelliten, die letzten 10 je Box. Verschlüsselt mit dem hinterlegten Sicherungs-Kennwort (🔑).</p>
<p style="color:#8888AA;font-size:11px;margin:0 0 10px;">Liegen auf dem Satelliten, die letzten ${fbB.keep || 10} je Box (einstellbar unter 🔑). Verschlüsselt mit dem hinterlegten Sicherungs-Kennwort (🔑).</p>
${_fbSfLine()}
<div style="display:flex;gap:8px;margin-bottom:10px;">
<button class="btn" onclick="fbCreateBackup()" ${fbB.busy ? 'disabled' : ''}>Jetzt sichern</button>
@@ -6468,6 +6472,23 @@
const r = await _fbRun('Wiederherstellen läuft', 'fritzbox.restore', { file: b.datei, confirmed: true });
if (fbB) _fbShowResult(r, 'Sicherung eingespielt');
}
async function fbDownloadBackup(i) {
const b = fbB.list[i];
if (!b) return;
_fbRender('Lade ' + b.datei + ' …');
const r = await fbCmd('fritzbox.backup_get', { file: b.datei }, 120000);
if (!fbB) return;
if (!r.ok || !r.result || !r.result.file_b64) { _fbRender('⚠ ' + (r.error || 'Download fehlgeschlagen')); return; }
const bin = atob(r.result.file_b64);
const bytes = new Uint8Array(bin.length);
for (let k = 0; k < bin.length; k++) bytes[k] = bin.charCodeAt(k);
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/octet-stream' }));
const a = document.createElement('a');
a.href = url; a.download = r.result.filename || b.datei;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 10000);
_fbRender('⬇ ' + (r.result.filename || b.datei) + ' heruntergeladen.');
}
async function fbDeleteBackup(i) {
const b = fbB.list[i];
if (!b || !await uiConfirm(`Sicherung vom ${_fbFmtDate(b.erstellt)} löschen?\n\nSie ist danach endgültig weg.`)) return;
@@ -6528,7 +6549,7 @@
credsModalSat = satId; credsModalIp = ip;
dcStored = {}; dcRevealed = false; dcFilled = false; dcFbReq = '';
_dcEl('dev-creds-title').textContent = (name || ip) + ' (' + ip + ')';
['dc-snmp-community','dc-v3-user','dc-v3-authkey','dc-v3-privkey','dc-http-user','dc-http-pass','dc-fb-user','dc-fb-pass','dc-fb-backuppass','dc-ssh-user','dc-ssh-port','dc-ssh-pass','dc-ssh-key'].forEach(id => {
['dc-snmp-community','dc-v3-user','dc-v3-authkey','dc-v3-privkey','dc-http-user','dc-http-pass','dc-fb-user','dc-fb-pass','dc-fb-backuppass','dc-fb-keep','dc-ssh-user','dc-ssh-port','dc-ssh-pass','dc-ssh-key'].forEach(id => {
const el = _dcEl(id);
el.value = '';
if (el.dataset.ph == null) el.dataset.ph = el.placeholder;
@@ -6555,6 +6576,7 @@
if (v3.privProto) _dcEl('dc-v3-privproto').value = v3.privProto;
_dcEl('dc-http-user').value = (detail.http || {}).user || '';
_dcEl('dc-fb-user').value = (detail.fritzbox || {}).user || '';
_dcEl('dc-fb-keep').value = (detail.fritzbox || {}).backupKeep || '';
_dcEl('dc-ssh-user').value = (detail.ssh || {}).user || '';
_dcEl('dc-ssh-port').value = (detail.ssh || {}).port || '';
}
@@ -6638,8 +6660,13 @@
if (!/[^A-Za-z0-9]/.test(fbp)) miss.push('ein Sonderzeichen');
if (miss.length) { document.getElementById('dc-status').textContent = '⚠ Sicherungs-Kennwort zu schwach — es fehlt: ' + miss.join(', ') + '.'; return; }
}
const fkeep = parseInt(document.getElementById('dc-fb-keep').value, 10);
if (document.getElementById('dc-fb-keep').value && !(fkeep >= 1 && fkeep <= 100)) {
document.getElementById('dc-status').textContent = '⚠ Maximale Sicherungen: eine Zahl von 1 bis 100.'; return;
}
if (fu || fp || fbp || dcStored['dc-fb-pass'] || dcStored['dc-fb-backuppass']) {
creds.fritzbox = { user: fu };
if (fkeep) creds.fritzbox.backupKeep = fkeep;
if (fp) creds.fritzbox.pass = fp;
if (fbp) creds.fritzbox.backupPass = fbp;
}