GEFUNDEN UM 01:22, von pruef-kreislauf -- und nur, weil nachts
gearbeitet wurde.
Die Pruefung legt einen Wunsch mit dem HEUTIGEN Datum an und macht
daraus einen Termin. Gemessen:
geschickt: datum = 2026-10-01 (heuteLokal auf dem Server)
gespeichert: datum = 2026-09-30
Der Termin lag also in der Vergangenheit. Auf „Was ansteht" sortiert
er sich damit in den Abschnitt „Vorbei" ein -- und der ist mit
Absicht zugeklappt. Beim Wunsch stand „Daraus wurde ein Termin", auf
dem Brett war er nicht zu sehen. Zwei Stunden jede Nacht (im Winter
eine), genau in den Stunden, in denen hier gearbeitet wird.
URSACHE
const jetzt = () => new Date().toISOString(); // UTC
... jetzt().slice(0, 10) ... // UTC-Tag
ES WAR NICHT EINE STELLE. Nachgemessen: SECHZEHN in vierzehn Modulen,
davon ZEHN, die den falschen Tag in die Datenbank schreiben --
Termine, Wuensche, Highlights, Talente, Leads, Videos, Vorlagen --
und sechs, die „heute" vergleichen (ueberfaellige Aufgaben, Berichte,
die Frist einer Entwicklungsaufgabe).
NICHT ANGEFASST, WEIL RICHTIG: Rechnungen auf einem Datumstext mit
fester Uhrzeit (`Date.parse(tag + "T12:00:00Z") + n * 86400000`). Die
bleiben in jeder Zeitzone am selben Kalendertag -- kalender, teamlage
und serien machen es so, und das bleibt.
WARUM ES NIEMAND GEMERKT HAT
pruef-struktur sucht dieses Muster seit dem 06.09.2026. Aber:
- sie sah NUR in die `pruef-*.mjs`, nie in die Anwendung
- sie kannte die Schreibweise ueber eine FUNKTION nicht
(`const jetzt = () => ...` statt `const jetzt = ...`)
Die Wache stand vor den Pruefungen, nicht vor dem Haus -- derselbe
Fehler wie heute Nacht bei den Messports: eine Sicherung, die nur die
halbe Menge kennt, faellt in der anderen Haelfte aus, und zwar
lautlos, denn sie meldet ja „nichts gefunden".
Jetzt sieht sie in beides und kennt beide Schreibweisen. Beim ersten
scharfen Lauf fand sie sofort 23 weitere Stellen in den Pruefdateien
selbst -- dieselben Zeitbomben, gegen die sie gebaut worden war.
EINE ZWEITE WACHE, WEIL ICH SELBST HINEINGELAUFEN BIN
Mein Umbauwerkzeug hat in elf Modulen `heuteLokal()` eingesetzt und
die Einfuhr weggelassen: Es hat erst ersetzt und DANN gefragt, ob der
Name schon in der Datei steht -- da stand er, mein eigener Aufruf.
`node --check` sagt dazu nichts, „Laedt jedes Server-Modul?" auch
nicht: Die Datei ist syntaktisch tadellos. Erst der Aufruf faellt um
mit `ReferenceError: heuteLokal is not defined`. Gefunden hat es
pruef-video, zufaellig. Die anderen zehn waeren durchgerutscht.
Deshalb neu: „Ruft ein Modul etwas, das es nie eingefuehrt hat?" --
die Namen des Hauses aus den export-Zeilen gelesen, nicht
aufgezaehlt. 460 Aufrufe in 350 Dateien, alle mit Einfuhr.
pruef-kreislauf STELLT JETZT DIE RICHTIGE FRAGE
Sie war rot und hat den Fehler dabei nur gestreift: „`.kette` wird
nicht sichtbar", Zeitsperre nach 15 s. Das klingt nach der Anzeige
und schickt einen zur falschen Stelle. Neu:
- eine Zeile fragt das DATUM (ohne Browser, nennt den Fehler beim
Namen)
- der Browserteil klappt zu, was zu ist, und misst dann die Kette;
„gar nicht da" wird von „da und unsichtbar" unterschieden
NEBENBEFUND IN pruef-ics
Die Probe „fast richtig" war `echt.slice(0, -1) + "A"`. Der
Schluessel ist base64url; sein letztes Zeichen ist eines von
sechzehn. Endet er auf „A", IST die Probe der echte Schluessel, der
Server antwortet zu Recht mit 200, und die Pruefung meldet ein Loch,
das es nicht gibt -- einmal je sechzehn Laeufe. Heute Nacht zweimal
hintereinander, und die Suche ging eine halbe Stunde in eine
Aenderung, die damit nichts zu tun hatte.
GEPRUEFT
pruef-struktur 44 -> 59 Pruefungen, 0 Fehler
pruef-ics 37 -> 38, 0 Fehler
pruef-kreislauf Absturz bei Nr. 17 -> 25 Pruefungen, 0 Fehler
und gruen geblieben: treff 85, arten 28, video 74, uebernahme 39,
entwicklung 79, content 45, vorlagen 24, zuteilung 75,
scout-zuteilung 37, unterstuetzung 70, aufbewahrung 45,
bewerbung 91, treff-start 42, uebergang 65, nachwuchs 262,
auskunft 46, modi-ideen 30, neue-seiten 109, spicy 85,
wege-nach-draussen 67, aufgabenbrett 49, agentur 62,
bereiche-lesend 37 -- beide Haeuser
GEGENPROBEN, DIE WIRKLICH ROT WERDEN
- den UTC-Tag im `daraus`-Weg wieder eingebaut: pruef-kreislauf
meldet „er liegt HEUTE, nicht gestern (2026-09-30, heute ist
2026-10-01)", 25 Pruefungen, 1 Fehler -- und der Browserteil
bleibt gruen, weil er jetzt aufklappt. Jede Frage bei ihrer
eigenen Pruefung.
- eine Einfuhr aus workspace-video.js entfernt: die neue Wache
meldet „workspace-video.js: heuteLokal() (aus helfer-tag.mjs)"
- beide Erkennungen je gegen einen gebauten Rueckschritt geprueft
(Funktion, Variable, zwei Schritte, Date.now-Rechnung) und gegen
das, was NICHT anschlagen darf (UTC-Mittag, voller Zeitstempel,
fremdes Date, Name im Kommentar, Eigenschaft am Objekt)
EIN FEHLER BEIM UMBAU, HIER FESTGEHALTEN: Mein erster Lauf ueber die
Pruefdateien hat stumpf ersetzt und dabei KOMMENTARE umgeschrieben --
in sieben Dateien stand die alte Schreibweise als Beleg in der
Begruendung, und daraus wurde das Gegenteil. Bemerkt hat es der
Vergleich der Zahlen (34 Stellen statt der gemessenen 24), nicht die
Absicht. Zurueckgenommen und mit Schutz fuer Kommentare und
Zeichenketten wiederholt.
Datenbank vorher gesichert. Keine Schemaaenderung.
Co-Authored-By: Claude Opus 5 <[email protected]>
518 lines
24 KiB
JavaScript
518 lines
24 KiB
JavaScript
/* =====================================================================
|
|
FERTIGE INHALTE STATT LEERER BRETTER (17.09.2026)
|
|
|
|
Filipe, mit drei Bildschirmfotos nebeneinander -- Der Treff, das
|
|
Anschlagbrett, Was ansteht, alle drei mit demselben grauen Kasten
|
|
"Noch kein Eintrag in diesem Bereich":
|
|
|
|
"ich meine alle kategorien sehen einfach so scheisse und leer aus,
|
|
ich will fertige sachen schon drin wo auch die community und alle
|
|
anderen benutzen koennen"
|
|
|
|
Er hatte recht, und es stand sogar im Plan (§5.1): "Acht leere Seiten
|
|
sind schlimmer als keine." Ein Bereich, der nur aus Filterknoepfen
|
|
und einem grauen Kasten besteht, sieht nicht neu aus, sondern
|
|
aufgegeben -- und dreimal derselbe Satz sieht aus wie ein Defekt.
|
|
|
|
---------------------------------------------------------------------
|
|
WAS HIER GEPRUEFT WIRD
|
|
|
|
1. Der Katalog selbst: Passt jede Art zu ihrem Bereich? Ist jeder
|
|
Text mehr als eine Ueberschrift?
|
|
2. Das Uebernehmen: einzeln, alle auf einmal -- und zweimal
|
|
uebernehmen darf nichts verdoppeln.
|
|
3. UND DER FALL, DEN ICH MIR SELBST GEBAUT HABE: Die Bremse aus
|
|
Stufe 4 zaehlt Beitraege pro Stunde. Wer fuenfzehn Regeln
|
|
uebernimmt, haette nach der zehnten eine halb gefuellte Seite
|
|
und die Meldung, er habe zu viel geschrieben.
|
|
4. Die Verborgenheit: Wer hier nicht schreiben darf, bekommt den
|
|
Katalog gar nicht.
|
|
|
|
Aufrufen mit: node server/pruef-treff-start.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
import { ersatzschreibungen } from "./helfer-deutsch.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-treff-start");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-start-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
import { heuteLokal } from "./helfer-tag.mjs";
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-treff-start");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", 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 { TREFF_START } = await import("./workspace-treff-start.js");
|
|
const { BEREICHE, BEITRAEGE_JE_STUNDE } = await import("./workspace-bereiche.js");
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const schluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", schluessel, jetzt);
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen"
|
|
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
|
}
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Tili", "creator", "CODE-CREA-0001");
|
|
d.close();
|
|
|
|
async function anmelden(kachel, code) {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: kachel, code }),
|
|
});
|
|
return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
const dogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const crea = await anmelden("creator", "CODE-CREA-0001");
|
|
const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS });
|
|
|
|
/* =======================================================================
|
|
1. DER KATALOG SELBST
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Was bereitliegt ===");
|
|
{
|
|
const gesamt = Object.values(TREFF_START).reduce((n, l) => n + l.length, 0);
|
|
ok(gesamt >= 40, `${gesamt} fertige Eintraege in ${Object.keys(TREFF_START).length} Bereichen`);
|
|
|
|
/* JEDE ART MUSS ES IM BEREICH GEBEN. Sonst landet beim Uebernehmen
|
|
eine Art in der Datenbank, die das Formular nicht kennt -- und die
|
|
Zeile laesst sich hinterher nicht mehr aendern. */
|
|
const falsch = [];
|
|
for (const [bereich, liste] of Object.entries(TREFF_START)) {
|
|
const arten = BEREICHE[bereich]?.arten || {};
|
|
for (const v of liste) if (!Object.hasOwn(arten, v.art)) falsch.push(`${bereich}/${v.art}`);
|
|
}
|
|
ok(falsch.length === 0,
|
|
falsch.length ? `unbekannte Arten: ${falsch.join(", ")}` : "jede Art gibt es in ihrem Bereich");
|
|
|
|
/* EIN TEXT, DER NUR DEN TITEL WIEDERHOLT, ist keiner. Dieselbe Regel
|
|
wie beim Aufgabenkatalog der Modis. */
|
|
const duenn = [];
|
|
for (const [bereich, liste] of Object.entries(TREFF_START)) {
|
|
for (const v of liste) {
|
|
if (!v.text || v.text.length < v.titel.length + 25) duenn.push(`${bereich}/${v.titel}`);
|
|
}
|
|
}
|
|
ok(duenn.length === 0,
|
|
duenn.length ? `zu duenn: ${duenn[0]}` : "jeder Text sagt mehr als sein Titel");
|
|
|
|
/* KEINE DOPPELTEN TITEL innerhalb eines Bereichs -- das Uebernehmen
|
|
erkennt "schon da" am Titel. */
|
|
let doppelt = 0;
|
|
for (const liste of Object.values(TREFF_START)) {
|
|
doppelt += liste.length - new Set(liste.map((v) => v.titel)).size;
|
|
}
|
|
ok(doppelt === 0, `kein Titel doppelt (${doppelt})`);
|
|
|
|
/* UND SIE TRAGEN UMLAUTE. Dieselbe Falle wie bei den Kategorien und
|
|
den Bereichsnamen -- beide Male auf einem Bildschirmfoto gefunden. */
|
|
const ascii = [];
|
|
for (const [bereich, liste] of Object.entries(TREFF_START)) {
|
|
for (const v of liste) {
|
|
const t = ersatzschreibungen(`${v.titel} ${v.text}`);
|
|
if (t.length) ascii.push(`${bereich}/${v.titel} [${t.join(",")}]`);
|
|
}
|
|
}
|
|
ok(ascii.length === 0,
|
|
ascii.length ? `Ersatzschreibungen: ${ascii[0]}` : "alle Texte tragen echte Umlaute");
|
|
|
|
/* ---- DIE TEXTE SIND AUF DOGFATHER BEZOGEN (17.09.2026) ----------
|
|
|
|
Filipe: "die sachen sollen immer auf mich bezogen sein bitte. ich,
|
|
dogfather bin der mittelpunkt immer."
|
|
|
|
Der erste Entwurf war messbar anonym: 47 Eintraege, "DogFather"
|
|
kam fuenfmal vor, ein namenloses "wir" sechzehnmal. Das las sich
|
|
wie der Community-Bereich von irgendwem.
|
|
|
|
WARUM EINE QUOTE UND KEIN "JEDER TEXT": Bei der Telefonseelsorge
|
|
oder "Melden ist kein Petzen" waere ein eingebautes "DogFather"
|
|
aufgesetzt. Ein Text, der den Namen trotz allem traegt, ist
|
|
schlechter als einer ohne. */
|
|
const BEZUG = /DogFather|Casper|HasiDog|Clips/;
|
|
{
|
|
const alle = Object.values(TREFF_START).flat();
|
|
const mit = alle.filter((v) => BEZUG.test(`${v.titel} ${v.text}`)).length;
|
|
ok(mit / alle.length >= 0.6,
|
|
`${mit} von ${alle.length} Texten nennen DogFather, seine Kanaele oder Casper `
|
|
+ `(${Math.round(mit / alle.length * 100)} %)`);
|
|
|
|
/* JEDES BRETT, DAS VORSCHLAEGE HAT, ganz -- sie handeln von
|
|
seinen Streams, und keiner kommt ohne ihn aus.
|
|
|
|
DIE LISTE WIRD ABGELEITET, NICHT ABGESCHRIEBEN (24.09.2026).
|
|
Hier stand `["treff", "anschlag", "wunsch", "highlight"]`. Am
|
|
24.09. fielen die Vorschlaege fuer Highlights weg (Filipe: "da
|
|
sollen nur die sachen rein kommen die die modis, linke und
|
|
rechte hand oder dogfather reinsetzen"), und die Pruefliste
|
|
wusste es nicht: Sie verlangte weiter Texte von einem Brett,
|
|
das keine mehr hat. Dieselbe Falle wie jede abgeschriebene
|
|
Liste in diesem Haus -- eine, die niemand pflegt, kann nicht
|
|
veralten.
|
|
|
|
DASS ES UEBERHAUPT WELCHE GIBT, wird eine Zeile tiefer
|
|
geprueft. Sonst waere "alle geprueft" auch dann gruen, wenn
|
|
morgen gar kein Brett mehr Vorschlaege haette -- ein
|
|
uebersprungener Test beweist nichts. */
|
|
/* DIE ABSICHT BLEIBT, DIE LISTE WIRD GEPRUEFT (berichtigt beim
|
|
zweiten Anlauf am 24.09.2026).
|
|
|
|
Mein erster Versuch nahm einfach ALLE Bretter mit Vorschlaegen.
|
|
Das war bequem und falsch: "Regeln", "Mitmachen" und "Was
|
|
ansteht" haben zu Recht Saetze ohne seinen Namen ("Turnier",
|
|
"Schritt 2: Sag im Rudel, dass du Lust hast") -- fuer die gilt
|
|
die 60-%-Regel eine Zeile weiter oben, nicht diese hier. Die
|
|
Pruefung waere dadurch rot geworden, ohne dass etwas kaputt
|
|
war, und ich haette ihre Aussage veraendert statt ihre Liste zu
|
|
reparieren.
|
|
|
|
Richtig ist die Schnittmenge: die Bretter, die die Community
|
|
sieht UND die ueberhaupt Vorschlaege haben. Faellt eines weg,
|
|
faellt es hier lautlos heraus -- deshalb steht direkt darunter,
|
|
WIE VIELE es sein muessen, und fuer Highlights ausdruecklich,
|
|
dass es keine mehr hat. */
|
|
const GEMEINT = ["treff", "anschlag", "wunsch", "highlight"];
|
|
const mitVorschlaegen = GEMEINT.filter((b) => (TREFF_START[b] || []).length > 0);
|
|
ok(mitVorschlaegen.length === 3,
|
|
`${mitVorschlaegen.length} der Bretter, die die Community sieht, haben `
|
|
+ `Vorschlaege (${mitVorschlaegen.join(", ")})`);
|
|
for (const b of mitVorschlaegen) {
|
|
const liste = TREFF_START[b] || [];
|
|
const ohne = liste.filter((v) => !BEZUG.test(`${v.titel} ${v.text}`));
|
|
ok(liste.length > 0 && ohne.length === 0,
|
|
ohne.length ? `${b}: ohne Bezug -- "${ohne[0].titel}"`
|
|
: `${b}: alle ${liste.length} Texte sind auf ihn bezogen`);
|
|
}
|
|
/* UND HIGHLIGHTS HAT AUSDRUECKLICH KEINE MEHR. Ohne diese Zeile
|
|
wuerde das Wegfallen nur dazu fuehren, dass eine Pruefung
|
|
weniger laeuft -- und niemand merkte, wenn sie zurueckkaemen. */
|
|
ok(!(TREFF_START.highlight || []).length,
|
|
"Highlights hat keine fertigen Vorschlaege (auf Ansage vom 24.09.2026)");
|
|
|
|
/* GEGENPROBE: Ein anonymer Text MUSS auffallen. Ohne sie hiesse
|
|
"alle bezogen" nur, dass das Muster auf alles passt. */
|
|
ok(!BEZUG.test("Wir machen demnaechst etwas Neues. Schreibt uns."),
|
|
"ein anonymer Satz wuerde erkannt");
|
|
ok(BEZUG.test("Was soll DogFather im naechsten Stream machen?"),
|
|
"und ein bezogener nicht faelschlich");
|
|
}
|
|
|
|
/* DIE BEIDEN NACHSCHLAGEWERKE muessen ordentlich gefuellt sein. Ein
|
|
leeres Regelwerk ist schlimmer als gar keins: Im Streitfall hat
|
|
dann niemand etwas in der Hand. */
|
|
ok((TREFF_START.regeln || []).length >= 12,
|
|
`Regeln & Hilfe hat ${(TREFF_START.regeln || []).length} Eintraege`);
|
|
ok((TREFF_START.mitmachen || []).length >= 8,
|
|
`Mitmachen hat ${(TREFF_START.mitmachen || []).length}`);
|
|
/* UND DIE HILFE NENNT EINE ECHTE NUMMER. Ein Community-Bereich fuer
|
|
junge Leute ohne Notrufnummer ist eine vertane Gelegenheit. */
|
|
ok((TREFF_START.regeln || []).some((v) => /0800 111 0 111|116 111/.test(v.text || "")),
|
|
"und die Hilfe nennt die Telefonseelsorge");
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. UEBERNEHMEN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Uebernehmen ===");
|
|
{
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, { headers: { cookie: dogi } });
|
|
const j = await a.json().catch(() => null);
|
|
ok(a.status === 200 && (j?.vorschlaege || []).length > 0,
|
|
`der Katalog kommt (${j?.offen} offen)`);
|
|
|
|
const e = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, {
|
|
method: "POST", headers: kopf(dogi), body: JSON.stringify({ nr: 0 }),
|
|
});
|
|
const ej = await e.json().catch(() => null);
|
|
ok(e.status === 201 && ej?.angelegt === 1, `einer einzeln (${ej?.angelegt})`);
|
|
|
|
/* ZWEIMAL DARF NICHTS VERDOPPELN. Wer den Knopf zweimal drueckt --
|
|
oder zwei Leute gleichzeitig --, soll dieselbe Seite bekommen. */
|
|
const z = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, {
|
|
method: "POST", headers: kopf(dogi), body: JSON.stringify({ nr: 0 }),
|
|
});
|
|
const zj = await z.json().catch(() => null);
|
|
ok(zj?.angelegt === 0 && zj?.schon === 1, `zweimal legt nichts nach (${zj?.angelegt})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. DIE BREMSE DARF DAS NICHT VERHINDERN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Fuenfzehn Regeln auf einmal ===");
|
|
{
|
|
const wie_viele = (TREFF_START.regeln || []).length;
|
|
ok(wie_viele > BEITRAEGE_JE_STUNDE,
|
|
`es sind ${wie_viele} Regeln -- mehr als die Bremse je Stunde zulaesst (${BEITRAEGE_JE_STUNDE})`);
|
|
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, {
|
|
method: "POST", headers: kopf(dogi), body: "{}",
|
|
});
|
|
const j = await a.json().catch(() => null);
|
|
/* DAS IST DER PUNKT. Ginge der Weg durch die Bremse, stuenden hier
|
|
zehn Regeln und eine Fehlermeldung -- eine halb gefuellte Seite
|
|
mit dem Vorwurf, man habe zu viel geschrieben. Genau die Sorte
|
|
Sicherung, die das richtige Verhalten bestraft. */
|
|
ok(a.status === 201 && j?.angelegt === wie_viele,
|
|
`alle ${wie_viele} auf einmal (${j?.angelegt})`);
|
|
|
|
const l = await fetch(`${BASIS}/workspace/api/bereich/regeln`, { headers: { cookie: dogi } });
|
|
const lj = await l.json().catch(() => null);
|
|
ok((lj?.eintraege || []).length === wie_viele,
|
|
`und sie stehen wirklich da (${(lj?.eintraege || []).length})`);
|
|
|
|
/* GEGENPROBE: Die Bremse ist deshalb nicht abgeschaltet. Ein
|
|
gewoehnlicher Beitrag danach muss sie spueren. */
|
|
let letzte = 0;
|
|
for (let i = 0; i < BEITRAEGE_JE_STUNDE + 2; i++) {
|
|
const r = await fetch(`${BASIS}/workspace/api/bereich/treff`, {
|
|
method: "POST", headers: kopf(dogi),
|
|
body: JSON.stringify({ art: "frage", titel: `Frage Nummer ${i}`,
|
|
datum: heuteLokal() }),
|
|
});
|
|
letzte = r.status;
|
|
if (r.status === 429) break;
|
|
}
|
|
ok(letzte === 429, `die Bremse greift trotzdem noch (${letzte})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. WER DEN KATALOG NICHT BEKOMMT
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Die Verborgenheit ===");
|
|
{
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, { headers: { cookie: crea } });
|
|
ok(a.status === 404, `ein Creator bekommt ihn nicht (${a.status})`);
|
|
const b = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, {
|
|
method: "POST", headers: kopf(crea), body: "{}",
|
|
});
|
|
ok(b.status === 403 || b.status === 404,
|
|
`und kann auch nichts uebernehmen (${b.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. JEDER BEREICH SAGT ETWAS ANDERES, WENN ER LEER IST
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Die leeren Bretter ===");
|
|
{
|
|
/* DREI BILDSCHIRMFOTOS MIT DEMSELBEN SATZ sahen aus wie ein Defekt.
|
|
Jetzt sagt jeder Bereich in seinen eigenen Worten, was hier
|
|
hingehoert. */
|
|
const saetze = Object.entries(BEREICHE)
|
|
.filter(([k]) => TREFF_START[k])
|
|
.map(([k, e]) => [k, e.leer || ""]);
|
|
ok(saetze.every(([, t]) => t.length > 25),
|
|
`alle ${saetze.length} Community-Bereiche haben einen eigenen Leertext`);
|
|
ok(new Set(saetze.map(([, t]) => t)).size === saetze.length,
|
|
"und keiner davon ist derselbe");
|
|
ok(saetze.every(([, t]) => !/Noch kein Eintrag in diesem Bereich/.test(t)),
|
|
"der alte Einheitssatz kommt nicht mehr vor");
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. IM BROWSER: VORHER UND NACHHER
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Wie es aussieht ===");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1100, height: 950 } });
|
|
await kontext.addCookies(dogi.split("; ").map((k) => {
|
|
const [name, ...rest] = k.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
|
|
|
|
/* "mitmachen" ist noch unberuehrt -- dort ist der Katalog voll. */
|
|
await seite.goto(`${BASIS}/workspace/bereich.html?b=mitmachen`, { waitUntil: "networkidle" });
|
|
await seite.waitForSelector("#startkatalog:not([hidden])", { timeout: 15_000 });
|
|
await seite.waitForTimeout(400);
|
|
|
|
const vorher = await seite.evaluate(() => ({
|
|
karten: document.querySelectorAll(".startkatalog__karte").length,
|
|
alleKnopf: document.querySelector(".startkatalog__alle")?.textContent?.trim() || "",
|
|
leer: document.querySelector(".leer-hinweis")?.textContent?.trim() || "",
|
|
}));
|
|
/* WIE VIELE STEHEN DA? NICHT EINE ZAHL VON GESTERN (24.09.2026).
|
|
|
|
Hier stand `>= 8`. Das stimmte, bis am 20.09.2026 das Fenster
|
|
eingefuehrt wurde -- seither kommen HOECHSTENS VIER auf einmal
|
|
(FENSTER = 4 in workspace-bereiche.js), und die Pruefung war
|
|
vier Tage lang rot, ohne dass es jemandem gesagt wurde. Genau
|
|
die Sorte Zahl, vor der CLAUDE.md warnt: eine Rechnung von
|
|
gestern, die still falsch wird.
|
|
|
|
Gefragt wird jetzt der Server selbst -- er weiss, wie viele er
|
|
schickt, und die Oberflaeche muss genau so viele zeichnen.
|
|
Zusaetzlich `> 0`, damit "gleich viele" nicht auch bei null
|
|
gruen ist. */
|
|
const ausDerAntwort = await seite.evaluate(async () => {
|
|
const a = await fetch("/workspace/api/bereich/mitmachen/start?ab=0",
|
|
{ credentials: "same-origin" });
|
|
if (!a.ok) return null;
|
|
const j = await a.json();
|
|
return { geschickt: (j.vorschlaege || []).length, vorrat: j.vorrat, offen: j.offen };
|
|
});
|
|
ok(!!ausDerAntwort && ausDerAntwort.geschickt > 0
|
|
&& vorher.karten === ausDerAntwort.geschickt,
|
|
`${vorher.karten} Vorschlaege stehen bereit -- genau so viele, wie der `
|
|
+ `Server schickt (${ausDerAntwort?.geschickt}), von ${ausDerAntwort?.offen} offenen`);
|
|
ok(/Alle \d+ übernehmen/.test(vorher.alleKnopf), `mit einem Knopf ("${vorher.alleKnopf}")`);
|
|
/* DER LEERTEXT IST DER DES BEREICHS, nicht der Einheitssatz. */
|
|
ok(vorher.leer.includes("dazugehören"),
|
|
`und der leere Bereich sagt seinen eigenen Satz ("${vorher.leer}")`);
|
|
|
|
if (process.env.SCHIRM) {
|
|
const { fileURLToPath } = await import("node:url");
|
|
await seite.screenshot({ path: fileURLToPath(new URL("pruef-treff-start-vorher.png",
|
|
import.meta.url)), fullPage: false });
|
|
}
|
|
|
|
await seite.click(".startkatalog__alle");
|
|
await seite.waitForTimeout(1500);
|
|
const nachher = await seite.evaluate(() => ({
|
|
katalogWeg: !!document.getElementById("startkatalog")?.hidden,
|
|
eintraege: document.querySelectorAll(".eintrag-karte").length,
|
|
leerWeg: !document.querySelector(".leer-hinweis"),
|
|
}));
|
|
ok(nachher.eintraege >= 8, `nach einem Klick stehen ${nachher.eintraege} Eintraege da`);
|
|
ok(nachher.leerWeg, "der leere Kasten ist weg");
|
|
/* UND DER KATALOG VERSCHWINDET, wenn nichts mehr zu holen ist --
|
|
ein Angebot, das leer weiterhin dasteht, ist eine Sackgasse. */
|
|
ok(nachher.katalogWeg, "und der Katalog verschwindet, weil nichts mehr offen ist");
|
|
ok(konsole.length === 0,
|
|
konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole");
|
|
|
|
if (process.env.SCHIRM) {
|
|
const { fileURLToPath } = await import("node:url");
|
|
await seite.screenshot({ path: fileURLToPath(new URL("pruef-treff-start-nachher.png",
|
|
import.meta.url)), fullPage: false });
|
|
melde(" -- Bilder: server/pruef-treff-start-vorher.png und -nachher.png");
|
|
}
|
|
await kontext.close();
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. JEDES BRETT HAT SEINE FORM (Stufe 7)
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Die Form je Brett ===");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1000, height: 900 } });
|
|
await kontext.addCookies(dogi.split("; ").map((k) => {
|
|
const [name, ...rest] = k.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
|
|
|
|
/* Regeln sind schon uebernommen (Abschnitt 3). */
|
|
await seite.goto(`${BASIS}/workspace/bereich.html?b=regeln`, { waitUntil: "networkidle" });
|
|
await seite.waitForSelector(".brettabschnitt", { timeout: 15_000 });
|
|
await seite.waitForTimeout(400);
|
|
const r = await seite.evaluate(() => ({
|
|
abschnitte: [...document.querySelectorAll(".brettabschnitt")]
|
|
.map((z) => z.firstChild?.textContent?.trim() || ""),
|
|
marken: document.querySelectorAll(".brettmarken__weg").length,
|
|
/* Springt die Marke wirklich zu einer Ueberschrift? Ein Verweis
|
|
ins Leere sieht aus wie ein kaputter Sprung. */
|
|
zieleDa: [...document.querySelectorAll(".brettmarken__weg")]
|
|
.every((a) => !!document.getElementById(a.getAttribute("href").slice(1))),
|
|
}));
|
|
ok(r.abschnitte.length >= 3,
|
|
`Regeln & Hilfe ist ein Dokument mit ${r.abschnitte.length} Abschnitten (${r.abschnitte.join(" · ")})`);
|
|
ok(r.marken >= 3, `mit ${r.marken} Sprungmarken oben`);
|
|
ok(r.zieleDa, "und jede Marke findet ihre Ueberschrift");
|
|
|
|
/* Das Anschlagbrett: Aushaenge statt Zeilen. */
|
|
await seite.goto(`${BASIS}/workspace/bereich.html?b=anschlag`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(600);
|
|
const gross = await seite.evaluate(async () => {
|
|
const knopf = document.querySelector(".startkatalog__alle");
|
|
if (knopf) { knopf.click(); await new Promise((r2) => setTimeout(r2, 1500)); }
|
|
const t = document.querySelector("#liste .eintrag-titel");
|
|
return {
|
|
aushang: document.getElementById("liste")?.dataset.aushang || "",
|
|
titelGroesse: t ? Math.round(parseFloat(getComputedStyle(t).fontSize)) : 0,
|
|
anzahl: document.querySelectorAll(".eintrag-karte").length,
|
|
};
|
|
});
|
|
ok(gross.aushang === "ja", "das Anschlagbrett ist als Aushangbrett gesetzt");
|
|
/* GROSS heisst messbar gross -- groesser als ein Titel auf einem
|
|
gewoehnlichen Brett (1.05rem = 17 px). */
|
|
ok(gross.titelGroesse >= 20,
|
|
`und seine Aushaenge sind gross (${gross.titelGroesse} px)`);
|
|
ok(gross.anzahl >= 4, `${gross.anzahl} Aushaenge haengen`);
|
|
|
|
ok(konsole.length === 0,
|
|
konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole");
|
|
|
|
if (process.env.SCHIRM) {
|
|
const { fileURLToPath } = await import("node:url");
|
|
await seite.goto(`${BASIS}/workspace/bereich.html?b=regeln`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(600);
|
|
await seite.screenshot({ path: fileURLToPath(new URL("pruef-regeln.png",
|
|
import.meta.url)), fullPage: false });
|
|
melde(" -- Bildschirmfoto: server/pruef-regeln.png");
|
|
}
|
|
await kontext.close();
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
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);
|