Anleitung, Etappen 3 bis 7: alle vier Fassungen, Pflege, Suche, Rundgang, Abnahme

Der Bauplan ist damit vollstaendig umgesetzt.

ETAPPE 3 -- SCOUT, MANAGER UND SPICY MEDIA.
Woertlich aus Kapitel 07 bis 09, zusammen 84 Karten. Die Zahlen des
Bauplans stimmen auf die Karte genau: 18/21/22/23, verteilt 5/6/7,
5/7/9, 6/9/7, 5/5/13.

EINE ZWEIDEUTIGKEIT IM PDF, GEMESSEN STATT GERATEN. Beim Manager
nennt die Merke-Spalte "61, 58, 51 feste Punkte", und Layout- und
Raw-Auszug ordnen sie verschiedenen Kacheln zu. Nachgezaehlt im Code:
LIVE 69, Content-Ideen 61, Community 58, Technik 51. Damit ist die
zeilenweise Lesart bewiesen richtig -- die Layout-Lesart haette "61"
der LIVE-Analyse gegeben, die 69 hat.

Die Zuordnungspruefung laeuft jetzt fuer alle vier Rollen, und die
erwartete Kartenzahl wird AUS DER KACHELLISTE abgeleitet, nicht aus
der Abnahmeliste abgeschrieben. Dazu eine Gegenprobe, dass sich die
vier Fassungen wirklich unterscheiden -- sonst waeren alle Zeilen
gruen, solange die Kachellisten zufaellig passen.

ETAPPE 4 -- DIE PFLEGE, fuer DogFather UND Spicy.
Bearbeitet wird an Ort und Stelle: Stufe und die drei Zeilen je Karte,
Titel und Text je Listenzeile, Leitgedanke je Rolle. Fehlende Karten
werden benannt und lassen sich mit einem Griff anlegen -- das ist der
Fall "Fuer diese Kachel fehlt ein Eintrag" aus Kapitel 11. Dazu "Wer
ist wie weit": eine Arbeitslage, keine Ueberwachung.

KEINE ZWEITE ROLLENAUSWAHL. Die Vorschau gibt es schon -- sie heisst
"Meine Sicht" und steht in der Kopfleiste. Geprueft wird fuers Pflegen
`req.person` und nicht `req.sicht`: Wer durch fremde Augen sieht, soll
nicht aus Versehen in fremdem Namen aendern.

DIE MARKE "NEU" HAT EIN ENDE: 14 Tage ODER bis die Person die Seite
nach der Aenderung geoeffnet hat. Der Bauplan sagt dazu nichts, und
ohne Ende waere nach einem halben Jahr alles neu.

ETAPPE 5 -- DIE SUCHE.
Die Karten stehen in der Kopfleisten-Suche, aber nur die der EIGENEN
Rolle: Faende ein Creator die Manager-Karte, stuende dort "Zuteilen,
freigeben" neben einer Kachel, die er nicht hat. Der Treffer traegt
den Zusatz hinter dem Fragezeichen, sonst landen fuenf Karten auf
derselben Seite.

ETAPPE 6 -- BEGRUESSUNG UND RUNDGANG.
Fuenf Schritte ueber die Zentrale, im letzten leuchten nur die
Muss-Kacheln. EIGENE DATEI (`rundgang.js`) und nicht ein Stueck
start.js: Er laeuft auf der meistbenutzten Seite des Hauses, und wenn
hier etwas schiefgeht, darf davon nichts anderes betroffen sein.
Keine gerechnete Koordinate, kein Loch in einer Abdeckung -- die Seite
wird leiser, das Ziel bleibt hell. Faellt das Skript aus, ist beim
naechsten Laden alles normal.

DER ROLLENWECHSEL IST KEINE SONDERREGEL, sondern der Schluessel:
`(Person, Rolle)`. Die neue Rolle hat schlicht noch keine Zeile, also
startet die Einweisung von selbst noch einmal. Eine Spalte
"zuruecksetzen", an die jemand denken muesste, waere die Stelle, an
der es vergessen wird.

EIN FUND AUS DER MESSUNG: Gemerkt wurde zuerst erst am ENDE des
Rundgangs. Wer "Los geht's" drueckte und dann wegging, bekam die
Begruessung bei jedem Aufruf wieder -- fuer immer. Jetzt wird beim
ZEIGEN gemerkt; das ist die Tatsache, um die es geht.

ETAPPE 7 -- DIE ABNAHME.
Dabei fiel auf, dass der Rollen-Rundgang SPICY MEDIA gar nicht kannte:
fuenf Durchgaenge, aber zweimal Manager, zweimal Scout und Spicy nie.
Ihr gehoert die Gruppe "Rund um das Team" samt Team-Lage -- diese
Kachel ist in keinem Durchgang je geoeffnet worden. Jetzt laeuft sie
mit: 452 statt 406 Pruefungen.

MITGENOMMEN, WEIL ES ROT WAR: `pruef-eventkarte` und
`pruef-haus-luecke` bildeten ihr Tagesdatum aus UTC. Zwischen 00:00
und 02:00 deutscher Zeit liegt UTC im Vortag -- ein naechtlicher Lauf
haette an einem Datum gesucht, das er selbst nicht geschrieben hat.
Beide benutzen jetzt `helfer-zeit`. `pruef-struktur` ist damit gruen.

Gemessen (alle gruen, kein einziger Befund)
  pruef-anleitung        125 Pruefungen (vorher 70)
  pruef-rollen           452 Pruefungen (vorher 406) -- mit Spicy Media
  pruef-struktur         gruen (vorher rot)
  pruef-rechtetafel, pruef-css-klassen, pruef-kachel-universum,
  pruef-suchfeld, pruef-kachelraster, pruef-start-ansicht,
  pruef-sicht, pruef-eventkarte     alle gruen
  mess-anleitung         Rundgang fuenf Schritte, im letzten genau 5
                         Muss-Kacheln hervorgehoben; beim zweiten
                         Aufruf keine Begruessung mehr; kein
                         Querschieben bei 412 und 1440 px

Sicherung: ~/sicherungen/workspace-vor-anleitung-e37-20261006-1536.db

OFFEN, NICHT VON MIR UND NICHT AUS DIESEM BAUPLAN: pruef-haus-luecke
endet mit Rueckgabewert 3 ("konnte nicht nachsehen") -- eine der
beiden Adressen liefert das Anschlagbrett nicht. Nachgemessen: Das war
vor dieser Arbeit genauso.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-06 15:37:00 +02:00
co-authored by Claude Opus 5
parent df67574f90
commit 98cfdc3ebc
58 changed files with 2765 additions and 707 deletions
+306 -5
View File
@@ -110,7 +110,7 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => {
"SELECT leitgedanke FROM anl_rolle WHERE haus = ? AND rolle = ?").get(HAUS, rolle);
const karten = d.prepare(
`SELECT kachel, stufe, wozu, tust, merke, reihe, geaendert
`SELECT id, kachel, stufe, wozu, tust, merke, reihe, geaendert
FROM anl_karte WHERE haus = ? AND rolle = ?
ORDER BY reihe, id`).all(HAUS, rolle);
@@ -130,6 +130,32 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => {
const schritte = nachArt("schritt");
const fertig = schritte.filter((z) => z.erledigt).length;
/* ==================================================================
DIE MARKE "NEU" -- UND WARUM SIE EIN ENDE BRAUCHT
Der Bauplan sagt: "Aendert DogFather einen Eintrag, traegt er
fuer alle die Marke Neu." Er sagt nicht, wann sie verschwindet.
Ohne Ende ist nach einem halben Jahr alles neu, und die Marke
sagt nichts mehr -- dieselbe Krankheit wie eine Warnung, die
immer kommt.
Sie gilt deshalb, solange BEIDES zutrifft:
* die Aenderung ist hoechstens 14 Tage her, UND
* die Person hat die Seite seit der Aenderung nicht geoeffnet.
Der zweite Teil ist der wichtigere: Wer nachgesehen hat, hat es
gesehen. Der erste faengt den ab, der monatelang nicht
hereinschaut -- fuer den waere nach der Rueckkehr sonst die
halbe Seite "neu", und das hilft niemandem.
`zuletzt` wird NACH dem Rechnen fortgeschrieben, nicht davor --
sonst waere die Marke schon beim ersten Laden weg. */
const besuch = d.prepare(
"SELECT gesehen_am FROM anl_besuch WHERE person_id = ?").get(person?.id ?? 0);
const zuletzt = besuch?.gesehen_am || "";
const FRIST = new Date(Date.now() - 14 * 86400_000).toISOString();
const istNeu = (geaendert) => !!geaendert && geaendert > FRIST && geaendert > zuletzt;
/* 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. */
@@ -153,12 +179,22 @@ 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,
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 })),
karten: karten.map((k) => ({ ...k, neu: istNeu(k.geaendert) })),
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 })),
anzahl: karten.length,
});
/* Erst antworten, dann den Besuch vermerken. Haette die Zeile
davor gestanden, waere jede Aenderung schon beim ersten Laden
nicht mehr "neu" gewesen -- die Marke haette nie jemand gesehen. */
if (person?.id) {
d.prepare(
`INSERT INTO anl_besuch (person_id, gesehen_am) VALUES (?,?)
ON CONFLICT(person_id) DO UPDATE SET gesehen_am = excluded.gesehen_am`)
.run(person.id, new Date().toISOString());
}
} catch (fehler) {
console.error("[workspace] Anleitung:", fehler?.message);
res.status(500).json({ fehler: "serverfehler" });
@@ -279,3 +315,268 @@ export function offeneHinweise(person) {
return [];
}
}
/* =====================================================================
DIE PFLEGE — NUR DOGFATHER UND SPICY (Etappe 4)
=====================================================================
Bauplan Kapitel 11: Rolle waehlen und Vorschau, Stufe aendern, Texte
bearbeiten, Stand je Person, Hinweis bei fehlendem Eintrag.
DIE VORSCHAU IST NICHT GEBAUT WORDEN -- es gibt sie schon. Der
Sicht-Umschalter in der Kopfleiste zeigt jede Seite mit den Augen
einer anderen Person, auch diese. Eine zweite Rollenauswahl waere
dieselbe Funktion an einer Stelle, an der sie niemand sucht.
WER DARF: `PFLEGE_ROLLEN` = admin und spicy. Entscheidung Filipe am
06.10.2026, dieselbe Regel wie bei den Manager-Zielen. Geprueft wird
`req.person`, NICHT `req.sicht`: Wer die Sicht eines Creators
einnimmt, soll sehen, was dieser sieht -- und in dem Moment nichts
aendern koennen, ohne es zu merken.
===================================================================== */
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" });
}
next();
}
const text = (v, max) => String(v ?? "").trim().slice(0, max);
/** Eine Karte aendern: Stufe und die drei Saetze. */
anleitungRouter.put("/workspace/api/anleitung/karte/:id", nurPflege, (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ fehler: "keine_nummer" });
const stufe = String(req.body?.stufe ?? "");
if (!STUFEN.some((s) => s.schluessel === stufe)) {
return res.status(400).json({ fehler: "unbekannte_stufe" });
}
const d = db();
const n = d.prepare(
`UPDATE anl_karte SET stufe = ?, wozu = ?, tust = ?, merke = ?,
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);
if (!n.changes) return res.status(404).json({ fehler: "keine_karte" });
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Anleitung Karte:", fehler?.message);
res.status(500).json({ fehler: "serverfehler" });
}
});
/** Eine fehlende Karte anlegen.
*
* Gebraucht fuer genau den Fall, den der Bauplan "Fuer diese Kachel
* fehlt ein Eintrag" nennt: Eine neue Kachel kommt ins Haus, und die
* Anleitung kennt sie noch nicht. Ohne diesen Weg muesste jemand in
* die Datenbank. */
anleitungRouter.post("/workspace/api/anleitung/karte", nurPflege, (req, res) => {
try {
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 (!kachel) return res.status(400).json({ fehler: "keine_kachel" });
if (!STUFEN.some((s) => s.schluessel === stufe)) {
return res.status(400).json({ fehler: "unbekannte_stufe" });
}
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;
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),
text(req.body?.tust, 400), text(req.body?.merke, 400), platz,
new Date().toISOString(), req.person.id);
} catch {
/* Die Tabelle hat UNIQUE(haus, rolle, kachel) -- eine zweite
Karte zur selben Kachel kann es nicht geben, und das ist der
Grund, warum die Zuordnungspruefung "genau eine" sagen darf. */
return res.status(409).json({ fehler: "gibt_es_schon" });
}
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Anleitung Karte anlegen:", fehler?.message);
res.status(500).json({ fehler: "serverfehler" });
}
});
/** Eine Karte entfernen -- fuer Kacheln, die es nicht mehr gibt. */
anleitungRouter.delete("/workspace/api/anleitung/karte/:id", nurPflege, (req, res) => {
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);
if (!n.changes) return res.status(404).json({ fehler: "keine_karte" });
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Anleitung Karte loeschen:", fehler?.message);
res.status(500).json({ fehler: "serverfehler" });
}
});
/** Eine Listenzeile aendern (Satz, Schritt, Nicht deins, Takt, Frage). */
anleitungRouter.put("/workspace/api/anleitung/zeile/:id", nurPflege, (req, res) => {
try {
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);
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
haelt es jemand fuer eine Zusage. */
const ziel = alt.art === "schritt" ? (text(req.body?.ziel, 200) || null) : null;
d.prepare(
`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);
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Anleitung Zeile:", fehler?.message);
res.status(500).json({ fehler: "serverfehler" });
}
});
/** Den Leitgedanken einer Rolle aendern. */
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" });
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),
new Date().toISOString(), req.person.id);
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Anleitung Rolle:", fehler?.message);
res.status(500).json({ fehler: "serverfehler" });
}
});
/** Wer ist wie weit? (Bauplan: "Stand je Person: fertig/offen mit Datum")
*
* Es ist eine ARBEITSLAGE, keine Ueberwachung -- dieselbe
* Unterscheidung wie bei der Team-Lage. Hier steht, wer seine
* Einweisung noch offen hat, damit jemand nachfragen kann. Nicht,
* was er gelesen hat oder wie lange er gebraucht hat. */
anleitungRouter.get("/workspace/api/anleitung/stand", nurPflege, (req, res) => {
try {
const leute = db().prepare(
`SELECT p.id, p.name, p.rolle,
(SELECT COUNT(*) FROM anl_zeile z
WHERE z.haus = ? AND z.rolle = p.rolle AND z.art = 'schritt') AS gesamt,
(SELECT COUNT(*) FROM anl_stand s JOIN anl_zeile z2 ON z2.id = s.zeile_id
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);
res.json({
leute: leute.map((p) => ({
name: p.name, rolle: p.rolle,
erledigt: Number(p.erledigt || 0), gesamt: Number(p.gesamt || 0),
fertig: Number(p.gesamt || 0) > 0 && Number(p.erledigt || 0) >= Number(p.gesamt || 0),
zuletzt: p.zuletzt || null,
})),
});
} catch (fehler) {
console.error("[workspace] Anleitung Stand:", fehler?.message);
res.status(500).json({ fehler: "serverfehler" });
}
});
/* =====================================================================
DIE EINWEISUNG BEIM ERSTEN ANMELDEN (Etappe 6)
=====================================================================
Zwei Wege, und beide sind klein: "Soll ich?" und "Ich habe sie
gezeigt." Alles andere -- die fuenf Schritte, das Hervorheben, der
Text -- steht im Browser, weil nur er weiss, wo die Dinge auf dem
Bildschirm liegen.
DIE ANTWORT IST IMMER 200, auch fuer die Crew und fuer DogFather --
dann eben mit `zeigen: false`. Ein 404 waere hier das Falsche: Das
Skript laeuft auf JEDER Startseite, und ein Fehlschlag in der
Konsole bei jedem Aufruf ist genau die Meldung, die die naechste
echte verdeckt.
===================================================================== */
anleitungRouter.get("/workspace/api/anleitung/einweisung", (req, res) => {
try {
/* NICHT `req.sicht`: Die Einweisung gehoert der Person, die
angemeldet ist. DogFather, der die Sicht eines Creators
einnimmt, soll nicht dessen Begruessung bekommen -- und erst
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)) {
return res.json({ zeigen: false, rolle });
}
const d = db();
const schon = d.prepare(
"SELECT 1 AS da FROM anl_einweisung WHERE person_id = ? AND rolle = ?")
.get(person.id, rolle);
/* Die Muss-Kacheln als SCHLUESSEL. Die Adressen kennt nur der
Browser (siehe oben); er rechnet sie mit derselben Funktion aus,
mit der auch die Karten verbunden werden. */
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);
res.json({
zeigen: !schon,
rolle,
name: person.name || "",
muss,
});
} catch (fehler) {
console.error("[workspace] Einweisung:", fehler?.message);
/* Auch im Fehlerfall kein 500: Eine kaputte Begruessung darf die
Startseite nicht mit einer roten Zeile begruessen. */
res.json({ zeigen: false });
}
});
/** Gezeigt -- egal ob durchgeklickt oder "Spaeter".
*
* BEIDES ZAEHLT, und das ist Absicht. Der Bauplan sagt: "Spaeter ist
* erlaubt. Dann steht unter 'Was ist dran': Erste Schritte noch
* offen." Wer "Spaeter" waehlt, hat die Begruessung gesehen und soll
* sie nicht bei jedem Aufruf wieder bekommen -- der Hinweis
* uebernimmt. Ein Fenster, das so lange wiederkommt, bis man das
* Richtige anklickt, ist keine Einladung, sondern eine Sperre. */
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)) {
return res.json({ ok: true });
}
db().prepare(
`INSERT INTO anl_einweisung (person_id, rolle, gezeigt_am) VALUES (?,?,?)
ON CONFLICT(person_id, rolle) DO NOTHING`)
.run(person.id, rolle, new Date().toISOString());
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Einweisung merken:", fehler?.message);
res.status(500).json({ fehler: "serverfehler" });
}
});