Filipe, mit einem Bildschirmfoto des Regelblocks: "man soll die titel
in den texten besser erkennen und sehen was zu was passt. ich will
auch dass man sieht welche geschenke bei dem event zaehlen und so.
ich will alle informationen die moeglich sind wenn man den link
hochlaedt. ... auch wenn man teilt soll es einfach perfekt sein. man
soll alle infos sehen die man braucht. die kacheln von den events
soll auch nicht so lang sein, die soll man aufklappen koennen. genau
wie eine trennung fuer heute, letzte woche letzten monat und so."
DER FEHLER LAG NICHT IM TEXT, SONDERN IN SEINER ANZEIGE. Im Feld
stand die ganze Zeit eine Gliederung -- Hinweise mit "* ", eine
Ueberschrift mit Untertitel hinter einem Gedankenstrich, Absaetze
ohne Zeichen, Aufzaehlungen mit "- ". Die Kachel hat daraus mit
punkteAusText() EINE flache Liste gemacht: jede Zeile derselbe
Strich, alle gleich gross.
gliederung() liest die Gliederung, die schon dasteht, statt
den Text umzuschreiben. Die beiden Kampagnen, die
heute in der Datenbank liegen, sehen dadurch
sofort richtig aus -- ohne Umstellung.
Geschenke kommen aus der gelesenen LISTE, nicht aus dem
Text, und stehen OHNE Aufklappen auf der Kachel.
Aufklappen alles Uebrige hinter einem Druck: gemessen 555 px
weniger je Kachel bei Gipfelstuermer.
Gruppen je Spalte laeuft / kommt / vorbei, Vergangenes
nach diese Woche, dieser Monat, aelter.
NEU GELESEN (stand in allen vier gespeicherten Seiten und wurde nie
angesehen): die Rangliste mit ihren Gruppen ("Superstars" bis
"5. Liga", bei Glow Up "Team Glow"/"Team Shine"), die Beschreibung je
Missionsgruppe, der Steckbrief (Kurznummer, Markt, Sprache, Zone),
der zweite Bildbaustein, und Ueberschriften INNERHALB der Klapptexte
-- "WELCHE LIGA BIN ICH?" hat jetzt sechs eingerueckte Unterpunkte
statt zwoelf gleichrangiger Zeilen.
EVENT_TEXT_MAX 6000 -> 20000. Der Regeltext von Gipfelstuermer ist
7679 Zeichen lang; es fielen bisher jedes Mal rund 1600 Zeichen
echter Inhalt weg. Die Zahl steht jetzt an EINER Stelle.
TEILEN: Was die KAMPAGNE sagt, geht hinaus (Aufgaben, Geschenke,
Regeln) -- wer WIR sind, nicht (Name, Rolle, Haus, Kampagnennummer,
Steckbrief). Und keine Hinweiszeile: "Die Preise konnten nicht
gelesen werden -- bitte nachtragen" ist eine Notiz an DogFather und
hat auf einer Seite, die er in Discord postet, nichts zu suchen.
BERICHTIGT, WEIL NACHGEMESSEN:
- Number(null) ist 0 und 0 ist endlich -- "Feuerwerk" bekam an drei
Stellen "0 Coins" angeschrieben, also eine falsche Preisangabe.
- Ein Kommentar im Leser behauptete, der Wertungszeitraum weiche
vom Kampagnenzeitraum ab. Nachgemessen ist er in allen drei
Seiten mit Rangliste auf die Millisekunde gleich; ich hatte zwei
verschiedene Kampagnen verglichen.
- bild-kampagne.mjs verlangte "jede Kachel traegt den Ton ihrer
Seite" und war damit seit dem 07.10. rot, weil an dem Tag bewusst
das Gegenteil entschieden wurde (Zustandston, Seitenkante). Nicht
aufgefallen, weil nach dem Lauf nur die Zahl der MESSUNGEN notiert
wurde, nicht die der FEHLER.
- .kv-seite > .eintrag-karte wird .kv-seite .eintrag-karte: Die
Karten haengen durch die Gruppen eine Ebene tiefer. Das waere der
fuenfte Direktkind-Selektor gewesen, der nach einem Umbau still
ausfaellt -- diesmal vorher gesehen.
GEPRUEFT: 186 (lesen, +58), 148 (Route/Teilen, +25), 158 (Kachel,
+64), 67 (Agentur, +5), 67 Messungen (Bild, +14), dazu struktur,
css-klassen, zeichen, deutsche-texte, tippziele, lesbarkeit und
haus-trennung (107, unveraendert). Sechs Gegenproben: alle sechs
Sabotagen werden bemerkt, alle Dateien byte-gleich wiederhergestellt.
Co-Authored-By: Claude Opus 5 <[email protected]>
909 lines
45 KiB
JavaScript
909 lines
45 KiB
JavaScript
/* DER SECHSTE BEREICH — "Die Agentur bleibt angebunden"
|
||
|
||
Kernprinzip 04 des Konzepts, und bis zum 06.09.2026 gab es dafuer
|
||
nichts: Fuenf Bereiche standen (LIVE, Content, Technik, Community,
|
||
Schutz), der sechste fehlte vollstaendig.
|
||
|
||
---------------------------------------------------------------------
|
||
DAS EIGENTLICHE RISIKO IST NICHT DER BEREICH, SONDERN DER UMBAU
|
||
|
||
Der erlaubte Wertebereich einer Spalte steckt in SQLite in einem
|
||
CHECK, und ein CHECK laesst sich nicht aendern -- die Tabelle muss
|
||
neu gebaut werden. Dabei werden ALLE vorhandenen Eintraege
|
||
umgeschrieben: LIVE-Auswertungen, Content-Ideen, Technikprobleme,
|
||
Community-Konflikte, Vorfaelle. Wer dort eine Spalte vergisst,
|
||
verliert ihren Inhalt still.
|
||
|
||
Und das ist ein Weg, den man nicht zurueckdrehen kann. Deshalb wird
|
||
er hier nicht "im Prinzip" geprueft, sondern WIRKLICH GEGANGEN: Die
|
||
Pruefung baut eine Datenbank im ALTEN Stand nach, fuellt sie, laesst
|
||
die echte Anwendung darueberlaufen und zaehlt danach jede Zeile und
|
||
jedes Feld nach.
|
||
|
||
GEGENPROBEN durchgehend -- vor allem die eine, auf die es ankommt:
|
||
Der CHECK muss danach immer noch BEISSEN. Eine Umstellung, die
|
||
nebenbei die Schranke entfernt, sieht aus wie ein Erfolg und ist der
|
||
schlimmere Ausgang.
|
||
===================================================================== */
|
||
|
||
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";
|
||
import { spawn } from "node:child_process";
|
||
import { DatabaseSync } from "node:sqlite";
|
||
import { scryptSync, randomBytes } from "node:crypto";
|
||
import { heuteLokal } from "./helfer-zeit.mjs";
|
||
|
||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||
const { eigenerPort } = await import("./helfer-port.mjs");
|
||
const PORT = await eigenerPort(import.meta, "die Agentur-Pruefung");
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-agentur-"));
|
||
const DB = join(ordner, "workspace.db");
|
||
|
||
const PROTOKOLL = join(process.cwd(), "pruef-agentur-lauf.txt");
|
||
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
||
const melde = (z) => {
|
||
console.log(z);
|
||
try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ }
|
||
};
|
||
let fehler = 0;
|
||
let gemacht = 0;
|
||
const ok = (b, t) => { gemacht++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
|
||
/* ---------------------------------------------------------------------
|
||
Die Anwendung als eigener Prozess -- weil sie ZWEIMAL starten muss:
|
||
einmal, um die Datenbank anzulegen, und einmal nach dem Zurueckbau,
|
||
damit die Wanderung wirklich laeuft. In einem Prozess ginge das
|
||
nicht: Ein Modul wird nur einmal geladen.
|
||
--------------------------------------------------------------------- */
|
||
let laufend = null;
|
||
/* DER DRITTE AUSGANG (11.09.2026). Diese Datei schreibt die
|
||
Serverausgabe immer schon mit -- und zeigte sie nur, wenn der Server
|
||
gar nicht erst hochkam. Faellt er spaeter mit 503 um (die Antwort bei
|
||
einem Datenbankfehler), standen hier 31-mal "FEHL" und kein einziges
|
||
Wort dazu, WARUM. Eine Pruefung, die nicht sagen kann, woran sie
|
||
scheitert, kostet eine Stunde Suchen. */
|
||
let letzteAusgabe = () => "";
|
||
/* UND SIE MUSS AUCH DANN KOMMEN, WENN DER LAUF ABSTUERZT. Playwright
|
||
wirft bei einem fehlenden Knopf eine Ausnahme -- der Prozess stirbt,
|
||
und mit ihm die Zusammenfassung samt Diagnose. Genau dann braucht
|
||
man sie am dringendsten. */
|
||
function serverAusgabeZeigen(warum) {
|
||
const t = letzteAusgabe();
|
||
console.log("");
|
||
console.log(`=== Was der Server dazu gesagt hat (${warum}, letzte 1800 Zeichen) ===`);
|
||
console.log(t ? t.slice(-1800) : "(nichts -- der Server hat geschwiegen)");
|
||
/* Und vollstaendig auf Zuruf, falls die Ursache weiter oben steht:
|
||
SERVERLOG=<datei>. Die letzten 1800 Zeichen zeigen den Schaden,
|
||
nicht immer seine Ursache. */
|
||
if (t && process.env.SERVERLOG) {
|
||
writeFileSync(process.env.SERVERLOG, t);
|
||
console.log(`(vollstaendig in ${process.env.SERVERLOG})`);
|
||
}
|
||
}
|
||
for (const art of ["uncaughtException", "unhandledRejection"]) {
|
||
process.on(art, (e) => {
|
||
console.log(`
|
||
!! ${art}: ${e?.message || e}`);
|
||
serverAusgabeZeigen(art);
|
||
process.exit(1);
|
||
});
|
||
}
|
||
async function serverStarten() {
|
||
const kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
||
env: {
|
||
...process.env,
|
||
WORKSPACE_DB: DB,
|
||
PORT: String(PORT),
|
||
SITE_ACCESS_SECRET: "lokaler-test",
|
||
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00",
|
||
},
|
||
stdio: ["ignore", "pipe", "pipe"],
|
||
});
|
||
let aus = "";
|
||
kind.stdout.on("data", (d) => { aus += d; });
|
||
kind.stderr.on("data", (d) => { aus += d; });
|
||
for (let i = 0; i < 80; i++) {
|
||
await new Promise((r) => setTimeout(r, 250));
|
||
if (kind.exitCode !== null) break;
|
||
try {
|
||
const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
||
if (a.status < 500) {
|
||
laufend = kind;
|
||
letzteAusgabe = () => aus;
|
||
return { kind, aus: () => aus };
|
||
}
|
||
} catch { /* noch nicht oben */ }
|
||
}
|
||
throw new Error("Server startete nicht:\n" + aus.slice(-1200));
|
||
}
|
||
async function serverStoppen() {
|
||
if (!laufend) return;
|
||
laufend.kill();
|
||
laufend = null;
|
||
/* Warten, bis der Port wirklich frei ist -- sonst startet der zweite
|
||
Lauf gegen einen halb geschlossenen Anschluss und misst Unsinn. */
|
||
for (let i = 0; i < 40; i++) {
|
||
await new Promise((r) => setTimeout(r, 150));
|
||
try { await fetch(BASIS + "/workspace/", { redirect: "manual" }); } catch { return; }
|
||
}
|
||
}
|
||
|
||
/* =======================================================================
|
||
1. Erster Start: die Datenbank entsteht
|
||
======================================================================= */
|
||
melde("=== Die Datenbank anlegen ===");
|
||
{
|
||
const { aus } = await serverStarten();
|
||
ok(true, "die Anwendung ist oben und hat das Schema angelegt");
|
||
void aus;
|
||
}
|
||
|
||
const jetzt = new Date().toISOString();
|
||
/* ORTSZEIT fuer das Tagesdatum, nicht der Schnitt aus dem UTC-Stempel.
|
||
`jetzt.slice(0, 10)` liegt zwischen Mitternacht und zwei Uhr einen Tag
|
||
daneben -- die Pruefung waere dann nachts rot, ohne dass sich etwas
|
||
geaendert haette. Gefunden von pruef-struktur, das genau danach sucht. */
|
||
const heute = heuteLokal();
|
||
|
||
function anlegen(d, name, rolle, code) {
|
||
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 (?,?,?,?,?,1,?)")
|
||
.run(name, rolle, hash, salz, 32768, jetzt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
|
||
/* Mia kam am 07.09.2026 dazu, und sie ist kein Beiwerk: Sie legt nichts
|
||
an, ihr gehoert nichts, sie betreut niemanden. Genau deshalb beweist
|
||
erst SIE, dass ein Agentur-Eintrag wirklich fuer alle sichtbar ist --
|
||
bei Luna allein bliebe offen, ob es an ihr oder an der Regel lag. */
|
||
let idDogi, idLuna, idMia;
|
||
{
|
||
const d = new DatabaseSync(DB);
|
||
idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001");
|
||
idLuna = anlegen(d, "Luna", "creator", "CODE-CREA-0001");
|
||
idMia = anlegen(d, "Mia", "creator", "CODE-CREA-0002");
|
||
d.close();
|
||
}
|
||
|
||
/* =======================================================================
|
||
2. Die Datenbank auf den ALTEN Stand zurueckbauen
|
||
=======================================================================
|
||
|
||
Genau so sieht sie auf dem Server aus, bevor die Wanderung laeuft.
|
||
Ohne diesen Schritt pruefte die ganze Datei nur eine frisch angelegte
|
||
Tabelle -- und damit ausgerechnet den Fall, den es in der Wirklichkeit
|
||
nicht gibt. */
|
||
melde("\n=== Zurueck auf den alten Stand (fuenf Bereiche) ===");
|
||
await serverStoppen();
|
||
|
||
const ALTE_EINTRAEGE = [
|
||
["live", "auswertung", "Auswertung Montag", "Der Hook saß, die Mitte hing.", 4, "hoch"],
|
||
["content", "idee", "Serie: Basteln mit Kindern", "Fünf Teile.", null, "mittel"],
|
||
["technik", "problem", "Ton bricht ab", "Nur bei OBS 30.", null, "hoch"],
|
||
["community", "konflikt", "Streit in den Kommentaren", "Zwei Zuschauer.", null, "niedrig"],
|
||
["schutz", "vorfall", "Meldung wegen Sprache", "Geklärt.", null, "hoch"],
|
||
];
|
||
|
||
{
|
||
const d = new DatabaseSync(DB);
|
||
d.exec("PRAGMA foreign_keys = OFF");
|
||
d.exec("DROP TABLE IF EXISTS eintraege");
|
||
/* WOERTLICH der alte Bauplan, mit allen Spalten, die zu diesem
|
||
Zeitpunkt schon da waren. */
|
||
d.exec(`
|
||
CREATE TABLE eintraege (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
bereich TEXT NOT NULL
|
||
CHECK (bereich IN ('live','content','technik','community','schutz')),
|
||
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
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id);`);
|
||
|
||
const setzen = d.prepare(`INSERT INTO eintraege
|
||
(bereich, art, titel, text, datum, bewertung, dringlichkeit, status,
|
||
creator_id, erstellt, erstellt_von, hook, creator_extern)
|
||
VALUES (?,?,?,?,?,?,?, 'offen', ?,?,?,?,?)`);
|
||
for (const [b, art, titel, text, bew, dring] of ALTE_EINTRAEGE) {
|
||
setzen.run(b, art, titel, text, heute, bew, dring, idLuna, jetzt, idDogi,
|
||
b === "content" ? "Der Hook: 'Das habt ihr noch nie gesehen'" : null,
|
||
b === "live" ? "Gastauftritt Mia" : null);
|
||
}
|
||
d.exec("PRAGMA foreign_keys = ON");
|
||
|
||
const n = d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n;
|
||
ok(n === ALTE_EINTRAEGE.length, `${n} Eintraege im alten Stand angelegt`);
|
||
|
||
/* GEGENPROBE: Der alte CHECK muss "agentur" WIRKLICH ablehnen --
|
||
sonst gaebe es nichts umzustellen, und der Rest dieser Datei
|
||
bewiese nichts. */
|
||
let abgelehnt = false;
|
||
try {
|
||
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt)
|
||
VALUES ('agentur','kampagne','Darf noch nicht',?,?)`).run(heute, jetzt);
|
||
} catch { abgelehnt = true; }
|
||
ok(abgelehnt, "im alten Stand wird 'agentur' von der Datenbank abgelehnt");
|
||
d.close();
|
||
}
|
||
|
||
/* =======================================================================
|
||
3. Die Wanderung -- der Weg, den man nicht zurueckdrehen kann
|
||
======================================================================= */
|
||
melde("\n=== Die Umstellung ===");
|
||
let ausgabe = "";
|
||
{
|
||
const { aus } = await serverStarten();
|
||
await new Promise((r) => setTimeout(r, 600));
|
||
ausgabe = aus();
|
||
}
|
||
|
||
/* 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)}`));
|
||
|
||
{
|
||
const d = new DatabaseSync(DB, { readOnly: true });
|
||
const plan = d.prepare(
|
||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='eintraege'").get()?.sql || "";
|
||
ok(/'agentur'/.test(plan), "der neue Bauplan kennt 'agentur'");
|
||
|
||
/* KEINE ZEILE DARF FEHLEN -- und keine darf sich VERAENDERT haben.
|
||
Die Zeilenzahl allein reichte nicht: Wer beim Umschreiben eine
|
||
Spalte vergisst, behaelt alle Zeilen und verliert ihren Inhalt.
|
||
Genau das faellt sonst erst Wochen spaeter auf. */
|
||
const alle = d.prepare("SELECT * FROM eintraege ORDER BY id").all();
|
||
ok(alle.length === ALTE_EINTRAEGE.length,
|
||
`alle ${alle.length} Eintraege sind noch da (vorher ${ALTE_EINTRAEGE.length})`);
|
||
|
||
let abweichungen = [];
|
||
ALTE_EINTRAEGE.forEach(([b, art, titel, text, bew, dring], i) => {
|
||
const z = alle[i];
|
||
if (!z) { abweichungen.push(`${b}: Zeile fehlt`); return; }
|
||
if (z.bereich !== b) abweichungen.push(`${b}: bereich = ${z.bereich}`);
|
||
if (z.art !== art) abweichungen.push(`${b}: art = ${z.art}`);
|
||
if (z.titel !== titel) abweichungen.push(`${b}: titel = ${z.titel}`);
|
||
if (z.text !== text) abweichungen.push(`${b}: text = ${z.text}`);
|
||
if (z.bewertung !== bew) abweichungen.push(`${b}: bewertung = ${z.bewertung}`);
|
||
if (z.dringlichkeit !== dring) abweichungen.push(`${b}: dringlichkeit = ${z.dringlichkeit}`);
|
||
if (z.creator_id !== idLuna) abweichungen.push(`${b}: creator_id = ${z.creator_id}`);
|
||
if (z.erstellt_von !== idDogi) abweichungen.push(`${b}: erstellt_von = ${z.erstellt_von}`);
|
||
});
|
||
ok(abweichungen.length === 0,
|
||
abweichungen.length ? `Inhalte haben sich veraendert: ${abweichungen.slice(0, 5).join(" | ")}`
|
||
: `jedes Feld jeder Zeile ist unveraendert (${alle.length} Zeilen x 8 Felder geprueft)`);
|
||
|
||
/* Auch die SELTENEN Spalten -- gerade die vergisst man beim
|
||
Abschreiben des Bauplans. */
|
||
const mitHook = alle.find((z) => z.bereich === "content");
|
||
const mitExtern = alle.find((z) => z.bereich === "live");
|
||
ok(/Das habt ihr noch nie gesehen/.test(mitHook?.hook || ""),
|
||
`die Spalte "hook" hat ihren Inhalt behalten ("${String(mitHook?.hook).slice(0, 30)}…")`);
|
||
ok(mitExtern?.creator_extern === "Gastauftritt Mia",
|
||
`die Spalte "creator_extern" ebenfalls ("${mitExtern?.creator_extern}")`);
|
||
|
||
/* DIE DREI EVENT-SPALTEN MUESSEN DEN TAUSCH UEBERLEBEN (07.09.2026).
|
||
|
||
Sie entstehen frueher als diese Umstellung (Spalten-Nachtrag in
|
||
workspace.js), und der Neubau hier schreibt eine feste Spaltenliste
|
||
ab. Wer sie dort vergisst, verliert sie samt Inhalt -- ohne
|
||
Fehlermeldung, und die Zeilenzahl stimmte weiterhin. Genau dieser
|
||
Weg wird hier wirklich gegangen: Die Tabelle stand vorher im ALTEN
|
||
Bauplan, ohne diese Spalten.
|
||
|
||
`PRAGMA table_info` und nicht "SELECT laeuft durch": Eine fehlende
|
||
Spalte faellt beim SELECT zwar auch auf -- aber erst irgendwann,
|
||
beim naechsten Event, in einem 503. */
|
||
const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name);
|
||
const fehlend = ["event_ende", "event_aufgaben", "event_regeln"].filter((s) => !spalten.includes(s));
|
||
ok(fehlend.length === 0,
|
||
fehlend.length ? `nach der Umstellung fehlen: ${fehlend.join(", ")}`
|
||
: "die drei Event-Spalten haben die Umstellung ueberlebt");
|
||
|
||
/* Der Index muss nach dem Tausch wieder da sein -- er wird beim DROP
|
||
mit entfernt. Ohne ihn laeuft alles weiter, nur langsam, und
|
||
niemand merkt es je. */
|
||
const indizes = d.prepare(
|
||
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='eintraege'").all()
|
||
.map((r) => r.name);
|
||
ok(indizes.includes("idx_eintraege_bereich"),
|
||
`der Index steht wieder (${indizes.join(", ")})`);
|
||
d.close();
|
||
}
|
||
|
||
/* GEGENPROBE, DIE WICHTIGSTE: Die Schranke muss noch BEISSEN. Eine
|
||
Umstellung, die nebenbei den CHECK entfernt, sieht aus wie ein Erfolg
|
||
und ist der schlimmere Ausgang -- ab da kann jeder Tippfehler einen
|
||
Bereich erfinden, den es nicht gibt. */
|
||
{
|
||
const d = new DatabaseSync(DB);
|
||
let abgelehnt = false;
|
||
try {
|
||
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt)
|
||
VALUES ('erfundenerbereich','x','Darf nicht',?,?)`).run(heute, jetzt);
|
||
} catch { abgelehnt = true; }
|
||
ok(abgelehnt, "ein erfundener Bereich wird weiterhin abgelehnt — der CHECK beisst noch");
|
||
|
||
let angenommen = false;
|
||
try {
|
||
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt)
|
||
VALUES ('agentur','kampagne','Direkt in die Datenbank',?,?)`).run(heute, jetzt);
|
||
angenommen = true;
|
||
d.exec("DELETE FROM eintraege WHERE titel = 'Direkt in die Datenbank'");
|
||
} catch { /* bleibt false */ }
|
||
ok(angenommen, "'agentur' wird jetzt angenommen");
|
||
d.close();
|
||
}
|
||
|
||
/* =======================================================================
|
||
4. Der Bereich im Betrieb
|
||
======================================================================= */
|
||
melde("\n=== Der Bereich im Betrieb ===");
|
||
|
||
async function anmelden(rolle, code) {
|
||
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ rolle, code }) });
|
||
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
||
return keks;
|
||
}
|
||
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
||
const keksLuna = await anmelden("creator", "CODE-CREA-0001");
|
||
const keksMia = await anmelden("creator", "CODE-CREA-0002");
|
||
|
||
async function ruf(art, weg, keks, koerper) {
|
||
const a = await fetch(BASIS + weg, {
|
||
method: art,
|
||
headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS },
|
||
body: koerper ? JSON.stringify(koerper) : undefined,
|
||
});
|
||
let daten = null;
|
||
try { daten = await a.json(); } catch { /* kein JSON */ }
|
||
return { status: a.status, daten };
|
||
}
|
||
|
||
/* Die vier Arten -- jede einzeln, denn eine, die der Server nicht kennt,
|
||
waere im Formular waehlbar und beim Speichern ein Fehler. */
|
||
const ARTEN = ["kampagne", "schulung", "anliegen", "zustaendig"];
|
||
{
|
||
const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksDogi);
|
||
ok(daten?.einstellung?.name === "Kampagnen & Events",
|
||
`der Bereich meldet sich als "${daten?.einstellung?.name}"`);
|
||
const arten = Object.keys(daten?.einstellung?.arten || {});
|
||
ok(arten.length === 4 && ARTEN.every((a) => arten.includes(a)),
|
||
`vier Arten: ${arten.join(", ")}`);
|
||
ok(daten?.einstellung?.dringlichkeit === true,
|
||
"Dringlichkeit ist an — ein Anliegen von gestern ist etwas anderes als eines von vor drei Wochen");
|
||
ok(daten?.einstellung?.bewertung === false,
|
||
"Bewertung ist aus — hier wird zusammengearbeitet, nicht benotet");
|
||
}
|
||
|
||
/* ANGELEGT WIE IM ECHTEN BETRIEB: OHNE ZUORDNUNG (07.09.2026).
|
||
|
||
Hier stand `creator_id: idLuna` -- und damit prueften die Zeilen
|
||
weiter unten einen Fall, den es im Alltag nicht gibt. Ein Agentur-
|
||
Event gehoert niemandem einzeln; Filipe legt es fuer alle an, das
|
||
Feld bleibt leer. Weil die Pruefung es aber jedes Mal Luna zuwies,
|
||
sah Luna es auch -- und die Zeile "sie sieht ihre 4 Eintraege" war
|
||
grün.
|
||
|
||
In Wirklichkeit war die Agentur-Seite fuer JEDEN Creator leer: Ein
|
||
Eintrag ohne creator_id traf weder `creator_id = ich` noch
|
||
`erstellt_von = ich`. Gemessen am 07.09.2026, bevor es repariert
|
||
wurde: von drei Eintraegen sah die Creatorin genau den einen, den
|
||
die Pruefung ihr zugeschoben hatte.
|
||
|
||
Das ist die dritte Sorte Fehler aus CLAUDE.md -- nicht uebersprungen,
|
||
nicht kaputt: gelaufen, gruen, und am Thema vorbei. Deshalb wird ab
|
||
jetzt der Weg gegangen, den es wirklich gibt. */
|
||
let angelegt = 0;
|
||
for (const art of ARTEN) {
|
||
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||
art, titel: `Test ${art}`, text: "Inhalt", datum: heute,
|
||
dringlichkeit: "mittel",
|
||
});
|
||
if (status === 200 || status === 201) angelegt++;
|
||
else ok(false, `Art "${art}" liess sich nicht anlegen (HTTP ${status})`);
|
||
}
|
||
ok(angelegt === ARTEN.length, `alle ${angelegt} Arten lassen sich anlegen`);
|
||
|
||
/* GEGENPROBE: eine erfundene Art muss abgelehnt werden. Sonst pruefte
|
||
die Zeile darueber nur, dass der Server irgendetwas annimmt. */
|
||
{
|
||
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||
art: "erfundeneart", titel: "Darf nicht", datum: heute,
|
||
});
|
||
ok(status >= 400, `eine erfundene Art wird abgelehnt (HTTP ${status})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
4b. FUER ALLE, NICHT FUER EINEN
|
||
|
||
Die Agentur ist der einzige Bereich, in dem ein Eintrag ALLEN gilt.
|
||
Geprueft wird deshalb beides: dass jeder Creator alles sieht -- und
|
||
dass diese Ausnahme NICHT auf die Betreuungsbereiche abfaerbt. Der
|
||
zweite Teil ist der wichtigere: Eine Sichtbarkeitsregel, die man an
|
||
der falschen Stelle aufmacht, gibt fremde Akten frei, und man sieht
|
||
es der Zeile nicht an.
|
||
======================================================================= */
|
||
melde("\n=== Fuer alle, nicht fuer einen ===");
|
||
{
|
||
const { status, daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksLuna);
|
||
ok(status === 200, `Luna darf den Bereich oeffnen (HTTP ${status})`);
|
||
ok((daten?.eintraege || []).length === ARTEN.length,
|
||
`sie sieht alle ${(daten?.eintraege || []).length} Eintraege — obwohl keiner ihr zugeordnet ist`);
|
||
ok((daten?.eintraege || []).every((e) => e.creator_id === null),
|
||
"und keiner davon traegt eine Creator-Zuordnung");
|
||
}
|
||
{
|
||
/* Mia ist eine ZWEITE Creatorin, die nichts angelegt hat und der
|
||
nichts gehoert. Wenn die Regel nur ueber `erstellt_von` gestolpert
|
||
waere, saehe Luna vielleicht etwas und Mia nichts. */
|
||
const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia);
|
||
ok((daten?.eintraege || []).length === ARTEN.length,
|
||
`auch Mia sieht alle ${(daten?.eintraege || []).length} — sie hat mit keinem davon etwas zu tun`);
|
||
}
|
||
{
|
||
/* Eine ausdrueckliche Zuordnung muss der Server verwerfen. Sonst
|
||
genuegt ein Aufruf an der Oberflaeche vorbei -- oder ein spaeteres
|
||
Formular, das das Feld wieder mitschickt --, und das Event ist
|
||
fuer alle anderen wieder unsichtbar. */
|
||
await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||
art: "kampagne", titel: "Versuch mit Zuordnung", datum: heute, creator_id: idLuna,
|
||
});
|
||
const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia);
|
||
const e = (daten?.eintraege || []).find((x) => x.titel === "Versuch mit Zuordnung");
|
||
ok(!!e, "ein Eintrag, der ausdruecklich Luna zugewiesen wurde, ist trotzdem fuer Mia da");
|
||
ok(e?.creator_id === null, `und seine Zuordnung wurde verworfen (creator_id = ${e?.creator_id})`);
|
||
/* Und derselbe Riegel fuer den freien Namen -- er wird an einer
|
||
ANDEREN Stelle gesetzt (externPruefen) und waere sonst das Loch
|
||
neben der geschlossenen Tuer. */
|
||
await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||
art: "kampagne", titel: "Versuch mit Namen", datum: heute, creator_extern: "Marke XY",
|
||
});
|
||
const { daten: d2 } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia);
|
||
const n = (d2?.eintraege || []).find((x) => x.titel === "Versuch mit Namen");
|
||
ok(n?.creator_extern === null || n?.creator_extern === undefined,
|
||
`auch ein frei getippter Name wird verworfen (creator_extern = ${JSON.stringify(n?.creator_extern)})`);
|
||
}
|
||
{
|
||
/* DIE ENTSCHEIDENDE GEGENPROBE.
|
||
|
||
Die Ausnahme haette man auch in sichtbar() einbauen koennen -- eine
|
||
Zeile weniger, und dieselbe Wirkung fuer die Agentur. Nur haengen
|
||
an derselben Funktion auch Aufgaben, Dateien, Termine und Calls.
|
||
Diese Pruefung ist der Grund, warum das auffiele. */
|
||
await ruf("POST", "/workspace/api/bereich/technik", keksDogi, {
|
||
art: "problem", titel: "Nur fuer Mia", datum: heute, creator_id: idMia,
|
||
});
|
||
const { daten } = await ruf("GET", "/workspace/api/bereich/technik", keksLuna);
|
||
const fremd = (daten?.eintraege || []).filter((e) => e.titel === "Nur fuer Mia");
|
||
ok(fremd.length === 0,
|
||
`Luna sieht Mias Technik-Eintrag NICHT — die Agentur-Ausnahme faerbt nicht ab (${fremd.length} gefunden)`);
|
||
}
|
||
{
|
||
/* Sehen ja, schreiben nein -- ausdruecklicher Wunsch: "die sollen
|
||
alles sehen was wir eintragen aber nicht eintragen koennen". */
|
||
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksLuna, {
|
||
art: "kampagne", titel: "Von Luna", datum: heute,
|
||
});
|
||
ok(status === 403, `Luna darf hier NICHTS eintragen (HTTP ${status})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
4c. AGENTUR-EVENTS: Zeitraum, Aufgaben, Regeln
|
||
======================================================================= */
|
||
melde("\n=== Agentur-Events ===");
|
||
{
|
||
/* ORTSZEIT, nicht UTC. `new Date(...).toISOString().slice(0,10)` waere
|
||
zwischen Mitternacht und zwei Uhr einen Tag daneben -- und weil der
|
||
Server den Zeitraum gegen `heute` (Ortszeit) prueft, waere diese
|
||
Pruefung nachts rot, ohne dass sich etwas geaendert haette.
|
||
Gefunden von pruef-struktur, das genau danach sucht. Mittags
|
||
gerechnet, damit auch eine Zeitumstellung im Zeitraum nichts
|
||
verschiebt. */
|
||
const ende = (() => {
|
||
const d = new Date(`${heute}T12:00:00`);
|
||
d.setDate(d.getDate() + 14);
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||
})();
|
||
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||
art: "kampagne", titel: "Sommer-Aktion", text: "Beschreibung", datum: heute,
|
||
event_ende: ende, event_aufgaben: "1 Punkt je Live-Stunde", event_regeln: "Nur eigenes Konto",
|
||
});
|
||
ok(status === 201, `ein Event mit Zeitraum, Aufgaben und Regeln laesst sich anlegen (HTTP ${status})`);
|
||
|
||
const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksLuna);
|
||
const e = (daten?.eintraege || []).find((x) => x.titel === "Sommer-Aktion");
|
||
ok(e?.event_ende === ende, `das Ende kommt beim Creator an (${e?.event_ende})`);
|
||
ok(e?.event_aufgaben === "1 Punkt je Live-Stunde", "die Aufgaben kommen an");
|
||
ok(e?.event_regeln === "Nur eigenes Konto", "die Regeln kommen an");
|
||
|
||
/* GEGENPROBEN. Ohne sie hiesse "es laesst sich anlegen" nur, dass der
|
||
Server irgendetwas annimmt. */
|
||
const verdreht = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||
art: "kampagne", titel: "Verdreht", datum: ende, event_ende: heute });
|
||
ok(verdreht.status === 400, `ein Ende VOR dem Beginn wird abgelehnt (HTTP ${verdreht.status})`);
|
||
|
||
const unsinn = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
|
||
art: "kampagne", titel: "Unsinn", datum: heute, event_ende: "31.12.2026" });
|
||
ok(unsinn.status === 400, `ein Datum in falscher Schreibweise wird abgelehnt (HTTP ${unsinn.status})`);
|
||
|
||
/* Beim AENDERN gilt dasselbe -- und zwar auch dann, wenn nur EINE
|
||
der beiden Seiten mitgeschickt wird. Genau hier waere die Pruefung
|
||
sonst blind: pruefe() sieht nur, was im Aufruf steht. */
|
||
const { daten: d3 } = await ruf("GET", "/workspace/api/bereich/agentur", keksDogi);
|
||
const id = (d3?.eintraege || []).find((x) => x.titel === "Sommer-Aktion")?.id;
|
||
const nurEnde = await ruf("PATCH", `/workspace/api/bereich/agentur/${id}`, keksDogi,
|
||
{ event_ende: "2020-01-01" });
|
||
ok(nurEnde.status === 400,
|
||
`auch beim Aendern nur des Endes schlaegt der verdrehte Zeitraum an (HTTP ${nurEnde.status})`);
|
||
const nurBeginn = await ruf("PATCH", `/workspace/api/bereich/agentur/${id}`, keksDogi,
|
||
{ datum: "2099-01-01" });
|
||
ok(nurBeginn.status === 400,
|
||
`und ebenso, wenn nur der Beginn nach hinten geschoben wird (HTTP ${nurBeginn.status})`);
|
||
|
||
/* Die Event-Felder gehoeren der Agentur. In einem anderen Bereich
|
||
muessen sie stillschweigend verfallen -- sonst haengt eines Tages
|
||
ein Preisausschreiben an einem Schutzvorfall. */
|
||
await ruf("POST", "/workspace/api/bereich/schutz", keksDogi, {
|
||
art: "vorfall", titel: "Kein Event", datum: heute, event_regeln: "darf nicht ankommen" });
|
||
const { daten: d4 } = await ruf("GET", "/workspace/api/bereich/schutz", keksDogi);
|
||
const v = (d4?.eintraege || []).find((x) => x.titel === "Kein Event");
|
||
ok(!v?.event_regeln, `im Schutz-Bereich verfallen die Event-Felder (${JSON.stringify(v?.event_regeln)})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
5. Kommt er ueberall an, wo Bereiche vorkommen?
|
||
======================================================================= */
|
||
melde("\n=== Ueberall, wo Bereiche vorkommen ===");
|
||
{
|
||
/* Der Wochenbericht. Hier stand die Liste der fuenf Bereiche noch
|
||
einmal von Hand abgeschrieben -- der neue waere schlicht nicht
|
||
vorgekommen, ohne Fehler und ohne Luecke. */
|
||
const { daten } = await ruf("GET", "/workspace/api/report", keksDogi);
|
||
const b = daten?.bereiche || {};
|
||
/* 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`);
|
||
}
|
||
{
|
||
/* Die Suche. Ein Treffer ohne Ueberschrift sieht aus wie ein Fehler. */
|
||
const { daten } = await ruf("GET", "/workspace/api/suche?q=" + encodeURIComponent("Test kampagne"), keksDogi);
|
||
const text = JSON.stringify(daten);
|
||
ok(/Kampagnen & Events/.test(text),
|
||
"die Suche beschriftet Treffer mit dem Namen des Bretts");
|
||
}
|
||
|
||
/* =======================================================================
|
||
6. Die Kachel — mit einem echten Browser
|
||
======================================================================= */
|
||
melde("\n=== Die Kachel ===");
|
||
{
|
||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await pw.chromium.launch();
|
||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||
const seite = await ctx.newPage();
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
||
await seite.click('.rolle[data-rolle="admin"]');
|
||
await seite.fill("#code", "CODE-DOGI-0001");
|
||
await seite.click("button[type=submit]");
|
||
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
||
await seite.waitForTimeout(1100);
|
||
|
||
const kacheln = await seite.$$eval(".kachel", (k) => k.map((x) => ({
|
||
name: x.querySelector(".kachel__name")?.textContent.trim(),
|
||
ziel: x.querySelector(".kachel__link")?.getAttribute("href"),
|
||
ton: x.dataset.ton,
|
||
pfade: x.querySelectorAll(".kachel__zeichen path").length,
|
||
})));
|
||
const a = kacheln.find((x) => x.name === "Kampagnen & Events");
|
||
ok(!!a, `die Kachel "Kampagnen & Events" steht da (${kacheln.length} Kacheln insgesamt)`);
|
||
ok(a?.ziel === "bereich.html?b=agentur", `sie fuehrt nach "${a?.ziel}"`);
|
||
ok(Number(a?.pfade) >= 4, `ihr Zeichen hat ${a?.pfade} Pfade — es ist wirklich gezeichnet`);
|
||
|
||
/* KEINE ZWEI KACHELN MIT DEMSELBEN FARBTON NEBENEINANDER. Das Auge
|
||
vergleicht nur, was benachbart ist -- zwei gleiche Toene direkt
|
||
nebeneinander lesen sich als Fehler. */
|
||
const toene = kacheln.map((x) => x.ton);
|
||
const nachbarnGleich = toene.filter((t, i) => i > 0 && t === toene[i - 1]);
|
||
ok(nachbarnGleich.length === 0,
|
||
nachbarnGleich.length ? `zwei Nachbarkacheln haben denselben Ton (${nachbarnGleich.join(", ")})`
|
||
: `keine zwei benachbarten Kacheln teilen einen Farbton (${toene.length} geprueft)`);
|
||
|
||
/* Die Seite selbst muss aufgehen und ihren Namen tragen. */
|
||
await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
const titel = await seite.$eval(".titel", (e) => e.textContent.trim()).catch(() => "");
|
||
ok(/Kampagnen & Events/i.test(titel), `die Bereichsseite heisst "${titel}"`);
|
||
|
||
/* .eintrag-karte -- der Name aus bereich.js. Beim ersten Anlauf stand
|
||
hier geraten ".eintrag, .karte"; das traf nichts und meldete "0
|
||
Eintraege", obwohl vier dastanden. Ein falscher Selektor meldet
|
||
einen Fehler, den es nicht gibt -- genauso schaedlich wie einer,
|
||
der etwas uebersieht. */
|
||
const eintraege = await seite.$$eval(".eintrag-karte", (e) => e.length).catch(() => 0);
|
||
ok(eintraege >= ARTEN.length,
|
||
`sie zeigt ${eintraege} Eintraege (mindestens die ${ARTEN.length} angelegten)`);
|
||
|
||
/* Und die Titel muessen die sein, die oben angelegt wurden -- sonst
|
||
bewiese die Zahl nur, dass IRGENDWAS gezeichnet wurde. */
|
||
const kartenTitel = await seite.$$eval(".eintrag-titel", (e) => e.map((x) => x.textContent.trim()));
|
||
const fehlend = ARTEN.map((a) => `Test ${a}`).filter((t) => !kartenTitel.includes(t));
|
||
ok(fehlend.length === 0,
|
||
fehlend.length ? `diese Eintraege fehlen auf der Seite: ${fehlend.join(", ")}`
|
||
: `alle vier Titel stehen da: ${kartenTitel.join(" · ")}`);
|
||
|
||
/* ---- Das Event auf dem Bildschirm --------------------------------
|
||
Bis hierher ist bewiesen, dass die Daten stimmen. Das sagt noch
|
||
nichts darueber, ob jemand sie SIEHT: Zeitraum, Aufgaben und
|
||
Regeln koennen vollstaendig in der Antwort stehen und trotzdem
|
||
nirgends gezeichnet werden -- ein vergessener Aufruf in karte()
|
||
reicht, und niemand merkt es, weil die Karte ja da ist. */
|
||
/* ==== AUF DIE NEUE KARTE UMGESTELLT (03.10.2026) ================
|
||
|
||
Die Eventkarte wurde umgebaut (siehe pruef-eventkarte.mjs): Buehne
|
||
mit Titel auf dem Banner, Zeitraum einmal statt dreimal, Aufgaben
|
||
als Liste zum Abhaken. Dabei haben die Bausteine neue Namen
|
||
bekommen -- `.event-zeitraum__marke` heisst jetzt `.ev-noch`,
|
||
`.event-block__text` ist zu einer Liste aus `.ev-punkt__text`
|
||
geworden.
|
||
|
||
DIESE PRUEFUNG WURDE DESHALB NICHT GELOESCHT, SONDERN NACHGEZOGEN:
|
||
Sie sagt weiterhin dieselben FUENF Dinge zu, nur an der neuen
|
||
Stelle. Eine Pruefung wegzunehmen, weil sie nach einem Umbau rot
|
||
wird, senkt die Zahl und beweist danach nichts mehr
|
||
(Projektgedaechtnis, 28.08.2026: 789 statt 804 Pruefungen, und
|
||
acht davon fielen still weg).
|
||
|
||
WAS SICH INHALTLICH AENDERT: Der Zustand steht nicht mehr als
|
||
"laeuft bis TT.MM.JJJJ" da, sondern als "noch N Tage" -- dieselbe
|
||
Aussage, nur beantwortet sie jetzt die Frage, die man wirklich
|
||
hat. Geprueft wird deshalb auf ein WORT (nicht nur Farbe), nicht
|
||
mehr auf einen bestimmten Satz. */
|
||
const event = await seite.$$eval(".eintrag-karte", (karten) => {
|
||
const k = karten.find((x) => x.querySelector(".eintrag-titel")?.textContent.trim() === "Sommer-Aktion");
|
||
if (!k) return null;
|
||
return {
|
||
stand: k.dataset.event,
|
||
marke: k.querySelector(".ev-noch")?.textContent.trim(),
|
||
spanne: k.querySelector(".ev-spanne")?.textContent.trim(),
|
||
schilder: [...k.querySelectorAll(".ev-block__schild")].map((s) => s.textContent.trim()),
|
||
/* `.ev-punkt__text` SIND DIE AUFGABEN, `.ev-zeile__text` DIE
|
||
REGELN (07.10.2026). Bis dahin war beides dieselbe flache
|
||
Liste; seit der Gliederung sind es zwei Sorten Zeile, und
|
||
die Regeln haben kein Kästchen mehr davor. Gelesen werden
|
||
beide -- und getrennt, damit die Prüfung unten sagen kann,
|
||
WELCHE Sorte fehlt, statt nur „einer von zweien fehlt". */
|
||
texte: [...k.querySelectorAll(".ev-punkt__text")].map((s) => s.textContent.trim()),
|
||
/* ZEILEN **UND** ABSAETZE. Eine Regelzeile MIT
|
||
Aufzaehlungszeichen wird ein `.ev-zeile`, eine OHNE ein
|
||
`.ev-absatz` -- und das ist richtig so: Sonst wuerden die
|
||
Prosa-Absaetze einer Kampagnenseite („LIVE Glow Up ist
|
||
eine exklusive 4-woechige Challenge ...") alle zu
|
||
Aufzaehlungspunkten, also wieder zu der flachen Liste, die
|
||
am 07.10.2026 abgeschafft wurde.
|
||
|
||
Dieses Testevent wird ueber die Schnittstelle angelegt,
|
||
ohne Zeichen davor. Der Zeileneditor im Formular setzt sie
|
||
sonst selbst (geprueft in pruef-eventkarte.mjs:
|
||
„gleichmaessig gesetzte Zeichen"), im echten Betrieb ist
|
||
also meist die erste Form zu sehen. Gemessen werden beide,
|
||
getrennt -- so sagt die Prüfung, WELCHE Form herauskam. */
|
||
regelzeilen: [...k.querySelectorAll(".ev-zeile__text")].map((s) => s.textContent.trim()),
|
||
regelabsaetze: [...k.querySelectorAll(".ev-absatz")].map((s) => s.textContent.trim()),
|
||
/* Und ob die Regeln hinter dem Aufklappen liegen -- das ist
|
||
der Grund, warum die Kachel kürzer geworden ist. */
|
||
klappZu: k.querySelector(".ev-mehr__inhalt")?.hidden ?? null,
|
||
klappWort: k.querySelector(".ev-mehr__wort")?.textContent.trim() || null,
|
||
};
|
||
});
|
||
ok(!!event, "die Event-Karte Sommer-Aktion steht auf der Seite");
|
||
ok(event?.stand === "laeuft", `sie ist als laufend gekennzeichnet ("${event?.stand}")`);
|
||
/* Ein Wort, kein leerer Platzhalter -- und es muss zum Zustand
|
||
passen: "noch N Tage" bzw. "heute ist der letzte Tag". */
|
||
ok(/^(noch \d+ Tage?|heute ist der letzte Tag)$/.test(event?.marke || ""),
|
||
`der Zustand steht als WORT da, nicht nur als Farbe ("${event?.marke}")`);
|
||
ok(/–/.test(event?.spanne || ""), `und der Zeitraum als Spanne ("${event?.spanne}")`);
|
||
/* „Regeln & Infos" SEIT DEM 07.10.2026. Der Block trägt nicht
|
||
mehr nur die Regeln, sondern die ganze Gliederung der
|
||
Kampagnenseite -- Zeitplan, Rangliste, Teilnahme. „Regeln"
|
||
allein wäre dafür zu wenig gesagt. */
|
||
ok(event?.schilder.join(",") === "Aufgaben,Regeln & Infos",
|
||
`beide Bloecke sind beschriftet (${event?.schilder.join(", ")})`);
|
||
ok(event?.texte.includes("1 Punkt je Live-Stunde"),
|
||
`die Aufgabe steht als abhakbarer Punkt da (${JSON.stringify(event?.texte)})`);
|
||
/* OHNE AUFZAEHLUNGSZEICHEN GETIPPT -> ABSATZ. Begruendung oben
|
||
bei der Messung. Geprueft wird, dass die Regel ANKOMMT, und
|
||
daneben, in welcher Form -- ein „irgendwo steht es" waere
|
||
auch dann gruen, wenn die Gliederung alles in einen Topf
|
||
wirft. */
|
||
ok(event?.regelabsaetze.includes("Nur eigenes Konto")
|
||
|| event?.regelzeilen.includes("Nur eigenes Konto"),
|
||
`die Regel kommt in der Gliederung an (Zeilen `
|
||
+ `${JSON.stringify(event?.regelzeilen)}, Absaetze `
|
||
+ `${JSON.stringify(event?.regelabsaetze)})`);
|
||
ok(event?.regelabsaetze.includes("Nur eigenes Konto"),
|
||
" und zwar als Absatz, weil kein Aufzaehlungszeichen davorstand");
|
||
/* GEGENPROBE: Sie steht NICHT zwischen den Aufgaben. Ohne sie
|
||
waere unbelegt, dass Aufgaben und Regeln getrennt bleiben --
|
||
und genau das war der Mangel im Bildschirmfoto vom
|
||
07.10.2026. */
|
||
ok(!event?.texte.includes("Nur eigenes Konto"),
|
||
"und nicht zwischen den abhakbaren Aufgaben");
|
||
|
||
/* ---- UND DIE REGELN LIEGEN HINTER EINEM DRUCK (07.10.2026) --
|
||
|
||
Filipe: „die kacheln von den events soll auch nicht so lang
|
||
sein, die soll man aufklappen koennen." Hier wird nur geprüft,
|
||
DASS es so ist; wie viel Höhe das spart, misst
|
||
pruef-eventkarte.mjs in Abschnitt 4b.
|
||
|
||
DIE ZEILE DARÜBER BLEIBT TROTZDEM GÜLTIG: `hidden` nimmt ein
|
||
Element nicht aus dem Baum, der Inhalt ist also vorhanden und
|
||
nur zugeklappt. Genau das soll es sein -- verborgen, nicht
|
||
verloren. */
|
||
ok(event?.klappZu === true,
|
||
`die Gliederung steht zugeklappt da (${event?.klappZu})`);
|
||
ok(event?.klappWort === "Alle Infos",
|
||
`und der Knopf dazu ist beschriftet ("${event?.klappWort}")`);
|
||
|
||
/* GEGENPROBE dazu: Eine Karte, die KEIN Event ist, darf nichts davon
|
||
haben. Sonst bewiese der Block oben nur, dass die Elemente
|
||
existieren -- nicht, dass sie zur richtigen Art gehoeren. */
|
||
const schulung = await seite.$$eval(".eintrag-karte", (karten) => {
|
||
const k = karten.find((x) => x.querySelector(".eintrag-titel")?.textContent.trim() === "Test schulung");
|
||
return k ? { event: k.dataset.event ?? null, bloecke: k.querySelectorAll(".ev-block").length } : null;
|
||
});
|
||
ok(schulung && !schulung.event && schulung.bloecke === 0,
|
||
`eine Schulung bekommt weder Zustand noch Event-Bloecke (${JSON.stringify(schulung)})`);
|
||
|
||
/* ---- Die Zuordnung bietet nur "Agentur" an ------------------------
|
||
Der Ausgangspunkt des ganzen Umbaus: Hier stand die Creator-Liste,
|
||
und ein versehentlicher Klick darauf machte das Event fuer alle
|
||
anderen unsichtbar. */
|
||
await seite.click("#neu-oeffnen");
|
||
await seite.waitForTimeout(400);
|
||
const wahl = await seite.$$eval("#f-creator option", (o) => o.map((x) => x.textContent.trim()));
|
||
/* HIER STEHT WEITERHIN „Agentur" -- und das ist richtig
|
||
(berichtigt 07.10.2026). Beim Umbenennen des BRETTS habe ich
|
||
diese Zeile mitgenommen, ohne hinzusehen. Das Feld „Gilt für"
|
||
nennt aber das HAUS, nicht das Brett: Der Server bildet es in
|
||
workspace-bereiche.js aus `person.haus` -- „Das Rudel",
|
||
„Team Dogi" oder „Agentur". „Gilt für: Kampagnen & Events"
|
||
waere Unsinn.
|
||
|
||
Zwei Dinge heissen aehnlich und sind verschieden; wer beim
|
||
Umbenennen beide anfasst, benennt eines falsch. */
|
||
ok(wahl.length === 1 && wahl[0] === "Agentur",
|
||
`die Zuordnung bietet genau eine Wahl an: ${JSON.stringify(wahl)}`);
|
||
const hinweis = await seite.$eval("#hinweis-creator", (e) => e.textContent.trim()).catch(() => "");
|
||
ok(/allen/.test(hinweis), `und sagt, was das bedeutet ("${hinweis}")`);
|
||
|
||
/* ---- Das Formular wechselt mit der Art ---------------------------
|
||
Beim Oeffnen steht die erste Art (Agentur-Events) drin, also
|
||
muessen die Event-Felder sichtbar sein. Wechselt man auf Schulung,
|
||
muessen sie verschwinden -- und geleert werden, sonst faehrt ein
|
||
unsichtbares Enddatum an einer Schulung mit. */
|
||
const sichtbar = (id) => seite.$eval(id, (e) => !e.hidden);
|
||
ok(await sichtbar("#feld-ende") && await sichtbar("#feld-aufgaben") && await sichtbar("#feld-regeln"),
|
||
"bei Agentur-Events stehen Zeitraum, Aufgaben und Regeln im Formular");
|
||
const schild = await seite.$eval("#schild-titel", (e) => e.textContent.trim());
|
||
ok(schild === "Titel des Events", `und die Beschriftung passt zur Art ("${schild}")`);
|
||
|
||
await seite.fill("#f-ende", "2030-01-01");
|
||
await seite.selectOption("#f-art", "schulung");
|
||
await seite.waitForTimeout(300);
|
||
ok(!(await sichtbar("#feld-ende")) && !(await sichtbar("#feld-aufgaben")),
|
||
"bei Schulung verschwinden sie wieder");
|
||
const restEnde = await seite.$eval("#f-ende", (e) => e.value);
|
||
ok(restEnde === "",
|
||
`und das Enddatum faehrt nicht unsichtbar mit (Wert: "${restEnde}")`);
|
||
|
||
/* GEGENPROBE zum Selektor: Ein erfundener Bereich darf NICHT wie ein
|
||
echter aussehen -- weder in der Ueberschrift noch mit Eintraegen.
|
||
Die Anwendung schickt einen dorthin zurueck, wo er hingehoert. */
|
||
await seite.goto(BASIS + "/workspace/bereich.html?b=gibtesnicht", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
const falsch = await seite.$eval(".titel", (e) => e.textContent.trim()).catch(() => "");
|
||
const falschKarten = await seite.$$eval(".eintrag-karte", (e) => e.length).catch(() => 0);
|
||
ok(!/Agentur/i.test(falsch) && falschKarten === 0,
|
||
`ein erfundener Bereich zeigt weder die Agentur noch Eintraege `
|
||
+ `("${falsch}", ${falschKarten} Karten)`);
|
||
|
||
await ctx.close();
|
||
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-<stempel>" 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 */ }
|
||
|
||
melde(`\n${gemacht} Pruefungen`);
|
||
if (fehler) {
|
||
const t = letzteAusgabe();
|
||
melde("");
|
||
melde("=== Was der Server dazu gesagt hat (letzte 1800 Zeichen) ===");
|
||
melde(t ? t.slice(-1800) : "(nichts -- der Server hat geschwiegen)");
|
||
}
|
||
melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
||
process.exit(fehler ? 1 : 0);
|