/* ===================================================================== wiederherstellung-proben.mjs — die Probe aufs Exempel. --------------------------------------------------------------------- WARUM ES DAS GIBT Es gibt seit dem 31.08.2026 eine Sicherung ausserhalb des Servers (tools/sicherung-holen.sh, taeglich per Aufgabenplanung). Sie prueft `PRAGMA integrity_check` und dass die Personentabelle nicht leer ist. Das ist besser als nichts und beweist nicht das, worauf es ankommt. `integrity_check` sagt: die Datei ist nicht zerschossen. Es sagt NICHT: * dass die Anwendung mit dieser Datei ueberhaupt startet (eine Sicherung von vor einer Schemaaenderung kann heute an einer Wanderung scheitern, und niemand haette es gemerkt) * dass man sich danach anmelden kann * dass die Seiten Inhalte zeigen statt leerer Geruste * dass die Daten VOLLSTAENDIG sind Eine Sicherung, die man nie zurueckgespielt hat, ist eine Hoffnung. Genau das ist der Satz, den man nach einem Plattenausfall nicht mehr sagen moechte. --------------------------------------------------------------------- WAS DIESE PROBE TUT 1. nimmt die JUENGSTE Sicherung, kopiert sie in ein Wegwerf- Verzeichnis (die Sicherung selbst wird nie angefasst) 2. zaehlt die Zeilen jeder wichtigen Tabelle VOR der Wanderung 3. startet die echte Anwendung gegen die Kopie -- damit laufen alle Schemawanderungen wirklich durch 4. zaehlt danach noch einmal: keine Zeile darf verschwunden sein 5. legt in der KOPIE einen Zugang an und meldet sich damit an 6. ruft JEDE Seite auf und misst, ob wirklich Inhalt da ist 7. raeumt alles weg --------------------------------------------------------------------- DREI AUSGAENGE, NICHT ZWEI (Regel aus CLAUDE.md, 03.09.2026) 0 in Ordnung -- die Sicherung laesst sich zurueckspielen 1 NICHT in Ordnung -- hier ist wirklich etwas kaputt 2 konnte nicht nachsehen -- keine Sicherung da, Playwright fehlt. Das ist KEIN Erfolg und KEIN Fehler, und es muss anders aussehen als beides. Eine Probe, die ihre Voraussetzung nicht hat und trotzdem "ok" meldet, ist schlimmer als gar keine. --------------------------------------------------------------------- IN EIN LIVE-SYSTEM KOMMEN KEINE TESTDATEN (Regel vom 04.09.2026) Der Zugang aus Schritt 5 entsteht ausschliesslich in der WEGWERF- KOPIE unter %TEMP%. Weder der Server noch die Sicherung selbst werden beschrieben. Am Ende wird das Verzeichnis geloescht. AUFRUF: node tools/wiederherstellung-proben.mjs ===================================================================== */ import { readdirSync, statSync, copyFileSync, mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs"; import { tmpdir, homedir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; import { DatabaseSync } from "node:sqlite"; import { scryptSync, randomBytes } from "node:crypto"; const HIER = dirname(fileURLToPath(import.meta.url)); const WURZEL = join(HIER, ".."); const SICHERUNGEN = join(homedir(), "Documents", "Obelix", "Sicherungen", "workspace"); const PORT = 4471; let fehler = 0; let gemacht = 0; const ok = (b, t) => { gemacht++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; /** Der dritte Ausgang. Mit Grund UND Anleitung -- eine Meldung "geht * nicht" ohne Angabe, was zu tun waere, laesst genau den Zustand * bestehen, den sie meldet. */ function kannNichtNachsehen(grund, anleitung) { console.log("\n╔══════════════════════════════════════════════════════════"); console.log("║ KONNTE NICHT NACHSEHEN"); console.log("║ " + grund); console.log("║"); console.log("║ " + anleitung); console.log("╚══════════════════════════════════════════════════════════"); process.exit(2); } /* --------------------------------------------------------------------- 1. Die juengste Sicherung finden --------------------------------------------------------------------- */ console.log("=== Welche Sicherung wird geprobt? ==="); if (!existsSync(SICHERUNGEN)) { kannNichtNachsehen( `Das Sicherungsverzeichnis gibt es nicht: ${SICHERUNGEN}`, "Einmalig einrichten mit: powershell -ExecutionPolicy Bypass -File tools\\sicherung-einrichten.ps1"); } const kandidaten = readdirSync(SICHERUNGEN) .filter((n) => n.endsWith(".db")) .map((n) => ({ name: n, pfad: join(SICHERUNGEN, n), zeit: statSync(join(SICHERUNGEN, n)).mtimeMs })) /* Nach dem DATUM IM NAMEN, nicht nach der Dateizeit: scp setzt beim Abholen bei allen Dateien dieselbe Zeit, und dann waere "die juengste" reiner Zufall. Im Namen steht, welcher Stand es ist. */ .map((k) => ({ ...k, stand: (k.name.match(/(\d{4}-\d{2}-\d{2})/) || [])[1] || "" })) .sort((a, b) => (a.stand < b.stand ? 1 : a.stand > b.stand ? -1 : b.zeit - a.zeit)); if (!kandidaten.length) { kannNichtNachsehen( `In ${SICHERUNGEN} liegt keine einzige .db-Datei.`, "Einmal von Hand holen mit: bash tools/sicherung-holen.sh"); } const gewaehlt = kandidaten[0]; console.log(` ${kandidaten.length} Sicherungen vorhanden, geprobt wird: ${gewaehlt.name}`); console.log(` Stand ${gewaehlt.stand}, ${Math.round(statSync(gewaehlt.pfad).size / 1024)} KB`); /* WIE ALT DARF SIE SEIN? Eine Sicherung von vor drei Wochen ist im Ernstfall drei Wochen Arbeit, die weg ist. Das ist kein Fehler dieser Probe -- aber es muss dastehen. */ const tage = Math.floor((Date.now() - Date.parse(gewaehlt.stand + "T12:00:00")) / 86400000); ok(Number.isFinite(tage) && tage <= 3, `die juengste Sicherung ist ${tage} Tage alt (bis 3 in Ordnung)`); /* --------------------------------------------------------------------- 2. Kopieren -- die Sicherung selbst bleibt unberuehrt --------------------------------------------------------------------- */ const arbeit = mkdtempSync(join(tmpdir(), "wiederherstellung-")); const kopie = join(arbeit, "workspace.db"); copyFileSync(gewaehlt.pfad, kopie); const vorherGroesse = statSync(kopie).size; ok(vorherGroesse === statSync(gewaehlt.pfad).size, `die Kopie ist vollstaendig (${vorherGroesse} Byte)`); /* --------------------------------------------------------------------- 2b. UND DIE DATEIEN. Eine Datenbank allein ist keine Wiederherstellung. Beim ersten Lauf dieser Probe (06.09.2026) blieb genau eine von achtzehn Seiten rot: der Steckbrief, mit einem 404 in der Konsole. Das war kein Fehler der Sicherung, sondern ihre wichtigste Aussage -- das Profilbild lag NICHT in der Datenbank, sondern daneben im Dateisystem. Wer nur die .db zurueckspielt, bekommt eine Anwendung, die laeuft, und eine Bibliothek voller Verweise auf Dateien, die es nicht mehr gibt. sicherung-holen.sh holt diese Ordner mit (dateien/, wissen/). Genau deshalb gehoeren sie auch in die Probe: Sonst beweist sie die Wiederherstellung nur zur Haelfte -- und ausgerechnet die Haelfte, die man im Ernstfall nicht nachbauen kann. */ function ordnerKopieren(von, nach) { if (!existsSync(von)) return 0; mkdirSync(nach, { recursive: true }); let n = 0; for (const e of readdirSync(von, { withFileTypes: true })) { const q = join(von, e.name), z = join(nach, e.name); if (e.isDirectory()) n += ordnerKopieren(q, z); else { copyFileSync(q, z); n++; } } return n; } /* Kopiert wird ALLES, was in der Sicherung als Ordner liegt -- nicht eine Liste von Hand. Kommt morgen ein vierter Datenordner dazu, ist er hier von selbst dabei. */ let dateienZahl = 0; const gesichert = readdirSync(SICHERUNGEN, { withFileTypes: true }) .filter((e) => e.isDirectory()).map((e) => e.name); for (const o of gesichert) { dateienZahl += ordnerKopieren(join(SICHERUNGEN, o), join(arbeit, o)); } ok(dateienZahl > 0, dateienZahl ? `${dateienZahl} hochgeladene Dateien mit zurueckgespielt ` + `(${gesichert.join(", ")})` : "KEINE Dateien in der Sicherung -- Bilder und PDFs waeren nach einem " + "Plattenausfall weg, auch wenn die Datenbank vollstaendig ist"); /* --------------------------------------------------------------------- DIE ENTSCHEIDENDE FRAGE: Sichert das Skript ALLE Ordner, die der Server ueberhaupt benutzt? Genau hier lag die Luecke. sicherung-holen.sh nannte dateien/ und wissen/; die Profilbilder liegen in einem dritten Ordner und wurden nie mitgeholt. Die Sicherung meldete trotzdem jeden Tag "in Ordnung" -- sie hat ja nie behauptet, vollstaendig zu sein. Eine Liste von Hand kann das nicht verhindern; sie altert genau so still wie die, die den Fehler verursacht hat. Deshalb wird hier NACHGEZAEHLT: Welche Ordner legt der Server an (aus dem Quelltext), und welche holt das Skript? Was fehlt, wird gemeldet. */ { const server = join(WURZEL, "server"); const benutzt = new Set(); for (const datei of readdirSync(server).filter((n) => /^workspace.*\.js$/.test(n))) { const text = (await import("node:fs")).readFileSync(join(server, datei), "utf8"); for (const t of text.matchAll(/join\(DATEN_ORDNER,\s*"([^"]+)"\)/g)) benutzt.add(t[1]); } /* sicherungen/ ist die Sicherung selbst -- die holt das Skript als .db-Dateien und nicht als Ordner. */ benutzt.delete("sicherungen"); const skript = (await import("node:fs")).readFileSync(join(HIER, "sicherung-holen.sh"), "utf8"); const geholt = (skript.match(/^ORDNER="([^"]+)"/m) || [])[1]?.split(/\s+/) || []; ok(benutzt.size > 0, `der Server benutzt ${benutzt.size} Datenordner: ${[...benutzt].join(", ")}`); const fehlend = [...benutzt].filter((o) => !geholt.includes(o)); ok(fehlend.length === 0, fehlend.length ? `NICHT GESICHERT: ${fehlend.join(", ")} -- diese Ordner legt der Server an, ` + `tools/sicherung-holen.sh holt sie aber nicht. Nach einem Plattenausfall ` + `waeren sie weg, ohne dass die taegliche Meldung je etwas gesagt haette.` : `alle ${benutzt.size} holt tools/sicherung-holen.sh (${geholt.join(", ")})`); /* GEGENPROBE: Erkennt der Vergleich einen fehlenden Ordner ueberhaupt? Ohne sie bewiese "nichts fehlt" nur, dass die Suche nichts findet. GEZAEHLT WIRD DIE DIFFERENZ, NICHT DIE SUMME (berichtigt am 23.09.2026). Hier stand `=== 1` -- und das setzte voraus, dass sonst nichts fehlt. An dem Tag, an dem wirklich etwas fehlte (material), wurde die Gegenprobe rot und zeigte damit auf sich selbst statt auf den Fund: zwei Meldungen, eine Ursache, und die zweite lenkt von der ersten ab. Eine Gegenprobe soll beweisen, dass der Vergleich REAGIERT. Das tut sie, wenn ein zusaetzlicher Ordner die Zahl um genau eins erhoeht -- unabhaengig davon, wie sie vorher stand. */ const fehlendJetzt = fehlend.length; ok([...benutzt, "erfundenerordner"].filter((o) => !geholt.includes(o)).length === fehlendJetzt + 1, `Gegenprobe: ein erfundener Ordner WIRD als fehlend erkannt (${fehlendJetzt} -> ${fehlendJetzt + 1})`); } /* --------------------------------------------------------------------- 3. Zaehlen, was drin ist -- VOR der Wanderung --------------------------------------------------------------------- */ console.log("\n=== Was steht in der Sicherung? ==="); /* Die Tabellen, deren Verlust wirklich weh taete. Ein Zaehlwerk ueber ALLE Tabellen waere hier falsch: Sitzungen und Protokolle wachsen und schrumpfen von allein, ein Rueckgang dort sagt nichts. */ const WICHTIG = ["personen", "aufgaben", "termine", "protokolle", "startcheck", "bereich_punkte", "dateien", "wissen", "creator_profile", "leads"]; function zaehlen(pfad) { const d = new DatabaseSync(pfad, { readOnly: true }); const da = new Set(d.prepare( "SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name)); const raus = {}; for (const t of WICHTIG) { if (!da.has(t)) { raus[t] = null; continue; } // Tabelle gab es damals noch nicht raus[t] = d.prepare(`SELECT COUNT(*) AS n FROM "${t}"`).get().n; } raus._tabellen = da.size; d.close(); return raus; } const vorher = zaehlen(kopie); for (const t of WICHTIG) { const n = vorher[t]; console.log(` ${t.padEnd(18)} ${n === null ? "(Tabelle gab es noch nicht)" : n}`); } ok(vorher.personen > 0, `es sind ${vorher.personen} Personen drin -- ohne die kaeme niemand hinein`); ok(vorher._tabellen >= 10, `${vorher._tabellen} Tabellen sind angelegt`); /* --------------------------------------------------------------------- 4. Die Anwendung gegen die Kopie starten --------------------------------------------------------------------- */ console.log("\n=== Startet die Anwendung damit? ==="); /* NUR WORKSPACE_DB wird gesetzt -- und das genuegt. Die Anwendung leitet ihren gesamten Datenordner davon ab (`DATEN_ORDNER = dirname(DB_PFAD)` in workspace.js). Dateiablage, Bibliothek und die eigenen Sicherungen landen damit alle im Wegwerf-Verzeichnis. Es gibt bewusst KEINE eigenen Umschalter dafuer; welche zu erfinden hiesse, sich auf Variablen zu verlassen, die es nicht gibt -- sie waeren wirkungslos und man merkte es nie. */ const umgebung = { ...process.env, WORKSPACE_DB: kopie, PORT: String(PORT), SITE_ACCESS_SECRET: "wiederherstellungsprobe", SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00", }; const kind = spawn(process.execPath, [join(WURZEL, "server", "index.js")], { env: umgebung, stdio: ["ignore", "pipe", "pipe"], }); let ausgabe = ""; kind.stdout.on("data", (d) => { ausgabe += d; }); kind.stderr.on("data", (d) => { ausgabe += d; }); let gestartet = false; for (let i = 0; i < 80; i++) { await new Promise((r) => setTimeout(r, 250)); if (kind.exitCode !== null) break; try { const a = await fetch(`http://127.0.0.1:${PORT}/workspace/`, { redirect: "manual" }); if (a.status < 500) { gestartet = true; break; } } catch { /* noch nicht oben */ } } ok(gestartet, gestartet ? "die Anwendung laeuft mit der zurueckgespielten Datenbank" : `die Anwendung startete NICHT (Exitcode ${kind.exitCode})`); /* Eine Wanderung, die scheitert, schreibt nach stderr und laesst die Anwendung trotzdem laufen -- deshalb wird die Ausgabe mitgelesen. */ const wanderungen = (ausgabe.match(/\[workspace\] .*(ergaenzt|angelegt|umgebaut)/g) || []); console.log(` ${wanderungen.length} Schemawanderungen liefen durch` + (wanderungen.length ? ":\n " + wanderungen.slice(0, 6).join("\n ") : "")); const schlimm = (ausgabe.match(/^.*(SQLITE_ERROR|no such column|no such table|UNIQUE constraint).*$/gm) || []); ok(schlimm.length === 0, schlimm.length ? `Datenbankfehler beim Start: ${schlimm.slice(0, 3).join(" | ")}` : "beim Start kam kein einziger Datenbankfehler"); if (!gestartet) { console.log("\n" + ausgabe.slice(-2000)); try { kind.kill(); } catch { /* schon weg */ } try { rmSync(arbeit, { recursive: true, force: true }); } catch { /* egal */ } console.log(`\n${gemacht} Pruefungen\n${fehler} FEHLER`); process.exit(1); } /* --------------------------------------------------------------------- 5. Ist nach der Wanderung noch alles da? --------------------------------------------------------------------- */ console.log("\n=== Ist nach der Wanderung noch alles da? ==="); const nachher = zaehlen(kopie); let verloren = []; for (const t of WICHTIG) { if (vorher[t] === null) continue; if (nachher[t] === null || nachher[t] < vorher[t]) { verloren.push(`${t}: ${vorher[t]} → ${nachher[t]}`); } } ok(verloren.length === 0, verloren.length ? `Zeilen sind bei der Wanderung verschwunden: ${verloren.join(", ")}` : `keine Zeile ist verlorengegangen (${WICHTIG.filter((t) => vorher[t] !== null).length} Tabellen verglichen)`); ok(nachher._tabellen >= vorher._tabellen, `${nachher._tabellen} Tabellen nach der Wanderung (vorher ${vorher._tabellen})`); /* GEGENPROBE zum Zaehlwerk: Wenn es funktioniert, muss es einen kuenstlich herbeigefuehrten Verlust auch MELDEN. Ein Zaehler, der nur "alles gut" sagen kann, sagt gar nichts. */ { const erfunden = { ...vorher, personen: vorher.personen + 5 }; const meldet = WICHTIG.some((t) => erfunden[t] !== null && nachher[t] !== null && nachher[t] < erfunden[t]); ok(meldet, "Gegenprobe: ein herbeigefuehrter Verlust WIRD erkannt"); } /* --------------------------------------------------------------------- 6. Kann man damit arbeiten? --------------------------------------------------------------------- */ console.log("\n=== Kann man sich anmelden und arbeiten? ==="); /* Der Zugang entsteht NUR in der Wegwerf-Kopie. Der echte Code steht in keiner Sicherung im Klartext (er ist gehasht), also legt die Probe sich einen eigenen an -- sonst koennte sie nur die Anmeldeseite ansehen und nichts dahinter. */ const CODE = "PROBE-" + randomBytes(6).toString("hex").toUpperCase(); { const d = new DatabaseSync(kopie); const salz = randomBytes(16).toString("hex"); const hash = scryptSync(CODE, salz, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare(`INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES ('Wiederherstellungsprobe','admin',?,?,?,1,?)`) .run(hash, salz, 32768, new Date().toISOString()); d.close(); } let pw = null; try { pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); } catch { /* nicht da */ } if (!pw) { try { kind.kill(); } catch { /* schon weg */ } try { rmSync(arbeit, { recursive: true, force: true }); } catch { /* egal */ } kannNichtNachsehen( "Playwright ist nicht auffindbar -- der Teil 'kann man damit arbeiten' konnte nicht laufen.\n" + `║ Die Datenbankproben liefen aber durch: ${gemacht} Pruefungen, ${fehler} Fehler.`, "Erwartet unter C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright"); } const browser = await pw.chromium.launch(); const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); const seite = await ctx.newPage(); const BASIS = `http://127.0.0.1:${PORT}`; await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); await seite.click('.rolle[data-rolle="admin"]'); await seite.fill("#code", CODE); await seite.click("button[type=submit]"); let angemeldet = true; try { await seite.waitForURL("**/start.html", { timeout: 25000 }); } catch { angemeldet = false; } ok(angemeldet, angemeldet ? "die Anmeldung mit der zurueckgespielten Datenbank geht" : "die ANMELDUNG SCHEITERT -- die Sicherung waere im Ernstfall nutzlos"); /* GEGENPROBE: Ein falscher Code darf NICHT hineinlassen. Ohne sie bewiese die Zeile darueber nur, dass irgendetwas weiterleitet. */ { const c2 = await browser.newContext(); const s2 = await c2.newPage(); await s2.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); await s2.click('.rolle[data-rolle="admin"]'); await s2.fill("#code", "PROBE-FALSCH-FALSCH"); await s2.click("button[type=submit]"); await s2.waitForTimeout(2500); ok(!/start\.html/.test(s2.url()), `ein falscher Code kommt nicht hinein (${s2.url().split("/").pop()})`); await c2.close(); } if (angemeldet) { /* JEDE Seite, aus dem Verzeichnis gelesen -- nicht aus einer Liste von Hand. Eine neue Seite ist damit von selbst dabei. */ const seiten = readdirSync(join(WURZEL, "workspace")) .filter((n) => n.endsWith(".html") && n !== "index.html").sort(); console.log(` ${seiten.length} Seiten werden aufgerufen`); let kaputte = []; for (const s of seiten) { const konsole = []; const auf = (m) => { if (m.type() === "error") konsole.push(m.text().slice(0, 90)); }; seite.on("console", auf); const antwort = await seite.goto(BASIS + "/workspace/" + s, { waitUntil: "networkidle" }); await seite.waitForTimeout(500); /* "Die Seite laedt" reicht nicht -- ein leeres Geruest laedt auch. Gemessen wird, ob wirklich Text im Hauptbereich steht. */ const inhalt = await seite.evaluate(() => (document.querySelector("main")?.innerText || "").trim().length); seite.off("console", auf); const gut = antwort?.status() === 200 && inhalt > 40 && konsole.length === 0; if (!gut) kaputte.push(`${s} (HTTP ${antwort?.status()}, ${inhalt} Zeichen` + (konsole.length ? `, Konsole: ${konsole[0]}` : "") + ")"); } ok(seiten.length > 10 && kaputte.length === 0, kaputte.length ? `${kaputte.length} von ${seiten.length} Seiten sind kaputt:\n ${kaputte.join("\n ")}` : `alle ${seiten.length} Seiten zeigen Inhalt`); /* Und die DATEN muessen wirklich da sein, nicht nur die Geruste. Genau hier liegt der Unterschied zwischen "die Anwendung startet" und "die Sicherung ist etwas wert". */ const daten = await seite.evaluate(async () => { const holen = async (w) => { try { const a = await fetch(w, { credentials: "same-origin" }); return a.ok ? await a.json() : null; } catch { return null; } }; const p = await holen("/workspace/api/personen"); const a = await holen("/workspace/api/aufgaben"); return { personen: Array.isArray(p?.personen) ? p.personen.length : null, aufgaben: Array.isArray(a?.aufgaben) ? a.aufgaben.length : null, }; }); /* Verglichen wird gegen die AKTIVEN. Die Schnittstelle liefert nur `aktiv = 1`; gegen die Gesamtzahl aus der Sicherung zu pruefen hiesse, bei jeder je abgeschalteten Person Alarm zu schlagen -- eine Warnung, die immer kommt, ist keine mehr. Die eine Person dieser Probe zaehlt hier mit und wird abgezogen. */ let aktive = 0; { const dd = new DatabaseSync(kopie, { readOnly: true }); aktive = dd.prepare("SELECT COUNT(*) AS n FROM personen WHERE aktiv = 1").get().n - 1; dd.close(); } ok(daten.personen !== null && daten.personen - 1 === aktive, `die Anwendung liefert alle ${aktive} aktiven Personen aus ` + `(${vorher.personen} in der Sicherung, davon ${vorher.personen - aktive} abgeschaltet)`); ok(daten.aufgaben !== null, `und ${daten.aufgaben} Aufgaben`); } await ctx.close(); await browser.close(); /* --------------------------------------------------------------------- 7. Aufraeumen --------------------------------------------------------------------- */ try { kind.kill(); } catch { /* schon weg */ } await new Promise((r) => setTimeout(r, 400)); try { rmSync(arbeit, { recursive: true, force: true }); } catch { /* Windows haelt manchmal fest */ } /* Der Beleg. Ohne ihn weiss beim naechsten Mal niemand, wann zuletzt wirklich zurueckgespielt wurde -- und "wir haben eine Sicherung" ist dann wieder nur ein Satz. */ const beleg = join(SICHERUNGEN, "wiederherstellung-probe.txt"); const zeile = `${new Date().toISOString().slice(0, 16).replace("T", " ")} ` + `${fehler === 0 ? "ok " : "FEHL"} ${gewaehlt.name} ` + `${gemacht} Pruefungen, ${fehler} Fehler, ${vorher.personen} Personen\n`; try { const alt = existsSync(beleg) ? (await import("node:fs")).readFileSync(beleg, "utf8") : ""; writeFileSync(beleg, alt + zeile, "utf8"); console.log(`\nEingetragen in ${beleg}`); } catch { console.log("\nHinweis: der Beleg liess sich nicht schreiben."); } console.log(`\n${gemacht} Pruefungen`); console.log(fehler === 0 ? "DIE SICHERUNG LAESST SICH ZURUECKSPIELEN." : `${fehler} FEHLER — die Sicherung ist NICHT verlaesslich.`); process.exit(fehler ? 1 : 0);