Die Einweisung gibt es jetzt auch fuer Team Dogi -- vier Fassungen, 106 Karten

Filipe hat den Bauplan "Willkommen & Anleitung -- Einweisung je Rolle"
(27 Seiten, Stand 06.10.2026) geschickt: pruefen, perfektionieren und
auf der Crew-Seite umsetzen.

AUSGANGSMESSUNG, bevor etwas angefasst wurde:

    agentur  creator 18 · manager 22 · scout 21 · spicy 23 Karten
    crew     0 Karten -- die Schnittstelle antwortete 404 "nicht_hier"

Die Seite lud also, war aber leer. Baulich vorbereitet war sie schon:
Alle drei Inhaltstabellen tragen `haus` mit UNIQUE (haus, rolle,
kachel), und die Aussaat nimmt das Haus als Parameter.

WAS JETZT DA IST

  gast   14 Karten (5 Muss, 4 Regelmaessig, 5 Bedarf)
  modi   29 Karten (7 / 7 / 15)
  linke  29 Karten (7 / 7 / 15)
  hand   34 Karten (8 / 7 / 19)

Jede Zahl entspricht Kapitel 13 des Bauplans. Dazu je Rolle
Leitgedanke, drei Saetze, fuenf Erste Schritte, "Nicht deins",
Rhythmus und Fragen -- woertlich aus Kapitel 06 bis 09.

VOR DEM SCHREIBEN ABGEGLICHEN, nicht danach: Ein Skript prueft jede
Karte gegen die echte Kachelliste der Rolle. Ergebnis fuer alle vier
Fassungen: Kartenzahl wie im Plan, jede Karte trifft eine Kachel,
jede Kachel hat eine Karte, keine doppelt.

ZWEI KACHELN AUF EINER SEITE. Auf der Crew-Adresse fuehren "Chat"
(das Team) und "Rudel-Chat" (`?raum=treff`) beide auf chat.html.
`schluesselVonZiel` behielt nur `b` -- beide ergaben denselben
Schluessel, und damit konnten sie keine eigenen Karten haben, obwohl
der Bauplan ihnen verschiedene Einordnungen gibt (fuer die rechte
Hand: Team-Chat Muss, Rudel-Chat nur bei Bedarf). Jetzt wird auch
`raum` unterschieden. Auf der Agenturadresse gibt es nur eine
Chat-Kachel; dort aendert sich nichts.

DAS HAUS KOMMT AUS DER ADRESSE, nicht aus der angesehenen Rolle.
`const HAUS = "agentur"` ist einer Funktion gewichen. Nimmt DogFather
die Sicht einer anderen Rolle ein, wechselt er die ROLLE, nicht die
ADRESSE -- wer hier `req.sicht` naehme, bekaeme auf der Crew-Adresse
Agenturinhalte, sobald eine Sicht gesetzt ist.

`FASSUNGEN` stand an zehn Stellen fest und haette die Crew ueberall
abgewiesen. Jetzt `fassungenFuer(haus)` -- eine Funktion statt zweier
Listen an zehn Stellen.

Neue Zeilenart `vorweg` fuer "Drei Dinge vorweg" (nur die Community,
Bauplan Kapitel 04). Fehlt die Liste bei einer Rolle, wird nichts
eingetragen -- kein Sonderfall noetig.

Eigene Datei fuer die Crew-Inhalte: anleitung-tabellen.js traegt
schon die Agenturfassungen und ist 856 Zeilen lang. Getrennte Dateien
sind hier dasselbe Prinzip wie getrennte Haeuser.

pruef-anleitung 125 -> 127. Fuenf Zeilen darin verteidigten den alten
Zustand ("ein Modi bekommt die Anleitung nicht, erwartet 404") -- sie
sind mitgewandert statt stehenzubleiben. Dazu eine neue Gegenprobe
zur Haustrennung: In der Crew-Fassung darf keine Agenturkachel
vorkommen.

Datenbank vorher gesichert und zurueckgelesen (integrity_check,
84 Karten).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-06 16:29:38 +02:00
co-authored by Claude Opus 5
parent a654f74dd1
commit 96eff525d0
53 changed files with 1553 additions and 744 deletions
+39 -29
View File
@@ -35,12 +35,25 @@
import express from "express";
import { db, sitzungLesen } from "./workspace.js";
import {
STUFEN, FASSUNGEN, PFLEGE_ROLLEN,
STUFEN, PFLEGE_ROLLEN, fassungenFuer,
} from "./anleitung-tabellen.js";
export const anleitungRouter = express.Router();
const HAUS = "agentur";
/** In welchem Haus sind wir?
*
* DIE ADRESSE ENTSCHEIDET, NICHT DIE ANGESEHENE ROLLE (06.10.2026).
* Hier stand `const HAUS = "agentur"` -- die Anleitung gab es nur
* im Agenturhaus, und die Crew bekam an fuenf Stellen ein 404
* ("nicht_hier"). Seit heute hat auch Team Dogi ihre vier
* Fassungen; das Haus kommt deshalb aus der Anmeldung.
*
* GEFRAGT WIRD IMMER `req.person`, NIE `req.sicht`: Nimmt DogFather
* die Sicht einer anderen Rolle ein, wechselt er damit die ROLLE,
* nicht die ADRESSE. Wer hier die angesehene Person naehme, bekaeme
* auf der Crew-Adresse Agenturinhalte, sobald eine Sicht gesetzt
* ist -- und genau das ist die Trennung, die seit dem 24.09. gilt. */
const hausVon = (p) => (p?.haus === "crew" ? "crew" : "agentur");
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
@@ -87,19 +100,18 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => {
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)) {
if (!fassungenFuer(hausVon(req.person)).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,
fassungen: fassungenFuer(hausVon(req.person)),
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.",
@@ -107,12 +119,12 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => {
}
const kopf = d.prepare(
"SELECT leitgedanke FROM anl_rolle WHERE haus = ? AND rolle = ?").get(HAUS, rolle);
"SELECT leitgedanke FROM anl_rolle WHERE haus = ? AND rolle = ?").get(hausVon(req.person), rolle);
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(HAUS, rolle);
ORDER BY reihe, id`).all(hausVon(req.person), rolle);
/* `erledigt` kommt aus derselben Abfrage und nicht aus einer
zweiten: Zwei Abfragen koennten zwischen den Zeilen
@@ -124,7 +136,7 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => {
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);
ORDER BY z.art, z.reihe, z.id`).all(wessenStand(req), hausVon(req.person), rolle);
const nachArt = (art) => zeilen.filter((z) => z.art === art);
const schritte = nachArt("schritt");
@@ -217,7 +229,6 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => {
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) {
@@ -233,7 +244,7 @@ anleitungRouter.put("/workspace/api/anleitung/schritt/:id", (req, res) => {
const zeile = d.prepare(
`SELECT id FROM anl_zeile
WHERE id = ? AND art = 'schritt' AND haus = ? AND rolle = ?`)
.get(id, HAUS, String(person?.rolle || ""));
.get(id, hausVon(req.person), String(person?.rolle || ""));
if (!zeile) return res.status(404).json({ fehler: "kein_schritt" });
if (erledigt) {
@@ -255,7 +266,7 @@ anleitungRouter.put("/workspace/api/anleitung/schritt/:id", (req, res) => {
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));
.get(person.id, hausVon(req.person), String(person.rolle));
res.json({
erledigt,
@@ -287,9 +298,9 @@ anleitungRouter.put("/workspace/api/anleitung/schritt/:id", (req, res) => {
===================================================================== */
export function offeneHinweise(person) {
try {
if (!person || person.haus === "crew") return [];
if (!person) return [];
const rolle = String(person.rolle || "");
if (!FASSUNGEN.includes(rolle)) return [];
if (!fassungenFuer(hausVon(person)).includes(rolle)) return [];
const stand = db().prepare(
`SELECT COUNT(*) AS gesamt,
@@ -297,7 +308,7 @@ export function offeneHinweise(person) {
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);
.get(person.id, hausVon(person), rolle);
const gesamt = Number(stand?.gesamt || 0);
const offen = gesamt - Number(stand?.erledigt || 0);
@@ -337,7 +348,6 @@ export function offeneHinweise(person) {
===================================================================== */
function nurPflege(req, res, next) {
if (req.person?.haus === "crew") return res.status(404).json({ fehler: "nicht_hier" });
if (!PFLEGE_ROLLEN.includes(String(req.person?.rolle || ""))) {
return res.status(403).json({ fehler: "nicht_erlaubt" });
}
@@ -361,7 +371,7 @@ anleitungRouter.put("/workspace/api/anleitung/karte/:id", nurPflege, (req, res)
geaendert = ?, geaendert_von = ?
WHERE id = ? AND haus = ?`)
.run(stufe, text(req.body?.wozu, 300), text(req.body?.tust, 400),
text(req.body?.merke, 400), new Date().toISOString(), req.person.id, id, HAUS);
text(req.body?.merke, 400), new Date().toISOString(), req.person.id, id, hausVon(req.person));
if (!n.changes) return res.status(404).json({ fehler: "keine_karte" });
res.json({ ok: true });
} catch (fehler) {
@@ -381,7 +391,7 @@ anleitungRouter.post("/workspace/api/anleitung/karte", nurPflege, (req, res) =>
const rolle = String(req.body?.rolle ?? "");
const kachel = text(req.body?.kachel, 80);
const stufe = String(req.body?.stufe ?? "bedarf");
if (!FASSUNGEN.includes(rolle)) return res.status(400).json({ fehler: "unbekannte_rolle" });
if (!fassungenFuer(hausVon(req.person)).includes(rolle)) return res.status(400).json({ fehler: "unbekannte_rolle" });
if (!kachel) return res.status(400).json({ fehler: "keine_kachel" });
if (!STUFEN.some((s) => s.schluessel === stufe)) {
return res.status(400).json({ fehler: "unbekannte_stufe" });
@@ -389,13 +399,13 @@ anleitungRouter.post("/workspace/api/anleitung/karte", nurPflege, (req, res) =>
const d = db();
const platz = d.prepare(
"SELECT COALESCE(MAX(reihe), -1) + 1 AS n FROM anl_karte WHERE haus = ? AND rolle = ?")
.get(HAUS, rolle).n;
.get(hausVon(req.person), rolle).n;
try {
d.prepare(
`INSERT INTO anl_karte (haus, rolle, kachel, stufe, wozu, tust, merke,
reihe, geaendert, geaendert_von)
VALUES (?,?,?,?,?,?,?,?,?,?)`)
.run(HAUS, rolle, kachel, stufe, text(req.body?.wozu, 300),
.run(hausVon(req.person), rolle, kachel, stufe, text(req.body?.wozu, 300),
text(req.body?.tust, 400), text(req.body?.merke, 400), platz,
new Date().toISOString(), req.person.id);
} catch {
@@ -416,7 +426,7 @@ anleitungRouter.delete("/workspace/api/anleitung/karte/:id", nurPflege, (req, re
try {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ fehler: "keine_nummer" });
const n = db().prepare("DELETE FROM anl_karte WHERE id = ? AND haus = ?").run(id, HAUS);
const n = db().prepare("DELETE FROM anl_karte WHERE id = ? AND haus = ?").run(id, hausVon(req.person));
if (!n.changes) return res.status(404).json({ fehler: "keine_karte" });
res.json({ ok: true });
} catch (fehler) {
@@ -431,7 +441,7 @@ anleitungRouter.put("/workspace/api/anleitung/zeile/:id", nurPflege, (req, res)
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ fehler: "keine_nummer" });
const d = db();
const alt = d.prepare("SELECT art FROM anl_zeile WHERE id = ? AND haus = ?").get(id, HAUS);
const alt = d.prepare("SELECT art FROM anl_zeile WHERE id = ? AND haus = ?").get(id, hausVon(req.person));
if (!alt) return res.status(404).json({ fehler: "keine_zeile" });
/* Ein Ziel gibt es nur beim Schritt. Bei einer Frage waere es ein
Feld, das nirgends angezeigt wird -- und beim naechsten Umbau
@@ -441,7 +451,7 @@ anleitungRouter.put("/workspace/api/anleitung/zeile/:id", nurPflege, (req, res)
`UPDATE anl_zeile SET titel = ?, text = ?, ziel = ?, geaendert = ?, geaendert_von = ?
WHERE id = ? AND haus = ?`)
.run(text(req.body?.titel, 200), text(req.body?.text, 600), ziel,
new Date().toISOString(), req.person.id, id, HAUS);
new Date().toISOString(), req.person.id, id, hausVon(req.person));
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Anleitung Zeile:", fehler?.message);
@@ -453,13 +463,13 @@ anleitungRouter.put("/workspace/api/anleitung/zeile/:id", nurPflege, (req, res)
anleitungRouter.put("/workspace/api/anleitung/rolle/:rolle", nurPflege, (req, res) => {
try {
const rolle = String(req.params.rolle || "");
if (!FASSUNGEN.includes(rolle)) return res.status(404).json({ fehler: "unbekannte_rolle" });
if (!fassungenFuer(hausVon(req.person)).includes(rolle)) return res.status(404).json({ fehler: "unbekannte_rolle" });
db().prepare(
`INSERT INTO anl_rolle (haus, rolle, leitgedanke, geaendert, geaendert_von)
VALUES (?,?,?,?,?)
ON CONFLICT(haus, rolle) DO UPDATE SET leitgedanke = excluded.leitgedanke,
geaendert = excluded.geaendert, geaendert_von = excluded.geaendert_von`)
.run(HAUS, rolle, text(req.body?.leitgedanke, 600),
.run(hausVon(req.person), rolle, text(req.body?.leitgedanke, 600),
new Date().toISOString(), req.person.id);
res.json({ ok: true });
} catch (fehler) {
@@ -484,8 +494,8 @@ anleitungRouter.get("/workspace/api/anleitung/stand", nurPflege, (req, res) => {
WHERE s.person_id = p.id AND z2.art = 'schritt') AS erledigt,
(SELECT MAX(s2.erledigt_am) FROM anl_stand s2 WHERE s2.person_id = p.id) AS zuletzt
FROM personen p
WHERE p.aktiv = 1 AND p.rolle IN (${FASSUNGEN.map(() => "?").join(",")})
ORDER BY p.rolle, p.name`).all(HAUS, ...FASSUNGEN);
WHERE p.aktiv = 1 AND p.rolle IN (${fassungenFuer(hausVon(req.person)).map(() => "?").join(",")})
ORDER BY p.rolle, p.name`).all(hausVon(req.person), ...fassungenFuer(hausVon(req.person)));
res.json({
leute: leute.map((p) => ({
name: p.name, rolle: p.rolle,
@@ -525,7 +535,7 @@ anleitungRouter.get("/workspace/api/anleitung/einweisung", (req, res) => {
recht nicht fuer ihn abhaken, dass sie gezeigt wurde. */
const person = req.person;
const rolle = String(person?.rolle || "");
if (person?.haus === "crew" || !FASSUNGEN.includes(rolle)) {
if (!fassungenFuer(hausVon(person)).includes(rolle)) {
return res.json({ zeigen: false, rolle });
}
const d = db();
@@ -539,7 +549,7 @@ anleitungRouter.get("/workspace/api/anleitung/einweisung", (req, res) => {
const muss = d.prepare(
`SELECT kachel FROM anl_karte
WHERE haus = ? AND rolle = ? AND stufe = 'muss' ORDER BY reihe`)
.all(HAUS, rolle).map((z) => z.kachel);
.all(hausVon(req.person), rolle).map((z) => z.kachel);
res.json({
zeigen: !schon,
@@ -567,7 +577,7 @@ anleitungRouter.post("/workspace/api/anleitung/einweisung/gesehen", (req, res) =
try {
const person = req.person;
const rolle = String(person?.rolle || "");
if (person?.haus === "crew" || !FASSUNGEN.includes(rolle)) {
if (!fassungenFuer(hausVon(person)).includes(rolle)) {
return res.json({ ok: true });
}
db().prepare(