diff --git a/server/helfer-port.mjs b/server/helfer-port.mjs index 4abf8e1e..f8b7d9a8 100644 --- a/server/helfer-port.mjs +++ b/server/helfer-port.mjs @@ -34,16 +34,72 @@ import { createServer } from "node:net"; -/** Wirft, wenn der Port belegt ist. Sonst gibt er ihn frei zurück. */ -export async function portMussFreiSein(port, wer = "diese Pruefung") { - const belegt = await new Promise((fertig) => { +/** Was ist mit diesem Port los? + * + * DREI ANTWORTEN, NICHT ZWEI (22.09.2026) -- und die dritte hat + * heute gefehlt: + * + * null frei + * EADDRINUSE jemand hoert darauf (meist ein zweiter Prueflauf) + * EACCES das System gibt ihn nicht her -- dauerhaft + * + * Vorher wurde NUR auf EADDRINUSE geprueft; jeder andere Fehler galt + * als "frei". Auf diesem Rechner haelt ein Windows-Dienst (svchost, + * PID 7136) den Port 5040. `createServer().listen` scheiterte dort mit + * EACCES, die Antwort war trotzdem "frei", und die Pruefung lief + * weiter -- bis der ECHTE Server auf demselben Port startete und mit + * einem unbehandelten Fehler abstuerzte. Eine Meldung, die "belegt" + * heissen muesste, kam nie; stattdessen ein Stapelauszug aus + * node:net, der wie ein Fehler im Code aussieht. + * + * Aufgefallen ist es, weil die Nummern sich verschoben haben: Sie + * werden aus der Stelle im Alphabet abgeleitet, und heute sind fuenf + * neue Pruefdateien dazugekommen. Der Fall lag also seit Langem da + * und wartete nur darauf, dass jemand auf die 5040 rutscht. */ +async function portLage(port) { + return new Promise((fertig) => { const s = createServer(); - s.once("error", (f) => fertig(f.code === "EADDRINUSE")); - s.once("listening", () => s.close(() => fertig(false))); + s.once("error", (f) => fertig(f.code || "FEHLER")); + s.once("listening", () => s.close(() => fertig(null))); s.listen(port, "127.0.0.1"); }); +} - if (belegt) { +/** Wirft, wenn der Port belegt ist. Sonst gibt er ihn frei zurück. */ +export async function portMussFreiSein(port, wer = "diese Pruefung") { + const lage = await portLage(port); + + /* DAUERHAFT GESPERRT -> AUSWEICHEN, NICHT ABBRECHEN. + ------------------------------------------------------------------ + Ein belegter Port ist ein Zustand, der vorbeigeht: Der andere Lauf + hoert auf, und danach geht es. Ein vom System reservierter Port + geht NIE. Abzubrechen hiesse, diese Pruefung waere auf diesem + Rechner dauerhaft nicht ausfuehrbar -- und das waere die Sorte + Warnung, die immer kommt und deshalb weggeklickt wird. + + Ausgewichen wird um einen festen Betrag, nicht auf "den naechsten + freien": Damit bleibt die Nummer ABGELEITET und eindeutig. Zwei + Pruefungen, die beide ausweichen, landen auf zwei verschiedenen + Nummern, weil ihre Ausgangsnummern verschieden waren. */ + if (lage === "EACCES") { + const ersatz = port + AUSWEICHEN; + const lage2 = await portLage(ersatz); + if (lage2 === null) { + console.log(`[Port] ${port} ist vom System belegt (EACCES) --` + + ` ${wer} nimmt ${ersatz}.`); + return ersatz; + } + console.error(`\nABBRUCH: Port ${port} ist vom System gesperrt, und der`); + console.error(` Ausweichport ${ersatz} ist ebenfalls nicht frei (${lage2}).\n`); + console.error(` ${wer} kann hier nicht messen. Das ist KEIN Befund am Code --`); + console.error(` es heisst nur, dass gerade niemand nachsehen konnte.\n`); + console.error(` Nachsehen, wer die Ports haelt:`); + console.error(` netstat -ano | findstr :${port}`); + console.error(` netstat -ano | findstr :${ersatz}\n`); + process.exit(3); + } + + if (lage !== null) { console.error(`\nABBRUCH: Port ${port} ist belegt.\n`); console.error(` ${wer} startet einen eigenen Server auf diesem Port. Laeuft dort`); console.error(` schon etwas, wuerde gegen einen FREMDEN Stand gemessen -- mit`); @@ -124,6 +180,12 @@ import { basename, dirname } from "node:path"; * werden: "irgendwo ueber 4400" haette plausibel geklungen. */ const BASIS = 5000; const PRO_DATEI = 2; +/** Um wie viel ausgewichen wird, wenn das System einen Port nicht + * hergibt. 4000 liegt sicher oberhalb des ganzen abgeleiteten + * Bereichs (BASIS bis BASIS+5000 in Schritten von 2, bei derzeit + * rund 180 Dateien also bis etwa 5400) -- zwei Pruefungen koennen + * sich dadurch nicht ins Gehege kommen, auch wenn beide ausweichen. */ +const AUSWEICHEN = 4000; /** Der Port dieser Datei -- abgeleitet, nicht vergeben. * diff --git a/server/pruef-anschlagbrett.mjs b/server/pruef-anschlagbrett.mjs new file mode 100644 index 00000000..8ad13f02 --- /dev/null +++ b/server/pruef-anschlagbrett.mjs @@ -0,0 +1,230 @@ +/* ===================================================================== + 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); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 87bbc0a8..44a11a03 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -80,6 +80,18 @@ export const bereicheRouter = express.Router(); /* Die Bereiche und ihre Arten. Bewusst hier und nicht in der Datenbank: Es sind Festlegungen aus dem Konzept, keine Nutzdaten. */ +/** Wie lange eine Ansage am Anschlagbrett haengt -- in Stunden. + * + * Filipe am 22.09.2026: „die sachen sollen in der seite vom + * anschlagbrett immer automatisch nach 24h von da verschwinden." + * + * ALS ZAHL AN EINER STELLE, damit die Anzeige („seit gestern") und + * die Regel (was noch haengt) nicht auseinanderlaufen koennen. Sie + * geht mit der Antwort an den Browser -- eine 24 im Skript waere die + * zweite Fassung, und beim naechsten Mal stuende dort eine andere + * Zahl als hier. */ +export const ANSCHLAG_STUNDEN = 24; + export const BEREICHE = { /* ===================================================================== DIE SIEBEN BRETTER DES TREFFS (11.09.2026) @@ -1303,6 +1315,52 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => { ORDER BY ${reihenfolge}`) .all(...(treffBrett ? [wer] : []), ...regel.werte, bereich); + /* ================================================================= + DAS ANSCHLAGBRETT HAELT 24 STUNDEN (22.09.2026) + ================================================================= + + Filipe: „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 -- die + 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. + + AB WANN DIE 24 STUNDEN LAUFEN: ab `erstellt`, also ab dem + Augenblick, in dem jemand sie aufgehaengt hat. Nicht ab `datum` + -- das 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. + + SIE VERSCHWINDET VON DER SEITE, NICHT AUS DER WELT. + Filipe sagt „von da verschwinden" -- von DA. Geloescht wird + nichts: Wer eine Ansage geschrieben hat, soll sie + wiederfinden, und eine Ansage rueckwirkend verschwinden zu + lassen waere etwas anderes als sie abzuhaengen. Die aelteren + kommen deshalb mit, aber als `abgehaengt` gekennzeichnet; die + Seite zeigt sie zugeklappt unter einer Zeile. + + WARUM DER SERVER DAS ENTSCHEIDET UND NICHT DER BROWSER: Im + Browser waere es eine zweite Fassung derselben Regel -- und die + eine wuerde irgendwann etwas anderes sagen als die andere. + Dieselbe Ueberlegung steht zehn Zeilen tiefer schon einmal, zum + Termin. + ================================================================= */ + if (bereich === "anschlag") { + const grenze = Date.now() - ANSCHLAG_STUNDEN * 3600 * 1000; + for (const e of eintraege) { + const auf = Date.parse(e.erstellt || ""); + /* OHNE LESBARES DATUM BLEIBT SIE HAENGEN. Ein unlesbarer + Zeitstempel ist ein Grund nachzusehen, kein Grund, etwas + verschwinden zu lassen. */ + e.abgehaengt = Number.isFinite(auf) ? auf < grenze : false; + } + } + /* STEHT DIESER ZETTEL IM KALENDER? (17.09.2026) Der Kalender zeigt seit heute, was auf den Brettern steht und @@ -1518,6 +1576,12 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => { res.json({ bereich, einstellung: { ...einstellung, ober, entscheidung }, treff: treffBrett, gehoert, weiter, + /* WIE LANGE EINE ANSAGE HAENGT -- vom Server, damit die Anzeige + („seit gestern abgehaengt") und die Regel dieselbe Zahl + benutzen. Nur am Anschlagbrett; ueberall sonst gibt es keine + Frist, und ein Feld, das immer 24 sagt, wuerde irgendwann + irgendwo ausgewertet. */ + ...(bereich === "anschlag" ? { anschlag_stunden: ANSCHLAG_STUNDEN } : {}), /* DARF DIESER MENSCH IN DEN KALENDER? Beantwortet von den zwei Stellen, die es entscheiden -- nicht von einer dritten Regel im Browser. Die Karte zeigt ihren Termin dann als Text statt diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index a3069908..1af70638 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +