348 lines
13 KiB
JavaScript
348 lines
13 KiB
JavaScript
"use strict";
|
|
|
|
const TYPE_LABELS = {group: "Gruppe", user: "Benutzer", computer: "Computer", unknown: "Unbekannt"};
|
|
const BACKUP_LABELS = {starting: "Startet", running: "Läuft", completed: "Abgeschlossen", failed: "Fehlgeschlagen", waiting: "Wartet", disabled: "Deaktiviert"};
|
|
|
|
function typstString(value) {
|
|
return '"' + String(value ?? "")
|
|
.replace(/\\/g, "\\\\")
|
|
.replace(/"/g, '\\"')
|
|
.replace(/\r/g, "\\r")
|
|
.replace(/\n/g, "\\n")
|
|
.replace(/\t/g, "\\t") + '"';
|
|
}
|
|
|
|
function formatBytes(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];
|
|
}
|
|
|
|
function formatDecimal(value, digits = 1) {
|
|
return Number(value || 0).toLocaleString("de-DE", {
|
|
minimumFractionDigits: digits,
|
|
maximumFractionDigits: digits,
|
|
});
|
|
}
|
|
|
|
function formatUtc(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",
|
|
].join("");
|
|
}
|
|
|
|
function fileTimestamp(value) {
|
|
const date = new Date(value);
|
|
const valid = Number.isNaN(date.getTime()) ? new Date() : date;
|
|
const pad = number => String(number).padStart(2, "0");
|
|
return [
|
|
valid.getUTCFullYear(),
|
|
"-",
|
|
pad(valid.getUTCMonth() + 1),
|
|
"-",
|
|
pad(valid.getUTCDate()),
|
|
"T",
|
|
pad(valid.getUTCHours()),
|
|
pad(valid.getUTCMinutes()),
|
|
pad(valid.getUTCSeconds()),
|
|
"Z",
|
|
].join("");
|
|
}
|
|
|
|
function backupLabel(value, enabled) {
|
|
const state = value || (enabled ? "waiting" : "disabled");
|
|
return BACKUP_LABELS[state] || state || "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.";
|
|
if (data.state === "completed") return "Sicherung abgeschlossen.";
|
|
if (data.state === "running") return "Sicherung läuft.";
|
|
if (data.state === "starting") return "Sicherung wird gestartet.";
|
|
return message;
|
|
}
|
|
|
|
function textCell(value, weight = "") {
|
|
const option = weight ? 'weight: "' + weight + '", ' : "";
|
|
return "[#text(" + option + typstString(value) + ")]";
|
|
}
|
|
|
|
function reportTable(columns, aligns, headers, rows) {
|
|
const cells = [];
|
|
for (const row of rows) {
|
|
for (const value of row) cells.push(textCell(value));
|
|
}
|
|
return [
|
|
"#table(",
|
|
" columns: (" + columns.join(", ") + ",),",
|
|
" align: (" + aligns.join(", ") + ",),",
|
|
" inset: (x: 4pt, y: 3pt),",
|
|
" stroke: 0.35pt + luma(75%),",
|
|
" table.header(" + headers.map(value => textCell(value, "semibold")).join(", ") + "),",
|
|
cells.length ? " " + cells.join(",\n ") + "," : "",
|
|
")",
|
|
].filter(Boolean).join("\n");
|
|
}
|
|
|
|
function keyValueTable(rows) {
|
|
return reportTable(["34%", "66%"], ["left", "left"], ["Angabe", "Wert"], rows);
|
|
}
|
|
|
|
function treeRows(nodes, depth = 0, result = []) {
|
|
for (const node of nodes || []) {
|
|
const type = TYPE_LABELS[node.type] || node.type || "Unbekannt";
|
|
result.push(
|
|
"#tree-row(" +
|
|
depth + ", " +
|
|
typstString(type) + ", " +
|
|
typstString(node.name || node.sam || "Unbekannt") + ", " +
|
|
typstString(node.sam || "") + ", " +
|
|
(node.cycle ? "true" : "false") +
|
|
")"
|
|
);
|
|
treeRows(node.members || [], depth + 1, result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function buildReportSource(data) {
|
|
const generatedAt = data.generatedAt || new Date().toISOString();
|
|
const storage = data.storage || {};
|
|
const totals = storage.totals || {};
|
|
const groupsSnapshot = data.groups || {};
|
|
const groups = groupsSnapshot.groups || [];
|
|
const backup = data.backup || {};
|
|
const system = data.system || {};
|
|
const tls = system.tls || {};
|
|
const checks = system.checks || {};
|
|
const totalUserBytes = Number(totals.privateBytes || 0) + Number(totals.fslogixBytes || 0);
|
|
const totalBytes = Number(totals.dataBytes || 0) + totalUserBytes;
|
|
const sans = (tls.sans || [])
|
|
.map(value => Array.isArray(value) ? value[1] : value)
|
|
.filter(Boolean)
|
|
.join(", ") || "—";
|
|
|
|
const parts = [
|
|
'#set document(title: "Dateiserver-Bericht", author: "Dateiserver")',
|
|
'#set page(paper: "a4", margin: (x: 15mm, top: 14mm, bottom: 17mm), footer: context align(center, text(size: 7pt, fill: luma(45%))[Seite #counter(page).display("1")]))',
|
|
'#set text(font: "Libertinus Serif", size: 9pt, lang: "de")',
|
|
"#set par(leading: 0.58em)",
|
|
"#set heading(numbering: none)",
|
|
'#let tree-row(depth, kind, name, sam, cycle) = block(width: 100%, above: 1.8pt, below: 1.8pt, breakable: false)[#grid(columns: (depth * 9pt + 8pt, 44pt, 1fr), column-gutter: 4pt, align: (left, left, left), [#text(size: 7.5pt, fill: luma(45%))[#if depth == 0 [•] else [>]]], [#text(size: 7.5pt, fill: luma(40%))[#kind]], [#name#if sam != "" and sam != name [#text(size: 7.5pt, fill: luma(40%))[ (#sam)]]#if cycle [#text(size: 7.5pt)[ (Zyklus)]]])]',
|
|
"",
|
|
'#text(size: 22pt, weight: "semibold")[Dateiserver-Bericht]',
|
|
"#v(5pt)",
|
|
keyValueTable([
|
|
["Server", system.hostname || "—"],
|
|
["Erstellt", formatUtc(generatedAt)],
|
|
["Verzeichnisstand", formatUtc(groupsSnapshot.fetchedAt)],
|
|
["Datenordner", String(groups.length)],
|
|
["Verzeichnisabfrage gekürzt", groupsSnapshot.truncated ? "Ja" : "Nein"],
|
|
["Speicherstand", formatUtc(storage.scannedAt)],
|
|
["Serverzeit", formatUtc(system.serverTime)],
|
|
]),
|
|
"",
|
|
"#v(8pt)",
|
|
"#text(size: 8pt)[Aktivitätsprotokoll und Sicherungsprotokoll sind nicht Bestandteil dieses Berichts.]",
|
|
"",
|
|
"= Speicherbelegung",
|
|
keyValueTable([
|
|
["Daten", formatBytes(totals.dataBytes)],
|
|
["Private", formatBytes(totals.privateBytes)],
|
|
["FSLogix", formatBytes(totals.fslogixBytes)],
|
|
["Private + FSLogix", formatBytes(totalUserBytes)],
|
|
["Gesamt", formatBytes(totalBytes)],
|
|
["Prüfdauer", formatDecimal(storage.scanSeconds, 3) + " s"],
|
|
]),
|
|
"",
|
|
"== Datenbelegung je Datenordner",
|
|
(storage.groups || []).length
|
|
? reportTable(
|
|
["1fr", "auto"],
|
|
["left", "right"],
|
|
["Datenordner", "Belegung"],
|
|
storage.groups.map(row => [row.name || "—", formatBytes(row.bytes)])
|
|
)
|
|
: "Noch keine Speicherprüfung vorhanden.",
|
|
"",
|
|
"== Benutzerbelegung",
|
|
(storage.users || []).length
|
|
? reportTable(
|
|
["1fr", "auto", "auto", "auto"],
|
|
["left", "right", "right", "right"],
|
|
["Benutzer", "Private", "FSLogix", "Gesamt"],
|
|
storage.users.map(row => [
|
|
row.name || "—",
|
|
formatBytes(row.privateBytes),
|
|
formatBytes(row.fslogixBytes),
|
|
formatBytes(row.totalBytes),
|
|
])
|
|
)
|
|
: "Noch keine Speicherprüfung vorhanden.",
|
|
"",
|
|
"= Dateifreigaben und Benutzerrechte",
|
|
groupsSnapshot.truncated
|
|
? "#text(weight: \"semibold\")[Hinweis: Die Verzeichnisabfrage wurde gekürzt; die nachfolgende Hierarchie ist nicht vollständig.]"
|
|
: "Die nachfolgenden Listen zeigen die individuell berechtigten Benutzer je Ordner.",
|
|
];
|
|
|
|
for (const group of groups) {
|
|
parts.push("");
|
|
parts.push("#heading(level: 2, " + typstString(group.folder || group.name || group.sam || "Unbekannt") + ")");
|
|
parts.push(keyValueTable([
|
|
["Freigabename", group.name || "—"],
|
|
["Ordner", group.folder || "—"],
|
|
["Objekt-GUID", group.guid || "—"],
|
|
["Status", group.active ? "Aktiv" : "Archiviert"],
|
|
["Wirksame Benutzer", String(group.userCount || 0)],
|
|
]));
|
|
const rows = treeRows(group.members || []);
|
|
parts.push(rows.length ? rows.join("\n") : "Keine Benutzer mit Zugriff.");
|
|
}
|
|
|
|
const backupState = backupLabel(backup.state, backup.enabled);
|
|
parts.push("");
|
|
parts.push("= Sicherung");
|
|
parts.push(keyValueTable([
|
|
["Eingerichtet", backup.enabled ? "Ja" : "Nein"],
|
|
["Automatik", backup.automaticEnabled ? "Aktiv" : "Ausgeschaltet"],
|
|
[
|
|
"Geplanter Start",
|
|
backup.automaticEnabled
|
|
? String(backup.scheduledHour ?? 0).padStart(2, "0") + ":00 UTC"
|
|
: "—",
|
|
],
|
|
["Status", backupState],
|
|
["Fortschritt", formatDecimal(backup.percent) + " %"],
|
|
["Übertragen", formatBytes(backup.transferredBytes) + " / " + formatBytes(backup.totalBytes)],
|
|
["Gestartet", formatUtc(backup.startedAt)],
|
|
["Beendet", formatUtc(backup.finishedAt)],
|
|
["Sicherungsstand", backup.snapshot || "—"],
|
|
["Aktuelle Quelle", backup.currentSource || "—"],
|
|
["Statusmeldung", backupMessage(backup)],
|
|
]));
|
|
|
|
if ((backup.activeFiles || []).length) {
|
|
parts.push("");
|
|
parts.push("== Dateien in Bearbeitung");
|
|
parts.push(reportTable(
|
|
["1fr", "auto", "auto"],
|
|
["left", "right", "right"],
|
|
["Datei", "Fortschritt", "Übertragen"],
|
|
backup.activeFiles.map(file => [
|
|
file.path || "—",
|
|
formatDecimal(file.percent) + " %",
|
|
formatBytes(file.transferredBytes) + " / " + formatBytes(file.totalBytes),
|
|
])
|
|
));
|
|
}
|
|
|
|
parts.push("");
|
|
parts.push("= System");
|
|
parts.push(keyValueTable([
|
|
["Domänenvertrauen", checks.domainTrust ? "In Ordnung" : "Fehlgeschlagen"],
|
|
["Samba-Konfiguration", checks.sambaConfig ? "Gültig" : "Fehlgeschlagen"],
|
|
]));
|
|
parts.push("");
|
|
parts.push("== TLS-Zertifikat");
|
|
parts.push(keyValueTable([
|
|
["Allgemeiner Name", (tls.subject || {}).commonName || "—"],
|
|
["Aussteller", (tls.issuer || {}).commonName || "—"],
|
|
["Gültig bis", formatUtc(tls.notAfter)],
|
|
["Namen", sans],
|
|
]));
|
|
parts.push("");
|
|
parts.push("#text(size: 8pt)[Dieser Bericht wurde vollständig im Browser mit Typst erzeugt.]");
|
|
|
|
return parts.join("\n");
|
|
}
|
|
|
|
let compilerPromise;
|
|
|
|
async function getCompiler() {
|
|
if (!compilerPromise) {
|
|
compilerPromise = (async () => {
|
|
const module = await import("/assets/vendor/typst/0.6.0-csp1/typst.mjs");
|
|
module.$typst.setCompilerInitOptions({
|
|
getModule: () => ({module_or_path: "/assets/vendor/typst/0.6.0-csp1/compiler.wasm"}),
|
|
});
|
|
module.$typst.use(
|
|
module.TypstSnippet.disableDefaultFontAssets(),
|
|
module.TypstSnippet.preloadFontFromUrl("/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Regular.otf"),
|
|
module.TypstSnippet.preloadFontFromUrl("/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Semibold.otf")
|
|
);
|
|
return module.$typst;
|
|
})();
|
|
}
|
|
return compilerPromise;
|
|
}
|
|
|
|
export async function createPdfReport(data, progress = () => {}) {
|
|
progress("Typst-Compiler wird im Browser geladen…");
|
|
const compiler = await getCompiler();
|
|
progress("PDF wird im Browser gesetzt…");
|
|
const pdf = await compiler.pdf({mainContent: buildReportSource(data)});
|
|
if (!(pdf instanceof Uint8Array) || pdf.length < 5) {
|
|
throw new Error("Typst hat keine gültige PDF-Datei erzeugt.");
|
|
}
|
|
|
|
const filename = "dateiserver-bericht-" + fileTimestamp(data.generatedAt) + ".pdf";
|
|
const url = URL.createObjectURL(new Blob([pdf], {type: "application/pdf"}));
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
return {filename, size: pdf.length};
|
|
}
|