/* ===================================================================== STUFE 4: DIE BREMSE UND DIE TEAM-SICHT (17.09.2026) Aus dem Plan im Vault, Stufe 4 -- und sie steht dort bewusst VOR Galerie und Gespraech: Mehr Sichtbarkeit heisst mehr Angriffsflaeche. Ein Bereich, der waechst und keine Bremse hat, waechst genau einmal. --------------------------------------------------------------------- WAS ES SCHON GAB -- UND WAS DAZWISCHEN FEHLTE Der Treff kennt Stufen (WER ueberhaupt schreiben darf: neu, dabei, stamm) und Massnahmen (wer NICHT MEHR darf: Hinweis, Pause, Ausschluss). Dazwischen fehlte die Frage, wie SCHNELL. Ein Stammgast konnte in einer Minute vierzig Beitraege absetzen -- aus Aerger, aus Versehen oder weil jemand ein Skript laufen laesst. --------------------------------------------------------------------- UND WAS BEWUSST NICHT GEBAUT WURDE Der Plan schlug zusaetzlich eine STILLE SPERRE vor (er schreibt, nur sieht es niemand). Beim Messen stellte sich heraus: `gast` ist eine Rolle mit Zugangscode, es gibt KEINE Selbstanmeldung. Wer ausgeschlossen wird, kann sich also nicht einfach neu anmelden -- und damit loest eine stille Sperre ein Problem, das dieses System gar nicht hat. Sie bliebe ein Werkzeug, das Menschen taeuscht, ohne etwas zu verhindern. --------------------------------------------------------------------- DREI FRAGEN 1. Greift sie ueberhaupt -- und sagt sie, WANN es weitergeht? 2. Trifft sie die richtige Arbeit? (Content-Ideen sind keine Community-Beitraege und duerfen nicht mitzaehlen.) 3. Laesst sie den NORMALEN Fall in Ruhe? Eine Bremse, die das richtige Verhalten bestraft, wird abgeschaltet -- und ab da ist sie wirkungslos. Aufrufen mit: node server/pruef-bremse.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { portMussFreiSein } from "./helfer-port.mjs"; import { heuteLokal } from "./helfer-tag.mjs"; const PORT = await portMussFreiSein(4485, "pruef-bremse"); const ordner = mkdtempSync(join(tmpdir(), "ws-bremse-")); 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"; await import("./index.js"); notbremse(180_000, "pruef-bremse"); 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++; }; /* DIE GRENZE WIRD NICHT ABGESCHRIEBEN, sondern aus dem Code geholt. Eine zweite Zahl hier waere beim naechsten Anpassen auseinander- gelaufen -- und die Pruefung haette dann etwas anderes gemessen als der Server tut. */ const { BEITRAEGE_JE_STUNDE } = await import("./workspace-bereiche.js") .then((m) => ({ BEITRAEGE_JE_STUNDE: m.BEITRAEGE_JE_STUNDE })); 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); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idRuhig = anlegen("Nane", "modi", "CODE-RUHIG-0001"); void idRuhig; 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 ruhig = await anmelden("creator", "CODE-RUHIG-0001"); const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS }); async function schreiben(keks, bereich, titel, extra = {}) { const a = await fetch(`${BASIS}/workspace/api/bereich/${bereich}`, { method: "POST", headers: kopf(keks), body: JSON.stringify({ art: bereich === "wunsch" ? "stream" : "regel", titel, datum: heuteLokal(), ...extra }), }); return { status: a.status, daten: await a.json().catch(() => null) }; } /* ======================================================================= 1. SIE GREIFT -- UND SAGT, WANN ES WEITERGEHT ======================================================================= */ melde(""); melde("=== 1. Die Bremse ==="); { ok(Number.isInteger(BEITRAEGE_JE_STUNDE) && BEITRAEGE_JE_STUNDE > 0, `die Grenze steht im Code: ${BEITRAEGE_JE_STUNDE} je Stunde`); let letzter = null; for (let i = 1; i <= BEITRAEGE_JE_STUNDE; i++) { letzter = await schreiben(dogi, "wunsch", `Wunsch Nummer ${i}`); if (letzter.status !== 201) break; } /* DER NORMALE FALL MUSS DURCHGEHEN. Eine Bremse, die das richtige Verhalten bestraft, wird abgeschaltet -- und ist ab da wirkungslos. Das ist in diesem Haus schon einmal passiert. */ ok(letzter?.status === 201, `die ersten ${BEITRAEGE_JE_STUNDE} gehen durch (${letzter?.status})`); const zuViel = await schreiben(dogi, "wunsch", "Einer zu viel"); ok(zuViel.status === 429, `der naechste wird gebremst (${zuViel.status})`); /* OHNE ZEITANGABE probiert jemand im Minutentakt weiter -- genau die Last, die man verhindern wollte. */ ok(/\d+\s+Minute/.test(zuViel.daten?.fehler || ""), `und die Absage sagt, wann es weitergeht ("${zuViel.daten?.fehler || ""}")`); ok(/\d+ Beiträge/.test(zuViel.daten?.fehler || ""), "und warum"); } /* ======================================================================= 2. SIE TRIFFT NUR DIE COMMUNITY ======================================================================= */ melde(""); melde("=== 2. Die richtige Arbeit ==="); { /* DIESELBE PERSON ist im Treff gerade gesperrt. In der Content-Planung muss sie weiterarbeiten koennen -- dort ist ein Eintrag Arbeit am eigenen Kanal, kein Community-Beitrag. Beim ersten Entwurf zaehlte die Bremse ALLE Eintraege. Dann haette eine Creatorin mit zehn Content-Ideen danach im Treff nichts mehr schreiben koennen. */ const a = await fetch(`${BASIS}/workspace/api/bereich/content`, { method: "POST", headers: kopf(dogi), body: JSON.stringify({ art: "idee", titel: "Eine Content-Idee", datum: heuteLokal() }), }); const s = a.status; if (s === 400 || s === 403 || s === 404) { melde(` -- Content-Bereich nicht beschreibbar (${s}) -- Abschnitt nicht messbar`); } else { ok(s === 201, `die Content-Planung bleibt offen (${s})`); } /* UND EIN ANDERES BRETT DES TREFFS ist mitgesperrt -- die Bremse gilt der Person, nicht dem Brett. Sonst wechselt man einfach. */ const b = await schreiben(dogi, "anschlag", "Eine Ansage"); ok(b.status === 429, `ein anderes Community-Brett ebenfalls (${b.status})`); } /* ======================================================================= 3. SIE TRIFFT NUR DIE RICHTIGE PERSON ======================================================================= */ melde(""); melde("=== 3. Nur wer zu viel schreibt ==="); { /* OHNE DIESEN ABSCHNITT hiesse "429" nur, dass gerade alles gebremst wird -- etwa durch eine Zaehlung, die die Person vergisst. */ const a = await schreiben(ruhig, "wunsch", "Mein einziger Wunsch"); ok(a.status === 201, `wer nichts geschrieben hat, schreibt weiter (${a.status} ${a.daten?.fehler || ""})`); } /* ======================================================================= 4. GEGENPROBE: ZAEHLT SIE WIRKLICH DIE STUNDE? ======================================================================= */ melde(""); melde("=== 4. Die Gegenprobe ==="); { /* Die Beitraege der gesperrten Person auf VORGESTERN zurueckdatieren. Dann muss sie sofort wieder schreiben duerfen -- sonst zaehlt die Bremse nicht die letzte Stunde, sondern alles. */ const e = new DatabaseSync(process.env.WORKSPACE_DB); const vorgestern = new Date(Date.now() - 2 * 86400_000).toISOString(); const wer = e.prepare("SELECT id FROM personen WHERE name = 'Filipe'").get().id; const n = e.prepare("UPDATE eintraege SET erstellt = ? WHERE erstellt_von = ?") .run(vorgestern, wer).changes; e.close(); ok(n >= BEITRAEGE_JE_STUNDE, `${n} Beitraege auf vorgestern zurueckdatiert`); const a = await schreiben(dogi, "wunsch", "Und jetzt wieder"); ok(a.status === 201, `danach geht es sofort wieder (${a.status})`); } /* ======================================================================= 5. WAS HEREINKOMMT -- DIE TEAM-SICHT ======================================================================= */ melde(""); melde("=== 5. Alle Bretter in einer Liste ==="); { /* Ein Moderator musste acht Bretter durchklicken, um zu sehen, ob etwas Neues da ist. Das tut niemand achtmal am Tag -- und was niemand ansieht, moderiert auch niemand. */ await schreiben(dogi, "anschlag", "Frische Ansage"); await schreiben(ruhig, "wunsch", "Ein frischer Wunsch"); const a = await fetch(BASIS + "/workspace/api/treff/zulauf", { headers: { cookie: dogi } }); const j = await a.json().catch(() => null); const z = j?.zulauf || []; ok(a.status === 200 && z.length > 0, `${z.length} Beitraege in der Team-Sicht`); /* AUS MEHREREN BRETTERN -- sonst waere es nur eine Liste mehr. */ const bretter = new Set(z.map((e) => e.bereich)); ok(bretter.size >= 2, `aus mehreren Brettern (${[...bretter].join(", ")})`); /* DAS NEUESTE ZUERST. In Abschnitt 4 wurden Filipes Beitraege auf vorgestern zurueckdatiert -- die frischen muessen darueber stehen. */ const zeiten = z.map((e) => Date.parse(e.erstellt)); ok(zeiten.every((t, i) => i === 0 || zeiten[i - 1] >= t), "das Neueste zuerst"); ok(z[0]?.titel === "Ein frischer Wunsch" || z[0]?.titel === "Frische Ansage", `und ganz oben steht der juengste ("${z[0]?.titel}")`); /* WER ES GESCHRIEBEN HAT gehoert dazu -- eine Liste ohne Namen sagt einem Moderator nichts ueber ein Muster. */ ok(z.every((e) => e.von_name), "jede Zeile nennt, wer geschrieben hat"); /* UND SIE IST NUR FUERS TEAM. Ein Creator sieht die Community-Bretter gar nicht -- er darf auch ihre Uebersicht nicht bekommen. */ const idC = 0; void idC; const creaKeks = await (async () => { const e = new DatabaseSync(process.env.WORKSPACE_DB); const salt2 = randomBytes(16).toString("hex"); const hash2 = scryptSync("CODE-CREA-0001", salt2, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); e.prepare("INSERT INTO personen" + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + " VALUES (?,?,?,?,?,?,1,?)") .run("Tili", "creator", hash2, salt2, 32768, createHmac("sha256", schluessel).update("CODE-CREA-0001").digest("hex"), jetzt); e.close(); return anmelden("creator", "CODE-CREA-0001"); })(); const b = await fetch(BASIS + "/workspace/api/treff/zulauf", { headers: { cookie: creaKeks } }); ok(b.status === 403 || b.status === 404, `ein Creator bekommt sie nicht (${b.status})`); } 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);