der Notiz
Jede Nacht laeuft tools/nachtlauf.mjs ueber 193 Pruefdateien und
schreibt das Ergebnis als Notiz in Filipes Vault. Diese Notiz ist das
Einzige, was er von dem ganzen Aufwand zu sehen bekommt.
Und sie war die einzige Datei im Haus, die niemand geprueft hat.
=== WARUM DAS VORHER GAR NICHT GING ===
nachtlauf.mjs startet BEIM IMPORT sofort einen Lauf. Wer die Notiz
messen wollte, haette damit eine halbe Stunde Rechenzeit ausgeloest --
und nebenbei das echte Ergebnis auf Filipes Startseite ueberschrieben.
Am 04.09.2026 hat genau so ein Griff bei RunOne einen roten Alarm ueber
einer fehlerfreien Buchhaltung erzeugt, am Morgen einer Vorstellung.
„Ein Prueflauf ist kein harmloser Lesevorgang."
Der Textbau steht deshalb jetzt in tools/nachtlauf-notiz.mjs: nur
Textbau, kein Nebeneffekt, Importieren ist folgenlos.
=== UND EIN STUECK DER NOTIZ GEHOERT AB JETZT DEM MENSCHEN ===
Oben in der Notiz stand: „von Hand aendern bringt nichts, beim
naechsten Lauf ist es wieder ueberschrieben." Das war ehrlich und
trotzdem falsch herum gedacht.
Das Wertvollste an diesem Pruefstand ist naemlich nicht die Zahl,
sondern der Satz daneben: „die hier ist rot, weil sie den Umbau vom
24.09. nicht mitbekommen hat" oder „die ist echt, Finger weg". Genau
dieser Satz wurde jede Nacht geloescht. Heute Nacht standen 37
Dateien in der Liste, und bei 25 davon waere so ein Satz die halbe
Arbeit gewesen.
Jetzt gibt es einen Bereich zwischen zwei Marken, den der Lauf wieder
einsetzt statt ihn zu ueberschreiben. Fehlt er, wird er leer neu
angelegt -- mit der Erklaerung darin, wozu er da ist.
=== 30 PRUEFUNGEN, JEDE MIT GEGENPROBE ===
1. Der Block „von Hand" ueberlebt einen Lauf -- UND das alte
Ergebnis daneben verschwindet wirklich. Ohne die zweite Haelfte
wuerde die erste nur beweisen, dass die Datei nicht angefasst
wurde.
2. Ein leerer Block zaehlt als nicht vorhanden; sonst waere die
Erklaerung fuer immer weg, sobald jemand sie einmal loescht.
3. Weniger Pruefungen als gestern werden gemeldet, auch wenn alles
gruen ist (die Lehre vom 28.08.: 789 statt 804, gruen, und acht
Pruefungen je Geraet waren still weggefallen). Gegenprobe: Bei
MEHR Pruefungen darf der Satz NICHT kommen -- eine Warnung, die
immer kommt, ist keine Warnung mehr.
4. Der dritte Ausgang sagt „konnte nicht nachsehen", nennt den Grund
und wie alt der letzte echte Stand ist -- und behauptet nirgends,
alles sei in Ordnung.
5. „Keine Pruefung gezaehlt" wird als das benannt, was es ist: kein
Befund, sondern ein Zaehlproblem. Acht Dateien standen heute Nacht
aus diesem Grund als rot da.
Punkt 5 der Pruefung hat sich beim ersten Lauf gleich bezahlt gemacht:
Sie zaehlt nach, ob JEDER der vier Aufrufe in nachtlauf.mjs den
Notizpfad mitreicht. Drei davon sind die seltenen Ausgaenge, die man
beim Ausprobieren nie erwischt -- und an jedem einzelnen waere der
Block „von Hand" spurlos verschwunden. Beim Umbau hatte ich genau
diese drei zuerst vergessen.
GEMESSEN: 30 Pruefungen, 0 Fehler. Die echte Notiz im Vault wird dabei
nicht angefasst -- gearbeitet wird in einem mktemp-Ordner.
Co-Authored-By: Claude Opus 5 <[email protected]>
289 lines
12 KiB
JavaScript
289 lines
12 KiB
JavaScript
/* =====================================================================
|
|
nachtlauf.mjs — einmal in der Nacht alles prüfen und es AUFSCHREIBEN
|
|
|
|
Filipe am 23.09.2026 auf die Frage, ob ein nächtlicher Lauf die
|
|
Prüfungen selbst anstoßen und morgens sagen soll, welche rot sind:
|
|
„ja".
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM ES DAS BRAUCHT — drei Funde an EINEM Tag
|
|
|
|
Am 23.09.2026 sind mir drei Prüfungen aufgefallen, die rot waren,
|
|
ohne dass es jemandem aufgefallen wäre:
|
|
|
|
pruef-modi-wortleck seit dem 22.09. — der verborgene Rollenname
|
|
stand offen im Netz
|
|
pruef-zuteilung stürzte ab und prüfte danach gar nichts mehr
|
|
pruef-push-ziel meldete einen Befund, der in Wahrheit eine
|
|
erfüllte Bestellung war
|
|
|
|
Alle drei habe ich durch ZUFALL gefunden — weil sie neben dem lagen,
|
|
woran ich gerade gebaut habe. Prüfungen laufen nur, wenn jemand sie
|
|
startet, und man startet die, an denen man arbeitet.
|
|
|
|
---------------------------------------------------------------------
|
|
WOHIN DAS ERGEBNIS GEHT — und wohin ausdrücklich NICHT
|
|
|
|
In eine Notiz im Vault. NICHT in die laufende Anwendung.
|
|
|
|
Das ist keine Bequemlichkeit, sondern die Lehre vom 04.09.2026: Ein
|
|
Prüflauf ist kein harmloser Lesevorgang, sein Ergebnis wird
|
|
VERÖFFENTLICHT. Damals hat ein Netzproblem bei npm einen nächtlichen
|
|
Selbsttest rot gemacht, und ab da stand VanVan in roter Schrift auf
|
|
dem Startbildschirm, die Buchführung sei womöglich verletzt. An der
|
|
Buchführung war nichts.
|
|
|
|
Wer einen Prüflauf anstößt, muss vorher überlegen, was der
|
|
SCHLECHTESTE Ausgang auf dem Bildschirm einer Nutzerin anrichtet.
|
|
Hier richtet er nichts an: Er schreibt eine Notiz, die Filipe und ich
|
|
lesen. Die Community sieht davon nichts.
|
|
|
|
---------------------------------------------------------------------
|
|
DREI AUSGÄNGE, NICHT ZWEI
|
|
|
|
in Ordnung alles grün
|
|
nicht in Ordnung mindestens eine Datei rot
|
|
KONNTE NICHT NACHSEHEN der Lauf hat gar nicht stattgefunden
|
|
|
|
Der dritte ist der wichtige. Wenn der Rechner nachts aus war, gibt es
|
|
kein Ergebnis — und ein FEHLENDES Ergebnis sieht sonst genauso aus
|
|
wie ein altes gutes. Die Notiz sagt deshalb immer, WANN zuletzt
|
|
wirklich gemessen wurde, und ab zwei Tagen ohne Lauf steht das ganz
|
|
oben.
|
|
|
|
---------------------------------------------------------------------
|
|
DIE ZAHL WIRD MIT GESTERN VERGLICHEN
|
|
|
|
Ein grüner Lauf ist kein Beweis, solange nicht auch die ANZAHL
|
|
stimmt (28.08.2026, RunOne: 789 statt 804 Prüfungen, weil acht je
|
|
Gerät stillschweigend weggefallen waren). Diese Datei merkt sich
|
|
deshalb den letzten Stand und meldet jeden Rückgang — auch wenn
|
|
alles grün ist.
|
|
|
|
AUFRUF:
|
|
node tools/nachtlauf.mjs normal
|
|
node tools/nachtlauf.mjs --jetzt auch wenn gerade gearbeitet wird
|
|
===================================================================== */
|
|
|
|
import { spawn } from "node:child_process";
|
|
import {
|
|
writeFileSync, readFileSync, existsSync, mkdirSync, unlinkSync, statSync,
|
|
} from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { notizSchreiben, zeitWort } from "./nachtlauf-notiz.mjs";
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const WURZEL = join(HIER, "..");
|
|
|
|
/* Der Vault -- dort liest Filipe. Absichtlich fest und nicht geraten:
|
|
Wenn der Ordner umzieht, soll das auffallen und nicht still in einen
|
|
neuen Ordner schreiben, den niemand aufmacht. */
|
|
const VAULT = "C:/Users/qciga/Documents/Obelix/DogFather";
|
|
const NOTIZ = join(VAULT, "02 Projekte", "Pruefstand.md");
|
|
const ZUSTAND = join(WURZEL, "tools", ".nachtlauf-stand.json");
|
|
const SCHLOSS = join(WURZEL, "tools", ".nachtlauf-laeuft");
|
|
|
|
/* ZWEI LÄUFE GLEICHZEITIG SIND KEIN LAUF, SONDERN ZWEI KAPUTTE
|
|
(06.09.2026, RunOne). Beide belegen dieselben Ports und legen
|
|
dieselben Testdaten an; heraus kommen Fehler, die es nicht gibt --
|
|
und wer sie für echt hält, sucht stundenlang an Stellen, an denen
|
|
nichts kaputt ist.
|
|
|
|
DREI STUNDEN, dann gilt das Schloss als vergessen. Ein Schloss, das
|
|
nie verfällt, blockiert beim nächsten Stromausfall für immer -- und
|
|
was blockiert, wird abgeschafft. */
|
|
const SCHLOSS_FRIST_MS = 3 * 60 * 60 * 1000;
|
|
|
|
/* `zeitWort` kommt aus nachtlauf-notiz.mjs -- zwei Kopien derselben
|
|
Datumsschreibweise waeren zwei Staende, die auseinanderlaufen. */
|
|
|
|
function schlossNehmen() {
|
|
if (existsSync(SCHLOSS)) {
|
|
let alter = Infinity;
|
|
try { alter = Date.now() - statSync(SCHLOSS).mtimeMs; } catch { /* egal */ }
|
|
if (alter < SCHLOSS_FRIST_MS) {
|
|
let wer = "";
|
|
try { wer = readFileSync(SCHLOSS, "utf8").trim(); } catch { /* egal */ }
|
|
return { ok: false, grund: `Es läuft schon einer (seit ${Math.round(alter / 60000)} min, ${wer}).` };
|
|
}
|
|
/* Älter als die Frist: Der gehört niemandem mehr. */
|
|
try { unlinkSync(SCHLOSS); } catch { /* egal */ }
|
|
}
|
|
try {
|
|
writeFileSync(SCHLOSS, `PID ${process.pid}, gestartet ${zeitWort()}\n`, { flag: "wx" });
|
|
return { ok: true };
|
|
} catch {
|
|
return { ok: false, grund: "Das Schloss ließ sich nicht setzen." };
|
|
}
|
|
}
|
|
function schlossAbgeben() { try { unlinkSync(SCHLOSS); } catch { /* egal */ } }
|
|
|
|
function standLesen() {
|
|
try { return JSON.parse(readFileSync(ZUSTAND, "utf8")); } catch { return null; }
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Der Lauf selbst -- alles-pruefen.mjs macht die eigentliche Arbeit.
|
|
|
|
KEIN ZWEITER LÄUFER: Dort stecken die Frist je Datei, das Zählen der
|
|
Einzelpunkte und die Regel "null Prüfungen ist ein Fehler". Das hier
|
|
noch einmal zu bauen hiesse, zwei Rechnungen über dieselbe Frage zu
|
|
haben -- und eine davon wäre irgendwann anders.
|
|
--------------------------------------------------------------------- */
|
|
function laufen() {
|
|
return new Promise((fertig) => {
|
|
const start = Date.now();
|
|
const kind = spawn(process.execPath, [join("tools", "alles-pruefen.mjs")],
|
|
{ cwd: WURZEL, stdio: ["ignore", "pipe", "pipe"] });
|
|
let aus = "";
|
|
kind.stdout.on("data", (d) => { aus += d; });
|
|
kind.stderr.on("data", (d) => { aus += d; });
|
|
kind.on("error", (f) => fertig({ ging: false, grund: String(f?.message || f), aus }));
|
|
kind.on("close", (code) => fertig({
|
|
ging: true, code, aus,
|
|
minuten: Math.round((Date.now() - start) / 60000),
|
|
}));
|
|
});
|
|
}
|
|
|
|
/** Aus der Ausgabe die Zahlen holen -- gelesen, nicht geschätzt. */
|
|
function auswerten(aus) {
|
|
const dateien = aus.match(/Dateien:\s+(\d+) von (\d+)/);
|
|
const punkte = aus.match(/Einzelpunkte:\s+(\d+) von (\d+)/);
|
|
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.
|
|
|
|
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 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,
|
|
dateien: dateien ? Number(dateien[2]) : null,
|
|
punkteOk: punkte ? Number(punkte[1]) : null,
|
|
punkte: punkte ? Number(punkte[2]) : null,
|
|
rote,
|
|
};
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Die Notiz
|
|
--------------------------------------------------------------------- */
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Los
|
|
--------------------------------------------------------------------- */
|
|
const vorher = standLesen();
|
|
const erzwingen = process.argv.includes("--jetzt");
|
|
|
|
const schloss = erzwingen ? { ok: true } : schlossNehmen();
|
|
if (!schloss.ok) {
|
|
/* DER DRITTE AUSGANG. Kein Fehler -- nur: hier war nichts zu messen. */
|
|
mkdirSync(dirname(NOTIZ), { recursive: true });
|
|
writeFileSync(NOTIZ, notizSchreiben({ vorher, konnteNicht: schloss.grund }, NOTIZ), "utf8");
|
|
console.log("Konnte nicht nachsehen: " + schloss.grund);
|
|
process.exit(0);
|
|
}
|
|
|
|
let ergebnis;
|
|
try {
|
|
console.log("Nachtlauf gestartet — das dauert. " + zeitWort());
|
|
ergebnis = await laufen();
|
|
} finally {
|
|
if (!erzwingen) schlossAbgeben();
|
|
}
|
|
|
|
if (!ergebnis.ging) {
|
|
mkdirSync(dirname(NOTIZ), { recursive: true });
|
|
writeFileSync(NOTIZ, notizSchreiben({
|
|
vorher, konnteNicht: `Der Läufer ließ sich nicht starten: ${ergebnis.grund}`,
|
|
}, NOTIZ), "utf8");
|
|
console.log("Konnte nicht nachsehen: " + ergebnis.grund);
|
|
process.exit(0);
|
|
}
|
|
|
|
const lage = auswerten(ergebnis.aus);
|
|
|
|
/* KONNTE DER LAUF ÜBERHAUPT ETWAS MESSEN? Steht keine Zahl in der
|
|
Ausgabe, ist etwas anderes schiefgegangen als eine rote Prüfung --
|
|
und "0 von 0 in Ordnung" wäre die beruhigendste Lüge von allen. */
|
|
if (lage.dateien == null || lage.punkte == null || lage.punkte === 0) {
|
|
mkdirSync(dirname(NOTIZ), { recursive: true });
|
|
writeFileSync(NOTIZ, notizSchreiben({
|
|
vorher,
|
|
konnteNicht: "Der Lauf hat keine einzige Zahl geliefert — vermutlich ist er"
|
|
+ " abgebrochen, bevor er anfangen konnte.\n\nLetzte Ausgabe:\n\n```\n"
|
|
+ ergebnis.aus.slice(-700) + "\n```",
|
|
}, NOTIZ), "utf8");
|
|
console.log("Konnte nicht nachsehen: keine Zahlen in der Ausgabe.");
|
|
process.exit(0);
|
|
}
|
|
|
|
mkdirSync(dirname(NOTIZ), { recursive: true });
|
|
writeFileSync(NOTIZ, notizSchreiben({ lage, vorher, minuten: ergebnis.minuten }, NOTIZ), "utf8");
|
|
|
|
/* Der Stand für den nächsten Vergleich -- erst NACH der Notiz, damit
|
|
ein Absturz beim Schreiben nicht den alten Vergleichswert wegnimmt. */
|
|
writeFileSync(ZUSTAND, JSON.stringify({
|
|
wann: zeitWort(), zeit: Date.now(),
|
|
dateien: lage.dateien, dateienOk: lage.dateienOk,
|
|
punkte: lage.punkte, punkteOk: lage.punkteOk,
|
|
rote: lage.rote.map((r) => r.datei),
|
|
}, null, 1), "utf8");
|
|
|
|
/* Die ganze Ausgabe daneben -- für den Fall, dass die Notiz nicht
|
|
reicht. Sie wird jedes Mal überschrieben; zwei Läufe zu vergleichen
|
|
ist Sache der Zahlen oben, nicht eines wachsenden Protokolls. */
|
|
writeFileSync(join(WURZEL, "tools", ".nachtlauf-letzte-ausgabe.txt"), ergebnis.aus, "utf8");
|
|
|
|
console.log(`Fertig nach ${ergebnis.minuten} min: `
|
|
+ `${lage.dateienOk} von ${lage.dateien} Dateien, `
|
|
+ `${lage.punkteOk} von ${lage.punkte} Einzelprüfungen.`);
|
|
console.log(`Ergebnis steht in: ${NOTIZ}`);
|
|
process.exit(lage.rote.length ? 1 : 0);
|