From 0d52c4dc05a0f3a202e6f4ec9719fc72e2d62454 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 8 Oct 2026 12:13:00 +0200 Subject: [PATCH] =?UTF-8?q?Das=20Regie-Tor:=20jeder=20sieht=20es,=20nur=20?= =?UTF-8?q?die=20zwei=20kommen=20rein=20--=20und=20Farbe=20f=C3=BCr=20die?= =?UTF-8?q?=20Reihen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filipe, zum Bildschirmfoto des Foyers: „unter den [Reihen] fehlt eine kachel, die viel kraesser und spezieller aussehen soll, wo nur dogfather oder vanvan reinkommen. mit ihren zugangscodes für die seite. und das soll die regie kachel sein. die muss wirklich ultra krass sein. komplett crazy. die anderen kacheln aber auch gerne farbiger machen und nicht so kaal und dunkel." Auf die Rückfrage, wie fest das Schloss sein soll: „punkt 2 aber die soll jeder sehen aber nur vanvan und dogfather sollen da rein kommen bitte." Haus: Team Dogi (crew.dogfather-universe.com). Die Agentur ist nicht berührt -- pruef-haus-trennung 107/0. DAS TOR Eine vierte Kachel über die GANZE BREITE unter den drei Reihen. Das ist die stärkste Aussage, die ein Raster treffen kann, und sie kostet keine einzige Farbe. Ein Licht läuft in sieben Sekunden darüber -- flach und schmal, wie der Schein einer Lampe über einem Mischpult. Kein Blinken: dasselbe Signal mit doppelter Belastung für die Augen, und diese Seite steht manchmal eine Stunde offen. prefers-reduced-motion bekommt den Schein stehend, nicht gar keinen -- der Zustand muss auch dann zu erkennen sein. Drei Zustände, und jeder sieht anders aus: verschlossen rot mit geschlossenem Bügel, aufgeschlossen grün mit aufspringendem Bügel, und für alle anderen gedämpftes Grau ohne Lauflicht, mit „nicht erlaubt" schon am Mauszeiger. ZWEI SCHLÖSSER HINTEREINANDER, UND NUR EINES IST GEHEIM (1) DIE ROLLE, und zwar VOR dem Code -- ohne ihn anzusehen. Das ist wichtiger, als es aussieht: Sonst könnte irgendwer im Haus mit Rateversuchen die `versuche`-Bremse für seine eigene IP vollaufen lassen und sich damit von der ANMELDUNG aussperren; beide zählen in derselben Tabelle. Geprüft mit Gegenprobe: Ein Gast schickt den RICHTIGEN Admin-Code, kommt nicht durch, und die Versuchszahl bleibt bei 0. (2) DER CODE, geprüft mit `codeGeprueft()` -- neu in workspace.js, neben der Anmeldung und mit deren Rechenvorschrift, deren Vergleich in konstanter Zeit und deren Bremse. Ein zweiter Codevergleich in einem anderen Modul wäre der, der beim nächsten Umbau der scrypt-Parameter stehen bleibt. Und er prüft GENAU DIESE PERSON. Die Anmeldung geht alle Personen einer Rolle durch -- dort ist der Code die Kennung. Hier wäre das falsch: VanVans Code öffnete DogFathers Tür, und im Protokoll stünde, ER sei hineingegangen. WAS DIE TÜR LEISTET UND WAS NICHT -- UND DASS ES DASTEHT Filipes „Punkt 2" heißt: Der Code öffnet die Tür, danach ist die Regie offen wie bisher. Die Routen der Sendung prüfen weiterhin nur die Rolle. Das ist die bewusste Wahl und keine vergessene Stelle -- eine Sperre, die mitten in einer Übertragung zuschnappen kann, richtet mehr Schaden an, als sie verhindert. Damit das niemand überschätzt, steht es als Satz IM FENSTER, nicht nur im Quelltext: „Das hält einen neugierigen Blick auf, nicht jemanden, der an deinem offenen Rechner sitzt." Eine Sicherung, die stärker aussieht, als sie ist, ist schlechter als gar keine. FARBE -- ABER NICHT AUF DER FLÄCHE Filipe hatte recht, und der Grund war meiner: Beim Umbau auf das Hausmaterial heute Vormittag habe ich die Farbe mit herausgenommen, weil die alte Fassung sie auf der FLÄCHE trug -- und genau das machte den Text schlecht lesbar. Richtig ist nicht „keine Farbe", sondern Farbe, wo kein Text steht: * Jede Reihe hat ihren Ton (`--ton`, derselbe Griff, über den module.css das Kantenlicht legt): Bernstein für das, was ansteht -- dieselbe Farbe wie „überfällig" --, Blau für die Sendung, rot sobald sie läuft, Grün für das, was hereinkommt: dieselbe Farbe, die ein angenommener Vorschlag trägt. Die Farben SAGEN etwas. * Ein Band im Kopf jeder Tafel, die Überschrift in ihrem Ton, die Schilder passend. Vorher war jedes Schild blau, egal in welcher Reihe es stand -- zwei Farben nebeneinander, die nichts voneinander wussten. * Ein Streifen am Zeilenanfang statt eines eingefärbten Kastens. Drei Pixel an der Kante sagen dasselbe, und der Text steht weiterhin auf dem Grund, auf dem er gemessen wurde. Die Kachel selbst trägt dieselbe Silhouette und dasselbe deckende Material wie jede Karte im Haus (`.regie-tor` steht in der Modulliste in module.css und in der Materialliste in start.css). „Krass" heißt hier nicht „anders als das Haus" -- genau das stand heute Vormittag schon einmal in foyer.css und war ein Fehler. PROTOKOLLWÖRTER, DIE MAN LESEN KANN `personen.js` baut den Anzeigetext aus dem Schlüssel: Unterstriche werden Leerzeichen, nur der erste Buchstabe wird groß. Aus `regie_code_falsch` wäre auf dem Bildschirm „Regie code falsch" geworden. Die Regel, die daraus folgt: Hauptwort plus Mittelwort, nie zwei Hauptwörter. Jetzt `regie_aufgeschlossen`, `regie_abgeschlossen`, `regie_verweigert`, `regie_unbefugt`. Und das Detail war ein ISO-Zeitstempel mitten in einer Zeile, die ein Mensch überfliegt -- jetzt steht dort „12 Stunden". GEMESSEN pruef-reaktion 571, 0 Fehler (vorher 538) -- Abschnitt 21 mess-foyer 96, 0 Fehler (vorher 70), 12 Bildschirmfotos pruef-handy-teamdogi 263 Seitenaufrufe, 0 Befunde pruef-breiten 23 auf 45 Seiten und fünf Breiten pruef-struktur 102 · pruef-css-klassen 37 · pruef-lesbarkeit 14 pruef-bewegung 9 · pruef-tippziele 13 · pruef-deutsche-texte 12 pruef-crew-adresse 173 · pruef-haus-trennung 107 pruef-community-sicht 10 · pruef-sackgassen 14 alle 0 Fehler UND EINE LÜCKE, DIE DIE EIGENE MESSUNG GEFUNDEN HAT Der Vorhang mit dem Codefeld schließt auf Esc und auf einen Druck daneben, und der Finger steht im Feld -- für jemanden ohne Maus war er trotzdem eine Falle: Die Tabulatortaste lief durch die Knöpfe DAHINTER weiter, sichtbar war aber das Codefeld. Man tippt auf A und bekommt B, nur eben mit der Tastatur. Der erste Riegel legte `#foyer` still -- und die neue Messung fiel sofort darüber: Die KOPFLEISTE steht außerhalb davon, der Fokus lief weiter nach „Abmelden". Jetzt wird alles neben dem Vorhang stillgelegt, ABGELEITET statt aufgezählt (`body.children`), und beim Schließen genau das wieder freigegeben, was ich selbst gesetzt habe. Und die Messung selbst war beim ersten Entwurf zu streng: Sie verlangte „der Fokus bleibt IMMER im Fenster" und wurde rot, obwohl die Sperre tadellos arbeitete -- am Ende des Tabulatorkreises gibt der Browser den Fokus an seine eigene Leiste ab, im Dokument steht dann `body`. `body` ist kein Bedienelement. Gefragt ist jetzt das Richtige: Wird je ein BEDIENELEMENT außerhalb erreicht? Die Gegenprobe nennt es beim Namen (`DRAUSSEN:zurueck`). mess-foyer misst beide Hälften von Filipes Satz: dass ein Gast das Tor SIEHT (und ein Druck ihm trotzdem kein Codefeld öffnet) und dass nur die zwei HINEINKOMMEN. Dazu der ganze Weg im Browser: falscher Code abgewiesen und Feld geleert, richtiger Code führt in den Saal, die Freigabe gilt in einem neuen Fenster -- und VanVans Tür ist trotzdem noch zu. EIN FEHLALARM IN DER EIGENEN MESSUNG, BEHOBEN mess-foyer suchte zuerst das WORT „Warteschlange" im Dokument eines Gastes und fand es -- im unsichtbaren Gerüst der linken Reihe, wo es als Überschrift steht. Zwei Gründe, warum das falsch war: Eine Beschriftung ist keine Auskunft, und dass es eine Warteschlange GIBT, steht seit heute im Regie-Tor, das jeder sieht. Eine Messung, die genau das als Leck zählt, widerspricht dem Entwurf -- und sie hätte bei jedem Lauf angeschlagen. Gefragt ist das Schärfere: Kommen DATEN an? Jetzt werden Titel und Videokennungen geprüft, und dass keine einzige Planzeile gebaut wurde. NICHT BEHOBEN, WEIL NICHT MEINS: pruef-meldungen bleibt rot (28 Kennungen ohne Satz, 3 Rohanzeigen) -- gemessen im worktree auf dem letzten Commit schon vorher, Zeichen für Zeichen dieselbe Liste. Meine Arbeit hat 2 Kennungen und 2 Sätze ergänzt (174 -> 176, 155 -> 157), die Zahlen gehen genau gleich hoch. Die Dateien gehören überwiegend der Agentur; das gehört in einen eigenen Durchgang. Co-Authored-By: Claude Opus 5 --- server/mess-foyer.mjs | 219 +++++++++++++++- server/pruef-reaktion.mjs | 183 ++++++++++++++ server/reaktion-tabellen.js | 37 +++ server/workspace-reaktion.js | 159 +++++++++++- server/workspace.js | 86 +++++++ workspace/anleitung.html | 28 +-- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/foyer.css | 348 +++++++++++++++++++++++++- workspace/assets/css/module.css | 16 +- workspace/assets/css/start.css | 2 +- workspace/assets/js/meldung.js | 6 + workspace/assets/js/reaktion-foyer.js | 203 ++++++++++++++- workspace/aufgaben.html | 38 +-- workspace/automation.html | 32 +-- workspace/befinden.html | 34 +-- workspace/bereich.html | 48 ++-- workspace/bewerben.html | 38 +-- workspace/bewerbungen.html | 36 +-- workspace/buehne.html | 6 +- workspace/calls.html | 34 +-- workspace/chat.html | 42 ++-- workspace/content.html | 34 +-- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +-- workspace/entwicklung.html | 36 +-- workspace/hilfe.html | 30 +-- workspace/index.html | 10 +- workspace/kalender.html | 34 +-- workspace/leistung.html | 32 +-- workspace/manager-ziele.html | 28 +-- workspace/material.html | 32 +-- workspace/notizen.html | 28 +-- workspace/personen.html | 32 +-- workspace/profil.html | 36 +-- workspace/reaktion-foyer.html | 119 +++++++-- workspace/reaktion.html | 38 +-- workspace/rechte.html | 34 +-- workspace/report.html | 36 +-- workspace/scouting.html | 32 +-- workspace/start.html | 38 +-- workspace/startcheck.html | 34 +-- workspace/steckbrief.html | 36 +-- workspace/support.html | 28 +-- workspace/tafel.html | 6 +- workspace/talente.html | 36 +-- workspace/team.html | 32 +-- workspace/teamlage.html | 34 +-- workspace/teilen.html | 30 +-- workspace/treff-moderation.html | 32 +-- workspace/treff-regeln.html | 32 +-- workspace/uebersicht.html | 32 +-- workspace/unsere-seiten.html | 30 +-- workspace/unterstuetzen.html | 32 +-- workspace/werdegang.html | 32 +-- workspace/willkommen.html | 2 +- workspace/wissen.html | 34 +-- 58 files changed, 2034 insertions(+), 724 deletions(-) diff --git a/server/mess-foyer.mjs b/server/mess-foyer.mjs index 624cef15..8d418bd9 100644 --- a/server/mess-foyer.mjs +++ b/server/mess-foyer.mjs @@ -25,6 +25,10 @@ * Und geht der ganze Weg durch: Gast schickt ein Video, DogFather sieht es MIT Namen, nimmt es -- und es steht links im Plan. + * Steht das Regie-Tor bei JEDEM da (Filipes ausdruecklicher + Wunsch) -- und kommt trotzdem nur durch, wer den richtigen + Code hat? Gemessen werden alle vier Lagen: Gast (kein Feld), + Host verschlossen, Host mit falschem Code, Host mit richtigem. ZU JEDER MESSUNG, DIE "in Ordnung" SAGEN KANN, GEHOERT EINE GEGENPROBE. Am Ende wird jede der tragenden Messungen absichtlich @@ -347,6 +351,29 @@ const massLesen = (seite) => seite.evaluate(() => { }; }); +/** Was sagt das Regie-Tor? */ +const torLesen = (seite) => seite.evaluate(() => { + const tor = document.getElementById('regie-tor'); + if (!tor) return null; + const r = tor.getBoundingClientRect(); + const t = (k) => tor.querySelector(k)?.textContent.trim() || ''; + return { + da: !!tor.offsetParent, + darf: tor.dataset.darf, + offen: tor.dataset.offen, + titel: t('.regie-tor__titel'), + satz: t('.regie-tor__satz'), + marke: t('.regie-tor__marke'), + breit: Math.round(r.width), + hoch: Math.round(r.height), + oben: Math.round(r.top), + /* Die Farbe, die das Licht traegt -- sie ist die Aussage ueber + den Zustand und nicht nur Zierde. */ + ton: getComputedStyle(tor).getPropertyValue('--ton').trim(), + vorhangDa: !document.getElementById('regie-vorhang').hidden, + }; +}); + /** Was steht in der Mitte? */ const mitteLesen = (seite) => seite.evaluate(() => { const t = (id) => document.getElementById(id)?.textContent.trim() || ""; @@ -593,13 +620,187 @@ console.log("\n=== 6. Anraten, sehen, nehmen ==="); } /* ===================================================================== - 7. DIE GEGENPROBEN + 7. DAS REGIE-TOR + + Filipe: „die soll jeder sehen aber nur vanvan und dogfather sollen + da rein kommen bitte." Beide Haelften werden gemessen -- dass sie + jeder SIEHT und dass nur die zwei HINEINKOMMEN. Eine davon allein + waere keine Aussage. + ===================================================================== */ +console.log("\n=== 7. Das Regie-Tor ==="); +{ + const gast = await alsWer("gast", 1440, 900); + const t = await torLesen(gast.seite); + ok(!!t && t.da, "der Gast SIEHT das Tor"); + ok(t?.darf === "nein", `fuer ihn steht darf=nein (${t?.darf})`); + ok(t?.marke === "Nur ihr zwei", `das Schild sagt es (${t?.marke})`); + ok(/nicht hinein|T\u00fcr zu/i.test(t?.satz || ""), + `und der Satz auch (${t?.satz})`); + + /* ==== ES DARF KEINE DATEN VERRATEN ============================ + + Der erste Entwurf suchte hier auch das WORT „Warteschlange" -- + und fand es, im unsichtbaren Geruest der linken Reihe, wo es als + Ueberschrift steht. Das war ein Fehlalarm aus zwei Gruenden: + + * Eine Beschriftung ist keine Auskunft. Dass es eine + Warteschlange GIBT, steht ohnehin im Regie-Tor, und das + sieht seit heute jeder -- auf Filipes ausdruecklichen + Wunsch. Eine Messung, die genau dieses Wort als Leck + zaehlt, widerspricht dem Entwurf. + * Und sie haette bei jedem Lauf angeschlagen. Eine Warnung, + die immer kommt, nimmt irgendwann die echte mit. + + Gefragt ist das Schaerfere: Kommen DATEN an? Titel und + Videokennungen aus der Warteschlange duerfen im Dokument eines + Gastes nicht vorkommen -- und gebaute Zeilen schon gar nicht. */ + const leck = await leckLesen(gast.seite, + [...liste0.map((z) => z.titel), ...liste0.map((z) => z.video)]); + ok(leck.length === 0, "das Tor verraet keine Daten aus der Regie" + + (leck.length ? " -> " + leck.join(", ") : "")); + const zeilen = await gast.seite.evaluate(() => ({ + plan: document.querySelectorAll("#plan-liste .plan-zeile").length, + leerzeile: document.querySelectorAll("#plan-liste .plan-leer").length, + })); + ok(zeilen.plan === 0 && zeilen.leerzeile === 0, + `und es ist auch keine Planzeile gebaut worden ` + + `(${zeilen.plan} Zeilen, ${zeilen.leerzeile} Leerhinweise)`); + + /* EIN DRUCK OEFFNET IHM KEIN FELD. Sonst waere eine Kachel, die + jeder sieht, eine Ratestelle fuer jeden im Haus. */ + await gast.seite.click("#regie-tor"); + await gast.seite.waitForTimeout(150); + const danach = await torLesen(gast.seite); + ok(danach?.vorhangDa === false, "ein Druck oeffnet ihm kein Codefeld"); + await bild(gast.seite, "10-tor-gast"); + await gast.kontext.close(); +} +{ + const chef = await alsWer("admin", 1440, 900); + const t = await torLesen(chef.seite); + ok(t?.darf === "ja", `DogFather darf (${t?.darf})`); + ok(t?.offen === "nein", `und es ist zu (${t?.offen})`); + ok(t?.marke === "Verschlossen", `das Schild sagt es (${t?.marke})`); + const r = await reihenLesen(chef.seite); + ok(!!t && t.breit > r.saal.breit, + `das Tor ist breiter als die breiteste Reihe (${t?.breit} zu ${r.saal?.breit} px)`); + ok(!!t && !!r.saal && t.oben > r.saal.oben, + `und es steht UNTER den Reihen (${t?.oben} nach ${r.saal?.oben} px)`); + + /* ---- Falscher Code ---- */ + await chef.seite.click("#regie-tor"); + await chef.seite.waitForSelector("#regie-vorhang:not([hidden])", { timeout: 5000 }); + ok(true, "ein Druck oeffnet ihm das Codefeld"); + await bild(chef.seite, "11-tor-vorhang"); + /* ==== DIE TABULATORTASTE DARF NICHT HINTER DEN VORHANG ======== + Esc schliesst, ein Druck daneben schliesst, der Finger steht im + Feld -- und trotzdem war der Vorhang fuer jemanden ohne Maus + eine Falle: Die Tabulatortaste lief durch die Knoepfe DAHINTER + weiter, sichtbar war aber das Codefeld. Gemessen wird deshalb + nicht, ob `inert` gesetzt ist, sondern wo der Fokus LANDET. */ + { + /* `body` IST KEIN BEDIENELEMENT, und das ist hier der Unterschied + zwischen einem Befund und einem Fehlalarm. Am Ende des + Tabulatorkreises gibt der Browser den Fokus an die Leiste des + Browsers ab; im Dokument steht dann `document.body`. Mein + erster Entwurf verlangte „immer drin" und wurde deshalb rot, + obwohl die Sperre tadellos arbeitete -- gemeldet wurde + „drin, body", und kein einziges Bedienelement ausserhalb. + + Gefragt ist also: Wird je ein BEDIENELEMENT ausserhalb des + Fensters erreicht? Deshalb sagt die Messung auch, WELCHES -- + ein Befund, der nur „draussen" meldet, schickt einen suchen. */ + const woher = () => chef.seite.evaluate(() => { + const e = document.activeElement; + if (!e || e === document.body || e === document.documentElement) return "body"; + return e.closest("#regie-vorhang") + ? "drin" + : "DRAUSSEN:" + (e.id || e.className || e.tagName); + }); + const wo = []; + for (let i = 0; i < 6; i++) { await chef.seite.keyboard.press("Tab"); wo.push(await woher()); } + ok(wo.includes("drin") && !wo.some((x) => x.startsWith("DRAUSSEN")), + `kein Bedienelement hinter dem Vorhang ist erreichbar ` + + `(6x Tab: ${[...new Set(wo)].join(", ")})`); + + /* GEGENPROBE: Ohne die Stilllegung MUSS ein Element dahinter + auftauchen. Sonst bewiese die Zeile darueber nur, dass sechsmal + Tab irgendwo landet -- und nicht, dass die Sperre es ist. */ + await chef.seite.evaluate(() => { + for (const e of document.body.children) e.inert = false; + }); + const raus = []; + for (let i = 0; i < 8; i++) { await chef.seite.keyboard.press("Tab"); raus.push(await woher()); } + ok(raus.some((x) => x.startsWith("DRAUSSEN")), + `Gegenprobe: ohne die Sperre wird eines erreicht ` + + `(${raus.find((x) => x.startsWith("DRAUSSEN")) || [...new Set(raus)].join(", ")})`); + await chef.seite.evaluate(() => { + for (const e of document.body.children) { + if (e.id !== "regie-vorhang") e.inert = true; + } + }); + } + + await chef.seite.fill("#regie-code", "CODE-XXX-999999"); + await chef.seite.click("#regie-los"); + await chef.seite.waitForSelector("#regie-fehler:not([hidden])", { timeout: 8000 }); + const meldung = await chef.seite.textContent("#regie-fehler"); + ok(/Code stimmt nicht/.test(meldung || ""), + `ein falscher Code wird abgewiesen, in ganzen Saetzen (${meldung})`); + const geleert = await chef.seite.inputValue("#regie-code"); + ok(geleert === "", "und das Feld ist danach leer"); + const nochZu = await torLesen(chef.seite); + ok(nochZu?.offen === "nein", "das Tor bleibt zu"); + + /* ---- Richtiger Code ---- */ + await chef.seite.fill("#regie-code", CODES.admin); + await Promise.all([ + chef.seite.waitForURL("**/reaktion.html", { timeout: 15000 }), + chef.seite.click("#regie-los"), + ]); + ok(/reaktion\.html/.test(chef.seite.url()), + `der richtige Code fuehrt in den Saal (${chef.seite.url().split("/").pop()})`); + await chef.kontext.close(); +} +{ + /* ---- Und danach ist es offen, auch in einem neuen Fenster ---- + Das ist der Punkt der ganzen Freigabe: Sie haengt an der Person + und nicht an dem Reiter, in dem getippt wurde. */ + const chef = await alsWer("admin", 1440, 900); + const t = await torLesen(chef.seite); + ok(t?.offen === "ja", `das Tor steht offen (${t?.offen})`); + ok(t?.marke === "Offen", `das Schild sagt es (${t?.marke})`); + ok(/Offen bis/.test(t?.satz || ""), `und bis wann (${t?.satz})`); + await bild(chef.seite, "12-tor-offen"); + + /* VanVan hat ihre eigene Tuer. Waere die Freigabe an der Rolle + festgemacht statt an der Person, stuende sie jetzt auch bei ihr + offen -- und im Protokoll staende, SIE sei hineingegangen. */ + const van = await alsWer("hand", 1440, 900); + const tv = await torLesen(van.seite); + ok(tv?.darf === "ja", `VanVan darf auch (${tv?.darf})`); + ok(tv?.offen === "nein", + `aber ihre Tuer ist ihre eigene und noch zu (${tv?.offen})`); + await van.kontext.close(); + + /* ---- Wieder abschliessen ---- */ + await chef.seite.evaluate(() => fetch('/workspace/api/reaktion/regie', + { method: 'DELETE', credentials: 'same-origin' })); + await chef.seite.reload({ waitUntil: "networkidle" }); + await chef.seite.waitForTimeout(400); + const zu = await torLesen(chef.seite); + ok(zu?.offen === "nein", `abschliessen geht auch wieder (${zu?.offen})`); + await chef.kontext.close(); +} + +/* ===================================================================== + 8. DIE GEGENPROBEN Fuenf Messungen oben koennen "in Ordnung" sagen. Hier wird jede absichtlich kaputtgemacht -- schlaegt sie nicht an, ist sie wertlos, und alle gruenen Haken davor beweisen nichts. ===================================================================== */ -console.log("\n=== 7. Gegenproben ==="); +console.log("\n=== 8. Gegenproben ==="); { const chef = await alsWer("admin", 390, 844); @@ -655,6 +856,16 @@ console.log("\n=== 7. Gegenproben ==="); await gast.seite.waitForTimeout(120); const sichtbar = await reihenLesen(gast.seite); ok(sichtbar.plan !== null, "Gegenprobe: eine aufgedeckte Reihe gilt als sichtbar"); + + /* (g) Und der Tor-Leser muss ein verschwundenes Tor auch + verschwunden nennen -- sonst waere „der Gast sieht es" oben nur + die Folge davon, dass er immer „da" sagt. */ + await gast.seite.evaluate(() => { + document.getElementById("regie-tor").style.display = "none"; + }); + await gast.seite.waitForTimeout(120); + const weg = await torLesen(gast.seite); + ok(weg?.da === false, "Gegenprobe: ein ausgeblendetes Tor gilt als weg"); await gast.kontext.close(); } @@ -669,8 +880,8 @@ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } einzige Messung, waere "0 Fehler" das beste Ergebnis ueberhaupt -- und voellig wertlos. */ console.log(`\n${geprueft} Messungen, ${fehler} Fehler`); -if (geprueft < 40) { - console.log(`NICHT NACHSEHBAR: nur ${geprueft} Messungen gelaufen, erwartet mindestens 40.`); +if (geprueft < 55) { + console.log(`NICHT NACHSEHBAR: nur ${geprueft} Messungen gelaufen, erwartet mindestens 55.`); process.exit(3); } if (fehler) { diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs index ff55179c..89419683 100644 --- a/server/pruef-reaktion.mjs +++ b/server/pruef-reaktion.mjs @@ -3508,6 +3508,189 @@ melde("=== 20. Das Foyer: Plan, Eintritt, Vorschlaege ==="); } } +/* ======================================================================= + 21. DAS REGIE-TOR (08.10.2026) + + Filipe: „unter den [Reihen] fehlt eine kachel, die viel kraesser und + spezieller aussehen soll, wo nur dogfather oder vanvan reinkommen. + mit ihren zugangscodes fuer die seite." Und auf die Rueckfrage nach + der Staerke des Schlosses: „punkt 2 aber die soll jeder sehen aber + nur vanvan und dogfather sollen da rein kommen bitte." + + ZWEI HAELFTEN, UND BEIDE WERDEN GEMESSEN: dass JEDER die Kachel + sieht, und dass nur die zwei HINEINKOMMEN. Eine davon allein waere + keine Aussage. + ======================================================================= */ +melde(""); +melde("=== 21. Das Regie-Tor ==="); +{ + /* Eigener Griff auf die Datenbank: `d` oben ist nach dem Anlegen + der Personen geschlossen, und jeder Abschnitt, der nachsieht, + oeffnet seinen eigenen. */ + const db = new DatabaseSync(process.env.WORKSPACE_DB); + /* ---- Wer was zu sehen bekommt ---- */ + for (const rolle of ["gast", "modi", "linke"]) { + const a = await roh(CREW, "/workspace/api/reaktion/regie", { keks: k[rolle] }); + ok(a.code === 200 && a.j?.darf === false, + `${rolle}: sieht das Tor, darf aber nicht hinein (${a.code}, darf=${a.j?.darf})`); + /* ES VERRAET NICHTS. Wer nicht hinein darf, bekommt auch nicht + zu wissen, ob gerade jemand drin ist. */ + ok(a.j?.offen === false && a.j?.bis === null, + `${rolle}: und erfaehrt nichts ueber den Zustand dahinter`); + } + for (const rolle of ["admin", "hand"]) { + const a = await roh(CREW, "/workspace/api/reaktion/regie", { keks: k[rolle] }); + ok(a.code === 200 && a.j?.darf === true && a.j?.offen === false, + `${rolle}: darf hinein, Tor ist noch zu (darf=${a.j?.darf}, offen=${a.j?.offen})`); + } + + /* ---- Wer nicht darf, erzeugt KEINEN Fehlversuch ---------------- + Sonst koennte irgendwer im Haus die Bremse fuer seine eigene IP + vollaufen lassen und sich damit von der ANMELDUNG aussperren -- + beide zaehlen in derselben Tabelle. */ + { + const vorher = db.prepare("SELECT COUNT(*) AS n FROM versuche").get().n; + const a = await roh(CREW, "/workspace/api/reaktion/regie", + { method: "POST", keks: k.gast, rumpf: { code: CODES.admin } }); + ok(a.code === 403 && a.j?.fehler === "regie_gehoert_den_zweien", + `ein Gast wird abgewiesen (${a.code} ${a.j?.fehler})`); + const nachher = db.prepare("SELECT COUNT(*) AS n FROM versuche").get().n; + ok(nachher === vorher, + `und sein Versuch zaehlt nicht gegen die Bremse (${vorher} -> ${nachher})`); + /* UND ZWAR AUCH DANN NICHT, WENN ER DEN RICHTIGEN CODE HAETTE. + Oben stand `CODES.admin` -- der Gast hat also den gueltigen + Code eines Hosts geschickt und kommt trotzdem nicht durch. + Die Rolle entscheidet VOR dem Code. */ + const z = db.prepare("SELECT COUNT(*) AS n FROM reaktion_regie").get().n; + ok(z === 0, `und niemand steht in der Freigabetabelle (${z})`); + } + + /* ---- Falscher Code ---- */ + { + const a = await roh(CREW, "/workspace/api/reaktion/regie", + { method: "POST", keks: k.admin, rumpf: { code: "CODE-XXX-999999" } }); + ok(a.code === 401 && a.j?.fehler === "code_falsch", + `ein falscher Code wird abgewiesen (${a.code} ${a.j?.fehler})`); + const b = await roh(CREW, "/workspace/api/reaktion/regie", { keks: k.admin }); + ok(b.j?.offen === false, "und das Tor bleibt zu"); + } + + /* ---- DER CODE DER ANDEREN OEFFNET DIE EIGENE TUER NICHT -------- + Die Anmeldung geht alle Personen einer ROLLE durch; dort ist der + Code die Kennung. Hier waere das falsch: VanVans Code wuerde + DogFathers Tuer oeffnen, und im Protokoll stuende, ER sei + hineingegangen. */ + { + const a = await roh(CREW, "/workspace/api/reaktion/regie", + { method: "POST", keks: k.admin, rumpf: { code: CODES.hand } }); + ok(a.code === 401 && a.j?.fehler === "code_falsch", + `VanVans Code oeffnet DogFathers Tuer nicht (${a.code})`); + } + + /* ---- Richtiger Code ---- */ + { + const a = await roh(CREW, "/workspace/api/reaktion/regie", + { method: "POST", keks: k.admin, rumpf: { code: CODES.admin } }); + ok(a.code === 200 && a.j?.offen === true, + `der eigene Code oeffnet (${a.code}, offen=${a.j?.offen})`); + /* ZWOELF STUNDEN, und die Zahl kommt aus der Datei -- nicht aus + dieser Pruefung. Eine abgeschriebene Zahl waere die, die beim + naechsten Umbau stehen bleibt. */ + const t = await import("./reaktion-tabellen.js"); + const stunden = (new Date(a.j.bis) - Date.now()) / 3600_000; + ok(Math.abs(stunden - t.REGIE_FREI_STUNDEN) < 0.2, + `und zwar fuer ${t.REGIE_FREI_STUNDEN} Stunden (gemessen ${stunden.toFixed(2)})`); + ok(a.j?.stunden === t.REGIE_FREI_STUNDEN, + "die Seite erfaehrt die Dauer vom Server und rechnet sie nicht selbst"); + } + + /* ---- Die Freigabe haengt an der PERSON, nicht an der Rolle ---- */ + { + const a = await roh(CREW, "/workspace/api/reaktion/regie", { keks: k.hand }); + ok(a.j?.darf === true && a.j?.offen === false, + `VanVans Tuer ist ihre eigene und noch zu (offen=${a.j?.offen})`); + } + + /* ---- Und sie gilt ueber eine neue Anfrage hinweg ---- */ + { + const a = await roh(CREW, "/workspace/api/reaktion/regie", { keks: k.admin }); + ok(a.j?.offen === true, "beim naechsten Nachsehen steht sie offen"); + } + + /* ---- Abgelaufen heisst zu, und die Zeile wird weggeraeumt ------ + Zurueckdatiert statt zwoelf Stunden gewartet. */ + { + db.prepare("UPDATE reaktion_regie SET bis = ? WHERE person_id = ?") + .run(new Date(Date.now() - 60_000).toISOString(), IDS.admin); + const a = await roh(CREW, "/workspace/api/reaktion/regie", { keks: k.admin }); + ok(a.j?.offen === false, "eine abgelaufene Freigabe gilt nicht mehr"); + const n = db.prepare("SELECT COUNT(*) AS n FROM reaktion_regie WHERE person_id = ?") + .get(IDS.admin).n; + ok(n === 0, `und die Zeile ist weg statt nur ignoriert (${n})`); + } + + /* ---- Wieder abschliessen ---- */ + { + await roh(CREW, "/workspace/api/reaktion/regie", + { method: "POST", keks: k.admin, rumpf: { code: CODES.admin } }); + const auf = await roh(CREW, "/workspace/api/reaktion/regie", { keks: k.admin }); + ok(auf.j?.offen === true, "Gegenprobe: aufgeschlossen ist sie wirklich offen"); + const zu = await roh(CREW, "/workspace/api/reaktion/regie", + { method: "DELETE", keks: k.admin }); + ok(zu.code === 200 && zu.j?.offen === false, + `und sie laesst sich wieder abschliessen (${zu.code})`); + } + + /* ---- Die Bremse ------------------------------------------------ + Acht Fehlversuche je IP in zehn Minuten, gemeinsam mit der + Anmeldung gezaehlt. Geprueft wird, DASS sie greift -- und danach + wird die Tabelle geleert, damit die naechsten Abschnitte nicht + gegen eine volle Bremse laufen. */ + { + let gebremst = 0; + let letzter = 0; + for (let i = 0; i < 12; i++) { + const a = await roh(CREW, "/workspace/api/reaktion/regie", + { method: "POST", keks: k.admin, rumpf: { code: `CODE-FAL-00000${i}` } }); + letzter = a.code; + if (a.code === 429) gebremst++; + } + ok(gebremst > 0, `nach genug Fehlversuchen bremst sie (${gebremst} von 12, zuletzt ${letzter})`); + /* GEGENPROBE: Nach dem Leeren geht es wieder -- sonst bewiese die + Zeile darueber nur, dass irgendwann 429 kommt, und nicht, dass + es an der Bremse lag. */ + db.prepare("DELETE FROM versuche").run(); + const b = await roh(CREW, "/workspace/api/reaktion/regie", + { method: "POST", keks: k.admin, rumpf: { code: CODES.admin } }); + ok(b.code === 200, `und nach dem Zuruecksetzen geht es wieder (${b.code})`); + await roh(CREW, "/workspace/api/reaktion/regie", { method: "DELETE", keks: k.admin }); + } + + /* ---- Was auf der Seite steht ---- */ + { + const html = readFileSync( + join(HIER, "..", "workspace", "reaktion-foyer.html"), "utf8"); + const js = readFileSync( + join(HIER, "..", "workspace", "assets", "js", "reaktion-foyer.js"), "utf8"); + const css = readFileSync( + join(HIER, "..", "workspace", "assets", "css", "foyer.css"), "utf8"); + ok(/id="regie-tor"/.test(html), "das Tor steht in der Seite"); + ok(/]*class="regie-tor"/.test(html), + "und ist ein Knopf und kein Link -- er fuehrt erst nach dem Code irgendwohin"); + ok(/type="password"/.test(html), + "das Codefeld zeigt nichts an (Bildschirm teilen)"); + ok(/id="regie-vorhang"/.test(html) && html.indexOf("id=\"regie-vorhang\"") > html.indexOf(""), + "der Vorhang steht NEBEN main -- sonst laege er unter der Kopfleiste"); + ok(!/rolle\s*===\s*['"]admin/.test(js), + "das Tor fuehrt keine eigene Rollenliste, es fragt den Server"); + ok(/regie\?\.darf/.test(js), "es fragt, ob die Auskunft es erlaubt"); + ok(/prefers-reduced-motion/.test(css), + "wer weniger Bewegung eingestellt hat, bekommt das Licht ohne Lauf"); + ok(/\.regie-tor\[data-darf="nein"\]/.test(css), + "und wer nicht hinein darf, sieht es gedaempft"); + } +} + melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler`); melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); diff --git a/server/reaktion-tabellen.js b/server/reaktion-tabellen.js index ef8ca926..f3260b1d 100644 --- a/server/reaktion-tabellen.js +++ b/server/reaktion-tabellen.js @@ -148,6 +148,26 @@ export const VORSCHLAG_BREMSE_SEKUNDEN = 45; /** Die Zustaende eines Vorschlags. */ export const VORSCHLAG_STAENDE = ["offen", "uebernommen", "abgelehnt"]; +/* ==== WIE LANGE DAS REGIE-TOR OFFEN BLEIBT (08.10.2026) ============ + + Filipe hat sich fuer die mittlere Staerke entschieden: Der Code + oeffnet die Tuer, danach ist die Regie offen wie bisher. + + ZWOELF STUNDEN, UND DIE ZAHL IST NICHT GERATEN. Kuerzer waere eine + Sicherung, die mitten in einem Sendeabend noch einmal fragt -- + genau die, die man nach dem dritten Mal umgeht. Laenger waere eine, + die am naechsten Morgen noch offen steht, ohne dass jemand daran + denkt. Ein Abend passt hinein, eine Nacht nicht. + + WICHTIG, DAMIT NIEMAND MEHR HINEINLIEST, ALS DRINSTEHT: Diese + Freigabe ist eine TUER und kein Schloss an den Regieknoepfen. Die + Routen der Sendung pruefen weiterhin nur die Rolle (`nurHost`) -- + wer `reaktion.html` direkt aufruft, kommt wie bisher an die Regie. + Das war die bewusste Wahl: Eine Sperre, die mitten in einer + Uebertragung zuschnappen kann, richtet mehr Schaden an, als sie + verhindert. Was sie wirklich leistet, steht im Foyer daneben. */ +export const REGIE_FREI_STUNDEN = 12; + /** Ein Chatbeitrag in der Reaction. */ export const TEXT_MAX = 400; @@ -548,6 +568,23 @@ export function reaktionTabellen(d) { CREATE INDEX IF NOT EXISTS idx_reaktion_vorschlag_stand ON reaktion_vorschlaege (stand, id); + /* ==== WER DAS REGIE-TOR AUFGESCHLOSSEN HAT (08.10.2026) ===== + + Eine Zeile je Person, nicht eine je Oeffnung: Gefragt ist + „steht die Tuer fuer dich gerade offen", nicht „wie oft warst + du schon drin". Wer die Geschichte will, findet sie im + Protokoll -- dort steht jedes Oeffnen und jedes Abschliessen. + + IN DER DATENBANK UND NICHT IM ARBEITSSPEICHER. Im Speicher + waere es nach jedem Dienstneustart weg, und ein Neustart + mitten in der Vorbereitung haette genau dann nach dem Code + gefragt, wenn am wenigsten Zeit dafuer ist. */ + CREATE TABLE IF NOT EXISTS reaktion_regie ( + person_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE, + seit TEXT NOT NULL, + bis TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_reaktion_liste_platz ON reaktion_liste (platz, id); CREATE INDEX IF NOT EXISTS idx_reaktion_chat_zeit diff --git a/server/workspace-reaktion.js b/server/workspace-reaktion.js index cebd9ac6..8c531e04 100644 --- a/server/workspace-reaktion.js +++ b/server/workspace-reaktion.js @@ -41,7 +41,7 @@ nicht, ein Handy geht einfach aus. ===================================================================== */ import express from "express"; -import { db, sitzungLesen, protokolliere, echteIp } from "./workspace.js"; +import { db, sitzungLesen, protokolliere, echteIp, codeGeprueft } from "./workspace.js"; import { stromAn } from "./workspace-chat.js"; import { buehneMelden } from "./workspace-buehne.js"; /* Der Spender-Glanz im Chat. EINE RICHTUNG: Die Reaction fragt die @@ -54,6 +54,7 @@ import { mitZugangsdaten, wegeAusfaechern } from "./workspace-turn.js"; import { SICHT_PLAETZE, GAESTE_MAX, EINLADUNG_SEKUNDEN, TEXT_MAX, SCHREIB_BREMSE, VORSCHLAEGE_JE_PERSON, VORSCHLAG_NOTIZ_MAX, VORSCHLAG_BREMSE_SEKUNDEN, + REGIE_FREI_STUNDEN, STAENDE, LAYOUTS, TEMPI, KAMERA_GROESSEN, KAMERA_ECKEN, MELDUNG_TAGE, @@ -1739,6 +1740,162 @@ reaktionRouter.delete("/workspace/api/reaktion/vorschlag/:id(\\d+)", (req, res) } }); +/* ======================================================================= + DAS REGIE-TOR (08.10.2026) + + Filipe: „unter den [Reihen] fehlt eine kachel, die viel kraesser und + spezieller aussehen soll, wo nur dogfather oder vanvan reinkommen. + mit ihren zugangscodes fuer die seite. und das soll die regie kachel + sein." Und auf die Rueckfrage, wie fest das Schloss sein soll: + „punkt 2 aber die soll jeder sehen aber nur vanvan und dogfather + sollen da rein kommen bitte." + + ----------------------------------------------------------------------- + JEDER SIEHT SIE -- UND DAS IST KEIN VERSEHEN + + Die linke Reihe steht fuer andere gar nicht erst da; hier ist es + umgekehrt, weil Filipe es ausdruecklich so wollte. Die Kachel + verraet deshalb NICHTS: kein Titel, kein Stand, keine Zahl. Sie ist + eine verschlossene Tuer mit einem Schild darauf. Was dahinter liegt, + steht nirgends in der Antwort fuer jemanden, der nicht hinein darf. + + ----------------------------------------------------------------------- + ZWEI SCHLOESSER HINTEREINANDER, UND NUR EINES DAVON IST GEHEIM + + (1) DIE ROLLE. Wer nicht admin oder rechte Hand ist, bekommt gar + kein Eingabefeld -- die Antwort sagt `darf: false`, und der + Server lehnt den Versuch ab, OHNE den Code anzusehen. Das ist + wichtig: Eine Kachel, die jeder sieht, waere sonst eine + Ratestelle fuer jeden im Haus. + (2) DER CODE. Fuer die zwei, die durchduerfen, zusaetzlich der + eigene Zugangscode -- geprueft mit `codeGeprueft()`, also mit + derselben Rechenvorschrift und derselben Bremse wie die + Anmeldung. Acht Fehlversuche je IP in zehn Minuten, gemeinsam + mit der Anmeldung gezaehlt. + + ----------------------------------------------------------------------- + WAS DIESE TUER LEISTET -- UND WAS NICHT + + Sie haelt den neugierigen Blick auf. Sie haelt NICHT jemanden auf, + der an einem offenen Rechner sitzt und `reaktion.html` direkt + aufruft: Die Regieknoepfe dort pruefen weiterhin nur die Rolle. + Das ist die bewusste Wahl (Filipes „Punkt 2") und keine vergessene + Stelle. Eine Sperre, die mitten in einer Uebertragung zuschnappen + kann, richtet mehr Schaden an, als sie verhindert. + + Damit das niemand falsch liest, steht es auch IM FOYER als Satz + neben der Kachel -- nicht nur hier im Quelltext. Eine Sicherung, + die staerker aussieht, als sie ist, ist schlechter als gar keine. + ======================================================================= */ + +/** Steht das Tor fuer diese Person gerade offen? */ +function regieFrei(personId) { + const z = db().prepare("SELECT seit, bis FROM reaktion_regie WHERE person_id = ?") + .get(personId); + if (!z) return null; + /* ABGELAUFENES WIRD WEGGERAEUMT, nicht nur ignoriert. Sonst stuende + in der Tabelle mit der Zeit eine Zeile je Person, die einmal + drin war -- und beim Nachsehen, wer gerade Zugang hat, muesste + man jedes Mal rechnen. */ + if (z.bis < jetzt()) { + db().prepare("DELETE FROM reaktion_regie WHERE person_id = ?").run(personId); + return null; + } + return z; +} + +/** Was die Kachel im Foyer wissen muss. */ +function regieLage(person) { + const darf = HOST_ROLLEN.has(person.rolle); + const z = darf ? regieFrei(person.id) : null; + return { + /* OB JEMAND HINEIN DARF, sagt der Server -- die Seite soll keine + zweite Rollenliste fuehren. */ + darf, + offen: !!z, + bis: z?.bis || null, + stunden: REGIE_FREI_STUNDEN, + }; +} + +reaktionRouter.get("/workspace/api/reaktion/regie", (req, res) => { + try { + res.json(regieLage(req.person)); + } catch (fehler) { + console.error("[reaktion] Regie-Lage:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + +reaktionRouter.post("/workspace/api/reaktion/regie", express.json({ limit: "2kb" }), + (req, res) => { + try { + const ip = echteIp(req); + /* ERST DIE ROLLE, DANN DER CODE -- und ohne den Code auch nur + anzusehen. Wer nicht hinein darf, soll keinen Fehlversuch + erzeugen koennen: Sonst koennte irgendwer im Haus die Bremse + fuer seine eigene IP vollaufen lassen und sich damit selbst + von der Anmeldung aussperren. */ + if (!HOST_ROLLEN.has(req.person.rolle)) { + /* DIE WOERTER SIND SO GEWAEHLT, DASS SIE AUF DEM BILDSCHIRM + LESBAR SIND. Die Personenseite macht aus dem Schluessel den + Anzeigetext: Unterstriche werden Leerzeichen, der erste + Buchstabe gross, der Rest klein. „regie_code_falsch" waere + dort zu „Regie code falsch" geworden -- Maschinensprache mit + einem grossen Anfangsbuchstaben. Zwei Woerter, Hauptwort und + Mittelwort, ergeben dagegen einen Satz. */ + protokolliere("regie_unbefugt", { + personId: req.person.id, rolle: req.person.rolle, ip, + }); + return res.status(403).json({ fehler: "regie_gehoert_den_zweien" }); + } + + const urteil = codeGeprueft(req.person, req.body?.code, ip); + if (urteil === "zu_viele") return res.status(429).json({ fehler: "zu_viele_versuche" }); + if (urteil === "unbekannt") return res.status(503).json({ fehler: "nicht_verfuegbar" }); + if (urteil !== "ok") { + protokolliere("regie_verweigert", { + personId: req.person.id, rolle: req.person.rolle, ip, + }); + return res.status(401).json({ fehler: "code_falsch" }); + } + + const seit = jetzt(); + const bis = new Date(Date.now() + REGIE_FREI_STUNDEN * 3600_000).toISOString(); + db().prepare(`INSERT INTO reaktion_regie (person_id, seit, bis) VALUES (?,?,?) + ON CONFLICT(person_id) DO UPDATE SET seit = excluded.seit, bis = excluded.bis`) + .run(req.person.id, seit, bis); + protokolliere("regie_aufgeschlossen", { + personId: req.person.id, rolle: req.person.rolle, ip, + /* UND DAS DETAIL AUCH LESBAR. Hier stand der Zeitpunkt als + ISO-Zeichenkette -- also „2026-10-08T23:43:11.004Z" mitten + in einer Zeile, die ein Mensch ueberfliegt. Die Dauer sagt + dasselbe und braucht keine Umrechnung im Kopf. */ + detail: `${REGIE_FREI_STUNDEN} Stunden`, + }); + res.json(regieLage(req.person)); + } catch (fehler) { + console.error("[reaktion] Regie oeffnen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + +/* WIEDER ABSCHLIESSEN. Eine Tuer, die man nur aufmachen kann, ist + keine: Wer den Rechner einem anderen gibt, muss sie zumachen + koennen, ohne zwoelf Stunden zu warten oder sich abzumelden. */ +reaktionRouter.delete("/workspace/api/reaktion/regie", (req, res) => { + try { + db().prepare("DELETE FROM reaktion_regie WHERE person_id = ?").run(req.person.id); + protokolliere("regie_abgeschlossen", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + }); + res.json(regieLage(req.person)); + } catch (fehler) { + console.error("[reaktion] Regie schliessen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + /** Eins hoch oder runter. * * GETAUSCHT WIRD MIT DEM NACHBARN, nicht neu durchnummeriert. Beim diff --git a/server/workspace.js b/server/workspace.js index e8a69f9b..3f5189ea 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -6910,6 +6910,92 @@ function zuVieleVersuche(ip) { return anzahl >= VERSUCHE_MAX; } +/* ===================================================================== + NOCH EINMAL DEN EIGENEN CODE -- FUER EINE TUER HINTER DER ANMELDUNG + (08.10.2026) + + Gebaut fuer das Regie-Tor im Foyer der Reaction. Filipe: die Kachel + soll jeder sehen, hinein kommen aber nur DogFather und VanVan, und + zwar mit ihrem Zugangscode. + + --------------------------------------------------------------------- + WARUM DAS HIER STEHT UND NICHT DORT + + Die Anmeldung weiter oben weiss, wie man einen Code prueft: scrypt + mit genau diesen Parametern, Vergleich in konstanter Zeit, ein + Eintrag in `versuche`, eine Bremse je IP. Wer das in einem anderen + Modul noch einmal schreibt, hat eine ZWEITE Antwort auf dieselbe + Frage -- und die zweite ist die, die beim naechsten Umbau der + Parameter stehen bleibt. Ein Codevergleich, der still mit einem + alten N rechnet, sperrt entweder jeden aus oder laesst jeden + durch, und beides faellt erst spaet auf. + + Deshalb EINE Funktion mit EINER Antwort. Sie bekommt die Person + (die kennt der Aufrufer aus der Sitzung) und gibt vier moegliche + Woerter zurueck. Der Aufrufer entscheidet, was er damit macht. + + --------------------------------------------------------------------- + SIE PRUEFT DEN CODE GENAU DIESER PERSON + + Die Anmeldung geht alle aktiven Personen einer ROLLE durch -- dort + ist der Code die Kennung, es gibt kein Benutzerfeld. Hier ist das + falsch: Wer schon angemeldet ist, hat einen Namen. Wuerde ich auch + hier die Rolle durchgehen, oeffnete VanVans Code die Tuer fuer + DogFathers Sitzung. Das waere kein Loch nach aussen, aber es waere + eine Luege im Protokoll -- dort stuende, DogFather sei hineingegangen. + + --------------------------------------------------------------------- + DIE BREMSE IST DIESELBE WIE AN DER ANMELDUNG + + Acht Fehlversuche je IP in zehn Minuten, gemeinsam gezaehlt. Das + ist Absicht: Wer am Tor raet, soll nicht acht weitere Versuche + bekommen, indem er zwischendurch die Anmeldung probiert. Und ein + gelungener Versuch raeumt die Liste NICHT -- anders als bei der + Anmeldung, wo ein erfolgreicher Login `versuche` leert. Hier sitzt + schon jemand Angemeldetes davor; die Bremse soll ihm gegenueber + nicht nachgiebiger sein als gegenueber einem Fremden. + + @param {{id:number, rolle:string}} person aus der Sitzung + @param {string} code was getippt wurde + @param {string} ip fuer die Bremse + @returns {"ok"|"falsch"|"zu_viele"|"unbekannt"} + "unbekannt" heisst: Die Person gibt es nicht mehr oder ihre + Zugangsdaten sind defekt -- der dritte Ausgang, und er darf + NICHT wie "falsch" aussehen, sonst sucht jemand den Fehler + bei sich. +*/ +export function codeGeprueft(person, code, ip) { + const roh = String(code ?? "").trim(); + if (!person?.id || roh.length < 4 || roh.length > 64) return "falsch"; + if (zuVieleVersuche(ip)) return "zu_viele"; + + const k = db() + .prepare("SELECT id, rolle, code_hash, code_salt, code_n FROM personen WHERE id = ? AND aktiv = 1") + .get(person.id); + if (!k) return "unbekannt"; + + let stimmt = false; + try { + stimmt = gleich(hashe(roh, k.code_salt, k.code_n), k.code_hash); + } catch (f) { + /* Dieselbe Lage wie in der Anmeldung: ein defekter Datensatz ist + ein Problem DIESER Person und keine falsche Eingabe. */ + console.error(`[workspace] Zugangsdaten von Person #${k.id} (${k.rolle}) sind defekt ` + + `-- sie kann keine Tuer mehr oeffnen. Grund: ${f?.message}`); + protokolliere("zugangsdaten_defekt", { + personId: k.id, rolle: k.rolle, ip, + detail: String(f?.message || "").slice(0, 80), + }); + return "unbekannt"; + } + + if (!stimmt) { + db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt()); + return "falsch"; + } + return "ok"; +} + /* ---------- Sitzung ---------------------------------------------------- */ export function sitzungLesen(req) { diff --git a/workspace/anleitung.html b/workspace/anleitung.html index 5187e34b..bfa30236 100644 --- a/workspace/anleitung.html +++ b/workspace/anleitung.html @@ -7,17 +7,17 @@ - - - - - + + + + + - - + + @@ -289,16 +289,16 @@ Und anleitung.js braucht es doppelt: Symbol, Farbe, Name und Adresse jeder Karte kommen von dort, nicht vom Server. --> - + - - - - - - + + + + + + diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 0ee4da73..d939995f 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +