"use strict";
const state = { session: null, timer: null, groups: null, storage: null, activity: null };
const loginView = document.querySelector("#login-view");
const appView = document.querySelector("#app-view");
const content = document.querySelector("#content");
const nav = document.querySelector("#navigation");
const toast = document.querySelector("#toast");
const esc = value => String(value ?? "").replace(/[&<>'"]/g, char => ({"&":"&","<":"<",">":">","'":"'",'"':"""}[char]));
const bytes = value => {
let number = Number(value || 0);
const units = ["B", "kB", "MB", "GB", "TB", "PB"];
let unit = 0;
while (Math.abs(number) >= 1024 && unit < units.length - 1) { number /= 1024; unit += 1; }
const digits = number >= 100 || unit === 0 ? 0 : 1;
return `${number.toLocaleString("de-DE", {minimumFractionDigits: digits, maximumFractionDigits: digits})} ${units[unit]}`;
};
const decimal = (value, digits = 1) => Number(value || 0).toLocaleString("de-DE", {minimumFractionDigits: digits, maximumFractionDigits: digits});
const utcTime = value => {
if (!value) return "—";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "—";
const pad = number => String(number).padStart(2, "0");
return `${pad(date.getUTCDate())}.${pad(date.getUTCMonth() + 1)}.${date.getUTCFullYear()} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} UTC`;
};
const actionLabel = value => ({read: "Lesen", write: "Schreiben", move: "Umbenennen", delete: "Löschen"}[value] || value || "—");
const nodeTypeLabel = value => ({group: "Gruppe", user: "Benutzer", computer: "Computer", unknown: "Unbekannt"}[value] || value);
const backupStateLabel = value => ({starting: "Startet", running: "Läuft", completed: "Abgeschlossen", failed: "Fehlgeschlagen", waiting: "Wartet", disabled: "Deaktiviert"}[value] || value || "Unbekannt");
function backupMessage(data) {
const message = String(data.message || "");
if (!message) {
if (!data.enabled) return "Sicherungen sind nicht eingerichtet.";
if (!data.automaticEnabled) return "Automatische Sicherungen sind ausgeschaltet; manueller Start ist verfügbar.";
return `Täglich um ${String(data.scheduledHour).padStart(2, "0")}:00 UTC geplant.`;
}
if (message === "Starting backup") return "Sicherung wird gestartet.";
if (message === "Creating consistent state database snapshot") return "Konsistenter Datenbankstand wird erstellt.";
if (message === "Creating encrypted group archives") return "Verschlüsselte Gruppenarchive werden erstellt.";
if (message.startsWith("Archiving group ")) return `Gruppenarchiv ${message.slice(16)} wird erstellt.`;
if (message === "Measuring backup payload") return "Sicherungsumfang wird ermittelt.";
if (message.startsWith("Backup payload: ")) return `Sicherungsumfang: ${message.slice(16)}`;
if (message.startsWith("Syncing ")) return `${message.slice(8)} wird synchronisiert.`;
const completed = message.match(/^Backup completed; (\d+) snapshot\(s\) retained$/);
if (completed) return `Sicherung abgeschlossen; ${completed[1]} Sicherungsstände werden aufbewahrt.`;
if (data.interrupted) return "Sicherung wurde durch einen Neustart oder Prozessabbruch unterbrochen.";
if (data.state === "failed") return "Sicherung fehlgeschlagen. Einzelheiten stehen im Sicherungsprotokoll.";
if (data.state === "completed") return "Sicherung abgeschlossen.";
if (data.state === "running") return "Sicherung läuft.";
if (data.state === "starting") return "Sicherung wird gestartet.";
return "Kein Laufstatus verfügbar.";
}
const isoDay = date => date.toISOString().slice(0, 10);
const sum = (rows, field) => (rows || []).reduce((total, row) => total + Number(row[field] || 0), 0);
async function api(path, options = {}) {
const response = await fetch(path, {
...options,
headers: {"Content-Type": "application/json", ...(options.headers || {})},
credentials: "same-origin",
});
let body = {};
try { body = await response.json(); } catch (_) { /* no body */ }
if (response.status === 401 && path !== "/api/login") {
showLogin();
throw new Error("Die Sitzung ist abgelaufen. Bitte erneut anmelden.");
}
if (!response.ok) throw new Error(body.error || `Anfrage fehlgeschlagen (${response.status})`);
return body;
}
function notice(message) {
toast.textContent = message;
toast.hidden = false;
window.setTimeout(() => { toast.hidden = true; }, 4000);
}
function showLogin() {
clearInterval(state.timer);
state.session = null;
appView.hidden = true;
loginView.hidden = false;
document.querySelector("#login-form input[name=username]").focus();
}
function showApp(session) {
state.session = {user: session.user, expiresAt: session.expiresAt};
document.querySelector("#session-user").textContent = session.user;
loginView.hidden = true;
appView.hidden = false;
navigate(location.pathname === "/" ? "/overview" : location.pathname, true);
}
function setLoading() { content.innerHTML = '
Wird geladen…
'; }
function pageHead(title, intro, extra = "") {
return `${esc(title)} ${esc(intro)}
${extra} `;
}
function empty(message) { return `${esc(message)}
`; }
function badge(text, kind = "") { return `${esc(text)} `; }
function routeFor(path) {
if (path.startsWith("/storage/data")) return "storage-data";
if (path.startsWith("/storage/users")) return "storage-users";
if (path.startsWith("/shares")) return "shares";
if (path.startsWith("/reconciliation")) return "reconciliation";
if (path.startsWith("/activity/fslogix")) return "activity-fslogix";
if (path.startsWith("/activity")) return "activity";
if (path.startsWith("/backup")) return "backup";
if (path.startsWith("/report")) return "report";
if (path.startsWith("/system")) return "system";
return "overview";
}
async function navigate(path, replace = false) {
clearInterval(state.timer);
state.timer = null;
const route = routeFor(path);
if (replace) history.replaceState({}, "", path); else history.pushState({}, "", path);
nav.querySelectorAll("a").forEach(link => link.classList.toggle("active", link.dataset.route === route));
document.querySelector(".sidebar").classList.remove("open");
setLoading();
try {
if (route === "overview") await renderOverview();
if (route === "shares") await renderShares();
if (route === "reconciliation") await renderReconciliation();
if (route === "storage-data") await renderStorage("data");
if (route === "storage-users") await renderStorage("users");
if (route === "activity") await renderActivity();
if (route === "activity-fslogix") await renderActivity("fslogix");
if (route === "backup") await renderBackup();
if (route === "report") await renderReport();
if (route === "system") await renderSystem();
content.focus();
} catch (error) {
content.innerHTML = pageHead("Seite konnte nicht geladen werden", error.message) + `${empty("Dienststatus prüfen und erneut versuchen.")} `;
}
}
function eventRows(events) {
if (!events?.length) return 'Keine passenden Ereignisse ';
return events.map(event => `
${esc(utcTime(event.timestamp))}
${esc(event.user)} ${esc(event.clientIp)} ${esc(event.share)}
${badge(actionLabel(event.action || event.operation))}
${esc(event.path || "—")}
${event.success ? badge("Erfolgreich") : badge(event.result || "Fehlgeschlagen", "error")}
`).join("");
}
async function renderOverview() {
const data = await api("/api/overview");
const usage = data.usage || {};
const totals = usage.totals || {};
const backup = data.backup || {};
content.innerHTML = pageHead("Übersicht", "Speicherbelegung, Aktivität und Sicherungsstatus.", `Stand ${esc(utcTime(usage.scannedAt))} `) + `
Daten ${bytes(totals.dataBytes)}
Private + FSLogix ${bytes(Number(totals.privateBytes || 0) + Number(totals.fslogixBytes || 0))}
Aktive Gruppen ${esc(data.activeGroups)}
Ereignisse · 48 Std. ${esc(data.eventCount)}
${usageTable((usage.groups || []).slice(0, 7), "group")}
${badge(backupStateLabel(backup.state || (backup.enabled ? "waiting" : "disabled")), backup.state === "failed" ? "error" : "")}${esc(backupMessage(backup))}
${decimal(backup.percent)} % · ${bytes(backup.transferredBytes)} / ${bytes(backup.totalBytes)}
Zeit (UTC) Benutzer Client Freigabe Aktion Pfad Ergebnis ${eventRows(data.recentEvents)}
`;
bindInternalLinks();
}
function usageTable(rows, type) {
if (!rows.length) return empty("Die erste Speicherprüfung ist noch nicht abgeschlossen.");
const maximum = Math.max(...rows.map(row => Number(type === "group" ? row.bytes : row.totalBytes)), 1);
return `${type === "group" ? "Gruppenordner" : "Benutzer"} ${type === "user" ? "Private FSLogix " : ""}Belegung ${rows.map(row => {
const value = Number(type === "group" ? row.bytes : row.totalBytes);
return `${esc(row.name)} ${type === "user" ? `${bytes(row.privateBytes)} ${bytes(row.fslogixBytes)} ` : ""}${bytes(value)} `;
}).join("")}
`;
}
function nodeMatches(node, query) {
if (!query) return true;
if (`${node.name} ${node.sam} ${node.type} ${nodeTypeLabel(node.type)}`.toLowerCase().includes(query)) return true;
return (node.members || []).some(child => nodeMatches(child, query));
}
function treeNodes(nodes, query = "") {
return nodes.filter(node => nodeMatches(node, query)).map(node => {
const children = treeNodes(node.members || [], query);
const title = `${esc(nodeTypeLabel(node.type))} ${esc(node.name)} ${node.sam && node.sam !== node.name ? ` ${esc(node.sam)} ` : ""}${node.cycle ? ` ${badge("Zyklus", "warn")}` : ""}`;
return children ? `${title} ${children} ` : `${title}
`;
}).join("");
}
async function renderShares() {
const data = await api("/api/groups");
state.groups = data;
const groups = data.groups || [];
content.innerHTML = `` + pageHead("Dateifreigaben", "FS_*-Ordnergruppen und ihre wirksamen verschachtelten Mitgliedschaften.", `
Verzeichnisstand ${esc(utcTime(data.fetchedAt))} `) + `
${data.truncated ? `
${badge("Ergebnis gekürzt", "warn")} WEB_MAX_GROUP_NODES erhöhen, um alle Mitglieder anzuzeigen.
` : ""}
`;
const list = document.querySelector("#group-list");
const tree = document.querySelector("#tree-panel");
let selected = groups[0] || null;
let query = "";
const draw = () => {
const visible = groups.filter(group => `${group.name} ${group.sam} ${group.folder}`.toLowerCase().includes(query) || group.members.some(node => nodeMatches(node, query)));
if (selected && !visible.includes(selected)) selected = visible[0] || null;
list.innerHTML = visible.length ? visible.map(group => `${esc(group.folder)} ${esc(group.sam)} ${group.userCount} Benutzer ${group.groupCount} Gruppen `).join("") : empty("Keine Gruppe entspricht dem Filter.");
if (!selected) tree.innerHTML = empty("Gruppenordner auswählen.");
else tree.innerHTML = `
${esc(selected.folder)} ${esc(selected.sam)} · ${selected.userCount} wirksame Benutzer ${selected.active ? badge("Aktiv") : badge("Archiviert", "warn")}
${treeNodes(selected.members, query) || empty("Keine direkten Mitglieder")}
`;
list.querySelectorAll("button[data-guid]").forEach(button => button.addEventListener("click", () => { selected = groups.find(group => group.guid === button.dataset.guid); draw(); }));
};
document.querySelector("#group-filter").addEventListener("input", event => { query = event.target.value.trim().toLowerCase(); draw(); });
draw();
}
async function renderStorage(type) {
const data = await api("/api/storage");
state.storage = data;
let query = "";
let sort = "size-desc";
content.innerHTML = pageHead(type === "data" ? "Datenbelegung" : "Benutzerbelegung", type === "data" ? "Belegter Speicher je aktivem Gruppenordner unter /Data." : "Gemeinsame Belegung von /Private und /FSLogix je Benutzer.", `Geprüft ${esc(utcTime(data.scannedAt))} in ${esc(data.scanSeconds || 0)} s `) + `
`;
const draw = () => {
const source = [...(type === "data" ? data.groups || [] : data.users || [])];
let rows = source.filter(row => row.name.toLowerCase().includes(query));
const field = type === "data" ? "bytes" : "totalBytes";
rows.sort((a, b) => sort === "name" ? a.name.localeCompare(b.name) : sort === "size-asc" ? Number(a[field]) - Number(b[field]) : Number(b[field]) - Number(a[field]));
document.querySelector("#storage-table").innerHTML = `${rows.length} Einträge · ${bytes(sum(rows, field))} angezeigt
${usageTable(rows, type === "data" ? "group" : "user")}`;
};
document.querySelector("#storage-filter").addEventListener("input", event => { query = event.target.value.trim().toLowerCase(); draw(); });
document.querySelector("#storage-sort").addEventListener("change", event => { sort = event.target.value; draw(); });
draw();
}
async function renderActivity(stream = "main") {
const today = new Date();
const yesterday = new Date(Date.now() - 86400000);
const fslogix = stream === "fslogix";
const endpoint = fslogix ? "/api/fslogix-activity" : "/api/activity";
content.innerHTML = pageHead(
fslogix ? "FSLogix-Protokoll" : "Aktivitätsprotokoll",
fslogix
? "FSLogix-Profilcontainer-Aktivitäten getrennt vom regulären Dateiprotokoll durchsuchen."
: "Lese-, Schreib-, Umbenennungs- und Löschvorgänge nach Datum, Identität, Freigabe, Ergebnis oder Pfad durchsuchen.",
) + `
Zeit (UTC) Benutzer Client Freigabe Aktion Pfad Ergebnis
Weitere laden
`;
const form = document.querySelector("#activity-filter");
let cursor = 0;
let loaded = 0;
let matched = null;
const load = async append => {
const params = new URLSearchParams(new FormData(form));
params.set("limit", "100");
if (cursor) params.set("cursor", String(cursor));
if (append) {
params.set("count", "0");
params.set("facets", "0");
}
const result = await api(`${endpoint}?${params}`);
state.activity = result;
const rows = document.querySelector("#activity-rows");
if (append) rows.insertAdjacentHTML("beforeend", eventRows(result.events)); else rows.innerHTML = eventRows(result.events);
loaded = append ? loaded + result.events.length : result.events.length;
if (!append) matched = result.matchedExact ? result.matched : null;
const shown = matched === null ? `${loaded.toLocaleString("de-DE")}${result.hasMore ? "+" : ""}` : matched.toLocaleString("de-DE");
const indexNote = result.indexing?.ready === false ? ` · Indexaufbau ${Number(result.indexing.percent || 0).toLocaleString("de-DE")} %` : "";
document.querySelector("#activity-summary").textContent = `${shown} passende Ereignisse${indexNote}`;
const more = document.querySelector("#load-more");
cursor = result.nextCursor || 0;
more.hidden = !result.hasMore;
if (!append) {
for (const [id, values] of [["users-list", result.facets.users], ["shares-list", result.facets.shares]]) {
document.querySelector(`#${id}`).innerHTML = values.filter(Boolean).map(value => ``).join("");
}
}
};
form.addEventListener("submit", async event => { event.preventDefault(); cursor = 0; try { await load(false); } catch (error) { notice(error.message); } });
document.querySelector("#load-more").addEventListener("click", async () => { try { await load(true); } catch (error) { notice(error.message); } });
await load(false);
}
function triggerLabel(value) {
return ({automatic: "Automatisch", web: "Weboberfläche", manual: "Befehlszeile", startup: "Systemstart"}[value] || value || "—");
}
function processIsActive(data) {
return ["starting", "running"].includes(data?.state);
}
function backupMarkup(data) {
const stateName = data.enabled ? (data.state || "waiting") : "disabled";
const active = data.activeFiles || [];
const automatic = data.automaticEnabled
? `Täglich um ${String(data.scheduledHour).padStart(2, "0")}:00 UTC`
: data.enabled ? "Ausgeschaltet" : "Nicht eingerichtet";
return `
Status ${esc(backupStateLabel(stateName))}
Fortschritt ${decimal(data.percent)} %
Übertragen ${bytes(data.transferredBytes)}
Automatik ${esc(automatic)}
Aktueller Lauf ${badge(backupStateLabel(stateName), stateName === "failed" ? "error" : "")}
${esc(backupMessage(data))}
${bytes(data.transferredBytes)} / ${bytes(data.totalBytes)} ${esc(data.currentSource || "")} ${esc(data.snapshot || "")}
Auslöser ${esc(triggerLabel(data.trigger))} Gestartet ${esc(utcTime(data.startedAt))} Beendet ${esc(utcTime(data.finishedAt))}
${active.length ? `Dateien in Bearbeitung Datei Fortschritt Übertragen ${active.map(file => `${esc(file.path)} ${bytes(file.transferredBytes)} / ${bytes(file.totalBytes)} `).join("")}
` : ""}
Sicherungsprotokoll Letzte ${data.log?.length || 0} Zeilen ${esc((data.log || []).join("\n") || "Noch keine Ausgabe vorhanden.")} `;
}
async function renderBackup() {
content.innerHTML = pageHead(
"Sicherungen",
"Sicherungen manuell starten sowie Fortschritt und Ausgabe verfolgen.",
'Sicherung jetzt starten '
) + '
';
const button = document.querySelector("#start-backup");
const body = document.querySelector("#backup-body");
let logLines = [];
let previousActive = false;
let polling = false;
const refresh = async includeLog => {
try {
const currentLog = document.querySelector("#backup-log");
const previousScrollTop = currentLog?.scrollTop || 0;
const data = await api(includeLog ? "/api/backup" : "/api/backup?log=0");
if (!body.isConnected || !button.isConnected) return null;
if (Array.isArray(data.log)) logLines = data.log;
data.log = logLines;
body.innerHTML = backupMarkup(data);
button.disabled = !data.manualEnabled || processIsActive(data);
button.title = data.manualEnabled ? "" : "Es ist kein Sicherungsziel eingerichtet.";
const logView = document.querySelector("#backup-log");
if (logView) {
logView.scrollTop = includeLog ? logView.scrollHeight : previousScrollTop;
}
return data;
} catch (error) {
notice(error.message);
return null;
}
};
const poll = async () => {
if (polling) return;
polling = true;
try {
const status = await refresh(false);
if (!status) return;
const active = processIsActive(status);
if (active || previousActive) {
const complete = await refresh(true);
previousActive = complete ? processIsActive(complete) : active;
} else {
previousActive = false;
}
} finally {
polling = false;
}
};
button.addEventListener("click", async () => {
if (!window.confirm("Sicherung jetzt starten?")) return;
button.disabled = true;
try {
await api("/api/actions/backup", {method: "POST", body: "{}"});
notice("Sicherung wurde gestartet.");
await poll();
} catch (error) {
notice(error.message);
button.disabled = false;
}
});
const initial = await refresh(true);
if (!body.isConnected || !button.isConnected) return;
previousActive = processIsActive(initial);
state.timer = window.setInterval(() => { void poll(); }, 2000);
}
const reconciliationStateLabel = value => ({
starting: "Startet",
running: "Läuft",
completed: "Abgeschlossen",
failed: "Fehlgeschlagen",
waiting: "Wartet",
}[value] || value || "Unbekannt");
const reconciliationPhaseLabel = value => ({
starting: "Vorbereitung",
winbind: "Domänenzwischenspeicher",
directory: "Active Directory",
database: "Freigabendatenbank",
"data-permissions": "Datenberechtigungen",
fslogix: "FSLogix",
private: "Private Ordner",
samba: "Samba-Konfiguration",
completed: "Abgeschlossen",
failed: "Fehlgeschlagen",
waiting: "Wartet",
}[value] || value || "—");
function reconciliationMarkup(data) {
const stateName = data.state || "waiting";
const phase = reconciliationPhaseLabel(data.phase);
const current = data.currentItem ? ` · ${data.currentItem}` : "";
const message = stateName === "failed"
? (data.message || "Der Freigabenabgleich ist fehlgeschlagen.")
: `${phase}${current}`;
return `
Status ${esc(reconciliationStateLabel(stateName))}
Fortschritt ${decimal(data.percent)} %
Gestartet ${esc(utcTime(data.startedAt))}
Automatik Alle ${esc(data.scheduledIntervalMinutes || 5)} Minuten
Aktueller Abgleich ${badge(reconciliationStateLabel(stateName), stateName === "failed" ? "error" : "")}
${esc(message)}
Phase ${esc(phase)} Auslöser ${esc(triggerLabel(data.trigger))} Beendet ${esc(utcTime(data.finishedAt))}
Abgleichsprotokoll Letzte ${data.log?.length || 0} Zeilen ${esc((data.log || []).join("\n") || "Noch keine Ausgabe vorhanden.")} `;
}
async function renderReconciliation() {
content.innerHTML = pageHead(
"Freigabenabgleich",
"Freigaben, Gruppenordner und Berechtigungen sofort mit Active Directory abgleichen.",
'Freigaben jetzt abgleichen '
) + '
';
const button = document.querySelector("#start-reconciliation");
const body = document.querySelector("#reconciliation-body");
let logLines = [];
let previousActive = false;
let polling = false;
const refresh = async includeLog => {
try {
const currentLog = document.querySelector("#reconciliation-log");
const previousScrollTop = currentLog?.scrollTop || 0;
const data = await api(
includeLog ? "/api/reconciliation" : "/api/reconciliation?log=0"
);
if (!body.isConnected || !button.isConnected) return null;
if (Array.isArray(data.log)) logLines = data.log;
data.log = logLines;
body.innerHTML = reconciliationMarkup(data);
button.disabled = processIsActive(data);
const logView = document.querySelector("#reconciliation-log");
if (logView) {
logView.scrollTop = includeLog ? logView.scrollHeight : previousScrollTop;
}
return data;
} catch (error) {
notice(error.message);
return null;
}
};
const poll = async () => {
if (polling) return;
polling = true;
try {
const status = await refresh(false);
if (!status) return;
const active = processIsActive(status);
if (active || previousActive) {
const complete = await refresh(true);
previousActive = complete ? processIsActive(complete) : active;
} else {
previousActive = false;
}
} finally {
polling = false;
}
};
button.addEventListener("click", async () => {
if (!window.confirm("Freigaben jetzt mit Active Directory abgleichen?")) return;
button.disabled = true;
try {
await api("/api/actions/reconciliation", {method: "POST", body: "{}"});
notice("Freigabenabgleich wurde gestartet.");
await poll();
} catch (error) {
notice(error.message);
button.disabled = false;
}
});
const initial = await refresh(true);
if (!body.isConnected || !button.isConnected) return;
previousActive = processIsActive(initial);
state.timer = window.setInterval(() => { void poll(); }, 1500);
}
async function renderReport() {
content.innerHTML = pageHead("PDF-Bericht", "Vollständiger, druckbarer Stand der Dateiserver-Konfiguration und Belegung.") + `
Enthaltene Informationen
Speicherbelegung von Daten-, Private- und FSLogix-Bereichen
Alle Dateifreigabegruppen mit vollständiger Mitgliedschaftshierarchie
Aktueller Sicherungsstatus und laufende Dateiübertragungen
Systemprüfungen und TLS-Zertifikatsdaten
Aktivitätsprotokoll und Sicherungsprotokoll sind ausdrücklich nicht enthalten.
PDF erstellen
Die Erstellung erfolgt vollständig in diesem Browser mit Typst.
`;
const button = document.querySelector("#create-report");
const status = document.querySelector("#report-status");
button.addEventListener("click", async () => {
button.disabled = true;
try {
status.textContent = "Berichtsdaten werden geladen…";
const data = await api("/api/report");
status.textContent = "Typst wird geladen…";
const {createPdfReport} = await import("/assets/report.mjs");
const result = await createPdfReport(data, message => { status.textContent = message; });
status.textContent = `PDF erstellt: ${result.filename} (${bytes(result.size)})`;
} catch (error) {
status.textContent = `PDF konnte nicht erstellt werden: ${error.message}`;
} finally {
button.disabled = false;
}
});
}
async function renderSystem() {
const data = await api("/api/system");
const usage = data.usage || {};
content.innerHTML = pageHead("System", "Status von Diensten, Zertifikat, Protokollerfassung und Speicherprüfung.", `Serverzeit ${esc(utcTime(data.serverTime))} `) + `
Dienstprüfungen ${esc(data.hostname)}
Domänenvertrauen ${data.checks.domainTrust ? badge("In Ordnung") : badge("Fehlgeschlagen", "error")}
Samba-Konfiguration ${data.checks.sambaConfig ? badge("Gültig") : badge("Fehlgeschlagen", "error")}
Aktivitätsdatenbank ${data.audit.days} Tage · ${bytes(data.audit.bytes)}
Speicherprüfung ${usage.scannedAt ? esc(utcTime(usage.scannedAt)) : "Ausstehend"}
TLS-Zertifikat Allgemeiner Name ${esc(data.tls.subject?.commonName || "—")} Aussteller ${esc(data.tls.issuer?.commonName || "—")} Gültig bis ${esc(utcTime(data.tls.notAfter))} Namen ${esc((data.tls.sans || []).map(value => value[1]).join(", ") || "—")}
Protokollspeicherung Ältester UTC-Tag ${esc(data.audit.oldest || "—")} Neuester UTC-Tag ${esc(data.audit.newest || "—")} Datenbankgröße ${bytes(data.audit.bytes)} Ereignisse werden dauerhaft und indexiert in der gemeinsamen SQLite-Datenbank gespeichert.
`;
}
function bindInternalLinks() {
content.querySelectorAll("a[data-nav]").forEach(link => link.addEventListener("click", event => { event.preventDefault(); navigate(link.pathname); }));
}
document.querySelector("#login-form").addEventListener("submit", async event => {
event.preventDefault();
const form = event.currentTarget;
const button = form.querySelector("button");
const error = document.querySelector("#login-error");
error.hidden = true; button.disabled = true;
try {
const values = new FormData(form);
const session = await api("/api/login", {method: "POST", body: JSON.stringify({username: values.get("username"), password: values.get("password")})});
form.reset();
showApp(session);
} catch (reason) { error.textContent = reason.message; error.hidden = false; }
finally { button.disabled = false; }
});
document.querySelector("#logout").addEventListener("click", async () => { try { await api("/api/logout", {method: "POST", body: "{}"}); } finally { showLogin(); } });
document.querySelector("#menu-toggle").addEventListener("click", () => document.querySelector(".sidebar").classList.toggle("open"));
nav.addEventListener("click", event => { const link = event.target.closest("a"); if (link) { event.preventDefault(); navigate(link.pathname); } });
window.addEventListener("popstate", () => navigate(location.pathname, true));
api("/api/session").then(showApp).catch(showLogin);