i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)
Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
69718122f3
commit
ad71682c2d
@@ -7,6 +7,7 @@
|
||||
// Strikt DOM API, kein innerHTML.
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
@@ -29,11 +30,11 @@ function formatiereBytes(bytes) {
|
||||
|
||||
/** Was eine Sicherung schützt — als Plakette. */
|
||||
function schutzPlakette(b) {
|
||||
if (b.fremdeLizenz) return erstellePlakette("Fremde Lizenz", "warnung");
|
||||
if (b.secrets === "Unlesbar") return erstellePlakette("Unlesbar", "gefahr");
|
||||
if (b.verschluesselt) return erstellePlakette(`Lizenz (${b.secretCount} Zugangsdaten)`, "erfolg");
|
||||
if (b.secrets === "Passphrase") return erstellePlakette(`Passphrase (${b.secretCount})`, "neutral");
|
||||
return erstellePlakette("Unverschlüsselt, ohne Zugangsdaten", "neutral");
|
||||
if (b.fremdeLizenz) return erstellePlakette(t("backup.foreignLicense"), "warnung");
|
||||
if (b.secrets === "Unlesbar") return erstellePlakette(t("backup.unreadable"), "gefahr");
|
||||
if (b.verschluesselt) return erstellePlakette(t("backup.licenseProtection", { count: b.secretCount }), "erfolg");
|
||||
if (b.secrets === "Passphrase") return erstellePlakette(t("backup.passphraseProtection", { count: b.secretCount }), "neutral");
|
||||
return erstellePlakette(t("backup.unencryptedProtection"), "neutral");
|
||||
}
|
||||
|
||||
function checkbox(id, text, an) {
|
||||
@@ -63,20 +64,20 @@ export async function rendereSicherung() {
|
||||
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
kopf.append(
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" },
|
||||
erstelleElement("h1", { text: "Datensicherung & Wiederherstellung" })),
|
||||
erstelleElement("h1", { text: t("backup.title") })),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" },
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Sicherungen neu laden",
|
||||
title: t("backup.refreshTooltip"),
|
||||
disabled: !instanzId,
|
||||
onclick: () => ladeSicherungen()
|
||||
}, erstelleIcon(PFADE.aktualisieren, 16), erstelleElement("span", { text: " Aktualisieren" })),
|
||||
}, erstelleIcon(PFADE.aktualisieren, 16), erstelleElement("span", { text: ` ${t("backup.refresh")}` })),
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche primaer",
|
||||
title: "Neue Sicherung erstellen",
|
||||
title: t("backup.newBackupTooltip"),
|
||||
disabled: !instanzId,
|
||||
onclick: () => oeffneSicherungsDialog()
|
||||
}, erstelleIcon(PFADE.plus, 16), erstelleElement("span", { text: " Neue Sicherung" }))
|
||||
}, erstelleIcon(PFADE.plus, 16), erstelleElement("span", { text: ` ${t("backup.newBackup")}` }))
|
||||
)
|
||||
);
|
||||
layout.append(kopf);
|
||||
@@ -87,10 +88,10 @@ export async function rendereSicherung() {
|
||||
|
||||
if (!instanzId) {
|
||||
inhaltBereich.append(
|
||||
erstelleKarte("Keine Instanz ausgewählt", [
|
||||
erstelleKarte(t("backup.noInstance"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Sicherungen zu verwalten."
|
||||
text: t("backup.selectInstancePrompt")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -107,7 +108,7 @@ export async function rendereSicherung() {
|
||||
// ─── Liste ───
|
||||
async function ladeSicherungen() {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Sicherungen…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("backup.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -115,12 +116,10 @@ export async function rendereSicherung() {
|
||||
const backups = daten?.backups ?? [];
|
||||
|
||||
infoBereich.replaceChildren(
|
||||
erstelleKarte("Sicherungsverzeichnis", [
|
||||
erstelleKarte(t("backup.backupDir"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Sicherungen werden als Ganzes mit dem Lizenzschlüssel des Instanzmanagers "
|
||||
+ "verschlüsselt und enthalten die Zugangsdaten. Auf einer anderen Maschine lassen "
|
||||
+ "sie sich mit demselben Lizenzschlüssel zurückspielen."
|
||||
text: t("backup.backupDirHint")
|
||||
}),
|
||||
erstelleElement("div", { klasse: "ordner-pfad-anzeige" },
|
||||
erstelleIcon(PFADE.server, 14),
|
||||
@@ -130,10 +129,10 @@ export async function rendereSicherung() {
|
||||
|
||||
if (backups.length === 0) {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleKarte("Vorhandene Sicherungen", [
|
||||
erstelleKarte(t("backup.existingBackups", { count: 0 }), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Für diese Instanz gibt es noch keine Sicherung. Lege über die Schaltfläche oben eine an."
|
||||
text: t("backup.noBackups")
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -146,13 +145,13 @@ export async function rendereSicherung() {
|
||||
const aktionen = erstelleElement("td", { klasse: "spalte-aktionen" },
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche klein sekundär",
|
||||
title: "Diese Sicherung wiederherstellen",
|
||||
title: t("backup.restoreTooltip"),
|
||||
disabled: b.secrets === "Unlesbar",
|
||||
onclick: () => oeffneRestoreDialog(b)
|
||||
}, erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: " Wiederherstellen" })),
|
||||
}, erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: ` ${t("backup.restore")}` })),
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche klein gefahr flach",
|
||||
title: "Sicherungsdatei unwiderruflich löschen",
|
||||
title: t("backup.deleteTooltip"),
|
||||
onclick: () => oeffneLoeschenDialog(b.fileName)
|
||||
}, erstelleIcon(PFADE.muelleimer, 14))
|
||||
);
|
||||
@@ -163,7 +162,7 @@ export async function rendereSicherung() {
|
||||
erstelleElement("td", { text: formatiereDatumZeit(b.createdAt) }),
|
||||
erstelleElement("td", { text: formatiereBytes(b.sizeBytes) }),
|
||||
erstelleElement("td", {}, schutzPlakette(b)),
|
||||
erstelleElement("td", { text: b.fileCount > 0 ? `${b.fileCount} Dateien` : "—" }),
|
||||
erstelleElement("td", { text: b.fileCount > 0 ? t("backup.fileCount", { count: b.fileCount }) : "—" }),
|
||||
aktionen
|
||||
));
|
||||
}
|
||||
@@ -171,15 +170,15 @@ export async function rendereSicherung() {
|
||||
const tabelle = erstelleElement("table", { klasse: "tabelle sicherungen-tabelle" },
|
||||
erstelleElement("thead", {},
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("th", { text: "Archivdatei" }),
|
||||
erstelleElement("th", { text: "Erstellt am" }),
|
||||
erstelleElement("th", { text: "Größe" }),
|
||||
erstelleElement("th", { text: "Schutz" }),
|
||||
erstelleElement("th", { text: "Dateien" }),
|
||||
erstelleElement("th", { klasse: "spalte-aktionen", text: "Aktionen" }))),
|
||||
erstelleElement("th", { text: t("backup.archiveFile") }),
|
||||
erstelleElement("th", { text: t("backup.createdAt") }),
|
||||
erstelleElement("th", { text: t("backup.size") }),
|
||||
erstelleElement("th", { text: t("backup.protection") }),
|
||||
erstelleElement("th", { text: t("backup.files") }),
|
||||
erstelleElement("th", { klasse: "spalte-aktionen", text: t("backup.actions") }))),
|
||||
tbody);
|
||||
|
||||
inhaltBereich.replaceChildren(erstelleKarte(`Vorhandene Sicherungen (${backups.length})`, [tabelle]));
|
||||
inhaltBereich.replaceChildren(erstelleKarte(t("backup.existingBackups", { count: backups.length }), [tabelle]));
|
||||
} catch (err) {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleKarte("Fehler", [
|
||||
@@ -192,14 +191,13 @@ export async function rendereSicherung() {
|
||||
// ─── Neue Sicherung ───
|
||||
function oeffneSicherungsDialog() {
|
||||
const form = erstelleElement("form", { klasse: "formular" });
|
||||
const chats = checkbox("backup-chk-chats", "Chatverläufe sichern", true);
|
||||
const logs = checkbox("backup-chk-logs", "Protokolldateien (Logs) einschließen", false);
|
||||
const chats = checkbox("backup-chk-chats", t("backup.includeChats"), true);
|
||||
const logs = checkbox("backup-chk-logs", t("backup.includeLogs"), false);
|
||||
|
||||
form.append(
|
||||
erstelleElement("p", {
|
||||
klasse: "form-hilfe",
|
||||
text: "Die Sicherung wird mit dem Lizenzschlüssel des Instanzmanagers verschlüsselt und "
|
||||
+ "enthält die Zugangsdaten. Ist ein FTP-Upload eingerichtet, wird sie anschließend hochgeladen."
|
||||
text: t("backup.dialogHint")
|
||||
}),
|
||||
chats.gruppe,
|
||||
logs.gruppe
|
||||
@@ -207,13 +205,13 @@ export async function rendereSicherung() {
|
||||
|
||||
let modal;
|
||||
const speichernBtn = erstelleElement("button", {
|
||||
type: "submit", klasse: "schaltflaeche primaer", text: "Sicherung erstellen"
|
||||
type: "submit", klasse: "schaltflaeche primaer", text: t("backup.createButton")
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
speichernBtn.disabled = true;
|
||||
speichernBtn.textContent = "Erstelle Sicherung…";
|
||||
speichernBtn.textContent = t("backup.creating");
|
||||
|
||||
try {
|
||||
const res = await api(`/verwaltung/instanzen/${pfadInstanz}/backups`, {
|
||||
@@ -222,19 +220,19 @@ export async function rendereSicherung() {
|
||||
});
|
||||
// Ein gescheiterter Upload steht in der Meldung; die Sicherung liegt trotzdem lokal.
|
||||
const warnt = (res.meldung || "").includes("gescheitert");
|
||||
zeigeMeldung(res.meldung || "Sicherung erstellt.", warnt ? "warnung" : "erfolg", warnt ? 8000 : undefined);
|
||||
zeigeMeldung(res.meldung || t("backup.created"), warnt ? "warnung" : "erfolg", warnt ? 8000 : undefined);
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Sichern: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.createFailed", { error: err.message }), "gefahr");
|
||||
speichernBtn.disabled = false;
|
||||
speichernBtn.textContent = "Sicherung erstellen";
|
||||
speichernBtn.textContent = t("backup.createButton");
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal("Neue Sicherung erstellen", form, [
|
||||
modal = erstelleModal(t("backup.dialogTitle"), form, [
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
|
||||
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
|
||||
}),
|
||||
speichernBtn
|
||||
]);
|
||||
@@ -255,26 +253,26 @@ export async function rendereSicherung() {
|
||||
form.append(erstelleElement("div", { klasse: "hinweis-box warnung" },
|
||||
erstelleIcon(PFADE.ausrufezeichen, 18),
|
||||
erstelleElement("div", {},
|
||||
erstelleElement("strong", { text: "Achtung: Überschreiben bestehender Daten" }),
|
||||
erstelleElement("strong", { text: t("backup.overwriteWarningTitle") }),
|
||||
erstelleElement("p", {
|
||||
text: `Die Wiederherstellung überschreibt die aktuellen Konfigurationen und Daten der Instanz mit dem Stand vom ${formatiereDatumZeit(backup.createdAt)}.`
|
||||
text: t("backup.overwriteWarningText", { date: formatiereDatumZeit(backup.createdAt) })
|
||||
}),
|
||||
erstelleElement("p", {
|
||||
text: "Die Instanz wird dafür geordnet angehalten und danach wieder gestartet."
|
||||
text: t("backup.instanceStopNotice")
|
||||
}))));
|
||||
|
||||
// Nur ältere, unverschlüsselte Sicherungen mit Passphrase brauchen sie noch.
|
||||
let passInput = null;
|
||||
if (backup.secrets === "Passphrase" && !backup.verschluesselt) {
|
||||
passInput = erstelleElement("input", {
|
||||
type: "password", klasse: "eingabe", placeholder: "Passphrase der Sicherung…", required: true
|
||||
type: "password", klasse: "eingabe", placeholder: t("backup.passphrasePlaceholder"), required: true
|
||||
});
|
||||
form.append(feld("Passphrase für Zugangsdaten", passInput));
|
||||
form.append(feld(t("backup.passphraseLabel"), passInput));
|
||||
}
|
||||
|
||||
let modal;
|
||||
const restoreBtn = erstelleElement("button", {
|
||||
type: "submit", klasse: "schaltflaeche warnung", text: "Jetzt wiederherstellen"
|
||||
type: "submit", klasse: "schaltflaeche warnung", text: t("backup.restoreNow")
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
@@ -282,36 +280,36 @@ export async function rendereSicherung() {
|
||||
const passphrase = passInput?.value?.trim() || null;
|
||||
|
||||
if (passInput && !passphrase) {
|
||||
zeigeMeldung("Bitte gib die Passphrase an.", "warnung");
|
||||
zeigeMeldung(t("backup.passphraseRequired"), "warnung");
|
||||
passInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
restoreBtn.disabled = true;
|
||||
restoreBtn.textContent = "Stelle wieder her…";
|
||||
restoreBtn.textContent = t("backup.restoring");
|
||||
|
||||
try {
|
||||
const res = await stelleWiederHer(backup, { passphrase });
|
||||
zeigeMeldung(res.meldung || "Wiederherstellung abgeschlossen.", "erfolg");
|
||||
zeigeMeldung(res.meldung || t("backup.restoreSuccess"), "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
// Our own key does not match — the backup comes from another installation.
|
||||
// Nothing was stopped for this; the server checks that beforehand.
|
||||
if (err.code === "fremde_lizenz") {
|
||||
// Der eigene Schlüssel passt nicht - die Sicherung stammt von einer anderen
|
||||
// Installation. Angehalten wurde dafür nichts; das prüft der Server vorher.
|
||||
modal.schliessen();
|
||||
frageLizenzschluessel(backup, err.message);
|
||||
return;
|
||||
}
|
||||
zeigeMeldung(`Wiederherstellung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.restoreFailed", { error: err.message }), "gefahr");
|
||||
restoreBtn.disabled = false;
|
||||
restoreBtn.textContent = "Jetzt wiederherstellen";
|
||||
restoreBtn.textContent = t("backup.restoreNow");
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal(`Sicherung wiederherstellen: ${backup.fileName}`, form, [
|
||||
modal = erstelleModal(t("backup.restoreTitle", { file: backup.fileName }), form, [
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
|
||||
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
|
||||
}),
|
||||
restoreBtn
|
||||
]);
|
||||
@@ -327,20 +325,19 @@ export async function rendereSicherung() {
|
||||
const eingabe = erstelleElement("input", {
|
||||
type: "password",
|
||||
klasse: "eingabe",
|
||||
placeholder: "Lizenzschlüssel der Installation, von der die Sicherung stammt…",
|
||||
placeholder: t("backup.sourceLicensePlaceholder"),
|
||||
autocomplete: "off",
|
||||
required: true
|
||||
});
|
||||
|
||||
form.append(
|
||||
erstelleElement("p", { text: grund }),
|
||||
feld("Lizenzschlüssel der Quelle", eingabe,
|
||||
"Groß- und Kleinschreibung sowie Leerzeichen spielen keine Rolle.")
|
||||
feld(t("backup.sourceLicenseLabel"), eingabe, t("backup.sourceLicenseHint"))
|
||||
);
|
||||
|
||||
let modal;
|
||||
const okBtn = erstelleElement("button", {
|
||||
type: "submit", klasse: "schaltflaeche warnung", text: "Mit diesem Schlüssel wiederherstellen"
|
||||
type: "submit", klasse: "schaltflaeche warnung", text: t("backup.restoreWithKey")
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
@@ -349,26 +346,26 @@ export async function rendereSicherung() {
|
||||
if (!lizenzschluessel) { eingabe.focus(); return; }
|
||||
|
||||
okBtn.disabled = true;
|
||||
okBtn.textContent = "Stelle wieder her…";
|
||||
okBtn.textContent = t("backup.restoring");
|
||||
|
||||
try {
|
||||
const res = await stelleWiederHer(backup, { lizenzschluessel });
|
||||
zeigeMeldung(res.meldung || "Wiederherstellung abgeschlossen.", "erfolg");
|
||||
zeigeMeldung(res.meldung || t("backup.restoreSuccess"), "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.code === "fremde_lizenz"
|
||||
? "Dieser Lizenzschlüssel passt nicht zu der Sicherung."
|
||||
: `Wiederherstellung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
? t("backup.keyMismatch")
|
||||
: t("backup.restoreFailed", { error: err.message }), "gefahr");
|
||||
okBtn.disabled = false;
|
||||
okBtn.textContent = "Mit diesem Schlüssel wiederherstellen";
|
||||
okBtn.textContent = t("backup.restoreWithKey");
|
||||
eingabe.select();
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal("Lizenzschlüssel der Quelle erforderlich", form, [
|
||||
modal = erstelleModal(t("backup.foreignLicenseTitle"), form, [
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
|
||||
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
|
||||
}),
|
||||
okBtn
|
||||
]);
|
||||
@@ -382,28 +379,28 @@ export async function rendereSicherung() {
|
||||
const loeschenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche gefahr",
|
||||
text: "Löschen",
|
||||
text: t("common.delete"),
|
||||
onclick: async () => {
|
||||
loeschenBtn.disabled = true;
|
||||
try {
|
||||
const res = await api(`/verwaltung/instanzen/${pfadInstanz}/backups/${encodeURIComponent(fileName)}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
zeigeMeldung(res.meldung || "Sicherung gelöscht.", "erfolg");
|
||||
zeigeMeldung(res.meldung || t("backup.deleted"), "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.deleteFailed", { error: err.message }), "gefahr");
|
||||
loeschenBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal("Sicherung löschen",
|
||||
erstelleElement("p", { text: `Soll die Sicherungsdatei '${fileName}' wirklich unwiderruflich gelöscht werden?` }),
|
||||
modal = erstelleModal(t("backup.deleteTitle"),
|
||||
erstelleElement("p", { text: t("backup.deleteConfirm", { file: fileName }) }),
|
||||
[
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
|
||||
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
|
||||
}),
|
||||
loeschenBtn
|
||||
]);
|
||||
@@ -416,13 +413,13 @@ export async function rendereSicherung() {
|
||||
// ─── FTP-Upload (für alle Instanzen) ───
|
||||
|
||||
async function rendereFtp(bereich) {
|
||||
bereich.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade FTP-Einstellungen…" }));
|
||||
bereich.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: t("backup.ftpLoading") }));
|
||||
|
||||
let stand;
|
||||
try {
|
||||
stand = await api("/verwaltung/sicherung/ftp");
|
||||
} catch (err) {
|
||||
bereich.replaceChildren(erstelleKarte("FTP-Upload", [
|
||||
bereich.replaceChildren(erstelleKarte(t("backup.ftpTitle"), [
|
||||
erstelleElement("p", { klasse: "fehler-meldung", text: `Einstellungen nicht ladbar: ${err.message}` })
|
||||
]));
|
||||
return;
|
||||
@@ -430,7 +427,7 @@ async function rendereFtp(bereich) {
|
||||
|
||||
const form = erstelleElement("form", { klasse: "formular" });
|
||||
|
||||
const an = checkbox("ftp-an", "Fertige Sicherungen automatisch hochladen", stand.enabled);
|
||||
const an = checkbox("ftp-an", t("backup.ftpAutoUpload"), stand.enabled);
|
||||
const host = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.host ?? "", placeholder: "ftp.example.org" });
|
||||
const port = erstelleElement("input", { type: "number", klasse: "eingabe", value: String(stand.port ?? 21), min: "1", max: "65535" });
|
||||
const benutzer = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.username ?? "", autocomplete: "off" });
|
||||
@@ -438,40 +435,39 @@ async function rendereFtp(bereich) {
|
||||
type: "password",
|
||||
klasse: "eingabe",
|
||||
autocomplete: "new-password",
|
||||
placeholder: stand.kennwortGesetzt ? "(gespeichert — leer lassen für unverändert)" : "Kennwort"
|
||||
placeholder: stand.kennwortGesetzt ? t("backup.ftpPasswordSaved") : t("backup.ftpPassword")
|
||||
});
|
||||
const ordner = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.remoteDirectory ?? "/clawd-backups" });
|
||||
const behalten = erstelleElement("input", { type: "number", klasse: "eingabe", value: String(stand.keepCount ?? 14), min: "1", max: "365" });
|
||||
const tls = checkbox("ftp-tls", "Nur mit TLS verbinden", stand.requireTls);
|
||||
const tls = checkbox("ftp-tls", t("backup.ftpRequireTls"), stand.requireTls);
|
||||
|
||||
form.append(
|
||||
erstelleElement("p", {
|
||||
klasse: "form-hilfe",
|
||||
text: "Die Sicherungen sind verschlüsselt — wer den Server liest, liest nichts Brauchbares. "
|
||||
+ "Das Kennwort des Servers geht ohne TLS aber im Klartext über die Leitung."
|
||||
text: t("backup.ftpHint")
|
||||
}),
|
||||
an.gruppe,
|
||||
feld("Server", host),
|
||||
feld("Port", port),
|
||||
feld("Benutzer", benutzer),
|
||||
feld("Kennwort", kennwort),
|
||||
feld("Zielordner", ordner, "Darunter bekommt jede Instanz einen eigenen Ordner."),
|
||||
feld("Sicherungen je Instanz behalten", behalten, "Ältere werden auf dem Server gelöscht."),
|
||||
feld(t("backup.ftpServer"), host),
|
||||
feld(t("backup.ftpPort"), port),
|
||||
feld(t("backup.ftpUser"), benutzer),
|
||||
feld(t("backup.ftpPassword"), kennwort),
|
||||
feld(t("backup.ftpDirectory"), ordner, t("backup.ftpDirectoryHint")),
|
||||
feld(t("backup.ftpKeepCount"), behalten, t("backup.ftpKeepCountHint")),
|
||||
tls.gruppe
|
||||
);
|
||||
|
||||
const speichernBtn = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: "Speichern" });
|
||||
const speichernBtn = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: t("common.save") });
|
||||
const testBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche",
|
||||
text: "Verbindung testen",
|
||||
text: t("backup.ftpTest"),
|
||||
onclick: async () => {
|
||||
testBtn.disabled = true;
|
||||
try {
|
||||
const res = await api("/verwaltung/sicherung/ftp/test", { method: "POST" });
|
||||
zeigeMeldung(res.meldung, res.ok ? "erfolg" : "warnung", 8000);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Test fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.ftpTestFailed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
@@ -495,11 +491,11 @@ async function rendereFtp(bereich) {
|
||||
keepCount: Number(behalten.value) || 14
|
||||
}
|
||||
});
|
||||
zeigeMeldung(res.meldung || "Gespeichert.", "erfolg");
|
||||
zeigeMeldung(res.meldung || t("backup.ftpSaved"), "erfolg");
|
||||
kennwort.value = "";
|
||||
rendereFtp(bereich);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Speichern fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(t("backup.ftpSaveFailed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
speichernBtn.disabled = false;
|
||||
}
|
||||
@@ -507,5 +503,5 @@ async function rendereFtp(bereich) {
|
||||
|
||||
form.append(erstelleElement("div", { klasse: "gruppe-horizontal" }, testBtn, speichernBtn));
|
||||
|
||||
bereich.replaceChildren(erstelleKarte("FTP-Upload der Sicherungen", [form]));
|
||||
bereich.replaceChildren(erstelleKarte(t("backup.ftpTitle"), [form]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user