Files
dogfather-universe/tools/wiederherstellung-proben.mjs
T
DogFatherGitandClaude Opus 5 70039714c5 Die Sicherung war zwei Loecher gross -- gefunden, weil sie zum ersten Mal wirklich zurueckgespielt wurde
"Wir haben eine Sicherung" war bis heute ein Satz, kein Nachweis. Die
taegliche Kopie ausserhalb des Servers prueft `PRAGMA integrity_check`
und meldet seit dem 31.08. jeden Tag "in Ordnung". Das heisst: die Datei
ist nicht zerschossen. Es heisst NICHT, dass man damit weiterarbeiten
kann.

tools/wiederherstellung-proben.mjs geht den Weg jetzt wirklich: juengste
Sicherung in ein Wegwerf-Verzeichnis kopieren, die echte Anwendung
dagegen starten (damit laufen alle Schemawanderungen durch), vorher und
nachher zaehlen, anmelden, jede Seite aufrufen. Drei Ausgaenge, nicht
zwei -- "konnte nicht nachsehen" ist weder Erfolg noch Fehler.

BEIM ERSTEN LAUF BLIEB GENAU EINE VON ACHTZEHN SEITEN ROT: der
Steckbrief, mit einem 404 fuer ein Bild. Daran hingen zwei echte Luecken:

  1. PROFILBILDER WAREN NIE GESICHERT. sicherung-holen.sh holte
     dateien/ und wissen/ -- die Bilder liegen aber in einem dritten
     Ordner (profilbilder/, siehe workspace-steckbrief.js). Nach einem
     Plattenausfall waeren alle Profilbilder weg gewesen, waehrend die
     taegliche Meldung weiter "in Ordnung" sagte. Sie hat ja nie
     behauptet, vollstaendig zu sein.

  2. DIE RUECKMELDUNG AN DEN SERVER LIEF INS LEERE. Das Skript meldete
     sich bei dogfather-universe.com/workspace/... -- seit dem Umzug am
     06.09. antwortet diese Adresse mit 410 "Gone". Der Server erfuhr
     also seit dem Umzug nicht mehr, dass die Kopie laeuft; auf der
     Automationen-Seite waere sie mit jedem Tag aelter erschienen,
     obwohl sie taeglich lief. Ein 410 wird jetzt eigens gemeldet -- es
     heisst etwas anderes als "Netz kaputt", naemlich "die Adresse
     stimmt nicht mehr".

Beides behoben. Und damit es kein drittes Mal passiert, vergleicht die
Probe die Ordnerliste des Sicherungsskripts gegen die Ordner, die der
Server im Quelltext wirklich benutzt (`join(DATEN_ORDNER, "...")`). Wer
morgen einen vierten Datenordner anlegt und ihn nicht sichert, bekommt
hier einen Fehler statt eines stillen Lochs -- mit Gegenprobe, dass der
Vergleich einen fehlenden Ordner auch wirklich erkennt.

Ergebnis nach den Reparaturen: 18 Pruefungen, alle gruen,
"DIE SICHERUNG LAESST SICH ZURUECKSPIELEN." Der Lauf traegt sich mit
Datum in die Sicherungsablage ein -- sonst weiss beim naechsten Mal
niemand, wann zuletzt wirklich zurueckgespielt wurde.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 23:20:10 +02:00

490 lines
22 KiB
JavaScript

/* =====================================================================
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. */
ok([...benutzt, "erfundenerordner"].filter((o) => !geholt.includes(o)).length === 1,
"Gegenprobe: ein erfundener Ordner WIRD als fehlend erkannt");
}
/* ---------------------------------------------------------------------
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);