ShineBridge v1.11.1 — Feature: Backup-Verwaltung im Einstellungen-Tab
Neue Sektion 'Automatische Backups': - Aufbewahrung (Tage) konfigurierbar statt hartcodierter 14 Tage (config_backup_retention_days, Teil der normalen Config-Persistenz) - Liste aller vorhandenen Backups (Datum, Größe), neueste zuerst - Download-Button pro Backup - Wiederherstellen-Button pro Backup (mit Bestätigungsdialog, da überschreibend) Backend: Validierung+Anwendung aus /api/import-config in _apply_import_data() extrahiert, von Datei-Upload und neuem Backup-Restore-Endpunkt gemeinsam genutzt. Neue Endpunkte GET /api/config-backups, GET .../download, POST .../restore — mit Path-Traversal-Schutz (_safe_backup_filename, nur Dateinamen aus dem Backup-Ordner selbst). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -321,6 +321,15 @@
|
||||
</div>
|
||||
<div id="import-result" style="margin-top:.5rem;font-size:.85rem"></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>Automatische Backups</h3>
|
||||
<p style="color:var(--text-dim);font-size:.85rem;margin:0 0 .75rem">Täglich automatisch nach <code style="font-size:11px;background:var(--surface2);padding:1px 5px;border-radius:3px">/share/shinebridge-backups/</code> gesichert — übersteht auch einen Add-on-Uninstall+Reinstall.</p>
|
||||
<div class="field" style="max-width:200px"><label>Aufbewahrung (Tage)</label>
|
||||
<input type="number" id="cfg-backup-retention" min="1" max="365" value="14"></div>
|
||||
<button class="btn btn-primary" onclick="saveBackupRetention()">Speichern</button>
|
||||
<div id="backup-list" style="margin-top:1rem;display:flex;flex-direction:column;gap:6px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flash/OTA-Panel ausgelagert nach growatt-diagnose-tool (v1.9.0) -->
|
||||
@@ -477,6 +486,7 @@ function switchTab(name) {
|
||||
});
|
||||
if (name === "energy" || name === "live") startRefresh(); else stopRefresh();
|
||||
if (name === "finance") loadFinance();
|
||||
if (name === "settings") loadBackupList();
|
||||
}
|
||||
|
||||
// ── Energy Dashboard ──────────────────────────────────────────
|
||||
@@ -1227,6 +1237,7 @@ async function loadSettings() {
|
||||
document.getElementById("billing-tracker-fields").style.display = trackerEnabled ? "" : "none";
|
||||
document.getElementById("cfg-monthly-rate").value = cfg.monthly_rate_eur ?? 0;
|
||||
document.getElementById("cfg-grundpreis").value = cfg.grundpreis_eur_per_month ?? 0;
|
||||
document.getElementById("cfg-backup-retention").value = cfg.config_backup_retention_days ?? 14;
|
||||
}
|
||||
|
||||
async function savePrices() {
|
||||
@@ -1304,6 +1315,61 @@ async function importConfig(input) {
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
// ── Automatische Backups ─────────────────────────────────────
|
||||
|
||||
async function saveBackupRetention() {
|
||||
const days = parseInt(document.getElementById("cfg-backup-retention").value) || 14;
|
||||
try {
|
||||
await fetchJSON(api("api/config"), {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ config_backup_retention_days: days }),
|
||||
});
|
||||
showToast("Gespeichert!", "ok");
|
||||
} catch(e) { showToast("Fehler beim Speichern", "err"); }
|
||||
}
|
||||
|
||||
function fBytes(n) {
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024*1024) return `${(n/1024).toFixed(1)} KB`;
|
||||
return `${(n/1024/1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
async function loadBackupList() {
|
||||
const el = document.getElementById("backup-list");
|
||||
let data;
|
||||
try { data = await fetchJSON(api("api/config-backups")); }
|
||||
catch(e) { el.innerHTML = '<div class="no-data">Fehler beim Laden</div>'; return; }
|
||||
|
||||
const backups = data.backups || [];
|
||||
if (backups.length === 0) {
|
||||
el.innerHTML = `<div style="font-size:12px;color:var(--text-dim)">Noch keine Backups — das erste entsteht beim nächsten Add-on-Start oder spätestens morgen.</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = backups.map(b => {
|
||||
const date = new Date(b.modified_at * 1000).toLocaleString('de-DE');
|
||||
return `<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:13px">
|
||||
<div><strong>${esc(b.filename)}</strong><br><span style="color:var(--text-dim);font-size:11px">${date} · ${fBytes(b.size_bytes)}</span></div>
|
||||
<div style="display:flex;gap:6px;flex-shrink:0">
|
||||
<button class="btn" style="padding:4px 10px;font-size:12px" onclick="downloadBackup('${esc(b.filename)}')">Download</button>
|
||||
<button class="btn btn-danger" style="padding:4px 10px;font-size:12px" onclick="restoreBackup('${esc(b.filename)}')">Wiederherstellen</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function downloadBackup(filename) {
|
||||
window.location.href = api(`api/config-backups/${encodeURIComponent(filename)}/download`);
|
||||
}
|
||||
|
||||
async function restoreBackup(filename) {
|
||||
if (!confirm(`"${filename}" wiederherstellen? Das überschreibt die aktuelle Konfiguration (Geräte, MQTT) komplett.`)) return;
|
||||
try {
|
||||
const res = await fetchJSON(api(`api/config-backups/${encodeURIComponent(filename)}/restore`), { method: "POST" });
|
||||
showToast(`${res.inverters} Gerät(e) wiederhergestellt`, "ok");
|
||||
setTimeout(() => { loadSettings(); loadInverters(); }, 3000);
|
||||
} catch(e) { showToast("Wiederherstellen fehlgeschlagen", "err"); }
|
||||
}
|
||||
|
||||
// ── Surplus Devices ───────────────────────────────────────────
|
||||
|
||||
let surplusDevices = [];
|
||||
|
||||
Reference in New Issue
Block a user