Stufe 1 (#764): Werkzeugeinstellungen, Sicherungszeitplan und Import als neue Instanz
- Werkzeugformulare aus den typisierten Einstellungsklassen (L8): Geheimnisse
nur schreibend, nur geaenderte Schluessel werden ersetzt, Rechte bleiben
- Database.allowedTables las ein JSON-Array als rohen Text und haette es beim
naechsten Speichern zerlegt
- Automatische Sicherung der Instanz in der Sicherungsansicht (L10)
- Sicherung hochladen und als neue Instanz einspielen (L11): Pfade der alten
Maschine werden zurueckgesetzt, eine Kopie neben dem Original bekommt eine
eigene ID, Autostart aus; Upload bis 4 GB
- api.js schickte eine Datei als "{}"; Dialoge schliessen beim Seitenwechsel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8552755bb6
commit
d2559c763a
@@ -97,11 +97,17 @@ export async function rendereSicherung() {
|
||||
);
|
||||
}
|
||||
|
||||
layout.append(infoBereich, inhaltBereich, ftpBereich);
|
||||
// #764: the schedule of the active instance, and importing a backup as a new instance.
|
||||
const scheduleArea = erstelleElement("div", { klasse: "sicherung-ftp-bereich" });
|
||||
const importArea = erstelleElement("div", { klasse: "sicherung-ftp-bereich" });
|
||||
|
||||
layout.append(infoBereich, inhaltBereich, scheduleArea, ftpBereich, importArea);
|
||||
container.append(layout);
|
||||
|
||||
// Der FTP-Upload gilt für alle Instanzen und ist deshalb auch ohne Auswahl da.
|
||||
rendereFtp(ftpBereich);
|
||||
renderImport(importArea);
|
||||
if (instanzId) renderSchedule(scheduleArea);
|
||||
|
||||
if (!instanzId) return;
|
||||
|
||||
@@ -505,3 +511,104 @@ async function rendereFtp(bereich) {
|
||||
|
||||
bereich.replaceChildren(erstelleKarte(t("backup.ftpTitle"), [form]));
|
||||
}
|
||||
|
||||
// ─── Automatic backup of the active instance (#764) ───
|
||||
//
|
||||
// The manager makes the daily backup and reads switch, time and retention from the instance
|
||||
// file. That file belongs to the instance process, so it is written through the instance
|
||||
// (/api/settings) — which is why the instance has to be running to change it.
|
||||
|
||||
async function renderSchedule(area) {
|
||||
let settings;
|
||||
try {
|
||||
settings = await api("/settings");
|
||||
} catch {
|
||||
area.replaceChildren(erstelleKarte(t("backup.scheduleTitle"), [
|
||||
erstelleElement("p", { klasse: "hinweis", text: t("backup.scheduleNeedsRunning") })
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
const form = erstelleElement("form", { klasse: "formular" });
|
||||
const enabled = checkbox("backup-auto", t("backup.scheduleEnabled"), settings.autoBackupEnabled);
|
||||
const time = erstelleElement("input", { type: "time", value: settings.autoBackupTime || "03:00" });
|
||||
const keep = erstelleElement("input", { type: "number", min: "0", max: "365", value: String(settings.backupKeepCount ?? 14) });
|
||||
const save = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: t("common.save") });
|
||||
|
||||
form.append(
|
||||
enabled.gruppe,
|
||||
feld(t("backup.scheduleTime"), time),
|
||||
feld(t("backup.scheduleKeep"), keep, t("backup.scheduleKeepHint")),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" }, save));
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
save.disabled = true;
|
||||
try {
|
||||
const result = await api("/settings", {
|
||||
method: "PUT",
|
||||
body: { autoBackupEnabled: enabled.box.checked, autoBackupTime: time.value, backupKeepCount: Number(keep.value) }
|
||||
});
|
||||
zeigeMeldung(result?.message ?? t("common.saved"), "erfolg");
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr");
|
||||
} finally {
|
||||
save.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
area.replaceChildren(erstelleKarte(t("backup.scheduleTitle"), [form]));
|
||||
}
|
||||
|
||||
// ─── Import a backup as a new instance (#764) ───
|
||||
//
|
||||
// The move to a fresh machine: there is no instance yet, so no backup list to pick from.
|
||||
// A backup from another installation needs that installation's license key; the server says
|
||||
// so (code "fremde_lizenz") and the field appears.
|
||||
|
||||
function renderImport(area) {
|
||||
const form = erstelleElement("form", { klasse: "formular" });
|
||||
const file = erstelleElement("input", { type: "file", accept: ".clawdbak,.zip", required: true });
|
||||
const name = erstelleElement("input", { type: "text", required: true, placeholder: t("backup.importNamePlaceholder") });
|
||||
const key = erstelleElement("input", { type: "password", autocomplete: "off", placeholder: t("backup.sourceLicensePlaceholder") });
|
||||
const keyField = feld(t("backup.sourceLicenseLabel"), key, t("backup.sourceLicenseHint"));
|
||||
keyField.hidden = true;
|
||||
const submit = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: t("backup.importButton") });
|
||||
|
||||
form.append(
|
||||
erstelleElement("p", { klasse: "gedaempft", text: t("backup.importHint") }),
|
||||
feld(t("backup.importFile"), file),
|
||||
feld(t("backup.importName"), name),
|
||||
keyField,
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" }, submit));
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const chosen = file.files?.[0];
|
||||
if (!chosen) return;
|
||||
|
||||
const headers = { "Content-Type": "application/octet-stream" };
|
||||
if (key.value.trim()) headers["X-Source-License-Key"] = key.value.trim();
|
||||
|
||||
submit.disabled = true;
|
||||
submit.textContent = t("backup.importing");
|
||||
try {
|
||||
const result = await api(`/verwaltung/backups/import?name=${encodeURIComponent(name.value.trim())}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: chosen
|
||||
});
|
||||
zeigeMeldung(result?.meldung ?? result?.message ?? t("backup.imported"), "erfolg", 8000);
|
||||
form.reset();
|
||||
keyField.hidden = true;
|
||||
} catch (err) {
|
||||
if (err.code === "fremde_lizenz") keyField.hidden = false;
|
||||
zeigeMeldung(t("backup.importFailed", { error: err.message }), "gefahr", 8000);
|
||||
} finally {
|
||||
submit.disabled = false;
|
||||
submit.textContent = t("backup.importButton");
|
||||
}
|
||||
});
|
||||
|
||||
area.replaceChildren(erstelleKarte(t("backup.importTitle"), [form]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user