ZWEI SACHEN, EINE DAVON EIN ECHTER BEFUND. 1) DIE KARTEN KAMEN AN DER RECHTETAFEL VORBEI Der Bauplan sagt in Kapitel 04 und noch einmal in der Abnahme: "Leite die sichtbaren Karten aus derselben Rechte-Tafel ab, die rechte.html verwendet. Eine Rolle sieht nur Karten zu Seiten, die sie oeffnen darf; pruefe das SERVERSEITIG." Ich habe dafuer eine Pruefung geschrieben, die die Seite ueber die ECHTE Schnittstelle sperrt (nicht mit einem UPDATE in der Datenbank -- nur so laeuft tafelLaden() mit). Sie wurde sofort rot: Nach dem Sperren von bereich.html fuer einen Modi lieferte der Server weiterhin ALLE ELF Bereichs-Karten. Im Browser fiel das nicht auf, und das ist der heikle Teil: Die Seite verbindet jede Karte mit einer Kachel, und die Kachelliste ist gefiltert -- die Karten verschwanden also auf dem Bildschirm. Die TEXTE gingen trotzdem hinaus. Eine Oberflaeche, die etwas nicht anzeigt, ist keine Schranke; genau deshalb steht im Bauplan "serverseitig". Jetzt siebt `darfKarte()` an der Quelle. Der Pfad wird aus dem Schluessel GERECHNET (`bereich:highlight` -> /workspace/bereich.html), nicht aus einer Zuordnungsliste -- das ist die Umkehrung von `schluesselVonZiel`, und eine gepflegte Tabelle daneben waere die, die beim naechsten neuen Brett fehlt. Entschieden wird mit `darfSeite` selbst; hier wird nichts davon nachgebaut. Gegenprobe in derselben Pruefung: zuruecksperren -> die Karten sind wieder da (29 von 29). Und eine Zeile dazwischen, die zaehlt, dass NUR sie verschwunden sind -- waere mit der Sperre die halbe Anleitung weggefallen, waere die erste Zeile auch gruen gewesen. 2) „AB DABEI" UND „AB STAMM" AN DER KARTE (Kapitel 04) "Kann ein Community-Mitglied etwas wegen seiner Stufe noch nicht, steht das an der Karte." Auch das abgeleitet: aus TREFF_SCHREIBEN, derselben Tafel, die das Schreiben ENTSCHEIDET. `null` darin heisst "von aussen schreibt hier niemand" -- das ist keine Stufe, die man erreicht, und bekommt deshalb keine Marke. "ab Stamm" an einem Anschlagbrett waere ein Versprechen, das nie eingeloest wird. Als Wort, nicht als Schloss-Symbol: Ein Schloss sagt "du darfst nicht" und laesst offen, ob jemals. "ab Dabei" sagt, dass es kommt -- und wann, steht mit Datum in der Karte "Deine Stufe" darueber. Ruhig gestaltet, nicht rot: Es ist keine Sperre, sondern eine Auskunft ueber die Zeit. GEPRUEFT (pruef-anleitung 166 -> 173) Die wichtigste der neuen Zeilen ist die, dass die Marke nach dem Aufstieg VERSCHWINDET. Eine Marke, die bleibt, ist schlimmer als keine: Sie sagt jemandem, er duerfe etwas nicht, das er laengst darf, und er probiert es gar nicht erst. Gemessen an drei Karten mit drei verschiedenen Antworten (Chat: Dabei, Highlights: Stamm, Anschlagbrett: gar nichts) -- waere die Rechnung grob falsch, traefe sie alle drei gleich. Dazu die Gegenprobe, dass eine Teamrolle keine einzige solche Marke bekommt. Mitgelaufen: pruef-rechtetafel 19, pruef-rechte-umstellen 56, pruef-css-klassen, pruef-tippziele 13 -- alle gruen. Die Abnahmezahlen aus Kapitel 13 (14/29/29/34 mit ihrer Aufteilung) sind nach dem neuen Sieb unveraendert; das ist der Beleg, dass es im Normalfall nichts wegnimmt. Im Browser nachgemessen (390 px, echte Anmeldung): Community sieht vier Marken (Rudel-Chat, Wunschliste, Mitmachen „ab Dabei", Highlights „ab Stamm"), die rechte Hand keine einzige. Co-Authored-By: Claude Opus 5 <[email protected]>
829 lines
37 KiB
JavaScript
829 lines
37 KiB
JavaScript
/* =====================================================================
|
||
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, AUSSEN_ROLLEN, bereicheFuer } from "./workspace.js";
|
||
import { darfSeite } from "./rechte.js";
|
||
import {
|
||
STUFEN, PFLEGE_ROLLEN, fassungenFuer,
|
||
} from "./anleitung-tabellen.js";
|
||
import {
|
||
stufeVon, STUFEN_NAME, DABEI_TAGE, STAMM_TAGE, STAMM_BEITRAEGE,
|
||
TREFF_SCHREIBEN, STUFEN as MITGLIED_STUFEN,
|
||
} from "./workspace-treff.js";
|
||
|
||
export const anleitungRouter = express.Router();
|
||
|
||
/** 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);
|
||
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;
|
||
}
|
||
|
||
/** Darf diese Person die Seite ueberhaupt oeffnen, die die Karte
|
||
* erklaert?
|
||
*
|
||
* Bauplan Kapitel 04 und 13: „Leite die sichtbaren Karten aus
|
||
* derselben Rechte-Tafel ab, die rechte.html verwendet. Eine Rolle
|
||
* sieht nur Karten zu Seiten, die sie oeffnen darf; pruefe das
|
||
* SERVERSEITIG."
|
||
*
|
||
* GEMESSEN AM 06.10.2026, UND ES STIMMTE NICHT: Wer in „Wer sieht
|
||
* was" fuer einen Modi `bereich.html` sperrte, bekam vom Server
|
||
* weiterhin alle elf Bereichs-Karten geliefert. Im Browser fielen sie
|
||
* zwar weg (die Seite verbindet jede Karte mit einer Kachel, und die
|
||
* Kachelliste ist gefiltert) -- aber die TEXTE gingen trotzdem
|
||
* hinaus, und eine Oberflaeche ist keine Schranke. Genau deshalb
|
||
* steht im Bauplan „serverseitig".
|
||
*
|
||
* DER PFAD WIRD AUS DEM SCHLUESSEL GERECHNET, nicht aus einer
|
||
* Zuordnungsliste: Der Schluessel ist der Seitenname ohne `.html`,
|
||
* bei Unterseiten mit einem Doppelpunkt dahinter
|
||
* (`bereich:highlight`, `chat:treff`). Das ist die Umkehrung von
|
||
* `Bereiche.schluesselVonZiel`; eine gepflegte Tabelle daneben waere
|
||
* die, die beim naechsten neuen Brett fehlt.
|
||
*
|
||
* OHNE EINTRAG IN DER TAFEL HEISST NEIN -- das entscheidet
|
||
* `darfSeite` selbst, und zwar fuer das ganze Haus gleich. Hier wird
|
||
* nichts davon nachgebaut. */
|
||
function darfKarte(person, schluessel) {
|
||
const name = String(schluessel || "").split(":")[0];
|
||
if (!name) return false;
|
||
return darfSeite(person, `/workspace/${name}.html`);
|
||
}
|
||
|
||
/** Kann diese Person das, was die Karte beschreibt, wegen ihrer
|
||
* Mitglieder-Stufe noch NICHT?
|
||
*
|
||
* Bauplan Kapitel 04: "Kann ein Community-Mitglied etwas wegen seiner
|
||
* Stufe noch nicht, steht das an der Karte („ab Dabei", „ab Stamm")."
|
||
*
|
||
* ABGELEITET AUS DER TAFEL, DIE ES ENTSCHEIDET. `TREFF_SCHREIBEN`
|
||
* sagt je Brett, welche Stufe zum Schreiben noetig ist -- dieselbe
|
||
* Tafel, die der Server beim Schreiben durchsetzt. Eine zweite Liste
|
||
* hier waere die, die eines Tages "ab Dabei" an eine Karte schreibt,
|
||
* bei der laengst jeder darf (oder umgekehrt, und das waere
|
||
* schlimmer: eine Karte ohne Marke, die beim Antippen abweist).
|
||
*
|
||
* `null` HAT ZWEI BEDEUTUNGEN IN DER TAFEL, und beide fuehren hier
|
||
* richtigerweise zu keiner Marke: Ein Brett ohne Eintrag kennt die
|
||
* Tafel nicht (dann geht es nicht ums Schreiben), und ein Brett mit
|
||
* `null` darf von aussen ueberhaupt niemand beschreiben -- das ist
|
||
* keine Stufe, die man erreicht, sondern eine dauerhafte Aufteilung.
|
||
* "ab Stamm" daranzuschreiben waere ein Versprechen, das nie
|
||
* eingeloest wird.
|
||
*
|
||
* DER SCHLUESSEL TRAEGT DAS BRETT SCHON IN SICH: `bereich:highlight`
|
||
* und `chat:treff` -- beide Formen entstehen in
|
||
* `Bereiche.schluesselVonZiel`, und beide tragen hinter dem
|
||
* Doppelpunkt genau den Namen, den die Tafel kennt. */
|
||
function erstAbFuer(person, schluessel) {
|
||
const hat = stufeVon(person);
|
||
if (!hat) return null; // Teamrolle: keine Mitglieder-Stufe
|
||
const brett = String(schluessel || "").split(":")[1] || "";
|
||
const noetig = TREFF_SCHREIBEN[brett];
|
||
if (!noetig) return null;
|
||
if (MITGLIED_STUFEN.indexOf(hat) >= MITGLIED_STUFEN.indexOf(noetig)) return null;
|
||
return STUFEN_NAME[noetig] || noetig;
|
||
}
|
||
|
||
/** „Deine Stufe" — nur fuer die Community (Bauplan, Kapitel 04).
|
||
*
|
||
* WARUM DIE ZAHLEN NICHT HIER STEHEN: `DABEI_TAGE` und die beiden
|
||
* anderen kommen aus workspace-treff.js, wo die Stufe auch BERECHNET
|
||
* wird. Haette ich „ab 7 Tagen" hier hingeschrieben, stuende die
|
||
* Regel an zwei Stellen -- und ausgerechnet der Text, der den Leuten
|
||
* erklaert, woran sie sind, waere der erste, der veraltet.
|
||
*
|
||
* `null` FUER ALLE ANDEREN. Eine Teamrolle hat keine Mitglieder-Stufe;
|
||
* eine Karte mit „Neu" waere dort schlicht falsch. Die Seite laesst
|
||
* den Abschnitt dann weg.
|
||
*
|
||
* NICHT MIT DER EINORDNUNG VERWECHSELN (Bauplan, Kapitel 02): Muss,
|
||
* Regelmaessig und Bei Bedarf sagen, was WICHTIG ist. Neu, Dabei und
|
||
* Stamm sagen, was jemand DARF. Deshalb heisst die Einordnung in der
|
||
* Oberflaeche nie „Stufe". */
|
||
function stufenkarte(person) {
|
||
const jetzt = stufeVon(person);
|
||
if (!jetzt) return null;
|
||
const erstellt = db().prepare("SELECT erstellt FROM personen WHERE id = ?")
|
||
.get(person.id)?.erstellt;
|
||
/* Ab wann geht mehr? Aus dem Beitrittstag gerechnet, nicht
|
||
geschaetzt -- "in ein paar Tagen" beantwortet die Frage nicht. */
|
||
const abTag = (tage) => {
|
||
if (!erstellt) return null;
|
||
const t = Date.parse(erstellt);
|
||
if (!Number.isFinite(t)) return null;
|
||
return new Date(t + tage * 86400_000).toISOString().slice(0, 10);
|
||
};
|
||
return {
|
||
jetzt,
|
||
name: STUFEN_NAME[jetzt] || jetzt,
|
||
reihen: [
|
||
{ schluessel: "neu", name: STUFEN_NAME.neu, ab: "ab dem ersten Tag",
|
||
kann: "Lesen und bei Wünschen mitstimmen." },
|
||
{ schluessel: "dabei", name: STUFEN_NAME.dabei, ab: `ab ${DABEI_TAGE} Tagen`,
|
||
kann: "Im Rudel schreiben, Wünsche einstellen, dich bei „Mitmachen“ melden.",
|
||
datum: abTag(DABEI_TAGE) },
|
||
{ schluessel: "stamm", name: STUFEN_NAME.stamm,
|
||
ab: `ab ${STAMM_TAGE} Tagen und ${STAMM_BEITRAEGE} eigenen Beiträgen`,
|
||
kann: "Clips und Bilder einstellen. Sie erscheinen, sobald das Team sie freigegeben hat.",
|
||
datum: abTag(STAMM_TAGE) },
|
||
],
|
||
};
|
||
}
|
||
|
||
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. */
|
||
|
||
const d = db();
|
||
const darfPflegen = PFLEGE_ROLLEN.includes(String(req.person?.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: 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.",
|
||
});
|
||
}
|
||
|
||
const kopf = d.prepare(
|
||
"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(hausVon(req.person), rolle)
|
||
/* DIE RECHTETAFEL SIEBT MIT -- siehe darfKarte(). Gefiltert wird
|
||
an der Quelle und nicht erst beim Senden: Darunter haengen der
|
||
Zaehler, die Stufen-Abschnitte und die Luecken-Anzeige der
|
||
Pflege, und die sollen alle dieselbe Menge meinen. */
|
||
.filter((k) => darfKarte(person, k.kachel));
|
||
|
||
/* `erledigt` kommt aus derselben Abfrage und nicht aus einer
|
||
zweiten: Zwei Abfragen koennten zwischen den Zeilen
|
||
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), hausVon(req.person), rolle);
|
||
|
||
const nachArt = (art) => zeilen.filter((z) => z.art === art);
|
||
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. */
|
||
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: karten.map((k) => ({
|
||
...k, neu: istNeu(k.geaendert), erst_ab: erstAbFuer(person, k.kachel),
|
||
})),
|
||
nicht: nachArt("nicht").map((z) => ({ id: z.id, titel: z.titel, text: z.text })),
|
||
takt: nachArt("takt").map((z) => ({ id: z.id, titel: z.titel, text: z.text })),
|
||
fragen: nachArt("frage").map((z) => ({ id: z.id, titel: z.titel, text: z.text })),
|
||
/* "DREI DINGE VORWEG" -- nur die Community bringt sie mit
|
||
(Bauplan, Kapitel 04). Bei allen anderen Rollen ist die Liste
|
||
leer, und die Seite laesst den Abschnitt dann weg. Kein
|
||
Sonderfall im Code, nur eine leere Liste. */
|
||
vorweg: nachArt("vorweg").map((z) => ({ id: z.id, titel: z.titel, text: z.text })),
|
||
stufe: stufenkarte(person),
|
||
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" });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
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;
|
||
|
||
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, hausVon(req.person), 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, hausVon(req.person), 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) return [];
|
||
const rolle = String(person.rolle || "");
|
||
if (!fassungenFuer(hausVon(person)).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, hausVon(person), 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 [];
|
||
}
|
||
}
|
||
|
||
|
||
/* =====================================================================
|
||
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 (!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, hausVon(req.person));
|
||
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 (!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" });
|
||
}
|
||
const d = db();
|
||
const platz = d.prepare(
|
||
"SELECT COALESCE(MAX(reihe), -1) + 1 AS n FROM anl_karte WHERE haus = ? AND rolle = ?")
|
||
.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(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 {
|
||
/* 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, hausVon(req.person));
|
||
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, 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
|
||
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, hausVon(req.person));
|
||
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 (!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(hausVon(req.person), 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 (${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,
|
||
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 (!fassungenFuer(hausVon(person)).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(hausVon(req.person), rolle).map((z) => z.kachel);
|
||
|
||
res.json({
|
||
zeigen: !schon,
|
||
rolle,
|
||
name: person.name || "",
|
||
muss,
|
||
/* IST DAS EINE TEAMROLLE? (06.10.2026, Bauplan Kapitel 10)
|
||
"Community drei Schritte, Team-Rollen fuenf."
|
||
|
||
Als Ja/Nein und nicht als Rollenname: rundgang.js laeuft im
|
||
Browser, und in dieser Datei steht bewusst kein einziger
|
||
Rollenname -- sie waere sonst am Tag nach der naechsten
|
||
Aenderung falsch, und zwar bei jedem, der sie schon geladen
|
||
hat. Gefragt wird dieselbe Menge, die auch sonst entscheidet,
|
||
wer von aussen kommt. */
|
||
team: !AUSSEN_ROLLEN.has(rolle),
|
||
});
|
||
} 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 });
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
„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
|
||
* 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 (!fassungenFuer(hausVon(person)).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" });
|
||
}
|
||
});
|