Die Rechtetafel siebt die Karten jetzt wirklich -- und „ab Dabei" steht dran

ZWEI SACHEN, EINE DAVON EIN ECHTER BEFUND.

1) DIE KARTEN KAMEN AN DER RECHTETAFEL VORBEI
Der Bauplan sagt in Kapitel 04 und noch einmal in der Abnahme:
"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."

Ich habe dafuer eine Pruefung geschrieben, die die Seite ueber die
ECHTE Schnittstelle sperrt (nicht mit einem UPDATE in der Datenbank --
nur so laeuft tafelLaden() mit). Sie wurde sofort rot: Nach dem Sperren
von bereich.html fuer einen Modi lieferte der Server weiterhin ALLE
ELF Bereichs-Karten.

Im Browser fiel das nicht auf, und das ist der heikle Teil: Die Seite
verbindet jede Karte mit einer Kachel, und die Kachelliste ist
gefiltert -- die Karten verschwanden also auf dem Bildschirm. Die
TEXTE gingen trotzdem hinaus. Eine Oberflaeche, die etwas nicht
anzeigt, ist keine Schranke; genau deshalb steht im Bauplan
"serverseitig".

Jetzt siebt `darfKarte()` an der Quelle. Der Pfad wird aus dem
Schluessel GERECHNET (`bereich:highlight` -> /workspace/bereich.html),
nicht aus einer Zuordnungsliste -- das ist die Umkehrung von
`schluesselVonZiel`, und eine gepflegte Tabelle daneben waere die, die
beim naechsten neuen Brett fehlt. Entschieden wird mit `darfSeite`
selbst; hier wird nichts davon nachgebaut.

Gegenprobe in derselben Pruefung: zuruecksperren -> die Karten sind
wieder da (29 von 29). Und eine Zeile dazwischen, die zaehlt, dass NUR
sie verschwunden sind -- waere mit der Sperre die halbe Anleitung
weggefallen, waere die erste Zeile auch gruen gewesen.

2) „AB DABEI" UND „AB STAMM" AN DER KARTE (Kapitel 04)
"Kann ein Community-Mitglied etwas wegen seiner Stufe noch nicht,
steht das an der Karte."

Auch das abgeleitet: aus TREFF_SCHREIBEN, derselben Tafel, die das
Schreiben ENTSCHEIDET. `null` darin heisst "von aussen schreibt hier
niemand" -- das ist keine Stufe, die man erreicht, und bekommt
deshalb keine Marke. "ab Stamm" an einem Anschlagbrett waere ein
Versprechen, das nie eingeloest wird.

Als Wort, nicht als Schloss-Symbol: Ein Schloss sagt "du darfst
nicht" und laesst offen, ob jemals. "ab Dabei" sagt, dass es kommt --
und wann, steht mit Datum in der Karte "Deine Stufe" darueber. Ruhig
gestaltet, nicht rot: Es ist keine Sperre, sondern eine Auskunft ueber
die Zeit.

GEPRUEFT (pruef-anleitung 166 -> 173)
Die wichtigste der neuen Zeilen ist die, dass die Marke nach dem
Aufstieg VERSCHWINDET. 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 an drei Karten mit drei
verschiedenen Antworten (Chat: Dabei, Highlights: Stamm,
Anschlagbrett: gar nichts) -- waere die Rechnung grob falsch, traefe
sie alle drei gleich. Dazu die Gegenprobe, dass eine Teamrolle keine
einzige solche Marke bekommt.

Mitgelaufen: pruef-rechtetafel 19, pruef-rechte-umstellen 56,
pruef-css-klassen, pruef-tippziele 13 -- alle gruen. Die
Abnahmezahlen aus Kapitel 13 (14/29/29/34 mit ihrer Aufteilung) sind
nach dem neuen Sieb unveraendert; das ist der Beleg, dass es im
Normalfall nichts wegnimmt.

Im Browser nachgemessen (390 px, echte Anmeldung): Community sieht
vier Marken (Rudel-Chat, Wunschliste, Mitmachen „ab Dabei",
Highlights „ab Stamm"), die rechte Hand keine einzige.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-06 17:44:10 +02:00
co-authored by Claude Opus 5
parent 1f0ab7860a
commit a7b5775268
51 changed files with 905 additions and 690 deletions
+7 -1
View File
@@ -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"}`);
+104
View File
@@ -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`);
+78 -3
View File
@@ -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 })),