diff --git a/server/anleitung-tabellen.js b/server/anleitung-tabellen.js index 36576883..73f7338d 100644 --- a/server/anleitung-tabellen.js +++ b/server/anleitung-tabellen.js @@ -221,6 +221,25 @@ CREATE TABLE IF NOT EXISTS anl_besuch ( gesehen_am TEXT NOT NULL ); +/* ====================================================================== + DER AUFSTIEG (Etappe 6 des Bauplans, Kapitel 10) + + "Erreicht ein Community-Mitglied die Stufe Dabei oder Stamm, zeige + auf der Startseite EINMAL die Karte 'Neu fuer dich'." + + JE PERSON UND STUFE, nicht je Person: Wer von Neu auf Dabei steigt, + bekommt die Karte, und spaeter beim Sprung auf Stamm noch einmal -- + mit anderem Inhalt. Eine einzelne Spalte "schon gesehen" an der + Person haette den zweiten Aufstieg verschluckt. + + KEINE SPALTE "stufe" AN DER PERSON, und das ist wichtig: Die Stufe + wird gerechnet (Beitrittstag, Tage, eigene Beitraege) und nirgends + gespeichert. Diese Tabelle merkt sich deshalb NICHT, welche Stufe + jemand hat, sondern nur, welche Karte er schon gesehen hat. Faellt + jemand rechnerisch zurueck (ein Beitrag wird geloescht), bekommt er + die Karte trotzdem nicht ein zweites Mal -- richtig so: Eine + Glueckwunschkarte, die man zweimal bekommt, ist keine. + ====================================================================== */ /* ====================================================================== DIE EINWEISUNG BEIM ERSTEN ANMELDEN (Etappe 6) @@ -236,6 +255,13 @@ CREATE TABLE IF NOT EXISTS anl_besuch ( Rollenwechsel jemand anfassen muesste, waere die Stelle, an der es vergessen wird. ====================================================================== */ +CREATE TABLE IF NOT EXISTS anl_stufe ( + person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, + stufe TEXT NOT NULL, + gezeigt_am TEXT NOT NULL, + PRIMARY KEY (person_id, stufe) +); + CREATE TABLE IF NOT EXISTS anl_einweisung ( person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, rolle TEXT NOT NULL, diff --git a/server/index.js b/server/index.js index 176f23c8..1f65f72d 100644 --- a/server/index.js +++ b/server/index.js @@ -18,7 +18,6 @@ import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js"; import { workspaceRouter, sichtSetzen } from "./workspace.js"; import { aufgabenRouter } from "./workspace-aufgaben.js"; import { zuteilungRouter } from "./workspace-zuteilung.js"; -import { willkommenRouter } from "./workspace-willkommen.js"; import { materialRouter } from "./workspace-material.js"; import { personenRouter } from "./workspace-personen.js"; import { profilRouter } from "./workspace-profil.js"; @@ -310,7 +309,6 @@ app.use(teamlageRouter); wuerde "/workspace/api/aufgaben/resuemee" als Aufgabe mit der Nummer "resuemee" behandeln. Die Reihenfolge ist hier keine Stilfrage. */ app.use(materialRouter); -app.use(willkommenRouter); app.use(zuteilungRouter); app.use(aufgabenRouter); app.use(personenRouter); diff --git a/server/mess-anleitung-crew.mjs b/server/mess-anleitung-crew.mjs index b7cec7da..e71a35a8 100644 --- a/server/mess-anleitung-crew.mjs +++ b/server/mess-anleitung-crew.mjs @@ -284,6 +284,90 @@ for (const [rolle, wie] of ROLLEN) { .replace(/^\/([A-Za-z]:)/, "$1"), fullPage: true }); await kontext.close(); } + /* ==== „NEU FUER DICH" BEIM AUFSTIEG (Bauplan Kapitel 10) ========== + + Gemessen wird der ganze Weg: Beitrittstag zurueckdatieren -> + Zentrale aufrufen -> steht die Karte da? -> auf einen ihrer Wege + tippen -> landet man auf der Anleitung BEI DER RICHTIGEN KARTE, + auch wenn sie in einem zugeklappten Abschnitt liegt? + + Der letzte Punkt ist der, den eine Schnittstellenpruefung nicht + beantworten kann: Ob ein Sprung ankommt, haengt daran, dass das +
vorher aufgeht. Sonst hat die Karte die Hoehe null, und + man landet irgendwo. */ + { + const d = new DatabaseSync(process.env.WORKSPACE_DB); + d.prepare("UPDATE personen SET erstellt = ? WHERE rolle = 'gast'") + .run(new Date(Date.now() - 10 * 86400_000).toISOString()); + d.close(); + + const an = await ruf("/workspace/api/anmelden", { + methode: "POST", + rumpf: JSON.stringify({ rolle: "gast", code: "CODE-GAST-0001", alter_ok: true }), + }); + const kontext = await browser.newContext({ + viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true, + }); + await kontext.addCookies(an.keks.split("; ").filter(Boolean).map((k) => { + const [x, ...r] = k.split("="); + return { name: x, value: r.join("="), domain: CREW, path: "/" }; + })); + const seite = await kontext.newPage(); + const klagen = []; + seite.on("pageerror", (f) => klagen.push(String(f?.message || f))); + await seite.goto(`https://${CREW}:${HP}/workspace/start.html`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(1600); + + const karte = await seite.evaluate(() => { + const k = document.querySelector(".aufstieg"); + const wege = [...document.querySelectorAll(".aufstieg__weg")]; + return { + da: !!k, + satz: (k?.querySelector(".aufstieg__satz")?.textContent || "").trim(), + wege: wege.map((a) => `${a.textContent.trim()} -> ${a.getAttribute("href")}`), + breite: k ? Math.round(k.getBoundingClientRect().width) : 0, + klein: wege.filter((a) => a.getBoundingClientRect().height < 44).length, + }; + }); + console.log(` +=== „Neu für dich" auf der Zentrale (Community, 10 Tage dabei) ===`); + console.log(` Karte da : ${karte.da}`); + console.log(` Satz : "${karte.satz}"`); + console.log(` Wege : ${karte.wege.join(" | ") || "keine"}`); + console.log(` Breite / zu klein : ${karte.breite} px / ${karte.klein}`); + + let gelandet = { pfad: "-", markiert: false, sichtbar: false, offen: false }; + if (karte.da && karte.wege.length) { + await seite.click(".aufstieg__weg"); + await seite.waitForTimeout(1800); + gelandet = await seite.evaluate(() => { + const k = document.querySelector('.an-karte[data-gesucht="ja"]'); + const r = k?.getBoundingClientRect(); + return { + pfad: location.pathname.split("/").pop() + location.search, + markiert: !!k, + /* „Sichtbar" heisst hier: im Fenster, nicht nur im Baum. */ + sichtbar: !!r && r.height > 0 && r.top < innerHeight && r.bottom > 0, + offen: !!k?.closest("details")?.open, + }; + }); + } + console.log(` Gelandet auf : ${gelandet.pfad}`); + console.log(` Karte markiert : ${gelandet.markiert}`); + console.log(` …aufgeklappt : ${gelandet.offen}`); + console.log(` …im Bild : ${gelandet.sichtbar}`); + + /* Kommt sie ein zweites Mal? Sie darf nicht. */ + await seite.goto(`https://${CREW}:${HP}/workspace/start.html`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(1600); + const nochmal = await seite.evaluate(() => !!document.querySelector(".aufstieg")); + console.log(` Beim zweiten Mal : ${nochmal ? "JA -- Befund" : "weg"}`); + console.log(` Browsermeldungen : ${klagen.length ? klagen.join(" | ") : "keine"}`); + await kontext.close(); + } + await browser.close(); vorbau.close(); } diff --git a/server/pruef-anleitung.mjs b/server/pruef-anleitung.mjs index 521348b7..ea7cc2ff 100644 --- a/server/pruef-anleitung.mjs +++ b/server/pruef-anleitung.mjs @@ -969,6 +969,223 @@ melde("\n=== Rundgang: drei Schritte / fuenf Schritte ==="); `kein Rollenname im Code von rundgang.js${gefunden.length ? " — " + gefunden.join(", ") : ""}`); } +/* ===================================================================== + WO DIE KACHEL STEHT (Bauplan Kapitel 03) + --------------------------------------------------------------------- + „Community: bleibt die erste, breite Kachel in der Gruppe + ‚Community'. Modi, Linke Hand, Rechte Hand: zusaetzlich als erste + Kachel in ‚Fuer dich', weil sie bei ihnen sonst weit unten steht." + + Gebaut ist das als VERSCHIEBEN statt Verdoppeln (der Grund steht in + workspace.js an anleitungNachVorne) -- deshalb prueft die letzte + Zeile jeder Rolle auch, dass es GENAU EINE gibt. Zwei gleich + benannte Wege zum selben Ort waeren hier der wahrscheinlichste + Rueckfall. + + Gemessen wird ueber /api/ich, also ueber genau das Feld, aus dem die + Startseite ihre Gruppen baut -- nicht ueber die Liste im Quelltext. + Und die Gruppennummer gehoert mit in die Bedingung: „steht in Fuer + dich" waere auch wahr, wenn „Fuer dich" eines Tages die letzte + Gruppe ist, und dann steht sie wieder weit unten. */ +melde("\n=== Die Kachel steht da, wo der Bauplan sie hinstellt ==="); +{ + const ichFuer = async (rolle, host) => { + const r = await roh("/workspace/api/ich", host, null, kekse[rolle]); + try { return JSON.parse(r.text); } catch { return null; } + }; + const lage = (liste) => { + const gruppen = []; + for (const k of liste) if (!gruppen.includes(k.gruppe)) gruppen.push(k.gruppe); + const treffer = liste.filter((k) => k.ziel === "anleitung.html"); + const k = treffer[0] || null; + const inGruppe = k ? liste.filter((x) => x.gruppe === k.gruppe) : []; + return { + anzahl: treffer.length, gruppe: k?.gruppe || null, gross: !!k?.gross, + nummer: k ? gruppen.indexOf(k.gruppe) + 1 : 0, gruppen: gruppen.length, + erste: inGruppe[0]?.ziel === "anleitung.html", + vorCommunity: k ? gruppen.indexOf(k.gruppe) < gruppen.indexOf("Community") : false, + }; + }; + + const jGast = await ichFuer("gast", CREW_HOST); + const g = Array.isArray(jGast?.bereiche) ? lage(jGast.bereiche) : null; + ok(g && g.anzahl === 1 && g.gruppe === "Community" && g.nummer === 1 + && g.erste && g.gross, + `Community: erste und breite Kachel der ersten Gruppe ` + + `(${g?.anzahl}x, ${g?.gruppe}, Gruppe ${g?.nummer}/${g?.gruppen}, ` + + `erste ${g?.erste}, breit ${g?.gross})`); + + for (const rolle of ["modi", "linke", "hand"]) { + const j = await ichFuer(rolle, CREW_HOST); + const l = Array.isArray(j?.bereiche) ? lage(j.bereiche) : null; + ok(l && l.anzahl === 1 && l.gruppe === "Für dich" && l.erste && l.vorCommunity, + `${rolle}: erste Kachel in „Für dich“, vor „Community“ ` + + `(${l?.anzahl}x, ${l?.gruppe}, Gruppe ${l?.nummer}/${l?.gruppen}, ` + + `erste ${l?.erste}, vor Community ${l?.vorCommunity})`); + } + + /* DAS ANDERE HAUS BLEIBT UNBERUEHRT -- und das ist hier mehr als + eine Hoeflichkeit: Die Umsortierung sitzt in `bereicheFuer`, also + in der Funktion, die BEIDE Haeuser benutzen. Fuer die Agentur + muss sie `null` liefern ("der Browser hat die Liste"); gaebe sie + dort eine Liste zurueck, bekaeme die Agenturseite ihre Kacheln + ploetzlich vom Server, und niemand haette es bemerkt, weil + dieselben Kacheln herauskaemen. */ + const jCre = await ichFuer("creator", null); + ok(jCre?.bereiche === null, + `die Agentur holt ihre Kacheln weiter aus dem Browser (bereiche ${ + jCre?.bereiche === null ? "null" : typeof jCre?.bereiche})`); +} + +/* ===================================================================== + „NEU FUER DICH" BEIM AUFSTIEG (Bauplan Kapitel 10, Etappe 6) + --------------------------------------------------------------------- + „Erreicht ein Community-Mitglied die Stufe Dabei oder Stamm, zeige + auf der Startseite EINMAL die Karte ‚Neu fuer dich' mit Sprung zur + passenden Karte." + + Die vier Zeilen in der Mitte sind der eigentliche Punkt: Der + Schluessel ist (Person, STUFE) und nicht (Person). Wer von Neu auf + Dabei steigt, bekommt die Karte, und Wochen spaeter beim Sprung auf + Stamm noch einmal -- mit anderem Inhalt. Eine einzelne Spalte „schon + gesehen" haette den zweiten Aufstieg verschluckt, und das waere + niemandem aufgefallen: Es fehlt ja nur etwas. + + DIE STUFE WIRD NICHT GESETZT, SIE WIRD GERECHNET. Deshalb wird hier + der Beitrittstag verschoben und es werden echte Beitraege angelegt + -- genau die zwei Dinge, aus denen `stufeVon` sie ableitet. Ein + „UPDATE personen SET stufe = 'stamm'" gaebe es gar nicht, und ein + Test, der eine Spalte setzt, die es nicht gibt, waere gruen ohne + etwas zu beweisen (siehe die Hausregel vom 01.09.2026). */ +melde("\n=== „Neu für dich“ beim Aufstieg ==="); +{ + const aufstieg = async (rolle, host = null) => { + const r = await roh("/workspace/api/anleitung/aufstieg", host, null, kekse[rolle]); + let j = null; try { j = JSON.parse(r.text); } catch { /* bleibt null */ } + return { code: r.code, j }; + }; + const merken = (rolle, host = null) => roh( + "/workspace/api/anleitung/aufstieg/gesehen", host, {}, kekse[rolle], "POST"); + const ziele = (j) => (j?.kacheln || []).map((k) => k.ziel).join(", "); + + 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; + })(); + braucht(!!gastId, "kein Mitglied in der Probe-Datenbank"); + + 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(); + }; + + /* ---- Stufe „Neu": nichts zu feiern ------------------------------- */ + const aNeu = await aufstieg("gast", CREW_HOST); + ok(aNeu.code === 200 && aNeu.j?.zeigen === false, + `frisch dabei: keine Karte (HTTP ${aNeu.code}, zeigen ${aNeu.j?.zeigen})`); + + /* ---- Stufe „Dabei" ------------------------------------------------ */ + beitritt(10); + const aDabei = await aufstieg("gast", CREW_HOST); + ok(aDabei.j?.zeigen === true && aDabei.j?.stufe === "dabei", + `nach ${10} Tagen kommt sie (zeigen ${aDabei.j?.zeigen}, Stufe ${aDabei.j?.stufe})`); + /* ABGELEITET AUS TREFF_SCHREIBEN, nicht aufgeschrieben: „Dabei" + schaltet Rudel-Chat, Wunschliste und Mitmachen frei. Steht hier + eines Tages etwas anderes, hat jemand die Tafel geaendert -- und + genau dann soll diese Zeile anschlagen. */ + const dabeiZiele = (aDabei.j?.kacheln || []).map((k) => k.ziel); + ok(dabeiZiele.length === 3 + && dabeiZiele.includes("chat.html?raum=treff") + && dabeiZiele.includes("bereich.html?b=wunsch") + && dabeiZiele.includes("bereich.html?b=mitmachen"), + `…mit den drei frisch freigeschalteten Wegen (${ziele(aDabei.j)})`); + + const m1 = await merken("gast", CREW_HOST); + const aWieder = await aufstieg("gast", CREW_HOST); + ok(m1.code === 200 && aWieder.j?.zeigen === false, + `gemerkt — sie kommt kein zweites Mal (HTTP ${m1.code}, zeigen ${aWieder.j?.zeigen})`); + + /* ---- Stufe „Stamm": dieselbe Person, neue Karte -------------------- */ + beitritt(40); + const aOhneBeitraege = await aufstieg("gast", CREW_HOST); + ok(aOhneBeitraege.j?.zeigen === false, + `40 Tage ohne eigene Beiträge bleiben „Dabei“ (zeigen ${aOhneBeitraege.j?.zeigen})`); + + { + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const satz = d.prepare( + `INSERT INTO eintraege (bereich, art, titel, datum, erstellt, erstellt_von) + VALUES ('highlight','clip',?,?,?,?)`); + const heute = new Date().toISOString(); + for (let i = 1; i <= 3; i++) satz.run(`Probe ${i}`, heute.slice(0, 10), heute, gastId); + d.close(); + } + const aStamm = await aufstieg("gast", CREW_HOST); + ok(aStamm.j?.zeigen === true && aStamm.j?.stufe === "stamm", + `der ZWEITE Aufstieg kommt eigenständig (zeigen ${aStamm.j?.zeigen}, ` + + `Stufe ${aStamm.j?.stufe})`); + ok((aStamm.j?.kacheln || []).length === 1 + && aStamm.j.kacheln[0].ziel === "bereich.html?b=highlight", + `…und zeigt auf die Highlights (${ziele(aStamm.j)})`); + + /* ---- Gegenprobe: eine Teamrolle hat gar keine Mitglieder-Stufe ---- */ + const aHand = await aufstieg("hand", CREW_HOST); + ok(aHand.code === 200 && aHand.j?.zeigen === false, + `eine Teamrolle bekommt nie eine (HTTP ${aHand.code}, zeigen ${aHand.j?.zeigen})`); +} + +/* ===================================================================== + DIE ABNAHME-CHECKLISTE DES BAUPLANS (Kapitel 13) + --------------------------------------------------------------------- + Woertlich: + Community: 14 Karten — 5 Muss, 4 Regelmaessig, 5 Bei Bedarf, + „Nicht deins" mit 5 Punkten. + Modi: 29 Karten — 7 / 7 / 15, „Nicht deins" 5. + Linke Hand: 29 Karten — 7 / 7 / 15, „Nicht deins" 5. + Rechte Hand: 34 Karten — 8 / 7 / 19, „Nicht deins" 4. + + DIE GESAMTZAHLEN STANDEN SCHON WEITER OBEN — die AUFTEILUNG nicht. + Das ist der Unterschied, auf den es ankommt: Verrutscht eine Kachel + von „Muss" nach „Bei Bedarf", bleibt die Summe gleich, und der + Fehler ist genau der, den die ganze Seite verhindern soll — jemand + bekommt beim ersten Anmelden das Falsche zuerst gezeigt. + + GEMESSEN WIRD AN DER SCHNITTSTELLE, NICHT AN DER TEXTDATEI. Die + Einordnung ist seit Etappe 4 in der Pflege aenderbar; die + Erstbefuellung ist nur der Anfangszustand. Wer die Textdatei fragt, + fragt eine Quelle, die nach der ersten Aenderung nicht mehr gilt. + + DIE STUFENSCHLUESSEL WERDEN NICHT ABGESCHRIEBEN, sondern aus + STUFEN_SCHLUESSEL genommen — kommt eine vierte Einordnung dazu, + faellt diese Pruefung auf, statt sie stillschweigend zu uebergehen. */ +melde("\n=== Die Abnahme-Checkliste (Bauplan Kapitel 13) ==="); +{ + const { STUFEN_SCHLUESSEL } = await import("./anleitung-tabellen.js"); + const SOLL = { + gast: { karten: 14, auf: [5, 4, 5], nicht: 5 }, + modi: { karten: 29, auf: [7, 7, 15], nicht: 5 }, + linke: { karten: 29, auf: [7, 7, 15], nicht: 5 }, + hand: { karten: 34, auf: [8, 7, 19], nicht: 4 }, + }; + ok(STUFEN_SCHLUESSEL.length === 3, + `drei Einordnungen, wie der Bauplan sie kennt (${STUFEN_SCHLUESSEL.join(", ")})`); + for (const [rolle, soll] of Object.entries(SOLL)) { + const j = (await holen(rolle, CREW_HOST)).j; + const karten = j?.karten || []; + const auf = STUFEN_SCHLUESSEL.map((st) => karten.filter((k) => k.stufe === st).length); + ok(karten.length === soll.karten + && auf.every((n, i) => n === soll.auf[i]) + && (j?.nicht || []).length === soll.nicht, + `${rolle}: ${soll.karten} Karten (${soll.auf.join("/")}) und ` + + `${soll.nicht}x „Nicht deins“ — bekommen: ${karten.length} ` + + `(${auf.join("/")}) und ${(j?.nicht || []).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/pruef-willkommen.mjs b/server/pruef-willkommen.mjs index 1f7e4d37..fc4867e7 100644 --- a/server/pruef-willkommen.mjs +++ b/server/pruef-willkommen.mjs @@ -1,39 +1,48 @@ /* ===================================================================== - pruef-willkommen.mjs — DIE SEITE, DIE ERKLAERT, WAS ES HIER GIBT + pruef-willkommen.mjs — DIE ALTE ADRESSE DARF NIEMANDEN VERLIEREN ===================================================================== - Auftrag vom 21.09.2026, Abschnitte 7 und 8. + WAS HIER FRUEHER STAND, UND WARUM ES WEG IST (06.10.2026) - WAS HIER GEMESSEN WIRD — und warum gerade das: + Diese Datei pruefte bis heute eine Seite, die es nicht mehr gibt: + `willkommen.html` erklaerte JEDE Kachel einer Rolle, alle gleich + ausfuehrlich. Genau das beschreibt der Bauplan „Willkommen & + Anleitung" in Kapitel 01 als das Problem — wer neu ist, bekam + sechsunddreissig gleichwertige Absaetze und wusste danach nicht, + womit er anfangen soll. An ihre Stelle ist `anleitung.html` + getreten: dieselben Kacheln, aber nach Muss, Regelmaessig und Bei + Bedarf sortiert, je Rolle. - (1) DASS DIE ERKLAERUNG ZU DEN RECHTEN PASST. Abschnitt 8 verlangt - woertlich, die Erklaerung solle sich "automatisch an die - tatsaechlich vorhandenen Rechte" anpassen. Der Fehler, den man - dabei macht, ist eine gepflegte Liste: Sie stimmt am ersten Tag - und erzaehlt am zweiten von einer Seite, die es fuer diesen - Menschen nie gab. Geprueft wird deshalb nicht "steht da etwas", - sondern: JEDE erklaerte Kachel muss diese Person auch oeffnen - duerfen — und zwar gemessen an derselben Rechtetafel, die der - Server durchsetzt. + DIE ALTE PRUEFUNG WAR DAMIT EINE, DIE EINE ZURUECKGENOMMENE REGEL + VERTEIDIGT — und sie hat es auch gemeldet: Beim ersten Lauf nach + dem Umbau wurde sie rot („der Name steht oben: leer", „0 Kacheln + werden erklaert") und lief danach in ihre Notbremse. Das war kein + Befund am Haus, sondern einer an ihr selbst. Eine Pruefung muss mit + der Aenderung mitgehen, sonst erzeugt sie Laerm, der die echten + Befunde zudeckt. - (2) DIE GEGENPROBE DAZU. Eine Pruefung, die nur "alles erlaubt" - bestaetigt, bestaetigt nichts. Also wird ausdruecklich gezaehlt, - dass ein Modi die Seiten, die er NICHT hat (Personen, Hilfe, - Bewerbungen), auch nicht erklaert bekommt — und dass DogFather - mehr erklaert bekommt als er. + MITGEGANGEN IST SIE SO: Die inhaltlichen Fragen („bekommt jede Rolle + ihre eigenen Texte?", „wird nur erklaert, was man oeffnen darf?") + beantwortet jetzt `pruef-anleitung.mjs` — an derselben Rechtetafel + und mit denselben Gegenproben. Hier bleibt die eine Frage, die dort + NICHT hingehoert und die sonst niemand stellt: - (3) DASS DIE ROLLEN WIRKLICH VERSCHIEDENE TEXTE BEKOMMEN. "Sie muss - vollstaendig rollenbasiert aufgebaut werden" ist nur erfuellt, - wenn sich die Texte auch unterscheiden. Vier gleiche Absaetze - mit vier verschiedenen Namen davor waeren keine Rollenseite. + Verliert die alte Adresse jemanden? - (4) DASS DIE AUSGELIEFERTE DATEI KEINE ROLLENNAMEN TRAEGT. Sie geht - an jeden Browser. Steht die Aufteilung dort, ist sie lesbar und - veraltet ausserdem. Dieselbe Regel wie bei pruef-modi-wortleck. + Es gibt Lesezeichen, Verweise in alten Nachrichten und + ausgedruckte Zettel. Der Bauplan verlangt in Kapitel 03 + ausdruecklich: „Adresse: bleibt willkommen.html. Alte Verweise + funktionieren weiter." - (5) UND AM ECHTEN BILDSCHIRM, dass ein Mensch es lesen und antippen - kann. Ein richtiger Datensatz, den niemand sieht, ist kein - Erfolg. + UND DIE ZWEITE FRAGE: Ist das Alte wirklich WEG — oder liegt nur + die Seite still und der Unterbau laeuft weiter? Ein Server, der + eine Schnittstelle anbietet, die niemand mehr aufruft, ist kein + harmloser Rest: Er ist die zweite Wahrheit, die beim naechsten + Rechteumbau vergessen wird. Deshalb wird hier nachgesehen, dass es + sie nicht mehr gibt — mit Gegenprobe, damit die Zeile nicht nur + deshalb gruen ist, weil sie gar nichts findet. + + AUFRUF: node server/pruef-willkommen.mjs ===================================================================== */ import { mkdtempSync, rmSync, readFileSync } from "node:fs"; @@ -45,7 +54,8 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-willk-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); import { notbremse } from "./helfer-notbremse.mjs"; -import { zertifikatBauen } from "./helfer-openssl.mjs"; +/* Kein HTTPS-Vorbau mehr: Hier wird keine Seite im Browser + geladen, sondern nur geantwortet. */ const { eigenerPort } = await import("./helfer-port.mjs"); const PORT = await eigenerPort(import.meta, "die Willkommenspruefung"); @@ -60,9 +70,12 @@ await import("./index.js"); notbremse(300_000, "pruef-willkommen"); await new Promise((r) => setTimeout(r, 1200)); -let fehler = 0; +let fehler = 0, geprueft = 0; const melde = (t) => console.log(t); -const ok = (b, t) => { melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +/* Die ANZAHL gehoert ans Ende, nicht nur das Ergebnis: Ein Rueckgang + ist genauso ein Befund wie ein Fehlschlag -- er heisst, dass still + etwas nicht mehr geprueft wird. */ +const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const HOST = "crew.dogfather-universe.com"; function rufe(pfad, wie = {}) { @@ -123,338 +136,82 @@ async function alsWer(rolle, code) { } /* --------------------------------------------------------------------- - 1. DIE SCHRANKE + 1. DIE ALTE ADRESSE FUEHRT WEITER --------------------------------------------------------------------- */ -melde("\n=== Die Schranke ==="); -{ - const ohne = await rufe("/workspace/api/willkommen"); - ok(ohne.code === 401, `unangemeldet gibt es nichts (${ohne.code})`); -} +melde("\n=== Die alte Adresse ==="); +const keksModi = await alsWer("modi", "CODE-MOD1-0001"); +const alt = await rufe("/workspace/willkommen.html", { keks: keksModi }); +ok(alt.code === 200, + `willkommen.html antwortet (${alt.code}) — und zwar mit einer Seite, ` + + "nicht mit einer Umleitung"); +/* DREI WEGE, UND ALLE DREI WERDEN GEBRAUCHT. Der Kopfbefehl wirkt auch + ohne JavaScript, `location.replace` wirkt sofort und OHNE Eintrag im + Verlauf (sonst landete man beim Zurueckgehen wieder hier), und der + sichtbare Verweis faengt den Fall ab, dass beides nicht greift. */ +ok(/http-equiv="refresh"[^>]*anleitung\.html/.test(alt.text), + "…mit dem Kopfbefehl, der auch ohne JavaScript weiterleitet"); +ok(/location\.replace\(/.test(alt.text), + "…mit location.replace — ohne Eintrag im Verlauf"); +ok(/]+href="anleitung\.html"/.test(alt.text), + "…und mit einem sichtbaren Verweis als letzter Rettung"); +/* Wer mit einem Zusatz in der Adresse kommt, soll dort landen, wohin + er wollte. Gemessen wird der Code, nicht die Absicht. */ +ok(/location\.search/.test(alt.text), + "…und der Zusatz in der Adresse wird mitgenommen"); + +/* Die Community kommt auf dieselbe Seite. Sie ist die Gruppe, bei der + die alte Kachel am laengsten im Gedaechtnis steht. */ +const keksGast = await alsWer("gast", "CODE-GAST-0001"); +const altGast = await rufe("/workspace/willkommen.html", { keks: keksGast }); +ok(altGast.code === 200 && /anleitung\.html/.test(altGast.text), + `auch fuer die Community (${altGast.code})`); + +/* GEGENPROBE ZUR SCHRANKE: Ohne Anmeldung gibt es sie nicht. Sonst + waeren die Zeilen oben auch gruen, wenn die Seite fuer jeden offen + stuende. */ +const altOhne = await rufe("/workspace/willkommen.html"); +ok(altOhne.code === 302 || altOhne.code === 401, + `ohne Anmeldung gibt es sie nicht (${altOhne.code})`); /* --------------------------------------------------------------------- - 2. JEDE ROLLE BEKOMMT IHRE EIGENE SEITE + 2. DAS ALTE IST WIRKLICH WEG --------------------------------------------------------------------- */ -melde("\n=== Jede Rolle bekommt ihre eigene Seite ==="); -const LEUTE = [ - { rolle: "admin", code: "CODE-DOGI-0001", name: "Filipe" }, - { rolle: "hand", code: "CODE-HAND-0001", name: "Funny" }, - { rolle: "linke", code: "CODE-LINK-0001", name: "Lino" }, - { rolle: "modi", code: "CODE-MOD1-0001", name: "Kessi" }, - { rolle: "gast", code: "CODE-GAST-0001", name: "Robin" }, -]; -const gesehen = {}; -for (const p of LEUTE) { - const keks = await alsWer(p.rolle, p.code); - const a = await rufe("/workspace/api/willkommen", { keks }); - gesehen[p.rolle] = { keks, daten: a.json }; - ok(a.code === 200, `${p.rolle}: die Seite antwortet (${a.code})`); - ok(a.json?.gruss === `Willkommen, ${p.name}!`, - ` und begruesst mit Namen ("${a.json?.gruss}")`); - ok(!!a.json?.einleitung?.was && !!a.json?.einleitung?.dazu, - " mit einer Einleitung, die aus zwei Teilen besteht"); -} +melde("\n=== Der abgeloeste Unterbau ==="); +const api = await rufe("/workspace/api/willkommen", { keks: keksModi }); +ok(api.code === 404, + `die alte Schnittstelle gibt es nicht mehr (${api.code})`); +/* GEGENPROBE: Eine Schnittstelle, die es GIBT, antwortet anders. + Ohne diese Zeile waere die obige auch dann gruen, wenn der ganze + Server nicht mehr liefe. */ +const lebt = await rufe("/workspace/api/anleitung", { keks: keksModi }); +ok(lebt.code === 200, + `Gegenprobe: die neue Schnittstelle antwortet sehr wohl (${lebt.code})`); -/* DIE EINLEITUNGEN MUESSEN SICH UNTERSCHEIDEN. Sonst ist es keine - rollenbasierte Seite, sondern eine mit Namensfeld. */ -{ - const saetze = LEUTE.map((p) => gesehen[p.rolle]?.daten?.einleitung?.was); - const verschieden = new Set(saetze).size; - ok(verschieden === LEUTE.length, - `${verschieden} von ${LEUTE.length} Einleitungen sind verschieden`); -} +const fehltDatei = (pfad) => { + try { readFileSync(new URL(pfad, import.meta.url)); return false; } + catch { return true; } +}; +ok(fehltDatei("./workspace-willkommen.js"), "server/workspace-willkommen.js ist fort"); +ok(fehltDatei("../workspace/assets/js/willkommen.js"), "assets/js/willkommen.js ist fort"); +ok(fehltDatei("../workspace/assets/css/willkommen.css"), "assets/css/willkommen.css ist fort"); +/* GEGENPROBE: Dieselbe Frage an eine Datei, die es gibt. Ohne sie + waeren die drei Zeilen darueber auch dann gruen, wenn `fehltDatei` + immer „ja" sagte — zum Beispiel, weil der Pfad falsch gebaut wird. */ +ok(!fehltDatei("./workspace-anleitung.js"), + "Gegenprobe: eine vorhandene Datei wird als vorhanden erkannt"); -/* --------------------------------------------------------------------- - 3. DER KERN: ERKLAERT WIRD NUR, WAS MAN AUCH DARF - --------------------------------------------------------------------- +/* Und niemand haengt den Router wieder ein. Kommentare werden vorher + entfernt — sonst wuerde dieser Hinweis hier selbst zum Treffer. */ +const indexRoh = readFileSync(new URL("./index.js", import.meta.url), "utf8"); +const indexCode = indexRoh + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/(^|[^:])\/\/[^\n]*/g, "$1 "); +ok(!/willkommenRouter/.test(indexCode), + "index.js haengt den alten Router nicht mehr ein"); +ok(/anleitungRouter/.test(indexCode), + "Gegenprobe: der neue Router steht sehr wohl darin"); - Gemessen gegen die Rechtetafel selbst, nicht gegen eine Liste in - dieser Datei -- sonst pruefe ich meine Abschrift gegen seine. */ -melde("\n=== Erklaert wird nur, was man auch oeffnen darf ==="); -const { darfSeite } = await import("./rechte.js"); -const { bereicheFuer, zusatzBereicheFuer } = await import("./workspace.js"); -const { istCrewAdresse } = await import("./crew-adresse.js"); - -/* DAS HAUS GEHOERT DAZU, und es wird ABGELEITET wie beim Server. - Gemessen am 21.09.: DogFather hat auf der Crew-Adresse 30 Kacheln - und auf der Agenturadresse 1 (die Tuer hinueber) -- er ist die - einzige Rolle, die in beiden Haeusern lebt. Eine Vergleichsrechnung - ohne Haus misst deshalb ausgerechnet bei ihm das falsche Haus und - meldet einen Fehler, den es nicht gibt. */ -const HAUS = istCrewAdresse(HOST) ? "crew" : "agentur"; -const alsPerson = (rolle) => ({ rolle, haus: HAUS }); - -for (const p of LEUTE) { - const daten = gesehen[p.rolle]?.daten; - const kacheln = (daten?.gruppen || []).flatMap((g) => g.kacheln || []); - ok(kacheln.length > 0, `${p.rolle}: es wird ueberhaupt etwas erklaert (${kacheln.length})`); - - const verboten = kacheln.filter((k) => { - const ziel = String(k.ziel || "").split("?")[0]; - if (!ziel || /^https?:/.test(ziel)) return false; - return !darfSeite(alsPerson(p.rolle), "/workspace/" + ziel); - }); - ok(verboten.length === 0, verboten.length === 0 - ? " keine einzige davon ist fuer ihn gesperrt" - : ` ERKLAERT GESPERRTE SEITEN: ${verboten.map((v) => v.ziel).join(", ")}`); - - /* Und die andere Richtung: die Zahl muss zu seinen echten Kacheln - passen. Zu WENIG waere genauso falsch -- dann verschweigt die - Seite etwas, das er hat. */ - const roh = [ - ...(bereicheFuer(alsPerson(p.rolle)) || []), - ...(zusatzBereicheFuer(alsPerson(p.rolle)) || []), - ]; - const sollte = roh.filter((k) => { - if (k?.aussen) return true; - const ziel = String(k?.ziel || ""); - if (!ziel) return false; - return darfSeite(alsPerson(p.rolle), "/workspace/" + ziel.split("?")[0]); - }).length; - ok(kacheln.length === sollte, - ` und es sind genau seine ${sollte} Kacheln (erklaert: ${kacheln.length})`); -} - -/* GEGENPROBE MIT ZAHLEN: Kann die Pruefung ueberhaupt Unterschiede - sehen? Wenn alle gleich viel bekaemen, waere oben nichts bewiesen. */ -{ - const zahl = (r) => (gesehen[r]?.daten?.gruppen || []).flatMap((g) => g.kacheln || []).length; - const chef = zahl("admin"), modi = zahl("modi"), gast = zahl("gast"); - ok(chef > modi && modi > gast, - `DogFather ${chef} > Modi ${modi} > Community ${gast} -- die Seite unterscheidet wirklich`); - - /* DIE DREI, DIE EIN MODI NICHT HAT. Namentlich, damit ein stiller - Durchrutscher auffaellt. */ - const modiZiele = (gesehen.modi?.daten?.gruppen || []) - .flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]); - for (const seite of ["personen.html", "bewerbungen.html", "rechte.html"]) { - ok(!modiZiele.includes(seite), `einem Modi wird "${seite}" nicht erklaert`); - } - /* Und der Gegenbeweis, dass diese Namen ueberhaupt vorkommen koennen: */ - const chefZiele = (gesehen.admin?.daten?.gruppen || []) - .flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]); - ok(chefZiele.includes("personen.html"), - "DogFather dagegen schon (sonst waere die Probe oben wertlos)"); -} - -/* --------------------------------------------------------------------- - 4. DIE LINKE HAND BEKOMMT IHRE GRENZEN GESAGT - --------------------------------------------------------------------- */ -melde("\n=== Niemand erfaehrt, was er NICHT hat ==="); -{ - /* UMGEDREHT AM 22.09.2026. Hier stand: "ihr wird gesagt, was sie - nicht hat -- einmal, klar, ohne Liste." - - Filipe: "keiner soll wissen was er nicht sieht das geht keinen was - an. sie sehen die sachen ja nicht weil es sie nichts angeht und das - muss auch nicht erwaehnt werden. kontrollier das ueberall bitte." - - Das "ueberall" ist der Grund, warum hier nicht eine Zeile - umgedreht, sondern JEDE ROLLE gemessen wird. Ein Satz, der - aufzaehlt, was fehlt, hebt ausserdem die Sorgfalt wieder auf, mit - der der Server ueberall 404 statt 403 antwortet -- damit ein "das - darfst du nicht" gar nicht erst verraet, dass es etwas gibt. */ - const VERRAETERISCH = [ - /nicht f(u|ue|\u00fc)r dich freigeschaltet/i, - /was du nicht siehst/i, - /was du nicht hast/i, - /hast du nicht/i, - /siehst du nicht/i, - /darfst du nicht/i, - /bleiben seine/i, - /kein zugriff/i, - ]; - const gefunden = []; - for (const pp of LEUTE) { - const d = gesehen[pp.rolle]?.daten; - const text = [ - d?.einleitung?.was, d?.einleitung?.dazu, - ...(d?.hinweise || []).flatMap((h) => [h.titel, h.text]), - ].filter(Boolean).join(" "); - for (const m of VERRAETERISCH) { - if (m.test(text)) gefunden.push(pp.rolle + ": " + m.source); - } - } - ok(gefunden.length === 0, gefunden.length === 0 - ? `keine der ${LEUTE.length} Rollen bekommt gesagt, was ihr fehlt` - : `ZAEHLT AUF, WAS FEHLT: ${gefunden.join(" | ")}`); - - /* GEGENPROBE: Wuerde dieses Muster so einen Satz ueberhaupt finden? - Ohne sie waere die Zeile oben gruen, sobald die Liste kaputt ist. */ - ok(VERRAETERISCH.some((m) => m.test("Das ist nicht fuer dich freigeschaltet.")), - "und ein solcher Satz wuerde hier wirklich auffallen"); - const ziele = (gesehen.linke?.daten?.gruppen || []) - .flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]); - ok(!ziele.includes("personen.html") && !ziele.includes("hilfe.html"), - "und Personen/Meldeweg stehen nicht in ihrer Erklaerung"); - ok(ziele.includes("aufgaben.html"), - "die Aufgaben dagegen schon"); -} - -/* --------------------------------------------------------------------- - 5. DIE AUSGELIEFERTE DATEI TRAEGT KEINE ROLLENAUFTEILUNG - --------------------------------------------------------------------- */ -melde("\n=== Was im Browser landet ==="); -{ - const js = readFileSync(new URL("../workspace/assets/js/willkommen.js", import.meta.url), "utf8"); - const html = readFileSync(new URL("../workspace/willkommen.html", import.meta.url), "utf8"); - const ohneKommentar = js - .replace(/\/\*[\s\S]*?\*\//g, "") - .replace(/(^|[^:])\/\/.*$/gm, "$1"); - - for (const wort of ["admin", "linke", "gast"]) { - const treffer = new RegExp("[\"'`]" + wort + "[\"'`]").test(ohneKommentar); - ok(!treffer, `die Datei kennt "${wort}" nicht (sie kommt vom Server)`); - } - ok(!/aufgaben\.html|personen\.html/.test(ohneKommentar), - "und fuehrt auch keine eigene Kachelliste"); - ok(html.includes("willkommen.css"), "die Seite laedt ihre Gestaltung"); - ok(html.includes("meldung.js"), "und den gemeinsamen Meldeweg"); - - /* GEGENPROBE: Findet dieses Muster ueberhaupt etwas? Sonst waere die - Aussage oben nur die Bestaetigung eines kaputten Suchmusters. */ - const probe = 'const x = "admin";'; - ok(new RegExp("[\"'`]admin[\"'`]").test(probe), - "das Suchmuster wuerde einen Rollennamen finden"); -} - -/* --------------------------------------------------------------------- - 6. DIE KACHEL FUEHRT HIN - --------------------------------------------------------------------- */ -melde("\n=== Der Weg zur Seite ==="); -{ - const kacheln = [ - ...(bereicheFuer(alsPerson("gast")) || []), - ...(zusatzBereicheFuer(alsPerson("gast")) || []), - ]; - const willk = kacheln.filter((k) => String(k.ziel || "").startsWith("willkommen.html")); - ok(willk.length === 1, `die Community hat genau eine Kachel dorthin (${willk.length})`); - ok(willk[0]?.name === "Willkommen", `und sie heisst so (${willk[0]?.name})`); - - /* DAS TREFF-BRETT DARF NICHT STILL VERSCHWINDEN. Gemessen am - 21.09.: null Eintraege, 29 Chatnachrichten daneben -- geredet wird - im Chat. Falls das Brett je wieder eine Kachel bekommt, soll diese - Zeile daran erinnern, dass hier einmal eine war. */ - const brett = kacheln.filter((k) => String(k.ziel || "").includes("b=treff")); - ok(brett.length === 0, - `das leere Rudel-Brett hat keine eigene Kachel mehr (${brett.length})`); - const chat = kacheln.filter((k) => String(k.ziel || "").includes("chat.html")); - ok(chat.length >= 1, `der Rudel-Chat dagegen schon (${chat.length})`); -} - -/* --------------------------------------------------------------------- - 7. AM ECHTEN BILDSCHIRM - --------------------------------------------------------------------- */ -melde("\n=== Am echten Bildschirm ==="); -{ - const { execFileSync } = await import("node:child_process"); - const { createServer: httpsServer } = await import("node:https"); - const { request: httpAnfrage } = await import("node:http"); - - const schluessel = join(ordner, "pruef.key"); - const zertifikat = join(ordner, "pruef.crt"); - zertifikatBauen(schluessel, zertifikat, HOST); - - const HTTPS_PORT = PORT + 1; - const vorbau = httpsServer({ - key: readFileSync(schluessel), cert: readFileSync(zertifikat), - }, (von, nach) => { - const weiter = httpAnfrage({ - host: "127.0.0.1", port: PORT, path: von.url, method: von.method, - headers: { ...von.headers, host: `${HOST}:${HTTPS_PORT}` }, - }, (antwort) => { - nach.writeHead(antwort.statusCode || 502, antwort.headers); - antwort.pipe(nach); - }); - von.pipe(weiter); - }); - await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r)); - const SEITE = `https://${HOST}:${HTTPS_PORT}`; - - const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); - const browser = await pw.chromium.launch({ - args: [`--host-resolver-rules=MAP ${HOST} 127.0.0.1`, "--ignore-certificate-errors"], - }); - const seitenFehler = []; - - async function alsSeite(rolle, code, breite = 1280) { - const ctx = await browser.newContext({ viewport: { width: breite, height: 950 }, hasTouch: breite <= 860 }); - const s = await ctx.newPage(); - s.on("pageerror", (e) => seitenFehler.push(`${rolle}: ${String(e).slice(0, 110)}`)); - await s.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" }); - await s.click(`.rolle[data-rolle="${rolle}"]`); - await s.fill("#code", code); - await s.click("button[type=submit]"); - await s.waitForTimeout(800); - await s.goto(`${SEITE}/workspace/willkommen.html`, { waitUntil: "networkidle" }); - await s.waitForTimeout(900); - return { ctx, seite: s }; - } - - /* ---- Ein Modi, am Handy ---- */ - const pModi = await alsSeite("modi", "CODE-MOD1-0001", 390); - try { - const bild = await pModi.seite.evaluate(() => { - const kacheln = [...document.querySelectorAll(".w-kachel")]; - const breite = document.documentElement.scrollWidth; - return { - gruss: document.getElementById("gruss")?.textContent || "", - rolle: document.getElementById("rollentext")?.textContent || "", - was: document.getElementById("einleitung-was")?.textContent || "", - kacheln: kacheln.length, - mitLang: kacheln.filter((k) => k.querySelector(".w-kachel__lang")).length, - laedtWeg: document.getElementById("laedt")?.hidden === true, - busy: document.getElementById("haupt")?.getAttribute("aria-busy"), - breite, - ueberlauf: kacheln.filter((k) => k.getBoundingClientRect().right > breite + 1).length, - zuKlein: kacheln.filter((k) => k.getBoundingClientRect().height < 44).length, - ersteZiel: kacheln[0]?.getAttribute("href") || "", - }; - }); - ok(bild.gruss.includes("Kessi"), `der Name steht oben ("${bild.gruss}")`); - ok(bild.was.length > 20, "darunter ein Satz, der etwas sagt"); - ok(bild.kacheln > 0, `${bild.kacheln} Kacheln werden erklaert`); - ok(bild.mitLang > 0, `davon ${bild.mitLang} mit ausfuehrlichem Satz`); - ok(bild.laedtWeg, "der Ladehinweis ist weg"); - ok(bild.busy !== "true", "und die Seite meldet sich nicht mehr als ladend"); - ok(bild.breite <= 390, `nichts sprengt das Handy (${bild.breite}px)`); - ok(bild.ueberlauf === 0, `keine Kachel ragt hinaus (${bild.ueberlauf})`); - ok(bild.zuKlein === 0, `jede ist mindestens 44px hoch (${bild.zuKlein} zu klein)`); - - /* SIE MUSS AUCH HINFUEHREN. Eine Erklaerung ohne Weg ist ein - Prospekt. */ - await pModi.seite.click(".w-kachel"); - await pModi.seite.waitForTimeout(700); - const wohin = pModi.seite.url(); - ok(!wohin.includes("willkommen.html"), - `ein Antippen fuehrt wirklich hin (${wohin.split("/").pop()})`); - } finally { await pModi.ctx.close(); } - - /* ---- DogFather, am Rechner ---- */ - const pChef = await alsSeite("admin", "CODE-DOGI-0001"); - try { - const bild = await pChef.seite.evaluate(() => ({ - gruss: document.getElementById("gruss")?.textContent || "", - kacheln: document.querySelectorAll(".w-kachel").length, - gruppen: [...document.querySelectorAll(".w-gruppe__titel")].map((t) => t.textContent), - hinweise: document.querySelectorAll(".w-hinweis").length, - })); - ok(bild.gruss.includes("Filipe"), `sein Name steht oben ("${bild.gruss}")`); - ok(bild.gruppen.length >= 2, - `nach Gruppen geordnet (${bild.gruppen.join(" · ")})`); - ok(bild.hinweise > 0, `mit ${bild.hinweise} Hinweisen zu seiner Rolle`); - ok(bild.kacheln > 10, `und allem, was er hat (${bild.kacheln})`); - } finally { await pChef.ctx.close(); } - - ok(seitenFehler.length === 0, seitenFehler.length === 0 - ? "kein Skript ist unterwegs gestolpert" - : `Skriptfehler: ${seitenFehler.join(" | ")}`); - - await browser.close(); - await new Promise((r) => vorbau.close(r)); -} - -melde(fehler === 0 - ? "\nAlles in Ordnung.\n" - : `\n${fehler} FEHLER.\n`); +/* ---------- Abschluss ------------------------------------------------ */ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +melde(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); process.exit(fehler ? 1 : 0); diff --git a/server/workspace-anleitung.js b/server/workspace-anleitung.js index 5743e9d3..449767ba 100644 --- a/server/workspace-anleitung.js +++ b/server/workspace-anleitung.js @@ -33,12 +33,13 @@ ===================================================================== */ import express from "express"; -import { db, sitzungLesen, AUSSEN_ROLLEN } from "./workspace.js"; +import { db, sitzungLesen, AUSSEN_ROLLEN, bereicheFuer } from "./workspace.js"; import { STUFEN, PFLEGE_ROLLEN, fassungenFuer, } from "./anleitung-tabellen.js"; import { stufeVon, STUFEN_NAME, DABEI_TAGE, STAMM_TAGE, STAMM_BEITRAEGE, + TREFF_SCHREIBEN, } from "./workspace-treff.js"; export const anleitungRouter = express.Router(); @@ -630,6 +631,101 @@ anleitungRouter.get("/workspace/api/anleitung/einweisung", (req, res) => { } }); +/* ===================================================================== + „NEU FUER DICH" — DIE KARTE BEIM AUFSTIEG (Bauplan Kapitel 10) + ===================================================================== + + „Erreicht jemand ‚Dabei' (ab 7 Tagen) oder ‚Stamm' (ab 30 Tagen und + 3 eigenen Beitraegen), zeigt die Startseite einmal die Karte ‚Neu + fuer dich: Du kannst jetzt …' mit Sprung zur passenden Karte." + + WAS JETZT GEHT, WIRD NICHT AUFGESCHRIEBEN, SONDERN ABGELEITET — + aus derselben Tafel, die es auch ENTSCHEIDET (`TREFF_SCHREIBEN`: + Brett → noetige Stufe). Haette ich hier eine Liste gepflegt („ab + Dabei darfst du schreiben, Wuensche einstellen, dich melden"), waere + sie beim naechsten neuen Brett still falsch — und zwar in der + Richtung, die weh tut: Jemand bekommt eine Glueckwunschkarte, die + ihm etwas verspricht, das er gar nicht darf, oder sie verschweigt + ihm, was er jetzt duerfte. + + DIE KACHELN KOMMEN AUS SEINER EIGENEN LISTE, nicht aus einer + zweiten. `bereicheFuer` hat die Rechtetafel schon angewandt; eine + Kachel, die diese Person nicht sehen darf, kann hier gar nicht erst + auftauchen. Gefunden werden sie ueber das Ziel (`b=` und `raum=`), + weil Namen in diesem Haus wandern und Ziele nicht. + + DREI EXIT-WEGE, und der dritte ist wichtig: Gibt es zu einer frisch + freigeschalteten Taetigkeit KEINE Kachel, erscheint die Karte + trotzdem — mit dem Satz, aber ohne Sprung. Die Karte wegzulassen, + weil ein Verweis fehlt, hiesse den Aufstieg zu verschweigen. */ +const AUFSTIEGS_STUFEN = new Set(["dabei", "stamm"]); + +function neuFuerDich(person) { + const jetzt = stufeVon(person); + if (!jetzt || !AUFSTIEGS_STUFEN.has(jetzt)) return null; + const schon = db().prepare( + "SELECT 1 AS da FROM anl_stufe WHERE person_id = ? AND stufe = ?") + .get(person.id, jetzt); + if (schon) return null; + + /* Welche Bretter wurden GERADE freigeschaltet? Die, deren noetige + Stufe genau diese ist — nicht die, die schon vorher gingen. */ + const frisch = Object.entries(TREFF_SCHREIBEN) + .filter(([, noetig]) => noetig === jetzt).map(([brett]) => brett); + + const liste = bereicheFuer(person); + const kacheln = []; + for (const brett of frisch) { + const k = (Array.isArray(liste) ? liste : []).find((x) => { + const z = String(x?.ziel || ""); + return z.includes(`b=${brett}`) || z.includes(`raum=${brett}`); + }); + if (k) kacheln.push({ name: k.name, ziel: k.ziel, zeichen: k.zeichen, ton: k.ton }); + } + + return { + zeigen: true, + stufe: jetzt, + name: STUFEN_NAME[jetzt] || jetzt, + kacheln, + }; +} + +anleitungRouter.get("/workspace/api/anleitung/aufstieg", (req, res) => { + try { + /* `req.person` und nicht `req.sicht`: Der Aufstieg gehoert der + Person, die angemeldet ist. Wer die Sicht eines Mitglieds + einnimmt, soll dessen Glueckwunsch nicht wegklicken. */ + res.json(neuFuerDich(req.person) || { zeigen: false }); + } catch (fehler) { + console.error("[workspace] Aufstieg:", fehler?.message); + /* Kein 500: Eine kaputte Glueckwunschkarte darf die Zentrale + nicht mit einer roten Zeile begruessen. */ + res.json({ zeigen: false }); + } +}); + +/** Die Karte wurde gezeigt — gemerkt beim ZEIGEN, nicht beim + * Wegklicken. Dieselbe Entscheidung wie bei der Begruessung, und aus + * demselben gemessenen Grund: Wer die Seite schliesst, ohne etwas + * anzuklicken, hat sie trotzdem gesehen. Sonst kommt sie wieder. Und + * beim uebernaechsten Mal auch. */ +anleitungRouter.post("/workspace/api/anleitung/aufstieg/gesehen", (req, res) => { + try { + const person = req.person; + const jetzt = stufeVon(person); + if (!jetzt || !AUFSTIEGS_STUFEN.has(jetzt)) return res.json({ ok: true }); + db().prepare( + `INSERT INTO anl_stufe (person_id, stufe, gezeigt_am) VALUES (?,?,?) + ON CONFLICT(person_id, stufe) DO NOTHING`) + .run(person.id, jetzt, new Date().toISOString()); + res.json({ ok: true }); + } catch (fehler) { + console.error("[workspace] Aufstieg merken:", fehler?.message); + res.status(500).json({ fehler: "serverfehler" }); + } +}); + /** Gezeigt -- egal ob durchgeklickt oder "Spaeter". * * BEIDES ZAEHLT, und das ist Absicht. Der Bauplan sagt: "Spaeter ist diff --git a/server/workspace-willkommen.js b/server/workspace-willkommen.js deleted file mode 100644 index 0418017f..00000000 --- a/server/workspace-willkommen.js +++ /dev/null @@ -1,293 +0,0 @@ -/* ===================================================================== - WILLKOMMEN — die Seite, die erklärt, was es hier gibt - ===================================================================== - - Auftrag vom 21.09.2026, Abschnitte 7 und 8: - - *"Beim Öffnen soll die Person persönlich begrüßt werden ... - Willkommen, [Name]! – danach folgt eine verständliche Einführung - in die Website ... Die Willkommensseite darf nicht für alle Nutzer - identisch sein. Sie muss vollständig rollenbasiert und individuell - aufgebaut werden ... Die Erklärung soll sich automatisch an die - tatsächlich vorhandenen Rechte des jeweiligen Nutzers anpassen."* - - DAS LETZTE IST DER GANZE BAU. Eine Erklärung, die aufzählt, was es - gibt, ist am Tag danach falsch — jemand baut eine Kachel dazu, und - die Seite verschweigt sie. Deshalb erklärt sie GENAU DIE KACHELN, - die diese Person wirklich hat: dieselbe Liste, aus der die - Startseite gebaut wird (`bereicheFuer`), durch denselben Filter - (`darfSeite`). Wer morgen eine Kachel bekommt, bekommt automatisch - auch ihre Erklärung. - - DREI EBENEN, und jede hat einen Grund: - - Der Untertitel steht schon an der Kachel. Er ist die kürzeste - wahre Auskunft und wird deshalb nicht neu - erfunden. - Der lange Satz nur bei den Seiten, wo eine Zeile zu wenig ist. - NACH ZIEL abgelegt, nicht nach Name: Namen - ändern sich (am 21.09. gleich zweimal), das - Ziel bleibt. - Die Einleitung je Rolle ein eigener Absatz. Das ist die eine - Stelle, an der wirklich nach Rolle unterschieden - wird — und es sind vier Sätze, keine vier - Seitenlisten. - - WAS NICHT ERKLÄRT WIRD, STEHT AUCH NICHT DA. Der Auftrag lässt die - Wahl zwischen "gar nicht erklären" und "deutlich als nicht relevant - behandeln". Gar nicht ist richtig: Eine Liste von Dingen, die man - nicht darf, ist keine Orientierung, sondern eine Kränkung. - ===================================================================== */ - -import express from "express"; -import { - sitzungLesen, bereicheFuer, zusatzBereicheFuer, rollentextFuer, - ROLLEN_NAME, istDogFather, darfAufgabenVerteilen, -} from "./workspace.js"; -import { darfSeite } from "./rechte.js"; - -export const willkommenRouter = express.Router(); - -/* --------------------------------------------------------------------- - DIE EINLEITUNG JE ROLLE - --------------------------------------------------------------------- - - Sie sagt in zwei, drei Sätzen, WOZU jemand hier ist. Die Kacheln - darunter sagen, WOMIT. Beides zu mischen ergäbe einen Text, den - niemand liest. - - Der Ton ist bewusst derselbe wie im ganzen Haus: auf Augenhöhe, - ohne jemanden über jemanden zu stellen. */ -const EINLEITUNG = { - admin: { - was: "Das hier ist deine Seite — von hier aus läuft alles.", - dazu: "Du siehst jede Kachel, die es gibt, und entscheidest am Ende, " - + "was passiert. Wenn dir etwas Vorgeschlagenes nicht passt, lehnst " - + "du es ab; nichts geschieht ohne dich.", - }, - hand: { - was: "Du vertrittst DogFather im Alltag und hältst das Team zusammen.", - dazu: "Du siehst dieselben Aufgaben, Ideen und Auswertungen wie er, " - + "verteilst Aufgaben an die Modis und legst neue Leute an — samt " - + "ihrem Zugangscode.", - }, - linke: { - was: "Du führst das Team durch den Tag — mit denselben Werkzeugen wie " - + "die rechte Hand.", - /* NUR, WAS DA IST (22.09.2026). - ---------------------------------------------------------------- - Hier stand: "Was du NICHT hast: Personen anlegen, Rollen ändern - und den vertraulichen Meldeweg." - - Filipe: "keiner soll wissen was er nicht sieht das geht keinen - was an. sie sehen die sachen ja nicht weil es sie nichts angeht - und das muss auch nicht erwaehnt werden." - - Er hat recht, und es passt zur Hausregel, die ueberall sonst - schon gilt: Der Server antwortet mit 404 statt 403, damit ein - "das darfst du nicht" nicht verraet, dass es etwas gibt. Ein - Satz, der dasselbe aufzaehlt, hebt diese Sorgfalt wieder auf. */ - dazu: "Aufgaben verteilen, Stände verfolgen, Rückmeldung geben — und " - + "dabei sehen, wie es bei jedem gerade läuft. Alles dafür findest " - + "du unten.", - }, - modi: { - was: "Schön, dass du da bist. Hier findest du alles für deinen Tag.", - dazu: "Unter „Aufgaben“ steht, was gerade für dich dran ist — du " - + "nimmst an, was du schaffst, und sagst ab, was nicht geht. Eine " - + "Absage ist völlig in Ordnung, schreib einfach kurz warum.", - }, - gast: { - was: "Schön, dass du da bist.", - dazu: "Hier triffst du die anderen aus der Community. Schau dich um, " - + "schreib etwas, oder lies erst mal mit — beides ist richtig.", - }, -}; - -/* --------------------------------------------------------------------- - DIE LANGEN SÄTZE — nach ZIEL, nicht nach Name - --------------------------------------------------------------------- - - Nur dort, wo eine Zeile zu wenig ist. Wer zu jeder Kachel einen - Absatz schreibt, schreibt eine Seite, die niemand zu Ende liest. - - Der Schlüssel ist der Dateiname ohne Parameter. Am 21.09. sind in - diesem Haus zwei Kachelnamen gewandert; ein Text, der am Namen - hängt, wäre dabei stumm geworden, ohne dass es jemand merkt. */ -const LANG = { - "aufgaben.html": - "Oben steht, wie es bei dir gerade aussieht — was offen ist, was läuft, " - + "was erledigt ist. Darunter die Karten. Bekommst du eine Aufgabe, " - + "kannst du sie annehmen oder ablehnen; wenn du ablehnst, schreib " - + "kurz warum, dann muss niemand nachfragen.", - "chat.html": - "Einzelgespräche und Gruppen. Mit @ und dem Namen sprichst du jemanden " - + "direkt an — er bekommt dann eine eigene Meldung, auch wenn er den " - + "Chat gerade nicht offen hat.", - "kalender.html": - "Alles mit Datum an einer Stelle. Termine über mehrere Tage sind " - + "durchgehend markiert, am Handy tippst du einen Tag an und siehst, " - + "was dort ansteht.", - "bereich.html": - "Ein Brett zum Sammeln. Schreib hinein, was dir einfällt — es geht " - + "nichts verloren, und niemand muss es sofort beantworten.", - "befinden.html": - "Ein paar Fragen über dich, nur für dich. Niemand aus dem Team sieht " - + "deine Antworten — am Ende bekommst du eine kurze Zusammenfassung.", - "steckbrief.html": - "Deine Seite: Foto, ein paar Worte über dich. Du entscheidest, was " - + "darauf steht.", - "hilfe.html": - "Wenn etwas vorgefallen ist, das niemand mitlesen soll. Nur DogFather " - + "und die rechte Hand sehen diese Meldungen.", - "notizen.html": - "Dein Block. Schreib hinein, was dir einfällt — es speichert sich von " - + "selbst, und niemand außer dir sieht es. Auch DogFather nicht.", - "support.html": - "Etwas an der Seite funktioniert nicht oder sieht falsch aus? Schreib " - + "einen Satz dazu und häng ein Bildschirmfoto an — DogFather und die " - + "rechte Hand bekommen es sofort und melden sich, wenn es erledigt ist.", - "treff-regeln.html": - "Wie wir hier miteinander umgehen. Kurz, und es lohnt sich einmal.", - "unsere-seiten.html": - "Ob gerade gestreamt wird, wann es losgeht, und wo du die Kanäle " - + "findest.", - "personen.html": - "Zugänge anlegen, sperren und Rollen ändern. Der Code wird genau " - + "einmal angezeigt — danach nie wieder.", - "teamlage.html": - "Wer gerade wie unterwegs ist. Gedacht zum Hinsehen, nicht zum " - + "Vergleichen.", - "werdegang.html": - "Wie sich jemand über die Zeit entwickelt hat. Keine Note, kein " - + "Ranking — nur die Bewegung.", - "willkommen.html": null, /* sich selbst erklärt sie nicht */ -}; - -/* --------------------------------------------------------------------- - DIE ROUTE - --------------------------------------------------------------------- */ -willkommenRouter.get("/workspace/api/willkommen", (req, res) => { - try { - const person = sitzungLesen(req); - if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); - - /* DIESELBE LISTE WIE DIE STARTSEITE, durch denselben Filter. Eine - eigene Aufzählung wäre die, die beim nächsten Umbau veraltet. */ - const roh = [ - ...(bereicheFuer(person) || []), - ...(zusatzBereicheFuer(person) || []), - ]; - const offen = roh.filter((k) => { - if (k?.aussen) return true; - const ziel = String(k?.ziel || ""); - if (!ziel) return false; - return darfSeite(person, "/workspace/" + ziel.split("?")[0]); - }); - - /* NACH GRUPPEN, in der Reihenfolge, in der sie auf der Startseite - stehen -- wer dort von oben liest, findet hier dasselbe wieder. */ - const gruppen = []; - for (const k of offen) { - const name = k.gruppe || "Sonst"; - let g = gruppen.find((x) => x.name === name); - if (!g) { g = { name, kacheln: [] }; gruppen.push(g); } - const datei = String(k.ziel || "").split("?")[0]; - g.kacheln.push({ - name: k.name, - unter: k.unter || "", - ziel: k.ziel, - zeichen: k.zeichen || null, - /* DERSELBE FARBTON WIE AUF DER STARTSEITE (22.09.2026). - Filipe: "die kacheln in der willkommens seite sollen die - selben farben haben wie in der startseite." - - Durchgereicht, nicht neu vergeben: `[data-ton="N"]` in - start.css setzt `--ton`, und start.css laedt diese Seite - ohnehin. Eine eigene Farbtabelle waere die, die beim - naechsten Farbwechsel auseinanderlaeuft -- und genau das ist - am 19.09. schon einmal passiert, als elf Seiten die neue - Kachelfarbe nie bekamen. */ - ton: Number.isFinite(k.ton) ? k.ton : null, - gross: !!k.gross, - /* Der lange Satz nur, wenn es einen gibt -- `null` heißt - ausdrücklich "hier reicht die Zeile". */ - lang: Object.prototype.hasOwnProperty.call(LANG, datei) ? LANG[datei] : null, - }); - } - - const ein = EINLEITUNG[person.rolle] || { - was: "Schön, dass du da bist.", - dazu: "Hier findest du alles, was für dich freigeschaltet ist.", - }; - - res.json({ - name: person.name, - rolle: person.rolle, - rollenname: ROLLEN_NAME[person.rolle] || null, - rollentext: rollentextFuer(person), - gruss: `Willkommen, ${person.name}!`, - einleitung: ein, - gruppen, - /* Ein paar Sätze, die nur für bestimmte Rollen gelten und zu - keiner einzelnen Kachel gehören. */ - hinweise: hinweiseFuer(person), - /* Damit die Seite die Zahl nennen kann, ohne sie zu zählen. */ - kachelzahl: offen.length, - }); - } catch (f) { - console.error("[willkommen]:", f?.message); - res.status(503).json({ fehler: "nicht_verfuegbar" }); - } -}); - -/** Sätze, die zu keiner Kachel gehören, aber zur Rolle. */ -function hinweiseFuer(person) { - const aus = []; - if (darfAufgabenVerteilen(person)) { - aus.push({ - titel: "Aufgaben verteilen", - text: "Beim Anlegen einer Aufgabe kannst du eine Person aussuchen — " - + "oder mehrere. Bei mehreren fragst du noch, ob jede ihren Teil " - + "macht oder ob die Erste, die Zeit hat, übernimmt. Ist jemand " - + "fertig, kannst du kurz Rückmeldung geben.", - }); - } - if (person.rolle === "modi") { - aus.push({ - titel: "Du musst nicht alles annehmen", - text: "Wenn eine Aufgabe gerade nicht geht, lehn sie ab und schreib " - + "einen Satz dazu. Das ist ausdrücklich vorgesehen und niemandem " - + "unangenehm.", - }); - } - /* HIER STAND "Was du nicht siehst" (entfernt 22.09.2026). - ------------------------------------------------------------------ - Der Hinweis zaehlte auf, was der linken Hand fehlt. Filipe: "das - geht keinen was an. sie sehen die sachen ja nicht weil es sie - nichts angeht und das muss auch nicht erwaehnt werden." - - Die Zeile bleibt als Kommentar stehen, damit niemand den Hinweis - spaeter "nachtraegt", weil er ihn fuer vergessen haelt. Er ist - entfernt worden, nicht uebersehen. - - Dieselbe Ueberlegung steht im Kopf dieser Datei schon: "Eine Liste - von Dingen, die man nicht darf, ist keine Orientierung, sondern - eine Kraenkung." Genau das galt fuer diesen Hinweis. */ - if (istDogFather(person)) { - aus.push({ - titel: "Du entscheidest am Ende", - text: "Überall, wo dir etwas vorgeschlagen wird, kannst du annehmen " - + "oder ablehnen. Ein Vorschlag ist ein Vorschlag.", - }); - } - if (person.rolle === "gast") { - aus.push({ - titel: "Nachts ist Ruhe", - text: "Zwischen 0 und 6 Uhr schläft der Chat — gelesen werden kann " - + "weiter, geschrieben ab 6 Uhr wieder. Das gilt für alle, auch " - + "für DogFather.", - }); - } - return aus; -} diff --git a/server/workspace.js b/server/workspace.js index 202a5572..42704dee 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -2257,9 +2257,73 @@ function ohneTotwege(liste, person) { }); } +/* ===================================================================== + DIE ANLEITUNG STEHT BEIM TEAM UNTER „FUER DICH" (06.10.2026) + ===================================================================== + + Bauplan Kapitel 03: „Modi, Linke Hand, Rechte Hand: zusaetzlich als + erste Kachel in ‚Fuer dich', weil sie bei ihnen sonst weit unten + steht." + + GEMESSEN STIMMTE DIE BEGRUENDUNG: Die Kachel sitzt in der Gruppe + „Community" -- bei einem Modi ist das die VIERTE Gruppe von oben, + nach Taeglich, Rund ums Live und Fuer dich. Seine eigene Einweisung + stand damit hinter den Brettern, die er moderiert. + + EINE ABWEICHUNG VOM PLAN, UND ZWAR EINE BEWUSSTE: Der Plan sagt + „zusaetzlich", also zweimal. Genau das hat dieses Haus am 19.09.2026 + schon einmal gehabt -- zwei Kacheln „Chat", gleicher Name, gleiches + Ziel, eine unter „Taeglich", eine unter „Community". Der Satz, der + damals daraus wurde, steht ein paar Hundert Zeilen weiter oben: + „Zwei gleich benannte Wege zum selben Ort sind schlimmer als ein + fehlender -- man klickt den falschen und sucht den Unterschied." + Deshalb wird sie VERSCHOBEN, nicht verdoppelt. Das Ziel des Plans + (sie steht oben, nicht weit unten) ist damit erreicht, der + Nebeneffekt bleibt aus. + + FUER DIE COMMUNITY AENDERT SICH NICHTS. Sie hat keine Gruppe + „Taeglich" und keine Arbeitslisten; bei ihr ist „Community" die + erste Gruppe, und dort steht die Kachel schon vorne und breit -- + genau wie der Plan es verlangt. Entschieden wird das nicht an einem + Rollennamen, sondern an AUSSEN_ROLLEN: Wer von aussen kommt, behaelt + seinen Platz. + + AM ZIEL ERKANNT, NICHT AM NAMEN, und die Zielgruppe wird GESUCHT, + nicht gezaehlt -- beides aus demselben Grund wie bei vorGruppe() + und bei der Support-Kachel weiter unten: Namen und Positionen + wandern in diesem Haus, Ziele nicht. */ +function anleitungNachVorne(liste, person) { + if (AUSSEN_ROLLEN.has(person?.rolle)) return liste; + const i = liste.findIndex((k) => k?.ziel === "anleitung.html"); + if (i < 0) return liste; + const j = liste.findIndex((k) => k?.gruppe === "Für dich"); + /* Keine Gruppe „Fuer dich"? Dann bleibt alles, wie es ist. Eine + Gruppe dafuer zu erfinden waere eine Kachelgruppe mit einem + einzigen Eintrag, und die sieht aus wie ein Fehler. */ + if (j < 0 || j === i) return liste; + const ohne = liste.filter((_, k) => k !== i); + const ziel = ohne.findIndex((k) => k?.gruppe === "Für dich"); + /* DIE STREIFEN BLEIBEN IN DER COMMUNITY-GRUPPE NICHT UEBRIG -- + sie wandern mit, aber die Kachel wird dabei schmal. Breit UND + bunt war sie, solange sie das Erste war, was ein Mitglied sieht. + Unter „Fuer dich" steht sie neben drei kleinen Schwestern; eine + breite Flaeche dort zieht den Blick von der Gruppe weg, in der + gerade etwas zu tun ist. */ + const kopie = { ...ohne[ziel >= 0 ? ziel : 0] }; + const kachel = { ...liste[i], gruppe: kopie.gruppe, gruppeUnter: kopie.gruppeUnter, + gross: false }; + return ziel < 0 + ? [...ohne, kachel] + : [...ohne.slice(0, ziel), kachel, ...ohne.slice(ziel)]; +} + export function bereicheFuer(person) { - const liste = ohneTotwege(bereicheRoh(person), person); - if (liste === null || !Array.isArray(liste) || !liste.length) return liste; + const roh = ohneTotwege(bereicheRoh(person), person); + /* `null` heisst „der Browser hat die Liste" (Agenturhaus), `[]` + heisst „diese Rolle bekommt gar nichts". Beides bleibt unangetastet + und faehrt hier heraus, bevor irgendetwas umsortiert wird. */ + if (roh === null || !Array.isArray(roh) || !roh.length) return roh; + const liste = anleitungNachVorne(roh, person); if (liste.some((k) => k?.ziel === "support.html")) return liste; /* VOR DEN VERTRAULICHEN MELDEWEG, NICHT ANS ENDE (24.09.2026). diff --git a/tools/kachel-regenbogen.mjs b/tools/kachel-regenbogen.mjs index c9fd3ea4..97448d8e 100644 --- a/tools/kachel-regenbogen.mjs +++ b/tools/kachel-regenbogen.mjs @@ -38,9 +38,13 @@ node tools/kachel-regenbogen.mjs nur zeigen node tools/kachel-regenbogen.mjs --schreiben eintragen - Und `pruef-willkommen` zählt nach, ob der Block noch zu den + Und `pruef-kachelfarben` zählt nach, ob der Block noch zu den tatsächlich benutzten Tönen passt — sonst wäre „einmal gelaufen" dasselbe wie „von Hand gepflegt", nur unsichtbar. + (Hier stand bis zum 06.10.2026 `pruef-willkommen`. Die Datei hat + diesen Block nie angesehen; der Hinweis zeigte auf eine Wache, die + es an der Stelle gar nicht gab. Gefunden beim Aufräumen, nachdem + `willkommen.html` zur Weiterleitung geworden war.) AUGENSCHONEND BLEIBT PFLICHT Die Töne werden nicht roh nebeneinandergelegt. Jeder wird mit dem diff --git a/workspace/anleitung.html b/workspace/anleitung.html index fbab67e2..2f3aa847 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 86179a00..9430b33b 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`. --> - - + + - +