Files
dogfather-universe/server/pruef-anschlagbrett.mjs
DogFatherGitandClaude Opus 5 189d0b3363 B1: Das Anschlagbrett haelt 24 Stunden -- und sieht aus wie ein Anschlagbrett
"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 BEGRUENDUNG IST DIE REGEL. Ein Anschlagbrett sagt, was GERADE
gilt. Was laenger gilt, hat seinen eigenen Bereich -- Regeln stehen bei
den Regeln, Termine bei "Was ansteht". Eine Ansage vom letzten
Dienstag, die noch haengt, ist keine Ansage mehr, sondern Papier an der
Wand.

DIE DREI ENTSCHEIDUNGEN, DIE ICH TREFFEN MUSSTE -- und woran ich sie
festgemacht habe:

1. AB WANN LAUFEN DIE 24 STUNDEN? Ab `erstellt`, nicht ab `datum`.
   `datum` ist ein vom Team gesetztes Feld und kann in der Zukunft
   liegen (es traegt die Termine). Eine Ansage, die morgen gilt, soll
   morgen verschwinden und nicht uebermorgen.

2. VERSCHWINDET SIE GANZ? Nein. Er sagt "von DA verschwinden" -- von
   der Seite. Geloescht wird nichts: Wer eine Ansage geschrieben hat,
   soll sie wiederfinden. Sie kommt als `abgehaengt` mit und steht
   zugeklappt unter einer leisen Zeile "Abgehaengt (3)". Auf dem Brett
   selbst steht sie damit nicht mehr.

3. WER ENTSCHEIDET? Der Server. Die Stunden im Browser nachzurechnen
   waere eine zweite Fassung derselben Regel, und eine davon waere
   irgendwann aelter. Die Frist geht als Zahl mit der Antwort.

DAS AUSSEHEN ("viel besser gestaltet, übersichtlicher, moderner,
hochwertiger, professioneller"): Was haengt, sieht jetzt aus wie etwas,
das haengt -- kraeftiger Rand links in der Farbe seiner ART (Ansage
babyblau, Neue Regel gold, Hinweis lila, Danke gruen), ein Schein von
links, mehr Luft. Die Art stand bisher nur im Text; jetzt sieht man
sie. Abgehaengtes ist grau statt durchsichtig -- durchsichtig hiesse,
das Buehnenbild scheint durch, also Text auf einem Foto.

pruef-anschlagbrett.mjs (NEU, 12/0) misst beide Seiten der Grenze
(23 h haengt, 25 h nicht), rechnet relativ zur jetzigen Uhrzeit statt
mit einem festen Datum, und hat den Eintrag, an dem sich alles
entscheidet: alt aufgehaengt, Datum von heute. Wer nach `datum`
filtert, laesst ihn haengen. Dazu zwei Gegenproben: auf jedem anderen
Brett gibt es das Merkmal gar nicht, und wo die Grenze WIRKLICH liegt,
wird aus den Eintraegen nachgerechnet statt aus der Zahl im Modul.

NEBENBEFUND, den erst dieser Commit ausgeloest hat
---------------------------------------------------
pruef-bereiche-lesend stuerzte ab: "listen EACCES 127.0.0.1:5040".

Die Portnummern werden aus der Stelle im Alphabet abgeleitet -- heute
sind fuenf neue Pruefdateien dazugekommen, und dadurch ist diese
Pruefung auf die 5040 gerutscht. Die haelt auf diesem Rechner ein
Windows-Dienst (svchost, PID 7136).

`portMussFreiSein` kannte nur ZWEI Antworten: EADDRINUSE oder frei.
EACCES galt als frei -- die Pruefung lief weiter, bis der echte Server
auf demselben Port startete und mit einem unbehandelten Fehler
abstuerzte. Ein Stapelauszug aus node:net, der wie ein Fehler im Code
aussieht.

Jetzt drei Antworten. Und ein belegter Port wird anders behandelt als
ein gesperrter: Belegt geht vorbei (der andere Lauf hoert auf),
gesperrt nie. Abzubrechen hiesse, die Pruefung waere auf diesem Rechner
dauerhaft nicht ausfuehrbar -- die Sorte Warnung, die immer kommt und
deshalb weggeklickt wird. Ausgewichen wird um einen festen Betrag
(+4000), damit die Nummer abgeleitet und eindeutig bleibt.

Mitgelaufen und gruen: pruef-bereiche-lesend, pruef-css-klassen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 23:47:52 +02:00

231 lines
11 KiB
JavaScript

/* =====================================================================
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);