diff --git a/server/pruef-nachruesten.mjs b/server/pruef-nachruesten.mjs new file mode 100644 index 00000000..6f3842d0 --- /dev/null +++ b/server/pruef-nachruesten.mjs @@ -0,0 +1,219 @@ +/* ===================================================================== + EINE ALTE DATENBANK BEKOMMT NEUE SPALTEN (17.09.2026) + + Entstanden aus einem Fehlalarm, den ich selbst verursacht habe. + + WAS PASSIERT IST: Nach dem Ausliefern des Trichter-Ausgangs prüfte + mein Deploy-Block vier Sekunden nach dem Neustart, ob + `talent_stufe` die drei neuen Spalten hat. Antwort: 0. Das sah aus + wie eine fehlgeschlagene Umstellung an lebenden Daten -- und war + keine. Die Spalten entstehen beim ERSTEN angemeldeten Aufruf des + Bereichs (`tabellen()` in workspace-entwicklung.js, gesteuert über + `bereit`), und vier Sekunden nach einem Neustart hat sich noch + niemand angemeldet. Die Prüfung verlangte eine Aussage, die zu + diesem Zeitpunkt gar nicht wahr sein KONNTE. + + WAS DABEI AUFFIEL, und das ist der eigentliche Fund: Der Weg „alte + Datenbank bekommt neue Spalten" war NIE GEPRÜFT. Jede Prüfung im + Haus startet auf einer frischen Datei, in der die Tabellen mit + allen Spalten neu entstehen -- der Nachrüst-Pfad wurde dabei nie + betreten. Gemessen: 27 Stellen in drei Dateien rüsten Spalten so + nach. Live ist das der einzige Weg, der überhaupt vorkommt. + + Das ist dieselbe Sorte Lücke wie die Sicherung, die nie + zurückgespielt wurde: Sie meldet jahrelang „in Ordnung" und sagt + nichts darüber, ob sie im Ernstfall trägt. + + WAS HIER GEPRÜFT WIRD: Eine Datenbank wird auf den Stand von VOR + der Änderung zurückgebaut -- genau die sieben Spalten, die am + 17.09. live standen. Dann läuft ein echter Aufruf, und danach muss + das Schema vollständig sein UND die Seite antworten. + + Aufrufen mit: node server/pruef-nachruesten.mjs + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4191, "pruef-nachruesten"); +const ordner = mkdtempSync(join(tmpdir(), "ws-nachruesten-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +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 }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(120_000, "pruef-nachruesten"); +await new Promise((r) => setTimeout(r, 800)); + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); + +/* ======================================================================= + DER ALTE ZUSTAND + + Die sieben Spalten sind am 17.09.2026 vom laufenden Server abgelesen + (`PRAGMA table_info(talent_stufe)`), nicht ausgedacht. Wer die Liste + ändert, muss wissen, warum -- sie beschreibt einen Stand, den es + wirklich gab. + ======================================================================= */ +const ALT = ["eintrag_id", "stufe", "seit", "von_id", "notiz", "buddy_id", "erste_schicht"]; +const NEU = ["beendet_am", "beendet_grund", "beendet_von"]; + +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync("CODE-DOGI-0001", 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("Filipe", "admin", hash, salt, 32768, new Date().toISOString()); + + d.exec("DROP TABLE IF EXISTS talent_stufe"); + d.exec(`CREATE TABLE talent_stufe ( + eintrag_id INTEGER PRIMARY KEY, + stufe TEXT NOT NULL, + seit TEXT NOT NULL, + von_id INTEGER, + notiz TEXT, + buddy_id INTEGER, + erste_schicht TEXT + )`); + /* EINE ZEILE MUSS DRIN SEIN. Über eine leere Tabelle läuft + `talentKarte` gar nicht -- und dann bewiese ein „200" nur, dass + der Weg ohne Daten funktioniert. Genau diese Sorte grüner Haken + hat im September schon zweimal nichts bewiesen. */ + d.exec(`INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von) + VALUES ('talente','empfohlen','Aus der alten Datenbank','x',date('now'),'offen',datetime('now'),1)`); + d.exec(`INSERT INTO talent_stufe (eintrag_id, stufe, seit, von_id) + SELECT id, 'probe', datetime('now'), 1 FROM eintraege WHERE bereich='talente'`); + + const spalten = d.prepare("PRAGMA table_info(talent_stufe)").all().map((s) => s.name); + d.close(); + + melde("=== 1. Der Ausgangszustand ==="); + ok(spalten.join(",") === ALT.join(","), + `die Tabelle steht auf dem alten Stand (${spalten.length} Spalten)`); + /* GEGENPROBE zur Voraussetzung: Wären die Spalten hier schon da, + bewiese der Rest nichts -- die Prüfung hätte dann nur bestätigt, + dass Vorhandenes vorhanden ist. */ + ok(NEU.every((s) => !spalten.includes(s)), + "und die drei neuen fehlen wirklich -- sonst prüft das Folgende nichts"); +} + +function roh(pfad, kopf = {}, koerper = null, methode = null) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, + method: methode || (koerper ? "POST" : "GET"), + headers: { Host: "workspace.dogfather-universe.com", + ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf }, + }, (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); + }); +} + +/* ======================================================================= + 2. EIN ECHTER AUFRUF RÜSTET NACH + ======================================================================= */ +melde(""); +melde("=== 2. Der erste Aufruf ==="); +const an = await roh("/workspace/api/anmelden", {}, { rolle: "admin", code: "CODE-DOGI-0001" }); +const keks = [].concat(an.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); +ok(an.code === 200 && !!keks, `angemeldet (${an.code})`); + +const lage = await roh("/workspace/api/talente/lage", { cookie: keks }); +ok(lage.code === 200, + lage.code === 200 ? "die Talente-Seite antwortet (200)" + : `die Talente-Seite antwortet mit ${lage.code}: ${lage.text.slice(0, 120)}`); + +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const spalten = d.prepare("PRAGMA table_info(talent_stufe)").all().map((s) => s.name); + d.close(); + const fehlt = NEU.filter((s) => !spalten.includes(s)); + ok(fehlt.length === 0, + fehlt.length ? `es fehlen noch: ${fehlt.join(", ")}` + : `die drei Spalten sind dabei entstanden (${spalten.length} insgesamt)`); + /* UND DIE ALTEN SIND NOCH DA. Ein Nachrüsten, das nebenbei etwas + verliert, ist schlimmer als keines -- und genau das ist am + 11.09. passiert, als eine abgeschriebene Spaltenliste beim + Tabellenneubau drei Spalten samt Inhalt weggeworfen hat, ohne + Fehlermeldung und bei unveränderter Zeilenzahl. */ + ok(ALT.every((s) => spalten.includes(s)), + "und keine der alten ist dabei verlorengegangen"); +} + +/* ======================================================================= + 3. UND DIE KARTE TRÄGT DAS NEUE FELD + + Dass die Spalte existiert, heißt noch nicht, dass die Anwendung sie + ausliefert. Das ist ein zweiter Schritt und eine zweite Frage. + ======================================================================= */ +melde(""); +melde("=== 3. Was ankommt ==="); +{ + let j = null; + try { j = JSON.parse(lage.text); } catch { /* bleibt null */ } + const k = (j?.kandidaten || [])[0]; + ok(!!k, `die Karte aus der alten Datenbank kommt an (${k?.titel})`); + ok(k && "beendet" in k, "und sie trägt das neue Feld"); + ok(k?.beendet === null, `das bei einem laufenden Kandidaten leer ist (${k?.beendet})`); +} + +/* ======================================================================= + 4. UND ES LÄSST SICH SOFORT BENUTZEN + + Der eigentliche Beweis: nicht nur lesen, sondern schreiben. Eine + nachgerüstete Spalte, in die sich nichts eintragen lässt, wäre ein + halber Umbau -- und der fiele erst auf, wenn jemand ihn braucht. + ======================================================================= */ +melde(""); +melde("=== 4. Schreiben in die neue Spalte ==="); +{ + /* DER DRITTE AUSGANG. Beim Gegenprobieren (Nachruesten ausgebaut) + lieferte die Seite 503, `j.kandidaten` war undefiniert -- und + dieser Abschnitt STUERZTE AB, statt rot zu melden. Ein Absturz + sagt weniger als ein Fehlschlag: Er verschluckt die vier Zeilen, + die danach gekommen waeren, und sieht aus wie ein Problem der + Pruefung statt wie eines der Sache. */ + let j = null; + try { j = JSON.parse(lage.text); } catch { /* bleibt null */ } + const id = j?.kandidaten?.[0]?.id; + if (!id) { + ok(false, "konnte nicht schreiben -- es kam schon keine Karte an (siehe oben)"); + ok(false, "und damit auch nichts nachlesen"); + } else { + const r = await roh(`/workspace/api/talente/${id}/beenden`, { cookie: keks }, + { grund: "Aus der alten Datenbank heraus beendet." }, "PUT"); + ok(r.code === 200, `beenden geht (${r.code})`); + const l2 = JSON.parse((await roh("/workspace/api/talente/lage", { cookie: keks })).text); + const b = (l2.beendete || []).find((x) => x.id === id); + ok(!!b && b.beendet?.grund?.startsWith("Aus der alten"), + `und der Satz steht in der nachgerüsteten Spalte (${b?.beendet?.grund})`); + } +} + +/* ------------------------------------------------------------------- */ +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0);