/* ===================================================================== WAS DOGFATHER EINTRÄGT, SEHEN AUCH DIE ANDEREN (17.09.2026) Filipe: „die community und alle anderen sollen die neuen sachen in allen kategorien auch sehen bitte. in allen die sie sehen soll auch jede änderung mit gehen, immer nur in denen die ihnen erlaubt ist zu sehen." --------------------------------------------------------------------- ZWEI FEHLER, DIE NIEMAND GEMELDET HÄTTE Beide sahen auf DogFathers eigenem Bildschirm vollkommen richtig aus. Das ist das Tückische daran: Ein leeres Brett sieht nicht nach Fehler aus, sondern nach „da ist eben noch nichts". 1. FREIGABE. `ansteht` und `highlight` zeigen der Community nur, was freigegeben ist. Ein aus dem Vorschlagskasten übernommener Eintrag hatte keine Freigabe -- gemessen sah die Community 0 von 4 und 0 von 3, während im Team alle drei Rollen alles sahen. 2. TEAM_DOGI_ROLLEN ist {hand, modi} -- OHNE "admin". Die Sichtbarkeitsregel für Team Dogi lautete damit wörtlich „Einträge von hand oder modi", und alles, was DogFather in Live, Technik, Community, Ideen, Angebote oder Rückmeldung schrieb, war für sein eigenes Team unsichtbar. --------------------------------------------------------------------- UND DIE ANDERE HÄLFTE DES SATZES WIRD AUCH GEPRÜFT „immer nur in denen die ihnen erlaubt ist zu sehen." Eine Prüfung, die nur zählt, ob alle alles sehen, wäre grün, wenn man sämtliche Schranken entfernte. Deshalb steht in Abschnitt 4 die Gegenprobe: Was zu ist, muss zu bleiben. Aufrufen mit: node server/pruef-alle-sehen-es.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-alle-sehen-es"); const ordner = mkdtempSync(join(tmpdir(), "ws-sicht-")); 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"; await import("./index.js"); notbremse(180_000, "pruef-alle-sehen-es"); await new Promise((r) => setTimeout(r, 700)); const CREW = "crew.dogfather-universe.com"; 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 { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const { TREFF_BRETTER } = await import("./workspace.js"); const { TREFF_FREIGABE_BRETTER } = await import("./workspace-treff.js"); const { BEREICHE } = await import("./workspace-bereiche.js"); const { ARBEIT_START } = await import("./workspace-arbeit-start.js"); const { TREFF_START } = await import("./workspace-treff-start.js"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const sch = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", sch, jetzt); const ROLLEN = ["admin", "hand", "modi", "gast"]; const codes = {}; for (const r of ROLLEN) { const code = `CODE-${r.toUpperCase()}-0001`; 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(r, r, hash, salt, 32768, createHmac("sha256", sch).update(code).digest("hex"), jetzt); codes[r] = code; } d.close(); function roh(pfad, kopf = {}, koerper = null, methode = null) { return new Promise((fertig, schief) => { const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode || (koerper ? "POST" : "GET"), headers: { Host: CREW, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf }, }, (antwort) => { let text = ""; antwort.on("data", (s) => { text += s; }); antwort.on("end", () => fertig({ code: antwort.statusCode, text })); }); a.on("error", schief); if (koerper) a.write(JSON.stringify(koerper)); a.end(); }); } const kekse = {}; for (const r of ROLLEN) { const antwort = await new Promise((fertig) => { const q = httpAnfrage({ host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST", headers: { Host: CREW, "Content-Type": "application/json" }, }, (w) => { let t = ""; w.on("data", (x) => { t += x; }); w.on("end", () => fertig(w)); }); q.write(JSON.stringify(r === "gast" ? { rolle: r, code: codes[r], alter_ok: true } : { rolle: r, code: codes[r] })); q.end(); }); kekse[r] = [].concat(antwort.headers["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); } async function liste(bereich, rolle) { const a = await roh(`/workspace/api/bereich/${bereich}`, { cookie: kekse[rolle] }); let j = null; try { j = JSON.parse(a.text); } catch { /* keine JSON-Antwort */ } return { code: a.code, n: (j?.eintraege || []).length }; } /* Der Katalog wird schon fuer Abschnitt 0 gebraucht -- er steht deshalb vor ihm und nicht mehr in Abschnitt 1. */ const KATALOG_VORAB = { ...ARBEIT_START, ...TREFF_START }; const KATALOG = KATALOG_VORAB; /* ======================================================================= 0. EIN BRETT OHNE KATALOG BEKOMMT SEINEN EINTRAG -- UND ZWAR ZUERST ======================================================================= „highlight" hat als einziges Treff-Brett keinen Katalog: Dort stehen echte Hoehepunkte, keine Vorlagen. Abschnitt 2 misst aber, ob die Community DASSELBE sieht wie DogFather -- und an einem leeren Brett ist das nicht zu messen (null gleich null waere auch dann wahr, wenn die Community gar nichts duerfte). WARUM GANZ AM ANFANG: Es gibt eine Schreibbremse -- rund fuenfzig Beitraege je Stunde. Der Katalog unten legt 110 an; danach ist sie ausgeloest, und der Versuch, hier noch etwas zu schreiben, bekommt 429. Genau so ist es im ersten Anlauf passiert. Die Bremse ist richtig und bleibt; die Pruefung stellt sich davor statt dahinter. UEBER DIE ECHTE ROUTE, nicht ueber die Datenbank: So ist der Weg, den auch ein Mensch nimmt, gleich mitgeprueft. ======================================================================= */ { /* NUR DIE BRETTER OHNE KATALOG. Der erste Anlauf fragte „ist es leer?" -- und vor dem Katalog sind ALLE leer. Er legte damit ueberall einen Eintrag an, und bei „ansteht" stimmte danach die Zahl nicht mehr (5 gegen 4). Gefragt wird deshalb, was wirklich gemeint ist: Gibt es fuer dieses Brett eine Vorlage? */ const ohneKatalog = TREFF_BRETTER.filter((b) => !KATALOG_VORAB[b]); ok(ohneKatalog.length >= 1, `${ohneKatalog.length} Treff-Brett(er) ohne Katalog: ${ohneKatalog.join(", ") || "keines"}`); for (const b of ohneKatalog) { const art = Object.keys(BEREICHE[b]?.arten || {})[0]; const neu = await roh(`/workspace/api/bereich/${b}`, { cookie: kekse.admin, origin: `https://${CREW}` }, { art, titel: "Damit dieses Brett etwas zeigt", text: "Ein Eintrag fuer die Messung -- dieses Brett hat keine Vorlagen." }); ok(neu.code === 201, `${b}: bekommt einen Eintrag (${neu.code})`); /* UND ER MUSS FREIGEGEBEN WERDEN. Auf den Freigabe-Brettern sieht die Community nur, was jemand aus dem Team durchgelassen hat -- beim Katalog passiert das automatisch (workspace-bereiche.js), beim gewoehnlichen Schreiben nicht. Ohne diesen Schritt maesse Abschnitt 2 „DogFather 1, Community 0" und haette recht: Der Eintrag ist wirklich nicht frei. */ if (TREFF_FREIGABE_BRETTER.includes(b)) { let id = null; try { id = JSON.parse(neu.text)?.eintrag?.id ?? JSON.parse(neu.text)?.id; } catch { /* steht unten als Fehler */ } const frei = id ? await roh(`/workspace/api/treff/eintrag/${id}/freigeben`, { cookie: kekse.admin, origin: `https://${CREW}` }, {}, "PUT") : { code: 0 }; ok(frei.code === 200, ` und wird freigegeben (${frei.code})`); } } } /* ======================================================================= 1. DOGFATHER FÜLLT ALLES ======================================================================= */ melde(""); melde("=== 1. Der Vorschlagskasten ==="); const gefuellt = []; { let summe = 0; for (const b of Object.keys(BEREICHE)) { const a = await roh(`/workspace/api/bereich/${b}/start`, { cookie: kekse.admin, origin: `https://${CREW}` }, {}); let j = null; try { j = JSON.parse(a.text); } catch { /* keine JSON-Antwort */ } if (a.code === 201 && j?.angelegt) { gefuellt.push(b); summe += j.angelegt; } } /* DIE ZAHL STEHT IN DER BEDINGUNG, nicht nur im Text. Ohne das wäre „alle sehen gleich viel" auch dann grün, wenn es null wäre. */ ok(summe > 50, `DogFather legt ${summe} fertige Einträge in ${gefuellt.length} Kategorien an`); ok(gefuellt.length >= 15, `und zwar in fast allen (${gefuellt.length} von ${Object.keys(BEREICHE).length})`); /* Die beiden personenbezogenen bleiben mit Absicht leer. */ ok(!KATALOG.talente && !KATALOG.entwicklung, "„talente\" und „entwicklung\" haben bewusst keinen Katalog – dort stünden erfundene Menschen"); } /* ======================================================================= 2. UND DIE COMMUNITY SIEHT ES AUCH ← Fehler 1 ======================================================================= */ melde(""); melde("=== 2. Was die Community sieht ==="); { /* ==== EIN LEERES BRETT BEWEIST NICHTS (25.09.2026) ============ „highlight" hat als einziges Treff-Brett keinen Katalog -- dort stehen echte Hoehepunkte und keine Vorlagen, und das ist richtig so. Die Folge war aber: null Eintraege bei DogFather, null bei der Community, und die Bedingung `a.n > 0` wurde rot. Die Pruefung meldete einen Fehler, wo schlicht nichts zu sehen war. GEMESSEN WERDEN SOLL DIE GLEICHHEIT, nicht der Katalog. Also legt die Pruefung dort selbst einen Eintrag an -- ueber die echte Route, mit der auch DogFather schreibt. Damit hat jedes Brett etwas, und die Frage „sieht die Community dasselbe?" laesst sich ueberall stellen. KEIN TESTDATENSATZ IM SINNE DES VERBOTS: Diese Pruefung laeuft auf ihrer eigenen Wegwerf-Datenbank in einem mktemp-Ordner. */ for (const b of TREFF_BRETTER) { const a = await liste(b, "admin"); const g = await liste(b, "gast"); const noetig = TREFF_FREIGABE_BRETTER.includes(b); ok(a.n > 0 && g.n === a.n, `${b.padEnd(10)} DogFather ${a.n}, Community ${g.n}` + (noetig ? " (Brett mit Freigabe – genau hier war der Fehler)" : "")); } } /* ======================================================================= 3. UND SEIN TEAM AUCH ← Fehler 2 ======================================================================= */ melde(""); melde("=== 3. Was das Team sieht ==="); { let geprueftHier = 0; for (const b of gefuellt) { const a = await liste(b, "admin"); if (!a.n) continue; for (const r of ["hand", "modi"]) { const x = await liste(b, r); /* 404 heißt: Diese Rolle darf den Bereich gar nicht -- das ist kein Fehler, sondern die andere Hälfte von Filipes Satz. Sie wird in Abschnitt 4 geprüft, hier übersprungen. */ if (x.code === 404) continue; geprueftHier++; ok(x.n === a.n, `${b.padEnd(12)} ${r.padEnd(5)} sieht ${x.n} von ${a.n}`); } } ok(geprueftHier >= 10, `es wurden ${geprueftHier} Kombinationen wirklich geprüft – nicht alle übersprungen`); } /* ======================================================================= 4. ABER NUR DA, WO SIE DÜRFEN ← die Gegenprobe ======================================================================= */ melde(""); melde("=== 4. Und was zu bleibt ==="); { /* OHNE DIESEN ABSCHNITT wäre die ganze Prüfung grün, wenn man jede Schranke entfernte. „Alle sehen alles" ist nicht das Ziel -- Filipe sagt ausdrücklich „immer nur in denen die ihnen erlaubt ist zu sehen". */ const zu = []; for (const b of Object.keys(BEREICHE)) { for (const r of ["hand", "modi", "gast"]) { const x = await liste(b, r); if (x.code === 404) zu.push(`${b}/${r}`); } } ok(zu.length >= 10, `${zu.length} Bereich-Rollen-Paare bleiben verschlossen (404)`); /* Und die Community kommt an keinen einzigen Arbeitsbereich. */ const arbeit = Object.keys(BEREICHE).filter((b) => !TREFF_BRETTER.includes(b)); const offenFuerGast = []; for (const b of arbeit) { const x = await liste(b, "gast"); if (x.code !== 404) offenFuerGast.push(`${b} (${x.code})`); } ok(offenFuerGast.length === 0, offenFuerGast.length ? `die Community kommt an: ${offenFuerGast.join(", ")}` : `die Community sieht keinen der ${arbeit.length} Arbeitsbereiche`); } /* ======================================================================= 5. UND DIE REGEL STEHT AN EINER STELLE ======================================================================= */ melde(""); melde("=== 5. Eine Quelle, nicht zwei ==="); { const { readFileSync } = await import("node:fs"); const quelle = readFileSync(new URL("./workspace-bereiche.js", import.meta.url), "utf8") .replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\r\n]*/g, " "); ok(/\[\.\.\.TEAM_DOGI_ROLLEN,\s*"admin"\]/.test(quelle), "die Team-Sicht schließt DogFather ein"); /* UND TEAM_DOGI_ROLLEN SELBST BLEIBT UNBERÜHRT. Stünde "admin" dort drin, wäre er stillschweigend ins andere Haus umgezogen -- diese Menge entscheidet auch darüber. */ const { TEAM_DOGI_ROLLEN } = await import("./workspace.js"); ok(!TEAM_DOGI_ROLLEN.has("admin"), `TEAM_DOGI_ROLLEN bleibt {${[...TEAM_DOGI_ROLLEN].join(", ")}} – sonst zöge DogFather ins Haus der Crew`); const treff = readFileSync(new URL("./workspace-bereiche.js", import.meta.url), "utf8"); ok(/INSERT OR IGNORE INTO treff_freigaben/.test(treff), "und ein übernommener Vorschlag wird gleich freigegeben"); } melde(""); melde(`${geprueft} Prüfungen, ${fehler} Fehler`); melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } process.exit(fehler ? 1 : 0);