Bisher wurde nur EINMAL gesichert: direkt vor einer Umstellung der
Datenbank. Faellt die Platte aus, ist alles weg -- die komplette
Betreuung, jede Aufgabe, jedes Protokoll. Das war das einzige Risiko im
System, das auf einen Schlag ALLES kostet.
Beim Nachsehen auf dem Server kam ein zweiter, schwerwiegenderer Befund
dazu:
workspace.db 4.096 B
workspace.db-wal 2.101.232 B
Im WAL-Betrieb landen Schreibvorgaenge zuerst im -wal und wandern erst
beim Checkpoint in die Hauptdatei. Der lief seit dem ersten Tag nicht.
Wer also workspace.db kopiert haette -- der naheliegendste Sicherungsweg
ueberhaupt -- haette eine LEERE Datenbank gesichert und es nicht gemerkt.
Der Test stellt genau das nach: aus der reinen Dateikopie waren 0 von 200
Aufgaben lesbar.
Deshalb:
- Nie eine Dateikopie. Immer VACUUM INTO -- SQLite schreibt selbst, liest
das WAL mit, Ergebnis ist in sich stimmig auch waehrend Schreibzugriffen.
- Vor jeder Sicherung ein erzwungener Checkpoint (TRUNCATE). Haelt die
Hauptdatei aktuell und das WAL klein.
- Jede frische Sicherung wird SOFORT wieder geoeffnet und geprueft: auf
Unversehrtheit UND darauf, dass Personen darin stehen -- gegen genau den
Fall einer technisch einwandfreien, aber leeren Datei. Faellt sie durch,
wird sie geloescht statt behalten.
- Erst unter Zwischennamen schreiben, dann pruefen, dann umbenennen. Sonst
stuende nach einem Abbruch eine halbe Datei unter dem richtigen Namen.
- Aufbewahrung 7 taeglich / 4 woechentlich / 6 monatlich, je Art getrennt
aufgeraeumt, damit taegliche nie die monatlichen verdraengen.
Laeuft IM Prozess, nicht als Systemdienst: keine Aenderung an Systemdateien
noetig, die Datenbank ist ohnehin offen, und es kommt mit jedem git pull
mit. Statt setTimeout auf 24 h wird viertelstuendlich geprueft, ob fuer
HEUTE schon eine liegt -- das ueberlebt Neustarts und Ausfaelle.
Dazu eine Zustandsansicht auf der Automationen-Seite: wann zuletzt, wie
gross, wie viele Personen darin, wie viel wartet noch im WAL, wie viel
Platz ist frei. Plus "Jetzt sichern" von Hand. Nur Leitung, 404 statt 403
fuer alle anderen.
Geprueft (server/pruef-sicherung.mjs, 27 Pruefungen) einschliesslich des
Ernstfalls: Sicherung schreiben, Datenbank zerstoeren, zurueckspielen,
Vollstaendigkeit und Schreibbarkeit nachweisen. Eine ungetestete Sicherung
ist eine Vermutung.
Co-Authored-By: Claude Opus 5 <[email protected]>
124 lines
5.9 KiB
JavaScript
124 lines
5.9 KiB
JavaScript
/* Browserpruefung des Sicherungs-Kastens auf der Automationen-Seite.
|
|
Geprueft wird vor allem: sieht man ueberhaupt, WANN zuletzt gesichert
|
|
wurde, funktioniert der Knopf, und verschwindet der Kasten fuer Rollen,
|
|
die davon nichts wissen sollen. */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-zustand-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = "4182";
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = "http://127.0.0.1:4182";
|
|
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 180_000).unref?.();
|
|
|
|
let fehler = 0;
|
|
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 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 (?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768, jetzt);
|
|
}
|
|
anlegen("Dogfather", "admin", "CODE-DOGI-0001");
|
|
anlegen("Ein Manager", "manager", "CODE-MANA-0001");
|
|
d.close();
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
|
|
async function seiteFuer(kontext, rolle, code) {
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await seite.fill("#code", code);
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.goto(BASIS + "/workspace/automation.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(600);
|
|
return seite;
|
|
}
|
|
|
|
const lesen = (seite) => seite.evaluate(() => ({
|
|
da: !document.getElementById("zustand-kasten")?.hidden,
|
|
zustand: document.getElementById("zustand-kasten")?.dataset.zustand,
|
|
titel: document.getElementById("zustand-titel")?.textContent,
|
|
text: document.getElementById("zustand-text")?.textContent,
|
|
zeilen: [...document.querySelectorAll("#zustand-daten li")].map((l) => l.textContent),
|
|
sicherungen: document.querySelectorAll("#sicherungen .verlauf__zeile").length,
|
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
}));
|
|
|
|
for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) {
|
|
console.log(`\n=== DogFather · ${wie}`);
|
|
const kontext = await browser.newContext({ viewport: { width: breite, height: 950 } });
|
|
const konsole = [];
|
|
const seite = await seiteFuer(kontext, "admin", "CODE-DOGI-0001");
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
|
|
|
const vorher = await lesen(seite);
|
|
ok(vorher.da, "der Kasten ist da");
|
|
/* Nur im ERSTEN Durchlauf ist noch nichts gesichert -- danach liegt die
|
|
Sicherung aus dem Durchlauf davor da. Wer das nicht unterscheidet,
|
|
baut sich einen Test, der beim zweiten Mal grundlos rot wird. */
|
|
if (breite === 1440) {
|
|
ok(/Noch keine Sicherung/.test(vorher.titel || ""),
|
|
`sagt ehrlich, dass noch nichts lief ("${vorher.titel}")`);
|
|
} else {
|
|
ok(/Zuletzt gesichert/.test(vorher.titel || ""),
|
|
`die Sicherung aus dem ersten Durchlauf steht noch da ("${vorher.titel}")`);
|
|
}
|
|
ok(vorher.ueber <= 0, `nichts steht über (${vorher.ueber}px)`);
|
|
ok(vorher.zeilen.some((z) => /WAL/.test(z)), "die wartenden Schreibvorgänge stehen da");
|
|
|
|
/* Der Knopf ist der eigentliche Test: Er muss wirklich sichern. */
|
|
await seite.click("#jetzt-sichern");
|
|
await seite.waitForTimeout(1500);
|
|
const nachher = await lesen(seite);
|
|
console.log(" ->", nachher.titel, "|", nachher.text);
|
|
ok(/Zuletzt gesichert/.test(nachher.titel || ""), "nach dem Knopfdruck steht ein Zeitpunkt da");
|
|
ok(/Personen/.test(nachher.text || ""), "und wie viele Personen darin stehen");
|
|
ok(nachher.zustand === "gut", `Zustand ist gut (${nachher.zustand})`);
|
|
ok(nachher.sicherungen >= 1, `die Sicherung taucht in der Liste auf (${nachher.sicherungen})`);
|
|
ok(nachher.ueber <= 0, `auch danach steht nichts über (${nachher.ueber}px)`);
|
|
ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
|
|
|
|
if (breite === 1440) await seite.screenshot({ path: "pruef-zustand.png", fullPage: true });
|
|
await kontext.close();
|
|
}
|
|
|
|
console.log("\n=== Manager darf es auch sehen");
|
|
{
|
|
const kontext = await browser.newContext({ viewport: { width: 1440, height: 950 } });
|
|
const seite = await seiteFuer(kontext, "manager", "CODE-MANA-0001");
|
|
const r = await lesen(seite);
|
|
ok(r.da && /Zuletzt gesichert/.test(r.titel || ""), `Manager sieht den Zustand ("${r.titel}")`);
|
|
await kontext.close();
|
|
}
|
|
|
|
await browser.close();
|
|
console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler === 0 ? 0 : 1);
|