BEFUND. Seit der Chat am 06.09. dazukam, LIEF DER GESAMTE
REGRESSIONSLAUF NICHT MEHR DURCH. Nicht "er wurde rot" -- er blieb
einfach stehen, bei Datei 3 von 62, ohne Fehlermeldung, ohne FEHL, ohne
Absturz. Nach zwoelf Minuten stand er immer noch dort. Von aussen sieht
"noch nicht fertig" genauso aus wie "haengt fuer immer"; deshalb ist
mir das gestern nicht aufgefallen, sondern erst, als ich den Lauf
gezielt beobachtet habe.
DIE URSACHE. pruef-alle-wege.mjs geht stumpf ueber ALLE 134
Schnittstellen und liest jede Antwort mit `await a.text()` aus. Der
Chat haelt seine Verbindung aber absichtlich offen und schickt neue
Nachrichten hinein, solange jemand zusieht (SSE). `text()` wartet, bis
der Server fertig ist -- und der wird nie fertig. Angemeldet als
DogFather trat der Lauf dort ein und kam nicht wieder heraus.
DIE ABHILFE, zwei Teile, die zusammengehoeren:
* Jeder Ruf hat jetzt eine Frist von 8 s. Laeuft sie ab, ist das ein
ERGEBNIS ("hing") und kein Absturz. Ein neuer Abschnitt meldet am
Ende, WELCHER Weg nicht geantwortet hat -- statt dass der Lauf
wortlos stehenbleibt.
* Bekannte Stroeme stehen in einer Liste MIT BEGRUENDUNG und werden
nicht uebersprungen, sondern anders geprueft: verbinden, Status
ablesen, abbrechen. Die Schranke wird damit genauso gemessen wie
ueberall. Zusaetzlich wird nachgemessen, dass ein eingetragener
Strom auch wirklich offen bleibt -- sonst verdeckte die Ausnahme
nur seine Inhaltspruefung.
Der "hing"-Zustand musste eigens gesammelt werden: In Abschnitt 1 haette
er ausgesehen wie "ohne Anmeldung erreichbar", in Abschnitt 2 waere er
ganz durchgefallen (`"hing" >= 500` ist false, Zeichenkette gegen Zahl).
Genau so verschwinden Befunde.
Ergebnis: 134 Schnittstellen, 532 Aufrufe, alles gruen, kein Haenger.
AUSSERDEM, gefunden beim Nachsehen:
* pruef-handy.mjs pruefte 15 Seiten -- aus einer Liste von Hand, die
veraltet war. chat.html, leistung.html und steckbrief.html standen
nicht darin: DREI von neunzehn Seiten waren nie auf einem Handy
gemessen worden, ausgerechnet der Chat. Die Liste kommt jetzt aus
dem Verzeichnis, die naechste neue Seite ist von selbst dabei.
* chat.html und leistung.html fehlte <link rel="manifest">. Auf dem
Handy heisst das: Wer die App installiert hat und ueber eine
Benachrichtigung dort landet, verlaesst den App-Rahmen -- die Seite
oeffnet im Browser, mit falscher Leistenfarbe. Ihre theme-color war
ausserdem eine andere als auf allen uebrigen Seiten. Beides behoben
UND als Pruefung in pruef-struktur nachgetragen, damit es beim
naechsten Mal nicht am Gedaechtnis haengt.
NEU: tools/wiederherstellung-proben.mjs — die Probe aufs Exempel.
Die Sicherung ausserhalb des Servers laeuft taeglich und prueft
`integrity_check`. Das sagt: die Datei ist nicht zerschossen. Es sagt
NICHT, ob die Anwendung damit startet, ob man sich anmelden kann und ob
die Daten vollstaendig sind. Eine Sicherung, die man nie zurueckgespielt
hat, ist eine Hoffnung. Das Werkzeug kopiert die juengste Sicherung in
ein Wegwerf-Verzeichnis, startet die echte Anwendung dagegen (damit
laufen alle Schemawanderungen wirklich durch), zaehlt vorher und
nachher, meldet sich an und ruft jede Seite auf. Drei Ausgaenge, nicht
zwei -- "konnte nicht nachsehen" ist weder Erfolg noch Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
407 lines
18 KiB
JavaScript
407 lines
18 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, 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)`);
|
|
|
|
/* ---------------------------------------------------------------------
|
|
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);
|