diff --git a/server/pruef-agentur.mjs b/server/pruef-agentur.mjs index b0ce5ed9..6dee3e8d 100644 --- a/server/pruef-agentur.mjs +++ b/server/pruef-agentur.mjs @@ -26,7 +26,7 @@ schlimmere Ausgang. ===================================================================== */ -import { mkdtempSync, rmSync, appendFileSync, writeFileSync } from "node:fs"; +import { mkdtempSync, rmSync, appendFileSync, writeFileSync, readdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -259,7 +259,13 @@ let ausgabe = ""; ausgabe = aus(); } -ok(/Bereich 'agentur' freigeschaltet/.test(ausgabe), +/* NICHT AUF DEN WORTLAUT FESTNAGELN (11.09.2026). Hier stand + /Bereich 'agentur' freigeschaltet/ -- der Satz des alten, von Hand + abgeschriebenen Blocks. Seit die Umstellung ueber + checkListeErweitern laeuft, heisst es "'agentur' in + eintraege.bereich freigeschaltet". Gemeint ist beides Mal dasselbe; + geprueft wird deshalb die AUSSAGE, nicht der Satzbau. */ +ok(/agentur.*freigeschaltet/.test(ausgabe), "die Anwendung meldet die Umstellung" + (/agentur/.test(ausgabe) ? "" : ` — Ausgabe: ${ausgabe.slice(-300)}`)); @@ -585,8 +591,37 @@ melde("\n=== Ueberall, wo Bereiche vorkommen ==="); vorgekommen, ohne Fehler und ohne Luecke. */ const { daten } = await ruf("GET", "/workspace/api/report", keksDogi); const b = daten?.bereiche || {}; - ok(Object.keys(b).length === 6, - `der Wochenbericht kennt ${Object.keys(b).length} Bereiche: ${Object.keys(b).join(", ")}`); + /* ABGELEITET STATT GEZAEHLT (11.09.2026). Hier stand `=== 6`. Das + war richtig, als es sechs Bereiche gab; inzwischen sind es elf, + und die Zeile war seither still falsch -- versteckt hinter einem + Absturz weiter oben, der den Lauf vorher beendete. Dieselbe feste + Zahl, dieselbe Falle wie in der Umstellung, die diese Datei + aufgedeckt hat. + + Verglichen wird jetzt gegen die Bereiche, die die DATENBANK + ueberhaupt zulaesst -- die CHECK-Regel ist die einzige Liste, an + der niemand vorbeikommt. Damit stimmt die Pruefung auch beim + zwoelften Bereich noch, ohne dass jemand sie anfasst. */ + const erlaubt = (() => { + const dd = new DatabaseSync(DB, { readOnly: true }); + const plan = dd.prepare( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='eintraege'").get()?.sql || ""; + dd.close(); + /* Ohne regulaeren Ausdruck: Der Bauplan enthaelt die Regel + woertlich als "bereich IN (...)". Ein Muster mit Klammern und + Leerzeichenklassen waere hier nur eine weitere Stelle, an der + man sich vertun kann. */ + const auf = plan.indexOf("bereich IN ("); + if (auf < 0) return []; + const zu = plan.indexOf(")", auf); + if (zu < 0) return []; + return plan.slice(auf + "bereich IN (".length, zu) + .split(",").map((t) => t.trim().split("'").join("")); + })(); + const fehlend = erlaubt.filter((n) => !(n in b)); + ok(erlaubt.length > 0 && fehlend.length === 0, + `der Wochenbericht kennt alle ${erlaubt.length} Bereiche der Datenbank` + + (fehlend.length ? ` -- es fehlen: ${fehlend.join(", ")}` : "")); ok(b.agentur && b.agentur.neu >= ARTEN.length, `und zaehlt ${b.agentur?.neu} neue Agentur-Eintraege`); } @@ -741,6 +776,28 @@ melde("\n=== Die Kachel ==="); await browser.close(); } +/* ======================================================================= + 6. UND SIE LAEUFT GENAU EINMAL (11.09.2026) + ======================================================================= + Der Fall, der auf dem echten Server gilt: Dort kennt die CHECK-Regel + 'agentur' laengst. Ein Neustart darf die Tabelle dann NICHT noch + einmal umbauen -- jeder Umbau ist ein Kopieren von Hand auf echten + Daten, und was man nicht tun muss, tut man auf einer Live-Datenbank + nicht. + + GEMESSEN AN DEN SICHERUNGEN, nicht an der Absicht: Vor jedem Umbau + legt die Anwendung eine Datei ".vor-agentur-" an. Diese + Datei ist der Beweis, dass umgebaut wurde. Waeren es zwei, haette + ein Neustart erneut angefasst -- und genau das soll hier auffallen. + Der Server ist zwischendurch mehrfach neu gestartet worden, der Fall + ist also wirklich durchgespielt und nicht nur behauptet. */ +{ + const spuren = readdirSync(ordner).filter((n) => n.includes(".vor-agentur-")); + ok(spuren.length === 1, + `die Agentur-Umstellung lief genau EINMAL, trotz mehrerer Neustarts ` + + `(${spuren.length} Sicherung(en))`); +} + await serverStoppen(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } diff --git a/server/workspace.js b/server/workspace.js index 3ef53239..f17e13cc 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1826,96 +1826,40 @@ function umstellungen(d) { Derselbe Umbau wie bei "abgebrochen" darueber -- ein CHECK laesst sich in SQLite nicht aendern, die Tabelle muss neu gebaut werden: erst sichern, dann tauschen, Zeilen zaehlen, Verweise pruefen. */ - const eintraegePlan = d.prepare( - "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'eintraege'").get()?.sql || ""; - if (eintraegePlan && !eintraegePlan.includes("'agentur'")) { - const sicherung = `${DB_PFAD}.vor-agentur-${jetztStempel}`; - try { - d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`); - console.log("[workspace] Sicherung vor der Umstellung:", sicherung); - } catch (fehler) { - console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message); - return; - } + /* ---- Der sechste Bereich: "agentur" (06.09.2026) ---- + ABGELEITET STATT ABGESCHRIEBEN (11.09.2026). - d.exec("PRAGMA foreign_keys = OFF"); - try { - const vorher = d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n; - d.exec("BEGIN"); - d.exec(` - CREATE TABLE eintraege_neu ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - bereich TEXT NOT NULL - CHECK (bereich IN ('live','content','technik','community','schutz','agentur')), - art TEXT NOT NULL, - titel TEXT NOT NULL, - text TEXT, - datum TEXT NOT NULL, - bewertung INTEGER CHECK (bewertung IS NULL OR (bewertung BETWEEN 1 AND 5)), - dringlichkeit TEXT NOT NULL DEFAULT 'mittel' - CHECK (dringlichkeit IN ('hoch','mittel','niedrig')), - status TEXT NOT NULL DEFAULT 'offen' - CHECK (status IN ('offen','erledigt')), - creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL, - erstellt TEXT NOT NULL, - erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL, - geaendert TEXT, - hook TEXT, - format TEXT, - saeule_id INTEGER, - geplant TEXT, - creator_extern TEXT, - /* Die drei Event-Spalten MUESSEN hier mit stehen (07.09.2026). + Hier stand bis heute der Bauplan der Tabelle von Hand abgeschrieben + -- alle Spalten, zweimal (einmal fuer CREATE, einmal fuer INSERT). + Am 07.09.2026 fiel auf, dass dabei drei Event-Spalten fehlten; sie + wurden nachgetragen, und daneben kam ein Kommentar, der genau vor + dieser Verlustart warnt. - Der Spalten-Nachtrag weiter oben laeuft frueher als dieser - Neubau. Auf einer Datenbank, die noch den alten CHECK hat - -- eine Sicherung von vor dem 06.09., in einen heutigen - Stand eingespielt --, waeren die drei Spalten also erst - angelegt und hier sofort wieder weggeworfen worden: mit - allem, was drinsteht, ohne Fehlermeldung, und die - Zeilenzahl haette weiterhin gestimmt. Genau die - Verlustart, vor der der Kommentar zu dieser Umstellung - warnt. */ - event_ende TEXT, - event_aufgaben TEXT, - event_regeln TEXT - ); - INSERT INTO eintraege_neu - (id, bereich, art, titel, text, datum, bewertung, dringlichkeit, status, - creator_id, erstellt, erstellt_von, geaendert, - hook, format, saeule_id, geplant, creator_extern, - event_ende, event_aufgaben, event_regeln) - SELECT id, bereich, art, titel, text, datum, bewertung, dringlichkeit, status, - creator_id, erstellt, erstellt_von, geaendert, - hook, format, saeule_id, geplant, creator_extern, - event_ende, event_aufgaben, event_regeln - FROM eintraege; - DROP TABLE eintraege; - ALTER TABLE eintraege_neu RENAME TO eintraege; - CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id); - `); - const nachher = d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n; - if (nachher !== vorher) { - d.exec("ROLLBACK"); - console.error(`[workspace] Umstellung abgebrochen: ${vorher} Eintraege vorher, ` - + `${nachher} nachher. Sicherung: ${sicherung}`); - } else { - d.exec("COMMIT"); - const kaputt = d.prepare("PRAGMA foreign_key_check").all(); - if (kaputt.length) { - console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length, - "verwaiste Verweise. Sicherung liegt unter", sicherung); - } else { - console.log(`[workspace] Bereich 'agentur' freigeschaltet, ${vorher} Eintraege, Verweise geprueft.`); - } - } - } catch (fehler) { - try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ } - console.error("[workspace] Umstellung 'agentur' fehlgeschlagen:", fehler?.message); - } finally { - d.exec("PRAGMA foreign_keys = ON"); - } - } + DIESELBE FALLE HAT DANACH EIN ZWEITES MAL ZUGESCHNAPPT. Seit dem + 07.09. kamen `einsatz`, `nur_leitung` und `gesendet_am` dazu. Der + Spalten-Nachtrag legt sie an, dieser Neubau warf sie unmittelbar + danach weg -- mit Inhalt, ohne Fehlermeldung, bei unveraenderter + Zeilenzahl. Gemessen: 24 Spalten hinein, 21 heraus. + + Gefunden hat es pruef-agentur, und zwar seit Tagen: 31-mal "FEHL" + mit HTTP 503. Sagen konnte sie es erst, seit sie die + Serverausgabe zeigt -- dort stand es dann in drei Zeilen + untereinander. + + DESHALB WIRD NICHTS MEHR ABGESCHRIEBEN. `checkListeErweitern` holt + den Bauplan aus sqlite_master und die Spaltenliste aus + PRAGMA table_info -- sie kann gar nicht veralten, weil sie nicht + gepflegt wird. Sie sichert vorher, zaehlt die Zeilen innerhalb der + Transaktion, nimmt die Indizes mit und prueft die Verweise, also + alles, was der Block hier auch tat. + + Die echten Daten auf dem Server sind nicht betroffen: Ihr CHECK + kennt 'agentur' laengst, die Umstellung laeuft dort nie wieder. + Gefaehrlich war es beim Zurueckspielen einer Sicherung von vor dem + 06.09.2026 -- also genau dann, wenn man sich am wenigsten einen + stillen Datenverlust leisten kann. */ + checkListeErweitern(d, "eintraege", "bereich", "agentur", + ["live", "content", "technik", "community", "schutz", "agentur"], jetztStempel); /* ---- Rolle "manager" erlauben ---- */ const bauplan = d.prepare(