diff --git a/server/mess-anleitung-crew.mjs b/server/mess-anleitung-crew.mjs index e71a35a8..17df9dec 100644 --- a/server/mess-anleitung-crew.mjs +++ b/server/mess-anleitung-crew.mjs @@ -220,8 +220,13 @@ for (const [rolle, wie] of ROLLEN) { const klein = [...document.querySelectorAll("button, a")] .filter((e) => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.height < 44; }).length; + /* „ab Dabei" / „ab Stamm" -- der Bauplan will sie an der Karte + sehen, nicht nur im Datenstrom. */ + const marken = [...document.querySelectorAll(".an-karte__abstufe")] + .map((m) => `${m.closest(".an-karte")?.querySelector(".an-karte__name") + ?.textContent || "?"}: ${m.textContent}`); return { - karten, quer: breit, klein, + karten, quer: breit, klein, marken, vorweg: z("vorweg-block") && !z("vorweg-block").hidden, stufe: z("stufe-block") && !z("stufe-block").hidden, stufeText: z("stufe-jetzt")?.textContent || "", @@ -236,6 +241,7 @@ for (const [rolle, wie] of ROLLEN) { console.log(` Deine Stufe : ${lage.stufe} ${lage.stufeText}`); console.log(` Querscrollen : ${lage.quer ? "JA -- Befund" : "nein"}`); console.log(` Tippziele unter 44 : ${lage.klein}`); + console.log(` Stufen-Marken : ${lage.marken.join(" | ") || "keine"}`); console.log(` Browsermeldungen : ${klagen.length ? klagen.join(" | ") : "keine"}`); console.log(` Fehlgeschlagen : ${schlecht.length ? schlecht.slice(0, 6).join(" | ") : "nichts"}`); diff --git a/server/pruef-anleitung.mjs b/server/pruef-anleitung.mjs index ea7cc2ff..e40cf475 100644 --- a/server/pruef-anleitung.mjs +++ b/server/pruef-anleitung.mjs @@ -1186,6 +1186,110 @@ melde("\n=== Die Abnahme-Checkliste (Bauplan Kapitel 13) ==="); } } +/* ===================================================================== + „AB DABEI" UND „AB STAMM" AN DER KARTE (Bauplan Kapitel 04) + --------------------------------------------------------------------- + „Kann ein Community-Mitglied etwas wegen seiner Stufe noch nicht, + steht das an der Karte." + + DIE WICHTIGE ZEILE IST DIE DRITTE: Nach dem Aufstieg muss die Marke + VERSCHWINDEN. Eine Marke, die bleibt, ist schlimmer als keine -- + sie sagt jemandem, er duerfe etwas nicht, das er laengst darf, und + er probiert es gar nicht erst. + + GEMESSEN WIRD AN DREI KARTEN MIT DREI VERSCHIEDENEN ANTWORTEN, und + das ist Absicht: Der Rudel-Chat braucht „Dabei", die Highlights + brauchen „Stamm", und das Anschlagbrett braucht gar nichts (dort + schreibt von aussen NIEMAND, auf keiner Stufe). Waere die Rechnung + grob falsch, traefe sie alle drei gleich. */ +melde("\n=== „ab Dabei“ und „ab Stamm“ an der Karte ==="); +{ + const markeVon = (j, kachel) => + (j?.karten || []).find((k) => k.kachel === kachel)?.erst_ab ?? null; + const gastId = (() => { + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const z = d.prepare("SELECT id FROM personen WHERE rolle = 'gast'").get(); + d.close(); return z?.id || 0; + })(); + const beitritt = (tageHer) => { + const d = new DatabaseSync(process.env.WORKSPACE_DB); + d.prepare("UPDATE personen SET erstellt = ? WHERE id = ?") + .run(new Date(Date.now() - tageHer * 86400_000).toISOString(), gastId); + d.close(); + }; + + beitritt(0); + const neuJ = (await holen("gast", CREW_HOST)).j; + ok(markeVon(neuJ, "chat:treff") === "Dabei" + && markeVon(neuJ, "bereich:highlight") === "Stamm" + && markeVon(neuJ, "bereich:anschlag") === null, + `Stufe Neu: Chat „ab ${markeVon(neuJ, "chat:treff")}“, Highlights ` + + `„ab ${markeVon(neuJ, "bereich:highlight")}“, Anschlagbrett ` + + `${markeVon(neuJ, "bereich:anschlag") || "ohne Marke"}`); + + beitritt(10); + const dabeiJ = (await holen("gast", CREW_HOST)).j; + ok(markeVon(dabeiJ, "chat:treff") === null + && markeVon(dabeiJ, "bereich:highlight") === "Stamm", + `Stufe Dabei: die Chat-Marke ist weg (${markeVon(dabeiJ, "chat:treff") || "weg"}), ` + + `die der Highlights bleibt (${markeVon(dabeiJ, "bereich:highlight")})`); + + /* Eine Teamrolle hat keine Mitglieder-Stufe -- an ihren Karten darf + nie eine solche Marke stehen. Ohne diese Zeile waere die Rechnung + auch dann gruen, wenn sie die Stufe gar nicht beachtete. */ + const handJ = (await holen("hand", CREW_HOST)).j; + const beimTeam = (handJ?.karten || []).filter((k) => k.erst_ab).length; + ok(beimTeam === 0, + `eine Teamrolle bekommt keine einzige Stufen-Marke (${beimTeam})`); + beitritt(0); +} + +/* ===================================================================== + EINE SEITE SPERREN — UND DIE KARTE IST WEG (Bauplan Kapitel 13) + --------------------------------------------------------------------- + „In ‚Wer sieht was' eine Seite sperren: Die Karte verschwindet bei + dieser Rolle." + + DAS IST DER PUNKT, AN DEM DIE ANLEITUNG GEFAEHRLICH WERDEN KANN. + Eine Karte ist eine Einladung mit einem Knopf „Hingehen". Bleibt sie + stehen, nachdem die Seite gesperrt wurde, schickt die Einweisung + jemanden gegen eine Tuer — und erzaehlt ihm vorher noch, was dahinter + zu tun waere. Der Bauplan verlangt deshalb ausdruecklich, dass die + Karten aus DERSELBEN Tafel kommen wie die Tuer. + + GESPERRT WIRD UEBER DIE ECHTE SCHNITTSTELLE, nicht mit einem + UPDATE in der Datenbank: Nur so laeuft auch `tafelLaden()` mit, und + genau dort koennte die Kette reissen. Danach wird die Sperre wieder + aufgehoben und nachgemessen, dass die Karte zurueckkommt — sonst + wuesste man nicht, ob sie verschwunden ist oder nur nie da war. */ +melde("\n=== Seite sperren, Karte weg (Abnahme Kapitel 13) ==="); +{ + const PFAD = "/workspace/bereich.html"; + const stellen = (erlaubt) => roh("/workspace/api/rechte/feld", CREW_HOST, + { pfad: PFAD, rolle: "modi", erlaubt }, kekse.admin, "PUT"); + + const vorher = (await holen("modi", CREW_HOST)).j?.karten || []; + const bereicheVorher = vorher.filter((k) => k.kachel.startsWith("bereich:")).length; + ok(bereicheVorher > 0, + `vorher erklaert die Anleitung dem Modi ${bereicheVorher} Bereichs-Karten`); + + const zu = await stellen(false); + const gesperrt = (await holen("modi", CREW_HOST)).j?.karten || []; + const bereicheGesperrt = gesperrt.filter((k) => k.kachel.startsWith("bereich:")).length; + ok(zu.code === 200 && bereicheGesperrt === 0, + `nach dem Sperren sind sie weg (HTTP ${zu.code}, noch ${bereicheGesperrt})`); + /* UND NUR SIE. Waere mit der Sperre die halbe Anleitung + verschwunden, waere die Zeile darueber auch gruen. */ + ok(gesperrt.length === vorher.length - bereicheVorher, + `und nur sie (${vorher.length} - ${bereicheVorher} = ${gesperrt.length})`); + + const auf = await stellen(true); + const zurueck = (await holen("modi", CREW_HOST)).j?.karten || []; + ok(auf.code === 200 && zurueck.length === vorher.length, + `Gegenprobe: zurueckgestellt sind sie wieder da (${zurueck.length} von ` + + `${vorher.length})`); +} + /* ---------- Abschluss ------------------------------------------------ */ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); diff --git a/server/workspace-anleitung.js b/server/workspace-anleitung.js index 449767ba..8ecd5fce 100644 --- a/server/workspace-anleitung.js +++ b/server/workspace-anleitung.js @@ -34,12 +34,13 @@ import express from "express"; import { db, sitzungLesen, AUSSEN_ROLLEN, bereicheFuer } from "./workspace.js"; +import { darfSeite } from "./rechte.js"; import { STUFEN, PFLEGE_ROLLEN, fassungenFuer, } from "./anleitung-tabellen.js"; import { stufeVon, STUFEN_NAME, DABEI_TAGE, STAMM_TAGE, STAMM_BEITRAEGE, - TREFF_SCHREIBEN, + TREFF_SCHREIBEN, STUFEN as MITGLIED_STUFEN, } from "./workspace-treff.js"; export const anleitungRouter = express.Router(); @@ -96,6 +97,73 @@ function wessenStand(req) { return (req.sicht || req.person)?.id ?? 0; } +/** Darf diese Person die Seite ueberhaupt oeffnen, die die Karte + * erklaert? + * + * Bauplan Kapitel 04 und 13: „Leite die sichtbaren Karten aus + * derselben Rechte-Tafel ab, die rechte.html verwendet. Eine Rolle + * sieht nur Karten zu Seiten, die sie oeffnen darf; pruefe das + * SERVERSEITIG." + * + * GEMESSEN AM 06.10.2026, UND ES STIMMTE NICHT: Wer in „Wer sieht + * was" fuer einen Modi `bereich.html` sperrte, bekam vom Server + * weiterhin alle elf Bereichs-Karten geliefert. Im Browser fielen sie + * zwar weg (die Seite verbindet jede Karte mit einer Kachel, und die + * Kachelliste ist gefiltert) -- aber die TEXTE gingen trotzdem + * hinaus, und eine Oberflaeche ist keine Schranke. Genau deshalb + * steht im Bauplan „serverseitig". + * + * DER PFAD WIRD AUS DEM SCHLUESSEL GERECHNET, nicht aus einer + * Zuordnungsliste: Der Schluessel ist der Seitenname ohne `.html`, + * bei Unterseiten mit einem Doppelpunkt dahinter + * (`bereich:highlight`, `chat:treff`). Das ist die Umkehrung von + * `Bereiche.schluesselVonZiel`; eine gepflegte Tabelle daneben waere + * die, die beim naechsten neuen Brett fehlt. + * + * OHNE EINTRAG IN DER TAFEL HEISST NEIN -- das entscheidet + * `darfSeite` selbst, und zwar fuer das ganze Haus gleich. Hier wird + * nichts davon nachgebaut. */ +function darfKarte(person, schluessel) { + const name = String(schluessel || "").split(":")[0]; + if (!name) return false; + return darfSeite(person, `/workspace/${name}.html`); +} + +/** Kann diese Person das, was die Karte beschreibt, wegen ihrer + * Mitglieder-Stufe noch NICHT? + * + * Bauplan Kapitel 04: "Kann ein Community-Mitglied etwas wegen seiner + * Stufe noch nicht, steht das an der Karte („ab Dabei", „ab Stamm")." + * + * ABGELEITET AUS DER TAFEL, DIE ES ENTSCHEIDET. `TREFF_SCHREIBEN` + * sagt je Brett, welche Stufe zum Schreiben noetig ist -- dieselbe + * Tafel, die der Server beim Schreiben durchsetzt. Eine zweite Liste + * hier waere die, die eines Tages "ab Dabei" an eine Karte schreibt, + * bei der laengst jeder darf (oder umgekehrt, und das waere + * schlimmer: eine Karte ohne Marke, die beim Antippen abweist). + * + * `null` HAT ZWEI BEDEUTUNGEN IN DER TAFEL, und beide fuehren hier + * richtigerweise zu keiner Marke: Ein Brett ohne Eintrag kennt die + * Tafel nicht (dann geht es nicht ums Schreiben), und ein Brett mit + * `null` darf von aussen ueberhaupt niemand beschreiben -- das ist + * keine Stufe, die man erreicht, sondern eine dauerhafte Aufteilung. + * "ab Stamm" daranzuschreiben waere ein Versprechen, das nie + * eingeloest wird. + * + * DER SCHLUESSEL TRAEGT DAS BRETT SCHON IN SICH: `bereich:highlight` + * und `chat:treff` -- beide Formen entstehen in + * `Bereiche.schluesselVonZiel`, und beide tragen hinter dem + * Doppelpunkt genau den Namen, den die Tafel kennt. */ +function erstAbFuer(person, schluessel) { + const hat = stufeVon(person); + if (!hat) return null; // Teamrolle: keine Mitglieder-Stufe + const brett = String(schluessel || "").split(":")[1] || ""; + const noetig = TREFF_SCHREIBEN[brett]; + if (!noetig) return null; + if (MITGLIED_STUFEN.indexOf(hat) >= MITGLIED_STUFEN.indexOf(noetig)) return null; + return STUFEN_NAME[noetig] || noetig; +} + /** „Deine Stufe" — nur fuer die Community (Bauplan, Kapitel 04). * * WARUM DIE ZAHLEN NICHT HIER STEHEN: `DABEI_TAGE` und die beiden @@ -174,7 +242,12 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => { const karten = d.prepare( `SELECT id, kachel, stufe, wozu, tust, merke, reihe, geaendert FROM anl_karte WHERE haus = ? AND rolle = ? - ORDER BY reihe, id`).all(hausVon(req.person), rolle); + ORDER BY reihe, id`).all(hausVon(req.person), rolle) + /* DIE RECHTETAFEL SIEBT MIT -- siehe darfKarte(). Gefiltert wird + an der Quelle und nicht erst beim Senden: Darunter haengen der + Zaehler, die Stufen-Abschnitte und die Luecken-Anzeige der + Pflege, und die sollen alle dieselbe Menge meinen. */ + .filter((k) => darfKarte(person, k.kachel)); /* `erledigt` kommt aus derselben Abfrage und nicht aus einer zweiten: Zwei Abfragen koennten zwischen den Zeilen @@ -241,7 +314,9 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => { sehen, kann sie aber nicht setzen -- es waere sonst sein eigener Fortschritt an fremden Schritten. */ darf_abhaken: String(req.person?.rolle || "") === rolle, - karten: karten.map((k) => ({ ...k, neu: istNeu(k.geaendert) })), + karten: karten.map((k) => ({ + ...k, neu: istNeu(k.geaendert), erst_ab: erstAbFuer(person, k.kachel), + })), nicht: nachArt("nicht").map((z) => ({ id: z.id, titel: z.titel, text: z.text })), takt: nachArt("takt").map((z) => ({ id: z.id, titel: z.titel, text: z.text })), fragen: nachArt("frage").map((z) => ({ id: z.id, titel: z.titel, text: z.text })), diff --git a/workspace/anleitung.html b/workspace/anleitung.html index 2f3aa847..635e85ae 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. --> - + - - - - - - + + + + + +