/* ===================================================================== DAS ANSCHLAGBRETT HÄLT 24 STUNDEN ===================================================================== Filipe am 22.09.2026: „die sachen sollen in der seite vom anschlagbrett immer automatisch nach 24h von da verschwinden. weil alles hat seine kategorie und was nicht ist auch nicht um ewig auf der seite zu bleiben fertig." SEINE BEGRÜNDUNG IST DIE REGEL. Ein Anschlagbrett sagt, was GERADE gilt. Was länger gilt, hat seinen eigenen Bereich — Regeln stehen bei den Regeln, Termine bei „Was ansteht". Eine Ansage vom letzten Dienstag, die noch hängt, ist keine Ansage mehr, sondern Papier an der Wand. --------------------------------------------------------------------- DREI DINGE, DIE EINE ZEITREGEL LEICHT FALSCH MACHT 1. DIE UHR ALS ANNAHME. Eine Prüfung, die „vor 25 Stunden" mit einem festen Datum nachbaut, ist grün, bis der Kalender sie überholt. Hier wird deshalb relativ gerechnet (`Date.now() - n * Stunde`) — dieselbe Lehre wie bei `gate-oeffnung.mjs` im Shop. 2. DIE FALSCHE SPALTE. `datum` ist ein vom Team gesetztes Feld und kann in der Zukunft liegen (es trägt die Termine). Gezählt wird ab `erstellt`. Gemessen wird das hier mit einem Eintrag, dessen beide Daten AUSEINANDERLIEGEN — sonst wäre nicht zu sehen, welches der beiden zählt. 3. DIE GRENZE SELBST. Bei genau 24 Stunden darf es nicht kippen, ohne dass jemand es merkt. Geprüft werden deshalb beide Seiten: kurz davor hängt sie, kurz danach nicht. Und der wichtigste Punkt: NICHTS WIRD GELÖSCHT. Er sagt „von DA verschwinden" — von der Seite. Wer eine Ansage geschrieben hat, soll sie wiederfinden. ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { notbremse } from "./helfer-notbremse.mjs"; import { eigenerPort } from "./helfer-port.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-anschlag-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const PORT = await eigenerPort(import.meta, "pruef-anschlagbrett"); process.env.PORT = `${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 }); }; await import("./index.js"); notbremse(90_000, "pruef-anschlagbrett"); await new Promise((r) => setTimeout(r, 800)); let fehler = 0, geprueft = 0, offen = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const unklar = (t) => { offen++; console.log(" -- " + t); }; const { ANSCHLAG_STUNDEN } = await import("./workspace-bereiche.js"); const STUNDE = 3600 * 1000; /* ---------- Leute und Einträge ---------------------------------------- */ 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; } const idChef = anlegen("Filipe", "admin", "CODE-DOGI-0001"); /* RELATIV ZUR JETZIGEN UHRZEIT, nie mit einem festen Datum. */ const vorStunden = (n) => new Date(Date.now() - n * STUNDE).toISOString(); /** Ein Aushang mit getrenntem `erstellt` und `datum`. */ function aushang(titel, stundenAlt, datumStundenAlt = stundenAlt) { d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von) VALUES ('anschlag', 'ansage', ?, ?, ?, 'offen', ?, ?)`) .run(titel, "Inhalt", vorStunden(datumStundenAlt).slice(0, 10), vorStunden(stundenAlt), idChef); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idFrisch = aushang("Gerade aufgehängt", 0.5); const idKurzDavor = aushang("Kurz vor der Grenze", ANSCHLAG_STUNDEN - 1); const idKurzDanach = aushang("Kurz nach der Grenze", ANSCHLAG_STUNDEN + 1); const idUralt = aushang("Von letzter Woche", 24 * 7); /* DER ENTSCHEIDENDE: `erstellt` ist alt, `datum` ist von heute. Wer nach `datum` filtert, lässt ihn hängen -- und genau das wäre der Fehler, den man sonst nie sieht. */ const idAltMitNeuemDatum = aushang("Alt aufgehängt, Datum von heute", ANSCHLAG_STUNDEN + 3, 0); d.close(); /* ---------- Anfragen --------------------------------------------------- */ const { request: httpAnfrage } = await import("node:http"); const CREW = "crew.dogfather-universe.com"; function anfrage(pfad, { methode = "GET", keks = "", rumpf = null } = {}) { return new Promise((fertig, schief) => { const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf)); const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, headers: { Host: CREW, Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/`, ...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}), ...(keks ? { Cookie: keks } : {}), }, }, (antwort) => { const st = []; antwort.on("data", (x) => st.push(x)); antwort.on("end", () => { const text = Buffer.concat(st).toString("utf8"); let json = null; try { json = JSON.parse(text); } catch { /* kein JSON */ } fertig({ code: antwort.statusCode, json, keks: [].concat(antwort.headers["set-cookie"] || []) .map((c) => c.split(";")[0]).join("; ") }); }); }); a.on("error", schief); if (daten) a.write(daten); a.end(); }); } const keks = (await anfrage("/workspace/api/anmelden", { methode: "POST", rumpf: { rolle: "admin", code: "CODE-DOGI-0001" } })).keks; console.log("=== Das Anschlagbrett haelt 24 Stunden ==="); console.log(""); console.log(`--- Die Frist steht an EINER Stelle: ${ANSCHLAG_STUNDEN} Stunden ---`); const a = await anfrage("/workspace/api/bereich/anschlag", { keks }); ok(a.code === 200, `das Brett antwortet (${a.code})`); const eintraege = a.json?.eintraege || []; const nach = new Map(eintraege.map((e) => [e.id, e])); /* NICHTS IST VERSCHWUNDEN. Alle fünf kommen mit -- was fehlt, wäre gelöscht, und das war ausdrücklich nicht gemeint. */ ok(eintraege.length === 5, `alle fünf Aushänge kommen mit (${eintraege.length}) — abgehängt ist nicht gelöscht`); ok(nach.get(idFrisch)?.abgehaengt === false, "was gerade aufgehängt wurde, hängt"); ok(nach.get(idKurzDavor)?.abgehaengt === false, `eine Stunde vor der Grenze hängt sie noch (${ANSCHLAG_STUNDEN - 1} h alt)`); ok(nach.get(idKurzDanach)?.abgehaengt === true, `eine Stunde danach nicht mehr (${ANSCHLAG_STUNDEN + 1} h alt)`); ok(nach.get(idUralt)?.abgehaengt === true, "die von letzter Woche erst recht nicht"); /* DIE WICHTIGSTE ZEILE. */ ok(nach.get(idAltMitNeuemDatum)?.abgehaengt === true, "gezählt wird ab `erstellt`, nicht ab `datum` — ein alter Aushang mit" + " heutigem Datum hängt NICHT weiter"); /* DIE FRIST GEHT AN DIE SEITE MIT. */ ok(a.json?.anschlag_stunden === ANSCHLAG_STUNDEN, `die Frist kommt mit der Antwort (${a.json?.anschlag_stunden})`); /* ===================================================================== GEGENPROBE: NUR DAS ANSCHLAGBRETT ===================================================================== Eine Regel, die versehentlich überall gilt, wäre schlimmer als keine: Auf den Regeln, bei „Was ansteht" und auf jedem anderen Brett soll nichts nach einem Tag verschwinden. Und `abgehaengt` darf dort gar nicht erst auftauchen -- ein Feld, das immer `false` sagt, wird irgendwann von jemandem ausgewertet. ===================================================================== */ console.log(""); console.log("--- Gegenprobe: nur hier, sonst nirgends ---"); { const dd = new DatabaseSync(process.env.WORKSPACE_DB); dd.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von) VALUES ('regeln', 'regel', ?, ?, ?, 'offen', ?, ?)`) .run("Uralte Regel", "Inhalt", vorStunden(24 * 30).slice(0, 10), vorStunden(24 * 30), idChef); dd.close(); const b = await anfrage("/workspace/api/bereich/regeln", { keks }); const regeln = b.json?.eintraege || []; ok(regeln.length >= 1, `das Regelbrett hat ${regeln.length} Eintrag/Einträge`); ok(regeln.length >= 1 && regeln.every((e) => e.abgehaengt === undefined), "dort gibt es das Merkmal gar nicht — die Regel gilt nur am Anschlagbrett"); ok(b.json?.anschlag_stunden === undefined, "und die Frist steht dort auch nicht in der Antwort"); } /* ===================================================================== GEGENPROBE ZUR MESSUNG: WÜRDE SIE EINEN FEHLER SEHEN? ===================================================================== Bis hierhin könnte alles auch dann grün sein, wenn `abgehaengt` schlicht dem Alter folgt und die Grenze woanders läge. Also einmal nachrechnen, wo sie wirklich liegt -- gemessen an den Einträgen selbst, nicht an der Zahl aus dem Modul. ===================================================================== */ console.log(""); console.log("--- Wo die Grenze wirklich liegt ---"); { const alter = (e) => (Date.now() - Date.parse(e.erstellt)) / STUNDE; const haengend = eintraege.filter((e) => !e.abgehaengt).map(alter); const ab = eintraege.filter((e) => e.abgehaengt).map(alter); const aeltesteHaengend = Math.max(...haengend); const juengsteAb = Math.min(...ab); ok(haengend.length > 0 && ab.length > 0 && aeltesteHaengend < ANSCHLAG_STUNDEN && juengsteAb > ANSCHLAG_STUNDEN, `die Grenze liegt zwischen ${aeltesteHaengend.toFixed(1)} h (hängt noch)` + ` und ${juengsteAb.toFixed(1)} h (abgehängt) — also bei ${ANSCHLAG_STUNDEN}`); } /* ---------- Ende ------------------------------------------------------ */ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt manchmal */ } console.log(""); console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}` + ` -- ${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`); process.exit(fehler === 0 ? 0 : 1);