Zwei Tabellenumbauten haetten 15 Spalten still geloescht
`personen` und `aufgaben` wurden beim Freischalten eines neuen
CHECK-Werts von Hand neu gebaut -- die Spaltenliste stand zweimal im
Code abgeschrieben (CREATE und INSERT). Gemessen:
aufgaben: 23 Spalten, 18 aufgezaehlt -> vorlage, kategorie,
aus_eintrag_id, aufwand, aus_punkt weg
personen: 19 Spalten, 9 aufgezaehlt -> bild, ueber_mich, tiktok,
instagram, youtube, twitch, chat_kachel, code_kennung,
stufe, alter_bestaetigt_am weg
Bei `personen` haengen daran die Profilfotos und die
Altersbestaetigung: Alle waeren nach einem Rueckspielen still wieder
unbestaetigt gewesen, ohne Fehlermeldung, bei unveraenderter
Zeilenzahl. Die Zeilenzaehlung, die als Sicherung gedacht war, kann
einen Spaltenverlust gar nicht sehen.
Es ist derselbe Fehler wie am 11.09. an der Eintragstabelle (24 hinein,
21 heraus). Damals wurde `checkListeErweitern` gebaut, das die Spalten
aus PRAGMA table_info ABLEITET -- eine Liste, die niemand pflegt, kann
nicht veralten. Diese beiden Bloecke waren aelter und wurden bei der
Umstellung uebersehen. Jetzt gehen alle 16 Umstellungen denselben Weg,
kein Neubau zaehlt mehr von Hand.
Auf dem laufenden Server ist nichts verloren (24 Spalten, CHECK
vollstaendig) -- die Falle stand fuer den Tag, an dem jemand eine
Sicherung zurueckspielt.
Gefunden hat es nicht das Lesen, sondern pruef-abbrechen: Sie baut
absichtlich eine ALTE Datenbank und liess die Umstellung darauf laufen
-- danach meldete der Server "no such column: a.aufwand".
Neu: pruef-umstellung.mjs (18 Pruefungen) haelt die Fehlerklasse
dauerhaft zu. Sie baut eine echte alte Datenbank mit gefuellten
Spalten, laesst die Umstellung laufen und zaehlt danach Spalten UND
Inhalte. Mit Gegenprobe: ein absichtlich fehlerhafter Umbau zeigt, dass
die Zeilenzahl dabei stimmt und trotzdem Daten fehlen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,225 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
pruef-umstellung.mjs — Ein Tabellenumbau darf keine Spalte verlieren
|
||||||
|
|
||||||
|
WARUM ES DIESE PRUEFUNG GIBT (21.09.2026)
|
||||||
|
|
||||||
|
SQLite kann einen CHECK nicht aendern. Wer einen neuen Wert erlauben
|
||||||
|
will -- eine neue Rolle, einen neuen Status --, muss die Tabelle neu
|
||||||
|
bauen: CREATE, INSERT ... SELECT, DROP, RENAME. Und genau dort wurde
|
||||||
|
in diesem Haus DREIMAL dieselbe Falle gestellt:
|
||||||
|
|
||||||
|
11.09.2026 eintraege: 24 Spalten hinein, 21 heraus
|
||||||
|
21.09.2026 aufgaben: 23 Spalten, 18 aufgezaehlt -> 5 verloren
|
||||||
|
21.09.2026 personen: 19 Spalten, 9 aufgezaehlt -> 10 verloren
|
||||||
|
|
||||||
|
Jedes Mal, weil die Spaltenliste von HAND im Code stand -- einmal im
|
||||||
|
CREATE, einmal im INSERT. Wer spaeter eine Spalte ergaenzt, traegt sie
|
||||||
|
dort nicht nach; es gibt keinen Grund, warum er daran denken sollte.
|
||||||
|
|
||||||
|
WAS DEN FEHLER SO GEFAEHRLICH MACHT: Er ist unsichtbar. Die Zeilenzahl
|
||||||
|
stimmt vorher wie nachher, es gibt keine Fehlermeldung, und die
|
||||||
|
Sicherung, die als Netz gedacht war, zaehlt Zeilen -- sie kann einen
|
||||||
|
Spaltenverlust gar nicht sehen. Bei `personen` waeren unter anderem
|
||||||
|
`bild` (Profilfotos) und `alter_bestaetigt_am` (Altersbestaetigung)
|
||||||
|
weggefallen: Alle waeren still wieder unbestaetigt gewesen.
|
||||||
|
|
||||||
|
AUF DEM ECHTEN SERVER ist keine dieser Umstellungen mehr offen -- sie
|
||||||
|
sind laengst gelaufen. Die Gefahr trifft den Tag, an dem jemand eine
|
||||||
|
Sicherung zurueckspielt oder eine neue Umgebung aus einem alten Stand
|
||||||
|
aufsetzt. Genau dann, wenn man sich auf das Netz verlaesst.
|
||||||
|
|
||||||
|
DIESE PRUEFUNG BAUT DESHALB DIE ALTE LAGE NACH: eine Datenbank mit
|
||||||
|
altem CHECK und allen spaeteren Spalten, gefuellt. Danach laeuft die
|
||||||
|
Umstellung, und es wird gezaehlt -- Spalten UND Inhalte.
|
||||||
|
===================================================================== */
|
||||||
|
|
||||||
|
import { mkdtempSync, rmSync, readdirSync, readFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { DatabaseSync } from "node:sqlite";
|
||||||
|
import { eigenerPort } from "./helfer-port.mjs";
|
||||||
|
|
||||||
|
let ok_ = 0, fehl = 0;
|
||||||
|
const ok = (b, text) => {
|
||||||
|
if (b) { ok_++; console.log(" ok " + text); }
|
||||||
|
else { fehl++; console.log(" FEHL " + text); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const ordner = mkdtempSync(join(tmpdir(), "ws-umstellung-"));
|
||||||
|
const alt = join(ordner, "alt.db");
|
||||||
|
process.env.WORKSPACE_DB = alt;
|
||||||
|
const PORT = await eigenerPort(import.meta, "pruef-umstellung");
|
||||||
|
process.env.PORT = String(PORT);
|
||||||
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||||
|
|
||||||
|
/* =======================================================================
|
||||||
|
1. Die alte Lage nachbauen
|
||||||
|
======================================================================= */
|
||||||
|
console.log("\n=== 1. Eine Datenbank, wie sie VOR der Umstellung aussah ===");
|
||||||
|
|
||||||
|
/* Der CHECK kennt weder 'manager' noch die spaeteren Rollen -- aber die
|
||||||
|
Spalten, die im Laufe der Zeit dazugekommen sind, sind da und
|
||||||
|
GEFUELLT. Genau diese Mischung entsteht beim Zurueckspielen: Die
|
||||||
|
ALTER-TABLE-Nachtraege laufen bei jedem Start, der Tabellenumbau nur
|
||||||
|
einmal. */
|
||||||
|
const SPAETER = [
|
||||||
|
["bild", "TEXT", "'abc123.jpg'"],
|
||||||
|
["ueber_mich", "TEXT", "'Ich streame seit 2024.'"],
|
||||||
|
["tiktok", "TEXT", "'@luna_live'"],
|
||||||
|
["instagram", "TEXT", "'luna.insta'"],
|
||||||
|
["youtube", "TEXT", "'@lunayt'"],
|
||||||
|
["twitch", "TEXT", "'luna_tv'"],
|
||||||
|
["chat_kachel", "TEXT", "'moos'"],
|
||||||
|
["code_kennung", "TEXT", "'LUNA-7'"],
|
||||||
|
["stufe", "TEXT", "'zwei'"],
|
||||||
|
["alter_bestaetigt_am", "TEXT", "'2026-09-01T10:00:00.000Z'"],
|
||||||
|
];
|
||||||
|
|
||||||
|
{
|
||||||
|
const a = new DatabaseSync(alt);
|
||||||
|
a.exec(`
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
CREATE TABLE personen (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
rolle TEXT NOT NULL CHECK (rolle IN ('admin','scout','creator')),
|
||||||
|
code_hash TEXT NOT NULL, code_salt TEXT NOT NULL, code_n INTEGER NOT NULL,
|
||||||
|
aktiv INTEGER NOT NULL DEFAULT 1, erstellt TEXT NOT NULL, letzter_login TEXT);
|
||||||
|
CREATE INDEX idx_personen_rolle ON personen (rolle);
|
||||||
|
`);
|
||||||
|
for (const [name, typ] of SPAETER) {
|
||||||
|
a.exec(`ALTER TABLE personen ADD COLUMN ${name} ${typ}`);
|
||||||
|
}
|
||||||
|
const jetzt = new Date().toISOString();
|
||||||
|
a.prepare(`INSERT INTO personen
|
||||||
|
(name, rolle, code_hash, code_salt, code_n, erstellt,
|
||||||
|
${SPAETER.map((x) => x[0]).join(", ")})
|
||||||
|
VALUES ('Luna','creator','h','s',32768,?,
|
||||||
|
${SPAETER.map((x) => x[2]).join(", ")})`).run(jetzt);
|
||||||
|
/* Eine zweite Person, damit die Zeilenzaehlung nicht zufaellig
|
||||||
|
stimmt: Bei EINER Zeile waere "1 vorher, 1 nachher" auch dann
|
||||||
|
richtig, wenn der Umbau die falsche Zeile kopiert haette. */
|
||||||
|
a.prepare(`INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, erstellt)
|
||||||
|
VALUES ('Filipe','admin','h2','s2',32768,?)`).run(jetzt);
|
||||||
|
|
||||||
|
const vorher = a.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
|
||||||
|
ok(vorher.length === 9 + SPAETER.length,
|
||||||
|
`die alte Tabelle hat ${vorher.length} Spalten (9 alte + ${SPAETER.length} nachgetragene)`);
|
||||||
|
const regel = a.prepare(
|
||||||
|
"SELECT sql FROM sqlite_master WHERE name = 'personen'").get()?.sql || "";
|
||||||
|
ok(!regel.includes("'manager'"),
|
||||||
|
"und ihr CHECK kennt 'manager' noch nicht -- die Umstellung steht also aus");
|
||||||
|
a.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =======================================================================
|
||||||
|
2. Den Server starten -- er stellt beim Hochfahren um
|
||||||
|
======================================================================= */
|
||||||
|
console.log("\n=== 2. Nach der Umstellung ===");
|
||||||
|
await import("./index.js");
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
|
||||||
|
{
|
||||||
|
const a = new DatabaseSync(alt);
|
||||||
|
const spalten = a.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
|
||||||
|
|
||||||
|
/* DER EIGENTLICHE BEWEIS. Nicht "es gibt viele Spalten", sondern
|
||||||
|
"es fehlt keine" -- mit Namen, damit die Meldung beim Scheitern
|
||||||
|
sagt, WELCHE. Eine Zahl allein ("erwartet 19, gefunden 9") laesst
|
||||||
|
den Suchenden allein. */
|
||||||
|
const fehlend = SPAETER.map((x) => x[0]).filter((n) => !spalten.includes(n));
|
||||||
|
ok(fehlend.length === 0,
|
||||||
|
`keine der ${SPAETER.length} nachgetragenen Spalten ist verschwunden`
|
||||||
|
+ (fehlend.length ? ` -- ES FEHLEN: ${fehlend.join(", ")}` : ""));
|
||||||
|
|
||||||
|
/* UND IHR INHALT. Eine Spalte, die den Umbau ueberlebt, aber leer
|
||||||
|
ankommt, ist derselbe Verlust mit besserem Aussehen: Das passiert,
|
||||||
|
sobald sie im CREATE steht, aber im INSERT fehlt. */
|
||||||
|
const luna = a.prepare("SELECT * FROM personen WHERE name = 'Luna'").get() || {};
|
||||||
|
const leer = SPAETER.filter(([n]) => luna[n] === null || luna[n] === undefined)
|
||||||
|
.map(([n]) => n);
|
||||||
|
ok(leer.length === 0,
|
||||||
|
"und keine steht plötzlich leer da"
|
||||||
|
+ (leer.length ? ` -- LEER: ${leer.join(", ")}` : ""));
|
||||||
|
|
||||||
|
ok(luna.alter_bestaetigt_am === "2026-09-01T10:00:00.000Z",
|
||||||
|
`die Altersbestaetigung steht unveraendert (${luna.alter_bestaetigt_am || "WEG"})`);
|
||||||
|
ok(luna.bild === "abc123.jpg", `und das Profilfoto auch (${luna.bild || "WEG"})`);
|
||||||
|
|
||||||
|
const n = a.prepare("SELECT COUNT(*) AS n FROM personen").get().n;
|
||||||
|
ok(n === 2, `beide Zeilen sind noch da (${n})`);
|
||||||
|
|
||||||
|
const regel = a.prepare(
|
||||||
|
"SELECT sql FROM sqlite_master WHERE name = 'personen'").get()?.sql || "";
|
||||||
|
for (const r of ["manager", "spicy", "modi", "hand", "gast"]) {
|
||||||
|
ok(regel.includes(`'${r}'`), `der CHECK erlaubt jetzt '${r}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Die Indizes haengen an der Tabelle und gehen beim DROP mit. Ohne
|
||||||
|
sie laeuft alles weiter -- nur langsamer, und das faellt niemandem
|
||||||
|
auf. */
|
||||||
|
const idx = a.prepare(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='personen'")
|
||||||
|
.all().map((z) => z.name).filter((x) => x.startsWith("idx_"));
|
||||||
|
ok(idx.includes("idx_personen_rolle"),
|
||||||
|
`der Index hat den Umbau ueberlebt (${idx.join(", ") || "keiner"})`);
|
||||||
|
|
||||||
|
const kaputt = a.prepare("PRAGMA foreign_key_check").all();
|
||||||
|
ok(kaputt.length === 0, `keine verwaisten Verweise (${kaputt.length})`);
|
||||||
|
a.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =======================================================================
|
||||||
|
3. Niemand zaehlt Spalten mehr von Hand
|
||||||
|
======================================================================= */
|
||||||
|
console.log("\n=== 3. Keine abgeschriebene Spaltenliste im Haus ===");
|
||||||
|
{
|
||||||
|
const vonHand = [];
|
||||||
|
for (const f of readdirSync(new URL(".", import.meta.url))) {
|
||||||
|
if (!f.endsWith(".js")) continue;
|
||||||
|
const s = readFileSync(new URL(f, import.meta.url), "utf8");
|
||||||
|
/* Kommentare zaehlen nicht -- in workspace.js steht der alte
|
||||||
|
Fehler als Beleg beschrieben, und ein Beleg ist kein Code. */
|
||||||
|
const code = s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
||||||
|
for (const m of code.matchAll(/INSERT INTO\s+(\w+)_neu\s*\(?/g)) vonHand.push(`${f}: ${m[1]}`);
|
||||||
|
}
|
||||||
|
ok(vonHand.length === 0,
|
||||||
|
`kein Tabellenumbau zaehlt seine Spalten von Hand auf (${vonHand.join(", ") || "—"})`);
|
||||||
|
|
||||||
|
const ws = readFileSync(new URL("./workspace.js", import.meta.url), "utf8");
|
||||||
|
const abgeleitet = (ws.match(/checkListeErweitern\(d,/g) || []).length;
|
||||||
|
ok(abgeleitet >= 15,
|
||||||
|
`${abgeleitet} Umstellungen leiten ihre Spalten aus PRAGMA table_info ab`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =======================================================================
|
||||||
|
4. Gegenprobe -- kann diese Pruefung auch NICHT IN ORDNUNG sagen?
|
||||||
|
======================================================================= */
|
||||||
|
console.log("\n=== 4. Gegenprobe (muss anschlagen) ===");
|
||||||
|
{
|
||||||
|
/* Der Verlust wird hier ABSICHTLICH nachgestellt: ein Umbau, der eine
|
||||||
|
Spalte auslaesst. Wuerde die Zaehlung oben ihn nicht bemerken, waere
|
||||||
|
sie eine Beruhigung ohne Aussage -- und genau so ist der Fehler
|
||||||
|
dreimal durchgerutscht. */
|
||||||
|
const probe = join(ordner, "gegenprobe.db");
|
||||||
|
const b = new DatabaseSync(probe);
|
||||||
|
b.exec(`
|
||||||
|
CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT);
|
||||||
|
INSERT INTO t (a, b) VALUES ('eins', 'zwei');
|
||||||
|
CREATE TABLE t_neu (id INTEGER PRIMARY KEY, a TEXT, b TEXT);
|
||||||
|
INSERT INTO t_neu (id, a) SELECT id, a FROM t;
|
||||||
|
DROP TABLE t;
|
||||||
|
ALTER TABLE t_neu RENAME TO t;
|
||||||
|
`);
|
||||||
|
const zeilen = b.prepare("SELECT COUNT(*) AS n FROM t").get().n;
|
||||||
|
const zeile = b.prepare("SELECT * FROM t").get();
|
||||||
|
ok(zeilen === 1,
|
||||||
|
"die Zeilenzahl stimmt nach dem fehlerhaften Umbau (1) -- sie sagt nichts");
|
||||||
|
ok(zeile.b === null,
|
||||||
|
"ABER der Inhalt von 'b' ist weg -- genau das findet die Pruefung oben");
|
||||||
|
b.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${ok_} Pruefungen, ${fehl} Fehler`);
|
||||||
|
console.log(fehl ? "NICHT IN ORDNUNG\n" : "ALLES IN ORDNUNG\n");
|
||||||
|
process.exit(fehl ? 1 : 0);
|
||||||
+59
-141
@@ -3068,92 +3068,42 @@ function umstellungen(d) {
|
|||||||
um "AND nicht abgebrochen" zu ergaenzen, und die eine vergessene
|
um "AND nicht abgebrochen" zu ergaenzen, und die eine vergessene
|
||||||
waere ein stiller Fehler gewesen: Die Aufgabe stuende weiter im
|
waere ein stiller Fehler gewesen: Die Aufgabe stuende weiter im
|
||||||
Brett, und niemand wuesste warum. */
|
Brett, und niemand wuesste warum. */
|
||||||
const aufgabenPlan = d.prepare(
|
/* ---- BIS ZUM 21.09.2026 STAND HIER EIN NEUBAU VON HAND ----
|
||||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'aufgaben'").get()?.sql || "";
|
|
||||||
if (aufgabenPlan && !aufgabenPlan.includes("'abgebrochen'")) {
|
|
||||||
const sicherung = `${DB_PFAD}.vor-abbruch-${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;
|
|
||||||
}
|
|
||||||
|
|
||||||
d.exec("PRAGMA foreign_keys = OFF");
|
Er zaehlte die Spalten einzeln auf -- einmal im CREATE, einmal im
|
||||||
try {
|
INSERT ... SELECT. Achtzehn Stueck. Die Tabelle hatte zu dem
|
||||||
/* Die Spalten stehen hier vollstaendig, weil die Schleife oben
|
Zeitpunkt DREIUNDZWANZIG: `vorlage`, `kategorie`, `aus_eintrag_id`,
|
||||||
sie zu diesem Zeitpunkt schon ergaenzt hat. Wer hier eine
|
`aufwand` und `aus_punkt` kamen spaeter dazu und wurden nie
|
||||||
vergisst, verliert ihren Inhalt still -- deshalb wird nach dem
|
nachgetragen.
|
||||||
Tausch die Zeilenzahl verglichen. */
|
|
||||||
const vorher = d.prepare("SELECT COUNT(*) AS n FROM aufgaben").get().n;
|
WAS DAS BEDEUTET HAETTE: Wer die Umstellung noch vor sich hat --
|
||||||
d.exec("BEGIN");
|
eine zurueckgespielte Sicherung, eine frische Anlage aus einem
|
||||||
d.exec(`
|
alten Stand -- verliert diese fuenf Spalten MIT INHALT. Ohne
|
||||||
CREATE TABLE aufgaben_neu (
|
Fehlermeldung, bei unveraenderter Zeilenzahl. Die Zaehlung, die
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
als Sicherung gedacht war, kann einen Spaltenverlust gar nicht
|
||||||
titel TEXT NOT NULL,
|
sehen: 40 Zeilen vorher, 40 Zeilen nachher, alles "in Ordnung".
|
||||||
beschreibung TEXT,
|
|
||||||
status TEXT NOT NULL DEFAULT 'offen'
|
GENAU DAS IST IN DIESEM HAUS SCHON EINMAL PASSIERT (11.09.2026,
|
||||||
CHECK (status IN ('offen','arbeit','review','erledigt','abgebrochen')),
|
Eintragstabelle: 24 Spalten hinein, 21 heraus). Damals wurde die
|
||||||
prioritaet TEXT NOT NULL DEFAULT 'mittel'
|
Lehre gezogen, die Liste abzuleiten statt zu pflegen -- und
|
||||||
CHECK (prioritaet IN ('hoch','mittel','niedrig')),
|
`checkListeErweitern` unten tut genau das (`PRAGMA table_info`).
|
||||||
creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
Dieser Block hier war aelter und wurde bei der Umstellung
|
||||||
verantwortlich_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
uebersehen; eine Liste, die niemand pflegt, kann nicht veralten,
|
||||||
frist TEXT,
|
aber eine zweite Fassung daneben eben doch.
|
||||||
erstellt TEXT NOT NULL,
|
|
||||||
erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
Gefunden hat es nicht das Lesen, sondern pruef-abbrechen: Sie baut
|
||||||
geaendert TEXT,
|
absichtlich eine ALTE Datenbank und laesst die Umstellung darauf
|
||||||
erledigt_am TEXT,
|
laufen. Danach meldete der Server
|
||||||
creator_extern TEXT,
|
"Aufgaben lesen: no such column: a.aufwand" -- und die Pruefung
|
||||||
verantwortlich_extern TEXT,
|
bekam eine 503 statt einer Liste.
|
||||||
abbruch_grund TEXT,
|
|
||||||
abgebrochen_am TEXT,
|
Auf dem echten Server ist die Umstellung laengst gelaufen (24
|
||||||
abbruch_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
Spalten, CHECK mit 'abgebrochen'); dort ist nichts verloren
|
||||||
status_vorher TEXT
|
gegangen. Der Aufruf unten ist deshalb im Betrieb ein Nulldurchgang
|
||||||
);
|
-- er steht hier fuer den Tag, an dem jemand eine Sicherung
|
||||||
INSERT INTO aufgaben_neu
|
zurueckspielt. */
|
||||||
(id, titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
|
checkListeErweitern(d, "aufgaben", "status", "abgebrochen",
|
||||||
frist, erstellt, erstellt_von, geaendert, erledigt_am,
|
["offen", "arbeit", "review", "erledigt", "abgebrochen"], jetztStempel);
|
||||||
creator_extern, verantwortlich_extern,
|
|
||||||
abbruch_grund, abgebrochen_am, abbruch_von, status_vorher)
|
|
||||||
SELECT id, titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
|
|
||||||
frist, erstellt, erstellt_von, geaendert, erledigt_am,
|
|
||||||
creator_extern, verantwortlich_extern,
|
|
||||||
abbruch_grund, abgebrochen_am, abbruch_von, status_vorher
|
|
||||||
FROM aufgaben;
|
|
||||||
DROP TABLE aufgaben;
|
|
||||||
ALTER TABLE aufgaben_neu RENAME TO aufgaben;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_aufgaben_status ON aufgaben (status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_aufgaben_creator ON aufgaben (creator_id);
|
|
||||||
`);
|
|
||||||
/* Nach dem RENAME heisst die neue Tabelle wieder "aufgaben" --
|
|
||||||
gezaehlt wird also unter dem alten Namen, und der Vergleich
|
|
||||||
laeuft noch INNERHALB der Transaktion. Stimmt er nicht, ist
|
|
||||||
ein ROLLBACK noch moeglich. */
|
|
||||||
const nachher = d.prepare("SELECT COUNT(*) AS n FROM aufgaben").get().n;
|
|
||||||
/* Stimmt die Zahl nicht, wird NICHT bestaetigt. Lieber laeuft das
|
|
||||||
Abbrechen noch nicht, als dass eine Aufgabe verschwindet. */
|
|
||||||
if (nachher !== vorher) {
|
|
||||||
d.exec("ROLLBACK");
|
|
||||||
console.error(`[workspace] Umstellung abgebrochen: ${vorher} Aufgaben 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] Status 'abgebrochen' freigeschaltet, ${vorher} Aufgaben, Verweise geprueft.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (fehler) {
|
|
||||||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
|
||||||
console.error("[workspace] Umstellung 'abgebrochen' fehlgeschlagen:", fehler?.message);
|
|
||||||
} finally {
|
|
||||||
d.exec("PRAGMA foreign_keys = ON");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Der sechste Bereich: "agentur" (06.09.2026) ----
|
/* ---- Der sechste Bereich: "agentur" (06.09.2026) ----
|
||||||
|
|
||||||
@@ -3210,64 +3160,32 @@ function umstellungen(d) {
|
|||||||
["live", "content", "technik", "community", "schutz", "agentur"], jetztStempel);
|
["live", "content", "technik", "community", "schutz", "agentur"], jetztStempel);
|
||||||
|
|
||||||
/* ---- Rolle "manager" erlauben ---- */
|
/* ---- Rolle "manager" erlauben ---- */
|
||||||
const bauplan = d.prepare(
|
/* ---- AUCH HIER STAND EIN NEUBAU VON HAND (ersetzt 21.09.2026) ----
|
||||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || "";
|
|
||||||
if (!bauplan.includes("'manager'")) {
|
|
||||||
const sicherung = `${DB_PFAD}.vor-manager-${jetztStempel}`;
|
|
||||||
try {
|
|
||||||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
|
||||||
console.log("[workspace] Sicherung vor der Umstellung:", sicherung);
|
|
||||||
} catch (fehler) {
|
|
||||||
/* Ohne Sicherung wird NICHT umgestellt. Lieber laeuft der Manager
|
|
||||||
noch nicht, als dass Daten ohne Netz angefasst werden. */
|
|
||||||
console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fremdschluessel muessen aus sein, weil andere Tabellen auf
|
Er zaehlte NEUN Spalten auf. `personen` hat neunzehn. Verloren
|
||||||
personen(id) zeigen -- und das laesst sich nicht innerhalb einer
|
gegangen waeren:
|
||||||
Transaktion umschalten. */
|
|
||||||
d.exec("PRAGMA foreign_keys = OFF");
|
|
||||||
try {
|
|
||||||
d.exec("BEGIN");
|
|
||||||
d.exec(`
|
|
||||||
CREATE TABLE personen_neu (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
rolle TEXT NOT NULL CHECK (rolle IN ('admin','manager','scout','creator')),
|
|
||||||
code_hash TEXT NOT NULL,
|
|
||||||
code_salt TEXT NOT NULL,
|
|
||||||
code_n INTEGER NOT NULL,
|
|
||||||
aktiv INTEGER NOT NULL DEFAULT 1,
|
|
||||||
erstellt TEXT NOT NULL,
|
|
||||||
letzter_login TEXT
|
|
||||||
);
|
|
||||||
INSERT INTO personen_neu
|
|
||||||
(id, name, rolle, code_hash, code_salt, code_n, aktiv, erstellt, letzter_login)
|
|
||||||
SELECT id, name, rolle, code_hash, code_salt, code_n, aktiv, erstellt, letzter_login
|
|
||||||
FROM personen;
|
|
||||||
DROP TABLE personen;
|
|
||||||
ALTER TABLE personen_neu RENAME TO personen;
|
|
||||||
`);
|
|
||||||
d.exec("COMMIT");
|
|
||||||
|
|
||||||
/* Nach dem Tausch pruefen, ob die Verweise noch stimmen. Findet
|
bild, ueber_mich, tiktok, instagram, youtube, twitch,
|
||||||
sich etwas, wird das laut gemeldet -- stillschweigend kaputte
|
chat_kachel, code_kennung, stufe, alter_bestaetigt_am
|
||||||
Verweise waeren das Schlimmste an dieser Stelle. */
|
|
||||||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
Das ist die empfindlichste Tabelle des Hauses, und zwei Eintraege
|
||||||
if (kaputt.length) {
|
darin sind mehr als Komfort: `bild` sind die Profilfotos, und
|
||||||
console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length,
|
`alter_bestaetigt_am` ist die Altersbestaetigung. Waere sie beim
|
||||||
"verwaiste Verweise. Sicherung liegt unter", sicherung);
|
Zurueckspielen einer Sicherung verschwunden, haette niemand eine
|
||||||
} else {
|
Fehlermeldung gesehen -- alle waeren still wieder unbestaetigt
|
||||||
console.log("[workspace] Rolle 'manager' freigeschaltet, Verweise geprueft.");
|
gewesen, auf einer Seite, die genau das nicht sein darf.
|
||||||
}
|
|
||||||
} catch (fehler) {
|
Derselbe Befund wie beim Block darueber, und dieselbe Loesung:
|
||||||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
`checkListeErweitern` leitet die Spaltenliste aus
|
||||||
console.error("[workspace] Umstellung fehlgeschlagen:", fehler?.message);
|
`PRAGMA table_info` ab. Eine Liste, die niemand pflegt, kann nicht
|
||||||
} finally {
|
veralten.
|
||||||
d.exec("PRAGMA foreign_keys = ON");
|
|
||||||
}
|
DIE KETTE IST KUMULATIV -- jeder Schritt nennt alle bis dahin
|
||||||
}
|
erlaubten Werte, und dieser ist der erste. Deshalb steht er VOR
|
||||||
|
den Aufrufen fuer spicy/modi/hand/gast weiter unten; die Reihenfolge
|
||||||
|
ist Teil der Bedeutung, nicht Zufall. */
|
||||||
|
checkListeErweitern(d, "personen", "rolle", "manager",
|
||||||
|
["admin", "manager", "scout", "creator"], jetztStempel);
|
||||||
|
|
||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
ROLLE "spicy" (Spicy Media) FREISCHALTEN — 07.09.2026
|
ROLLE "spicy" (Spicy Media) FREISCHALTEN — 07.09.2026
|
||||||
|
|||||||
Reference in New Issue
Block a user