Files
DogFatherGitandClaude Opus 5 7dc5356c80 Datum: der Tag kommt aus der Ortszeit, nicht aus UTC
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]>
2026-10-01 01:45:16 +02:00

399 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
Auskunft auf Knopfdruck — Art. 15 DSGVO (15.09.2026)
---------------------------------------------------------------------
DIE EINE FRAGE, DIE ZÄHLT: IST SIE VOLLSTÄNDIG?
Eine Auskunft, die eine Tabelle vergisst, ist schlimmer als keine —
sie ist eine falsche Aussage über die Daten eines Menschen. Deshalb
prüft diese Datei nicht, ob *etwas* zurückkommt, sondern ob **jede**
Stelle gefunden wird, an der etwas liegt:
1. Jede Spalte mit Fremdschlüssel auf `personen` ist erfasst –
abgeleitet aus der Datenbank, nicht gegen eine Zahl.
2. Jede Spalte, die auf `_id` endet und KEINEN Fremdschlüssel hat,
ist eingeordnet: entweder „ist eine Person" oder „ist keine".
Kommt eine fünfte dazu, wird das hier rot — und genau daran
stirbt sonst jede gepflegte Liste.
3. Daten werden wirklich gefunden: In sechs Tabellen wird etwas
angelegt, und alle sechs müssen in der Auskunft auftauchen.
---------------------------------------------------------------------
UND ZWEI, DIE NIEMALS DRINSTEHEN DÜRFEN
Zugangsgeheimnisse (Hash, Salt, Kennung) — Risiko ohne Nutzen
Andere Menschen — deren Daten gehören ihnen
Aufrufen mit: node server/pruef-auskunft.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta, "pruef-auskunft");
const ordner = mkdtempSync(join(tmpdir(), "ws-ausk-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
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-auskunft");
await new Promise((r) => setTimeout(r, 700));
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 CREW = "crew.dogfather-universe.com";
const HAUS = "workspace.dogfather-universe.com";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const modul = await import("./workspace-auskunft.js");
const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
let idDogi, idRieke, idGast;
{
const d = oeffne();
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);
const 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);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
};
idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
idRieke = anlegen("Rieke", "hand", "CODE-HAND-0001");
idGast = anlegen("Kessi", "gast", "CODE-GAST-0001");
/* SPUREN IN SECHS TABELLEN -- eine Auskunft ueber einen Menschen ohne
Daten beweist nichts. Bewusst verteilt: eigener Eintrag, fremder
Eintrag mit mir darin, Chat, Protokoll (ohne Fremdschluessel) und
treff_entfernt.autor_id (ebenfalls ohne). */
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)"
+ " VALUES ('treff','hallo','KESSI-EINTRAG','Hallo zusammen',?,?,?)")
.run(heuteLokal(), jetzt, idGast);
const raum = (() => {
d.prepare("INSERT INTO chat_raeume (art, name, erstellt, erstellt_von) VALUES ('gruppe','Testraum',?,?)")
.run(jetzt, idDogi);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
})();
d.prepare("INSERT INTO chat_teilnehmer (raum_id, person_id, seit) VALUES (?,?,?)").run(raum, idGast, jetzt);
d.prepare("INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)")
.run(raum, idGast, "KESSI-NACHRICHT", jetzt);
d.prepare("INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)")
.run(raum, idDogi, "FREMDE-NACHRICHT", jetzt);
d.prepare("INSERT INTO protokoll (zeitpunkt, person_id, rolle, aktion, detail, ip)"
+ " VALUES (?,?,?,?,?,?)").run(jetzt, idGast, "gast", "anmeldung", "KESSI-PROTOKOLL", "203.0.113.5");
d.prepare("INSERT INTO treff_massnahmen (person_id, art, grund, von, zeitpunkt)"
+ " VALUES (?,?,?,?,?)").run(idGast, "hinweis", "KESSI-MASSNAHME", idDogi, jetzt);
d.close();
}
function roh(pfad, host, kopf = {}) {
return new Promise((fertig, schief) => {
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: "GET",
headers: { Host: host, ...kopf } }, (antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
});
a.on("error", schief);
a.end();
});
}
function post(pfad, host, kopf, koerper) {
return new Promise((fertig, schief) => {
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: "POST",
headers: { Host: host, "Content-Type": "application/json", ...kopf } }, (antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
});
a.on("error", schief);
a.write(JSON.stringify(koerper));
a.end();
});
}
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
async function anmelden(rolle, code, host) {
/* Die Community bestaetigt beim ersten Mal ihr Alter (seit 15.09.2026).
Nur fuer 'gast' -- fuer alle anderen muss der Server das Feld gar
nicht erst sehen. */
const r = await post("/workspace/api/anmelden", host, {},
rolle === "gast" ? { rolle, code, alter_ok: true } : { rolle, code });
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
}
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
const kRieke = await anmelden("hand", "CODE-HAND-0001", CREW);
const kGast = await anmelden("gast", "CODE-GAST-0001", CREW);
ok(!!kDogi && !!kRieke && !!kGast, "DogFather, rechte Hand und ein Community-Mitglied sind angemeldet");
/* =======================================================================
1. DIE QUELLEN SIND ABGELEITET — UND VOLLSTÄNDIG EINGEORDNET
======================================================================= */
melde("");
melde("=== 1. Vollständigkeit der Quellen ===");
{
const d = oeffne();
const spalten = modul.personenSpalten(d);
ok(spalten.length > 30, `${spalten.length} Spalten im Haus zeigen auf einen Menschen`);
ok(spalten.some((s) => s.quelle === "fremdschluessel"), "die meisten über einen Fremdschlüssel");
ok(spalten.some((s) => s.quelle === "nachgetragen"), "und zwei sind nachgetragen");
/* DIE PRUEFUNG, DIE DIE LISTE AM LEBEN HAELT: Jede Spalte auf `_id`
ohne Fremdschluessel muss eingeordnet sein -- als Person oder als
keine. Baut jemand eine fuenfte, wird das hier rot, statt dass sie
still aus der Auskunft faellt. */
const tabellen = d.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
const undeklariert = [];
for (const { name } of tabellen) {
const fk = new Set(d.prepare(`PRAGMA foreign_key_list(${name})`).all().map((f) => f.from));
for (const c of d.prepare(`PRAGMA table_info(${name})`).all()) {
if (!/_id$/i.test(c.name) || !/INT/i.test(c.type)) continue;
if (fk.has(c.name)) continue;
undeklariert.push(`${name}.${c.name}`);
}
}
ok(undeklariert.length > 0, `${undeklariert.length} Spalten auf _id ohne Fremdschlüssel`);
const alsPerson = new Set(modul.ZUSATZ.map((z) => `${z.tabelle}.${z.spalte}`));
const alsNicht = new Set(modul.KEINE_PERSON);
const uneingeordnet = undeklariert.filter((s) => !alsPerson.has(s) && !alsNicht.has(s));
ok(uneingeordnet.length === 0,
uneingeordnet.length ? `NICHT EINGEORDNET: ${uneingeordnet.join(", ")}`
: `alle ${undeklariert.length} sind eingeordnet (${alsPerson.size} Person, ${alsNicht.size} nicht)`);
/* Und keine Karteileiche: Was eingeordnet ist, muss es auch geben. */
const erfunden = [...alsPerson, ...alsNicht].filter((s) => !undeklariert.includes(s));
ok(erfunden.length === 0,
erfunden.length ? `EINGEORDNET, GIBT ES ABER NICHT: ${erfunden.join(", ")}` : "und keine Karteileiche dabei");
ok(modul.ZUSATZ.every((z) => z.warum && z.warum.length > 20),
"jede nachgetragene Spalte sagt, warum sie keinen Fremdschlüssel hat");
d.close();
}
/* =======================================================================
2. DIE DATEN WERDEN WIRKLICH GEFUNDEN
======================================================================= */
melde("");
melde("=== 2. Was über Kessi gespeichert ist ===");
{
const a = await roh("/workspace/api/auskunft/mich", CREW, { cookie: kGast });
ok(a.code === 200, `ein Community-Mitglied bekommt seine Auskunft (${a.code})`);
const d = json(a);
ok(d.person?.name === "Kessi", `die eigene Zeile steht drin (${d.person?.name})`);
ok(d.durchsucht > 30, `${d.durchsucht} Stellen durchsucht`);
ok(d.gefunden >= 5, `an ${d.gefunden} davon liegt etwas über sie`);
const namen = (d.tabellen || []).map((t) => t.tabelle);
for (const muss of ["eintraege", "chat_nachrichten", "chat_teilnehmer",
"protokoll", "treff_massnahmen"]) {
ok(namen.includes(muss), `${muss} ist dabei`);
}
/* protokoll und treff_entfernt.autor_id haben KEINEN Fremdschluessel.
Dass protokoll trotzdem auftaucht, beweist, dass die nachgetragene
Liste wirkt -- ohne sie fiele es lautlos heraus. */
ok(namen.includes("protokoll"),
"auch das Protokoll – und das hat gar keinen Fremdschlüssel");
const roher = a.text;
ok(/KESSI-EINTRAG/.test(roher) && /KESSI-NACHRICHT/.test(roher) && /KESSI-MASSNAHME/.test(roher),
"die Inhalte stehen wirklich drin, nicht nur die Tabellennamen");
/* GEGENPROBE: Was NICHT ihres ist, ist auch nicht drin. */
ok(!/FREMDE-NACHRICHT/.test(roher),
"die Nachricht eines anderen aus demselben Raum steht NICHT drin");
ok(d.nicht_lesbar?.length === 0, "und jede Stelle ließ sich lesen");
}
/* =======================================================================
3. ZWEI DINGE, DIE NIEMALS DRINSTEHEN
======================================================================= */
melde("");
melde("=== 3. Was nicht drinsteht ===");
{
const a = await roh("/workspace/api/auskunft/mich", CREW, { cookie: kGast });
const roher = a.text;
const d = json(a);
/* 1. Zugangsgeheimnisse. */
ok(/Zugangsgeheimnis/.test(roher), "die Geheimnisfelder sind als solche markiert");
const dd = oeffne();
const echt = dd.prepare("SELECT code_hash, code_salt, code_kennung FROM personen WHERE id = ?").get(idGast);
dd.close();
ok(!roher.includes(echt.code_hash), "der echte Hash steht NICHT darin");
ok(!roher.includes(echt.code_salt), "das Salt auch nicht");
ok(!roher.includes(echt.code_kennung), "und die Kennung ebenso wenig");
/* 2. Andere Menschen. Die Maßnahme wurde von DogFather gesetzt --
seine Nummer ist sein Datum, nicht ihres. */
const massnahme = (d.tabellen || []).find((t) => t.tabelle === "treff_massnahmen");
const zeile = massnahme?.zeilen?.[0];
ok(zeile && zeile.von === "(eine andere Person)",
`wer die Maßnahme gesetzt hat, steht als „eine andere Person“ da (${zeile?.von})`);
/* GEGENPROBE -- ohne sie beweist die Zeile darueber nichts: IHRE
eigene Nummer bleibt stehen, sonst waere die Auskunft unbrauchbar. */
ok(zeile && Number(zeile.person_id) === Number(idGast),
`ihre eigene Nummer steht unverändert da (${zeile?.person_id})`);
ok(!new RegExp(`"von":\\s*${idDogi}\\b`).test(roher),
"und nirgends im Text steht die Nummer des anderen in einer Personenspalte");
}
/* =======================================================================
4. WER WEN FRAGEN DARF
======================================================================= */
melde("");
melde("=== 4. Wer wen fragen darf ===");
{
const eigene = await roh("/workspace/api/auskunft/mich", CREW, { cookie: kRieke });
ok(eigene.code === 200, `die rechte Hand bekommt ihre eigene (${eigene.code})`);
ok(json(eigene).person?.name === "Rieke", "und zwar ihre, nicht die eines anderen");
const fremd = await roh(`/workspace/api/auskunft/${idGast}`, CREW, { cookie: kRieke });
ok(fremd.code === 404, `die eines anderen bekommt sie nicht (${fremd.code})`);
ok(!/KESSI-NACHRICHT/.test(fremd.text), "und kein Wort daraus");
const gastFremd = await roh(`/workspace/api/auskunft/${idRieke}`, CREW, { cookie: kGast });
ok(gastFremd.code === 404, `ein Community-Mitglied erst recht nicht (${gastFremd.code})`);
/* GEGENPROBE: DogFather kann es -- er ist der Verantwortliche. Ohne
diese Zeile waere „404" oben auch dann gruen, wenn es den Weg gar
nicht gibt. */
const chef = await roh(`/workspace/api/auskunft/${idGast}`, HAUS, { cookie: kDogi });
ok(chef.code === 200, `DogFather schon (${chef.code})`);
ok(json(chef).person?.name === "Kessi", "und bekommt die richtige Person");
const ohne = await roh("/workspace/api/auskunft/mich", HAUS, {});
ok(ohne.code === 401, `ohne Anmeldung gar nichts (${ohne.code})`);
const erfunden = await roh("/workspace/api/auskunft/999999", HAUS, { cookie: kDogi });
ok(erfunden.code === 404, `eine erfundene Nummer ergibt 404 (${erfunden.code})`);
}
/* =======================================================================
5. JEDE AUSKUNFT HINTERLÄSST EINE SPUR
======================================================================= */
melde("");
melde("=== 5. Nachvollziehbar ===");
{
const d = oeffne();
const selbst = d.prepare("SELECT COUNT(*) AS n FROM protokoll WHERE aktion = 'auskunft_selbst'").get().n;
const fremd = d.prepare("SELECT detail FROM protokoll WHERE aktion = 'auskunft_fremd'").all();
d.close();
ok(selbst >= 3, `${selbst} eigene Auskünfte im Protokoll`);
ok(fremd.length >= 1, `${fremd.length} Auskunft über einen anderen Menschen`);
ok(/Kessi/.test(fremd[0]?.detail || ""),
`und dort steht, über wen (${fremd[0]?.detail})`);
}
/* =======================================================================
6. DER KNOPF STEHT AUF DEN SEITEN, DIE DEM MENSCHEN GEHÖREN
======================================================================= */
melde("");
melde("=== 6. Der Knopf ===");
{
/* NICHT "AUF DIESEN ZWEI SEITEN", SONDERN "FUER JEDE ROLLE
ERREICHBAR" (umgestellt 21.09.2026).
Hier stand woertlich: `ok(!tafel["…/steckbrief.html"]
.includes("gast"), "die Community hat keinen Steckbrief")`.
Am 19.09. hat Filipe genau das Gegenteil entschieden -- "jeder
soll ein steckbrief haben, jeder der ein account hat ... jeder
soll genau wie ich foto und so hinzufuegen koennen" -- und die
Community hat seitdem einen. Die Zeile war also rot, WEIL etwas
richtig gemacht wurde.
Das ist die unangenehmste Sorte roter Pruefung: Sie bestraft die
Verbesserung. Wer sie zweimal so erlebt, faengt an, rote Laeufe
zu erklaeren statt sie zu lesen.
WAS SIE EIGENTLICH SCHUETZEN WOLLTE, steht in ihrem eigenen
Kommentar: "Ohne die zweite Stelle haette die Gruppe mit den
wenigsten Rechten auch dieses nicht -- und sie ist die groesste."
Die Sorge ist richtig und hat mit der Anzahl der Seiten nichts zu
tun. Die Frage lautet: KOMMT JEDE ROLLE AN IHRE AUSKUNFT?
Gemessen wird sie jetzt so: Welche Seiten tragen den Knopf (aus
den Dateien gelesen, nicht aufgezaehlt), und erreicht jede Rolle
mindestens eine davon (aus der Rechtetafel gelesen, nicht
angenommen)? Kommt morgen eine dritte Seite dazu oder faellt eine
weg, bleibt die Aussage dieselbe. */
const { readFileSync, readdirSync } = await import("node:fs");
const ordnerSeiten = new URL("../workspace/", import.meta.url);
const mitKnopf = [];
for (const datei of readdirSync(ordnerSeiten)) {
if (!datei.endsWith(".html")) continue;
const inhalt = readFileSync(new URL(datei, ordnerSeiten), "utf8");
if (!/id="auskunft-knopf"/.test(inhalt)) continue;
mitKnopf.push(datei);
/* Ein Knopf ohne sein Skript ist eine Sackgasse, die aussieht wie
ein Weg -- man tippt darauf und es passiert nichts. */
ok(/assets\/js\/auskunft\.js/.test(inhalt), `${datei} lädt das Skript dazu`);
}
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Faende die Suche keine einzige
Seite -- umbenannt, verschoben, anderer Bezeichner --, waere
"jede Rolle kommt hin" unten fuer alle falsch ODER fuer alle
wahr, je nach Schleife. Beides waere ohne diese Zeile nicht zu
unterscheiden. */
ok(mitKnopf.length >= 1,
`${mitKnopf.length} Seite(n) tragen den Knopf (${mitKnopf.join(", ")})`);
const rechte = await import("./rechte.js");
const tafel = rechte.tafelJetzt();
const wege = (rolle) => mitKnopf.filter(
(d) => (tafel["/workspace/" + d] || []).includes(rolle));
/* JEDE ROLLE, NICHT EINE AUSGEWAEHLTE. Die Liste kommt aus dem
Haus, damit eine neue Rolle nicht stillschweigend ungeprueft
bleibt -- genau so entstehen Gruppen, die etwas nicht koennen und
es niemandem sagen. */
const ohneWeg = rechte.ALLE_ROLLEN.filter((r) => wege(r).length === 0);
ok(ohneWeg.length === 0,
ohneWeg.length
? `DIESE ROLLEN KOMMEN NICHT AN IHRE AUSKUNFT: ${ohneWeg.join(", ")}`
: `alle ${rechte.ALLE_ROLLEN.length} Rollen erreichen mindestens eine `
+ `Seite mit dem Knopf`);
/* Und die groesste Gruppe ausdruecklich benannt: Sie hat die
wenigsten Rechte, also faellt bei ihr zuerst etwas weg. */
ok(wege("gast").length >= 1,
`die Community kommt ueber ${wege("gast").join(", ") || "KEINE Seite"} hin`);
/* GEGENPROBE: Kann diese Rechnung ueberhaupt "nein" sagen? Eine
Rolle, die es nicht gibt, darf keine einzige Seite erreichen --
sonst waere `wege()` kaputt und alles darueber wertlos. */
ok(wege("diese-rolle-gibt-es-nicht").length === 0,
"Gegenprobe: eine erfundene Rolle erreicht keine");
}
/* ------------------------------------------------------------------- */
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);