diff --git a/.gitignore b/.gitignore
index 5d3cb8e3..57069a29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -110,3 +110,11 @@ zz-*
# ist die Entscheidung "gehoert das ins Verzeichnis?" ein Umbenennen
# und kein Vergessen.
tools/_*
+
+# Der naechtliche Lauf schreibt hier seinen Zustand hin -- Rohausgabe,
+# Vergleichszahlen und die Schlossdatei. Alles Laufzeit, nichts davon
+# gehoert in die Geschichte: Es aendert sich jede Nacht, und im Repo
+# waere es ein taeglicher Konflikt ohne Aussage. Das ERGEBNIS steht in
+# der Vault-Notiz (02 Projekte/Pruefstand.md), die Werkzeuge selbst
+# sind versioniert.
+tools/.nachtlauf-*
diff --git a/server/pruef-pruefzaehler.mjs b/server/pruef-pruefzaehler.mjs
new file mode 100644
index 00000000..f03f191a
--- /dev/null
+++ b/server/pruef-pruefzaehler.mjs
@@ -0,0 +1,107 @@
+/* =====================================================================
+ ZAEHLT DER LAEUFER AUCH WIRKLICH ALLE? (25.09.2026)
+ =====================================================================
+
+ Der naechtliche Lauf meldete am 25.09.2026 „37 Pruefungen sind rot".
+ Acht davon waren gar nicht rot: Sie liefen fehlerfrei durch,
+ Exitcode 0, keine einzige FEHL-Zeile -- aber `alles-pruefen.mjs`
+ zaehlte bei ihnen NULL Einzelpruefungen, und „null Pruefungen ist
+ ein Fehler" ist eine richtige Regel.
+
+ DER GRUND WAR EIN GROSSBUCHSTABE. Der Zaehler suchte `(ok|FEHL)`;
+ diese acht Dateien schreiben ` OK `. Zwei Schaeden auf einmal:
+
+ 1. Ihre Punkte fehlten in der Gesamtzahl -- allein in fuenf der
+ acht waren das 114 Stueck. Ausgerechnet die Zahl, die vor
+ stillen Aussetzern schuetzen soll, war selbst eine Luecke.
+ 2. Acht dauerhaft rote Eintraege in einer Notiz, die morgens
+ gelesen wird. Eine Warnung, die immer kommt, ist keine mehr.
+
+ WARUM ES DAFUER EINE PRUEFUNG GIBT UND NICHT NUR EINEN KOMMENTAR:
+ Weil der Kommentar den naechsten Fall nicht verhindert. Morgen legt
+ jemand eine Pruefdatei an, die ihre Ergebnisse wieder anders
+ schreibt -- mit drei Leerzeichen, mit einem Haken, auf englisch --
+ und sie verschwindet genauso lautlos aus der Gesamtzahl. Diese
+ Datei startet deshalb ein paar der schnellsten Pruefungen WIRKLICH
+ und vergleicht, was der Ausdruck aus ihrer Ausgabe herausliest.
+
+ DER AUSDRUCK WIRD AUS alles-pruefen.mjs GELESEN, nicht hier noch
+ einmal hingeschrieben. Zwei Fassungen desselben Musters waeren die
+ Sorte Dopplung, die genau dann auseinanderlaeuft, wenn es darauf
+ ankommt.
+
+ Aufrufen mit: node server/pruef-pruefzaehler.mjs
+ ===================================================================== */
+import { readFileSync } from "node:fs";
+import { spawn } from "node:child_process";
+import { join, dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
+
+let fehler = 0;
+const ok = (gut, text) => { if (!gut) fehler++; console.log(` ${gut ? "ok " : "FEHL"} ${text}`); };
+
+/* ---- Den Ausdruck holen, den der Laeufer wirklich benutzt --------- */
+const laeufer = readFileSync(join(WURZEL, "tools", "alles-pruefen.mjs"), "utf8");
+const treffer = laeufer.match(/const einzeln = \(aus\.match\((\/.+?\/gm)\)/);
+if (!treffer) {
+ console.log(" FEHL KONNTE NICHT NACHSEHEN: der Zaehl-Ausdruck steht nicht mehr da,");
+ console.log(" wo diese Pruefung ihn sucht (tools/alles-pruefen.mjs).");
+ console.log(" Das ist kein Befund am Programm -- diese Datei muss nachgezogen werden.");
+ process.exit(1);
+}
+const quelle = treffer[1];
+console.log(`Der Laeufer zaehlt mit: ${quelle}\n`);
+const bauen = () => new RegExp(quelle.slice(1, quelle.lastIndexOf("/")), "gm");
+
+/* ---- Die schnellsten Vertreter beider Schreibweisen --------------- */
+const PROBEN = [
+ { datei: "pruef-tippziele.mjs", schreibweise: "OK" },
+ { datei: "pruef-turn-wege.mjs", schreibweise: "OK" },
+ { datei: "pruef-leerzustand.mjs", schreibweise: "OK" },
+ { datei: "pruef-deutsche-texte.mjs", schreibweise: "ok" },
+];
+
+const laufen = (datei) => new Promise((fertig) => {
+ const kind = spawn(process.execPath, [join(WURZEL, "server", datei)],
+ { cwd: WURZEL, stdio: ["ignore", "pipe", "pipe"] });
+ let aus = "";
+ kind.stdout.on("data", (d) => { aus += d; });
+ kind.stderr.on("data", (d) => { aus += d; });
+ const wecker = setTimeout(() => { try { kind.kill("SIGKILL"); } catch { /* weg */ } }, 120_000);
+ kind.on("close", (code) => { clearTimeout(wecker); fertig({ aus, code }); });
+ kind.on("error", () => { clearTimeout(wecker); fertig({ aus, code: -1 }); });
+});
+
+console.log("=== Jede Schreibweise wird gezaehlt ===");
+for (const p of PROBEN) {
+ const r = await laufen(p.datei);
+ const zahl = (r.aus.match(bauen()) || []).length;
+ ok(zahl > 0,
+ `${p.datei.padEnd(26)} schreibt "${p.schreibweise}" -> ${zahl} Punkte erkannt`);
+}
+
+/* ---- GEGENPROBE: der Ausdruck darf nicht alles zaehlen ------------ */
+console.log("\n=== Gegenprobe: er zaehlt nicht wahllos mit ===");
+const beispiel = [
+ "OK ohne Einrueckung ist kein Pruefpunkt",
+ " OK zu weit eingerueckt",
+ " OKAY ist kein eigenes Wort",
+ " Ergebnis: alles gut",
+ " ok ein echter",
+ " OK auch ein echter",
+ " FEHL ein dritter",
+].join("\n");
+const gezaehlt = (beispiel.match(bauen()) || []).length;
+ok(gezaehlt === 3,
+ `von sieben Zeilen zaehlen genau die drei echten (${gezaehlt})`);
+
+/* Und er muss einen Aussetzer auch WIRKLICH sehen koennen. */
+ok(((" ok etwas\n ok noch etwas").match(bauen()) || []).length === 2,
+ "zwei Punkte werden als zwei gezaehlt");
+ok((("kein einziger Punkt hier").match(bauen()) || []).length === 0,
+ "und eine Ausgabe ohne Punkte als null -- sonst waere 'null ist ein Fehler' blind");
+
+console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : fehler + " Fehler"}`);
+process.exit(fehler === 0 ? 0 : 1);
diff --git a/server/pruef-rueckmeldung.mjs b/server/pruef-rueckmeldung.mjs
index 976655d7..48a88f8a 100644
--- a/server/pruef-rueckmeldung.mjs
+++ b/server/pruef-rueckmeldung.mjs
@@ -341,8 +341,23 @@ console.log("=== Die Kachel ===");
findet. */
const alle = await kacheln(dogi.keks);
const toene = alle.map((x) => x.ton).filter((t) => t !== undefined);
- ok(new Set(toene).size === toene.length,
- `kein Farbton doppelt (${toene.length} Kacheln vom Server)`);
+ /* UND WENN DOCH, DANN WELCHER -- die Meldung nennt die Nummer und
+ beide Kacheln. Vorher stand da nur "kein Farbton doppelt (34
+ Kacheln vom Server)"; daraus laesst sich nicht ablesen, WO man
+ suchen muss, und die Suche in vier Listen ueber zwei Dateien
+ dauert dann laenger als die ganze Reparatur. Dritter Ausgang im
+ Kleinen: Eine Pruefung, die nur "nein" sagt, ist eine halbe. */
+ const wo = new Map();
+ for (const k of alle) {
+ if (k.ton === undefined) continue;
+ if (!wo.has(k.ton)) wo.set(k.ton, []);
+ wo.get(k.ton).push(k.name || k.ziel || "?");
+ }
+ const doppelt = [...wo].filter(([, n]) => n.length > 1)
+ .map(([t, n]) => `Ton ${t}: ${n.join(" + ")}`);
+ ok(doppelt.length === 0,
+ `kein Farbton doppelt (${toene.length} Kacheln vom Server`
+ + `${doppelt.length ? " -- " + doppelt.join("; ") : ""})`);
}
d.close();
diff --git a/server/workspace.js b/server/workspace.js
index 69bf0b96..bf8eb56c 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -2857,7 +2857,19 @@ export function zusatzBereicheFuer(person) {
? "Das Rudel, das Team und die Moderation \u2014 auf ihrer eigenen Adresse"
: "Creator, Scouts und die Agentur \u2014 auf ihrer eigenen Adresse",
zeichen: "hinaus",
- ton: 40,
+ /* KEIN TON (25.09.2026). Hier stand 40 -- dieselbe Nummer wie
+ „Entwicklung", und DogFather sieht beide nebeneinander. Zwei
+ Kacheln in einer Farbe sind auf einer Wand, die ueber Farbe
+ funktioniert, keine Kleinigkeit: Die Farbe ist der einzige
+ Hinweis, den man ohne Lesen bekommt.
+
+ Eine 46. Bereichsfarbe waere die naheliegende Antwort und die
+ falsche: Gerechnet bliebe sie unter der Hausgrenze fuer den
+ Abstand (0,0862 statt 0,09) -- der Farbraum ist voll. Diese
+ Kachel gehoert aber auch gar keinem Bereich an, sie fuehrt
+ hinaus. Ohne `ton` faellt sie auf `--akzent` zurueck und ist
+ damit von allen 45 Bereichskacheln unterscheidbar. Die
+ Begruendung steht ausfuehrlich in start.css bei Ton 45. */
szene: "portal",
ziel: (nachCrew ? CREW_ADRESSE : WORKSPACE_ADRESSE) + "/workspace/start.html",
/* AUSSEN heisst: Diese Kachel fuehrt aus dem Haus hinaus. Ohne das
diff --git a/tools/alles-pruefen.mjs b/tools/alles-pruefen.mjs
index 46d8f0c3..c6a8ede6 100644
--- a/tools/alles-pruefen.mjs
+++ b/tools/alles-pruefen.mjs
@@ -76,7 +76,28 @@ const laufen = (datei) => new Promise((fertig) => {
kind.on("close", (code) => {
clearTimeout(wecker);
const sek = Math.round((Date.now() - start) / 100) / 10;
- const einzeln = (aus.match(/^\s{2}(ok|FEHL)\s/gm) || []).length;
+ /* ==== GROSS ODER KLEIN -- BEIDES ZAEHLT (25.09.2026) ==========
+
+ Hier stand `(ok|FEHL)`. Acht Pruefdateien schreiben aber „OK"
+ in Grossbuchstaben (pruef-kachelfarben, -livepunkt, -tippziele,
+ -turn-wege, -leerzustand, -nachfrage, -tagesruf,
+ -anruf-klingelt). Fuer sie zaehlte dieser Ausdruck NULL --
+ und weil „null Pruefungen ist ein Fehler" (die Regel direkt
+ darunter, und sie ist richtig), standen sie Nacht fuer Nacht
+ als ROT in der Notiz, obwohl sie fehlerfrei durchliefen.
+ Exitcode 0, keine FEHL-Zeile, trotzdem rot.
+
+ Zwei Schaeden auf einmal: Ihre Pruefungen fehlten in der
+ Gesamtzahl (also genau der Zahl, die vor stillen Aussetzern
+ schuetzen soll), und acht dauerhaft rote Eintraege gewoehnen
+ einem das Hinsehen ab.
+
+ DIE SCHREIBWEISE WIRD NICHT VEREINHEITLICHT, SONDERN GELESEN.
+ Acht Dateien umzuschreiben waere acht Gelegenheiten, etwas
+ kaputtzumachen -- und die neunte Datei, die jemand morgen
+ anlegt, schreibt ohnehin wieder, was sie will. Ein Zaehler, der
+ beide Schreibweisen kennt, braucht niemanden zu erziehen. */
+ const einzeln = (aus.match(/^\s{2}(ok|OK|FEHL)\s/gm) || []).length;
const schlecht = (aus.match(/^\s{2}FEHL\s/gm) || []).length;
if (abgewuergt) {
aus += `\n\n FEHL DIE PRUEFUNG HING. Nach ${Math.round(FRIST_MS / 1000)} s kam nichts mehr`
diff --git a/tools/nachtlauf.mjs b/tools/nachtlauf.mjs
index 3c89b18f..665e19f3 100644
--- a/tools/nachtlauf.mjs
+++ b/tools/nachtlauf.mjs
@@ -156,11 +156,53 @@ function auswerten(aus) {
const rote = [];
for (const treffer of aus.matchAll(/^### (pruef-[\w-]+\.mjs) \(Exitcode (-?\d+)\)$/gm)) {
/* Die erste FEHL-Zeile darunter ist die Auskunft, die man braucht --
- der Rest ist Zusammenhang. */
+ der Rest ist Zusammenhang.
+
+ ZWEI FENSTER, NICHT EINS (25.09.2026): Bisher wurde nur in den
+ ersten 900 Zeichen gesucht. Prüfungen mit viel Vorlauf (die
+ browsergestützten vor allem) haben ihre FEHL-Zeile weiter
+ hinten -- vier davon standen deshalb am 25.09. als „keine
+ Fehlerzeile gefunden" in der Notiz, und man konnte ihnen nicht
+ ansehen, ob sie kaputt waren oder nur lang. Jetzt wird zuerst
+ eng gesucht (damit die erste, wichtigste Zeile gewinnt) und
+ danach im ganzen Abschnitt. */
const ab = aus.indexOf(treffer[0]) + treffer[0].length;
- const stueck = aus.slice(ab, ab + 900);
- const erste = (stueck.split("\n").find((z) => /FEHL|Error|error:/.test(z)) || "").trim();
- rote.push({ datei: treffer[1], code: Number(treffer[2]), erste: erste.slice(0, 160) });
+ const bisNaechste = aus.indexOf("\n### pruef-", ab);
+ const ganz = aus.slice(ab, bisNaechste === -1 ? ab + 20000 : bisNaechste);
+ const suchen = (text) => (text.split("\n")
+ .find((z) => /FEHL|Error|error:/.test(z)) || "").trim();
+ let erste = suchen(ganz.slice(0, 900)) || suchen(ganz);
+
+ /* UND WENN GAR NICHTS DASTEHT, IST DAS AUCH EINE AUSKUNFT.
+ Dann sind die letzten Zeilen das Beste, was es gibt -- zusammen
+ mit dem Exitcode. Eine Meldung „kaputt, weiss nicht warum" ist
+ ehrlich; „keine Fehlerzeile gefunden" klingt nach einer
+ Kleinigkeit und ist doch dasselbe. */
+ let ohneGrund = false;
+ if (!erste) {
+ ohneGrund = true;
+ const letzte = ganz.split("\n").map((z) => z.trim())
+ .filter(Boolean).slice(-3).join(" / ");
+ erste = letzte || "(die Prüfung hat gar nichts ausgegeben)";
+ }
+
+ /* DIE HAEUFIGSTE URSACHE HAT EINEN NAMEN -- und sie steht in der
+ Uebersichtszeile ganz oben, nicht im Fehlerabschnitt. Am
+ 25.09.2026 waren acht der 37 roten Dateien von dieser Sorte:
+ fehlerfrei durchgelaufen, Exitcode 0, aber der Zaehler hat ihre
+ Punkte nicht erkannt (sie schreiben „OK" statt „ok"). „Null
+ Pruefungen ist ein Fehler" ist dann richtig UND irrefuehrend --
+ es klingt nach einer Pruefung, die nichts tut, und war eine,
+ die niemand zaehlt. */
+ const uebersicht = new RegExp(
+ `^\\s*FEHL\\s+${treffer[1].replace(/[.*+?^$()|[\]\\]/g, "\\$&")}\\s.*$`, "m");
+ const zeile = (aus.match(uebersicht) || [""])[0];
+ const nullPunkte = /KEINE PRUEFUNG GELAUFEN/.test(zeile);
+
+ rote.push({
+ datei: treffer[1], code: Number(treffer[2]),
+ erste: erste.slice(0, 220), ohneGrund, nullPunkte,
+ });
}
return {
dateienOk: dateien ? Number(dateien[1]) : null,
@@ -238,7 +280,27 @@ function notizSchreiben({ lage, vorher, minuten, konnteNicht }) {
for (const r of lage.rote) {
zeilen.push(`### \`${r.datei}\``);
zeilen.push("");
- zeilen.push(r.erste ? `\`\`\`\n${r.erste}\n\`\`\`` : "*(keine Fehlerzeile gefunden)*");
+ if (r.nullPunkte) {
+ zeilen.push(`**Kein Befund – es wurde keine einzige Prüfung GEZÄHLT** `
+ + `(Exitcode ${r.code}).` );
+ zeilen.push("");
+ zeilen.push("Das ist etwas anderes als „kaputt“: Die Datei ist "
+ + "durchgelaufen. Der Zähler hat ihre Punkte nur nicht erkannt – "
+ + "meist, weil sie ihre Ergebnisse anders schreibt als erwartet "
+ + "(` OK ` statt ` ok `, andere Einrückung, eigenes Format). "
+ + "Nachsehen lohnt trotzdem: Eine Datei, deren Punkte niemand "
+ + "zählt, kann still aufhören zu prüfen, ohne dass es auffällt. "
+ + "Zuletzt stand da:");
+ zeilen.push("");
+ } else if (r.ohneGrund) {
+ zeilen.push(`**Konnte nicht sagen, woran es lag** (Exitcode ${r.code}). `
+ + "Das ist etwas anderes als ein Befund: Die Prüfung ist nicht "
+ + "durchgelaufen, hat aber auch keine FEHL-Zeile hinterlassen. "
+ + "Meist steckt dahinter eine Zeitüberschreitung oder ein Absturz "
+ + "im Vorlauf, kein Fehler im Programm. Zuletzt stand da:");
+ zeilen.push("");
+ }
+ zeilen.push(`\`\`\`\n${r.erste}\n\`\`\``);
zeilen.push("");
zeilen.push(`Einzeln nachsehen: \`node server/${r.datei}\``);
zeilen.push("");
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 839adc78..5e7e2ca0 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+