/* ===================================================================== workspace-anleitung.js — die Einweisung je Rolle (Etappe 1) Bauplan: „Kachel Anleitung", Stand 06.10.2026, geprueft in `02 Projekte/Anleitung-Kachel – Bauplan.md`. --------------------------------------------------------------------- WAS DIESE SCHNITTSTELLE LIEFERT UND WAS NICHT Sie liefert die TEXTE und die STUFE je Kachel. Sie liefert NICHT den Namen, das Symbol, die Farbe oder die Adresse einer Kachel — die stehen in `workspace/assets/js/bereiche.js`, und nur dort. Die Seite verbindet beides ueber den Kachel-Schluessel (`window.Bereiche.schluesselVonZiel`). Das ist Absicht und der Grund, warum diese Datei so kurz ist: Der Server kennt die Kacheln des Agenturhauses gar nicht. `bereicheFuer` gibt dort `null` zurueck — „nimm die Liste aus dem Browser". Haette ich sie hier noch einmal hingeschrieben, um „alles an einer Stelle" zu haben, waere genau das die zweite Liste, die beim naechsten Umbau veraltet und dann eine Kachel erklaert, die es nicht mehr gibt. --------------------------------------------------------------------- DREI SCHRANKEN, NICHT EINE 1. `rechte.js` sperrt die Seite fuer die Crew. 2. Diese Datei prueft Haus und Rolle noch einmal selbst. 3. Die Hinweise (ab Etappe 2) pruefen es ein drittes Mal. Am 15.09.2026 ist ein Weg fuer die Community genau durch Luecke 3 gerutscht, obwohl Tafel und Seite sauber gesperrt waren. Die Hinweise sind eine eigene Tuer. ===================================================================== */ import express from "express"; import { db, sitzungLesen } from "./workspace.js"; import { STUFEN, FASSUNGEN, PFLEGE_ROLLEN, } from "./anleitung-tabellen.js"; export const anleitungRouter = express.Router(); const HAUS = "agentur"; function angemeldet(req, res, next) { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); req.person = person; next(); } anleitungRouter.use("/workspace/api/anleitung", angemeldet); /** Wessen Fassung wird gezeigt? * * `req.sicht` ist der HAUSEIGENE Umschalter: DogFather kann die Sicht * eines anderen einnehmen, und dann zeigt jede Seite das, was dieser * andere sieht. Hier gilt dasselbe — und genau deshalb braucht die * Anleitung keine eigene „Vorschau": Die gibt es schon, sie heisst * „Meine Sicht" und steht in der Kopfleiste. * * Ohne gewaehlte Sicht ist DogFather `admin`, und fuer `admin` gibt es * keine Fassung (Bauplan, Kapitel 11). Dann sagt die Seite das, statt * ihm irgendeine fremde Rolle unterzuschieben. */ function fassungFuer(req) { const person = req.sicht || req.person; return { person, rolle: String(person?.rolle || "") }; } /** WESSEN Haken werden gezeigt? * * Die der angesehenen Person. DogFather, der die Sicht eines Creators * einnimmt, soll sehen, wie weit DIESER Creator ist — das ist der * Sinn der Sicht. Setzen darf er sie nicht (`darf_abhaken`). * * Ohne gewaehlte Sicht ist es die eigene Person, und das ist der * Normalfall. */ function wessenStand(req) { return (req.sicht || req.person)?.id ?? 0; } anleitungRouter.get("/workspace/api/anleitung", (req, res) => { try { const { person, rolle } = fassungFuer(req); /* DIE CREW HAT HIER NICHTS ZU SUCHEN. 404 und nicht 403: Auf der Crew-Adresse gibt es diese Seite nicht, und „gibt es nicht" ist die richtige Auskunft — 403 waere der Hinweis, dass es sie woanders doch gibt. */ if (person?.haus === "crew") return res.status(404).json({ fehler: "nicht_hier" }); const d = db(); const darfPflegen = PFLEGE_ROLLEN.includes(String(req.person?.rolle || "")); if (!FASSUNGEN.includes(rolle)) { /* DogFather ohne gewaehlte Sicht. Kein Fehler, sondern eine Auskunft: Er bekommt keine Einweisung, kann aber jede ansehen. Die Rollen stehen MIT Namen dabei, damit die Seite keine zweite Rollenliste braucht. */ return res.json({ rolle, hat_fassung: false, darf_pflegen: darfPflegen, fassungen: FASSUNGEN, satz: "Für dich gibt es keine Einweisung – du kennst den Laden. " + "Wähle oben unter „Meine Sicht“ eine Rolle, dann siehst du genau das, " + "was diese Person zu lesen bekommt.", }); } const kopf = d.prepare( "SELECT leitgedanke FROM anl_rolle WHERE haus = ? AND rolle = ?").get(HAUS, rolle); const karten = d.prepare( `SELECT kachel, stufe, wozu, tust, merke, reihe, geaendert FROM anl_karte WHERE haus = ? AND rolle = ? ORDER BY reihe, id`).all(HAUS, rolle); /* `erledigt` kommt aus derselben Abfrage und nicht aus einer zweiten: Zwei Abfragen koennten zwischen den Zeilen auseinanderlaufen, und dann zeigte der Fortschritt eine andere Zahl als die Haken darunter. */ const zeilen = d.prepare( `SELECT z.id, z.art, z.titel, z.text, z.ziel, z.reihe, (s.person_id IS NOT NULL) AS erledigt FROM anl_zeile z LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ? WHERE z.haus = ? AND z.rolle = ? ORDER BY z.art, z.reihe, z.id`).all(wessenStand(req), HAUS, rolle); const nachArt = (art) => zeilen.filter((z) => z.art === art); const schritte = nachArt("schritt"); const fertig = schritte.filter((z) => z.erledigt).length; /* DIE ZAHL GEHOERT IN DIE ANTWORT, nicht nur in die Liste. Die Seite kann damit sagen „18 Karten" — und eine Pruefung kann nachrechnen, ohne die Liste selbst zu zaehlen. */ res.json({ rolle, hat_fassung: true, darf_pflegen: darfPflegen, leitgedanke: kopf?.leitgedanke || "", stufen: STUFEN, saetze: nachArt("satz").map((z) => z.text), schritte: schritte.map((z) => ({ id: z.id, titel: z.titel, text: z.text, ziel: z.ziel, erledigt: !!z.erledigt, })), /* DER FORTSCHRITT ZAEHLT NUR DIE ERSTEN SCHRITTE -- so steht es im Bauplan, und das ist der Grund, warum es ihn gibt: "Niemand muss 18 Karten abhaken. Fertig ist, wer die Ersten Schritte erledigt hat." */ fortschritt: { erledigt: fertig, gesamt: schritte.length }, /* Abhaken darf nur, wer diese Fassung auch IST. DogFather, der sich die Sicht eines Creators ansieht, bekommt die Haken zu sehen, kann sie aber nicht setzen -- es waere sonst sein eigener Fortschritt an fremden Schritten. */ darf_abhaken: String(req.person?.rolle || "") === rolle, karten, nicht: nachArt("nicht").map((z) => ({ titel: z.titel, text: z.text })), takt: nachArt("takt").map((z) => ({ titel: z.titel, text: z.text })), fragen: nachArt("frage").map((z) => ({ titel: z.titel, text: z.text })), anzahl: karten.length, }); } catch (fehler) { console.error("[workspace] Anleitung:", fehler?.message); res.status(500).json({ fehler: "serverfehler" }); } }); /* ===================================================================== EINEN ERSTEN SCHRITT ABHAKEN — ODER DEN HAKEN WEGNEHMEN ===================================================================== PUT und nicht POST: Der Aufruf setzt einen Zustand auf einen Wert. Zweimal „erledigt" nacheinander ist derselbe Zustand wie einmal — bei einem wackeligen Mobilfunknetz ist das der Unterschied zwischen „nochmal tippen hilft" und „jetzt ist es doppelt". WER DARF: nur die Person selbst, und nur an Schritten IHRER Rolle. Beides wird hier geprueft und nicht nur in der Oberflaeche — eine ausgeblendete Schaltflaeche hat noch nie etwas geschuetzt. ===================================================================== */ anleitungRouter.put("/workspace/api/anleitung/schritt/:id", (req, res) => { try { const person = req.person; if (person?.haus === "crew") return res.status(404).json({ fehler: "nicht_hier" }); const id = Number(req.params.id); if (!Number.isInteger(id) || id <= 0) { return res.status(400).json({ fehler: "keine_nummer" }); } const erledigt = req.body?.erledigt === true; const d = db(); /* DIE ZEILE MUSS EIN SCHRITT SEIN UND DER EIGENEN ROLLE GEHOEREN. Ohne `art = 'schritt'` liesse sich jede Zeile abhaken — auch eine Frage oder eine Rhythmus-Zeile. Das faellt niemandem auf, bis der Fortschritt eines Tages 7 von 5 meldet. */ const zeile = d.prepare( `SELECT id FROM anl_zeile WHERE id = ? AND art = 'schritt' AND haus = ? AND rolle = ?`) .get(id, HAUS, String(person?.rolle || "")); if (!zeile) return res.status(404).json({ fehler: "kein_schritt" }); if (erledigt) { d.prepare( `INSERT INTO anl_stand (person_id, zeile_id, erledigt_am) VALUES (?,?,?) ON CONFLICT(person_id, zeile_id) DO NOTHING`) .run(person.id, id, new Date().toISOString()); } else { d.prepare("DELETE FROM anl_stand WHERE person_id = ? AND zeile_id = ?") .run(person.id, id); } /* Der neue Stand kommt zurueck, damit die Seite ihn nicht selbst hochrechnen muss. Eine Seite, die mitzaehlt, zaehlt irgendwann anders als der Server. */ const stand = d.prepare( `SELECT COUNT(*) AS gesamt, SUM(CASE WHEN s.person_id IS NOT NULL THEN 1 ELSE 0 END) AS erledigt FROM anl_zeile z LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ? WHERE z.haus = ? AND z.rolle = ? AND z.art = 'schritt'`) .get(person.id, HAUS, String(person.rolle)); res.json({ erledigt, fortschritt: { erledigt: Number(stand?.erledigt || 0), gesamt: Number(stand?.gesamt || 0) }, }); } catch (fehler) { console.error("[workspace] Anleitung abhaken:", fehler?.message); res.status(500).json({ fehler: "serverfehler" }); } }); /* ===================================================================== DER HINWEIS: „Erste Schritte noch offen" ===================================================================== EINE QUELLE, ZWEI ANZEIGEN. Diese Funktion speist sowohl die Zeile unter „Was ist dran" als auch die Zahl an der Kachel — und ab dieser Etappe zusaetzlich die breite Kachel oben auf der Startseite. Das ist kein Nebeneffekt, sondern die Regel des Hauses: Der Bauplan wollte einen Punkt bei „Was ist dran" UND einen eigenen Zaehler an der Kachel. Zwei getrennt gebaute Zaehler zeigen irgendwann zwei verschiedene Zahlen uebereinander auf einem Bildschirm, und dann glaubt man keiner mehr. `try` um alles: Ein Fehler hier wuerde sonst die GANZE Hinweisliste mitnehmen — Aufgaben, Termine, Reviews. Ein kaputter Hinweis darf nicht die zwoelf gesunden daneben loeschen. ===================================================================== */ export function offeneHinweise(person) { try { if (!person || person.haus === "crew") return []; const rolle = String(person.rolle || ""); if (!FASSUNGEN.includes(rolle)) return []; const stand = db().prepare( `SELECT COUNT(*) AS gesamt, SUM(CASE WHEN s.person_id IS NOT NULL THEN 1 ELSE 0 END) AS erledigt FROM anl_zeile z LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ? WHERE z.haus = ? AND z.rolle = ? AND z.art = 'schritt'`) .get(person.id, HAUS, rolle); const gesamt = Number(stand?.gesamt || 0); const offen = gesamt - Number(stand?.erledigt || 0); /* Gibt es fuer diese Rolle noch gar keine Schritte (Etappe 3 steht aus), ist `gesamt` 0 und `offen` 0 — kein Hinweis. Richtig: Ein „0 Schritte offen" waere eine Meldung ueber nichts. */ if (offen <= 0) return []; return [{ art: "anl_schritte", stufe: "offen", text: "Erste Schritte noch offen", ziel: "anleitung.html", anzahl: offen, }]; } catch (fehler) { console.error("[workspace] Anleitung-Hinweise:", fehler?.message); return []; } }