From 98cfdc3ebc928e34217f5ea78594af8d5f290451 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Tue, 6 Oct 2026 15:37:00 +0200 Subject: [PATCH] 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 --- server/anleitung-tabellen.js | 452 +++++++++++++++++++++++++++++ server/mess-anleitung.mjs | 73 +++++ server/pruef-anleitung.mjs | 284 ++++++++++++++++++ server/pruef-eventkarte.mjs | 10 +- server/pruef-haus-luecke.mjs | 6 +- server/pruef-rollen.mjs | 12 + server/workspace-anleitung.js | 311 +++++++++++++++++++- server/workspace-suche.js | 51 ++++ workspace/anleitung.html | 136 ++++++++- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/anleitung.css | 291 +++++++++++++++++++ workspace/assets/js/anleitung.js | 202 +++++++++++++ workspace/assets/js/rundgang.js | 266 +++++++++++++++++ workspace/aufgaben.html | 38 +-- workspace/automation.html | 32 +- workspace/befinden.html | 34 +-- workspace/bereich.html | 44 +-- workspace/bewerben.html | 38 +-- workspace/bewerbungen.html | 36 +-- workspace/buehne.html | 6 +- workspace/calls.html | 34 +-- workspace/chat.html | 40 +-- workspace/content.html | 34 +-- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +-- workspace/entwicklung.html | 36 +-- workspace/hilfe.html | 30 +- workspace/index.html | 10 +- workspace/kalender.html | 34 +-- workspace/leistung.html | 32 +- workspace/manager-ziele.html | 28 +- workspace/material.html | 32 +- workspace/notizen.html | 28 +- workspace/personen.html | 32 +- workspace/profil.html | 36 +-- workspace/reaktion.html | 38 +-- workspace/rechte.html | 34 +-- workspace/report.html | 36 +-- workspace/scouting.html | 32 +- workspace/start.html | 42 ++- workspace/startcheck.html | 34 +-- workspace/steckbrief.html | 36 +-- workspace/support.html | 28 +- workspace/tafel.html | 6 +- workspace/talente.html | 36 +-- workspace/team.html | 32 +- workspace/teamlage.html | 34 +-- workspace/teilen.html | 30 +- workspace/treff-moderation.html | 32 +- workspace/treff-regeln.html | 32 +- workspace/uebersicht.html | 32 +- workspace/unsere-seiten.html | 30 +- workspace/unterstuetzen.html | 32 +- workspace/werdegang.html | 32 +- workspace/willkommen.html | 30 +- workspace/wissen.html | 34 +-- 58 files changed, 2765 insertions(+), 707 deletions(-) create mode 100644 workspace/assets/js/rundgang.js diff --git a/server/anleitung-tabellen.js b/server/anleitung-tabellen.js index eb60bc59..eed899e8 100644 --- a/server/anleitung-tabellen.js +++ b/server/anleitung-tabellen.js @@ -186,6 +186,42 @@ CREATE TABLE IF NOT EXISTS anl_stand ( PRIMARY KEY (person_id, zeile_id) ); CREATE INDEX IF NOT EXISTS idx_anl_stand_person ON anl_stand (person_id); + +/* ====================================================================== + WANN JEMAND ZULETZT HINEINGESEHEN HAT (Etappe 4) + + Einzig fuer die Marke "Neu": Sie soll verschwinden, sobald die + Person die geaenderte Stelle gesehen hat. Dafuer reicht ein + Zeitstempel je Person -- NICHT je Eintrag. Wer eine Seite oeffnet, + sieht sie ganz; Buch zu fuehren, welche Karte wie lange im Bild + war, waere Ueberwachung und nicht Pflege. + ====================================================================== */ +CREATE TABLE IF NOT EXISTS anl_besuch ( + person_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE, + gesehen_am TEXT NOT NULL +); + +/* ====================================================================== + DIE EINWEISUNG BEIM ERSTEN ANMELDEN (Etappe 6) + + Der Bauplan: "Nur einmal automatisch. Gespeichert pro Person, nicht + pro Geraet." -- deshalb hier und nicht im Browserspeicher. Wer sich + am Telefon anmeldet und spaeter am Rechner, hat sie schon gesehen. + + DER SCHLUESSEL IST (Person, ROLLE) UND NICHT NUR DIE PERSON. Damit + erledigt sich die Sonderregel aus dem Bauplan ("Bekommt jemand eine + neue Rolle, startet die Einweisung fuer die neue Rolle beim + naechsten Login einmal neu") von selbst: Die neue Rolle hat + schlicht noch keine Zeile. Eine Spalte "zuruecksetzen", die beim + Rollenwechsel jemand anfassen muesste, waere die Stelle, an der es + vergessen wird. + ====================================================================== */ +CREATE TABLE IF NOT EXISTS anl_einweisung ( + person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, + rolle TEXT NOT NULL, + gezeigt_am TEXT NOT NULL, + PRIMARY KEY (person_id, rolle) +); `; /* ===================================================================== @@ -329,6 +365,422 @@ export const INHALTE = { text: "Dein Team: Bild, Name und was du über dich geschrieben hast." }, ], }, + + scout: { + leitgedanke: + "Finden, begleiten, übergeben. Du hast deine eigene Pipeline und betreust " + + "die Creator, die du gebracht hast, bis ein Manager übernimmt.", + saetze: [ + "Dein Platz ist die Scout-Pipeline. Dort beginnt und endet dein Tag.", + "Die Manager-Ziele sind Pflicht und starten jeden Monat neu.", + "Die Betreuungs-Kacheln brauchst du nur für Creator, die noch bei dir sind.", + ], + schritte: [ + { titel: "Mein Steckbrief", ziel: "steckbrief.html", + text: "Bild, ein Satz über dich, deine Kanäle." }, + { titel: "Scout-Pipeline", ziel: "scouting.html", + text: "Mit „Neuer Kontakt“ den ersten anlegen, mit Datum für den nächsten Schritt." }, + { titel: "Manager-Ziele", ziel: "manager-ziele.html", + text: "Ansehen: deine vier festen Monatsaufgaben." }, + { titel: "Dashboard", ziel: "uebersicht.html", + text: "Unter „Wo ich diese Woche nachfassen würde“ steht, wer dich braucht." }, + { titel: "Installieren", ziel: null, text: "Oben antippen." }, + ], + karten: [ + { kachel: "scouting", stufe: "muss", + wozu: "Dein Kontakt-Verzeichnis.", + tust: "Kontakte durch fünf Stufen führen: Neu, Angesprochen, Gespräch, Interessiert, Übergeben.", + merke: "Immer ein Datum setzen. Nur du und DogFather sehen deine Pipeline." }, + { kachel: "manager-ziele", stufe: "muss", + wozu: "Deine Pflichtaufgaben im Monat.", + tust: "Eintragen: 3 Creator rekrutiert, 2 Manager-Meetings, 2 Schulungen oder Community Talks, 1 Werbevideo.", + merke: "Acht Einträge pro Monat. Am besten bis Monatsmitte." }, + { kachel: "aufgaben", stufe: "muss", + wozu: "Wer macht was.", + tust: "Aufgaben anlegen und zuteilen, aus der Vorlage mit 80 Aufgaben übernehmen.", + merke: "Vier Spalten: Offen, In Arbeit, Zur Freigabe, Erledigt." }, + { kachel: "chat", stufe: "muss", + wozu: "Nachrichten mit dem Team.", + tust: "Personen, Gruppen und Kanäle.", + merke: "Vereinbarungen gehören ins Protokoll, nicht nur in den Chat." }, + { kachel: "steckbrief", stufe: "muss", + wozu: "So sehen dich die anderen.", + tust: "Einmal ausfüllen: Bild, Satz, Kanäle.", + merke: "Dein Bild erscheint überall neben deinem Namen." }, + + { kachel: "uebersicht", stufe: "regelmaessig", + wozu: "Deine Creator auf einen Blick.", + tust: "Sehen, wer lange nicht da war und wo ein Gespräch fehlt.", + merke: "Mit „Schreiben“ direkt in den Chat." }, + { kachel: "kalender", stufe: "regelmaessig", + wozu: "Termine und Calls.", + tust: "Termine anlegen, Wiederholungen einrichten.", + merke: "Vier Ansichten: Monat, Woche, Liste, Zeitstrahl." }, + { kachel: "calls", stufe: "regelmaessig", + wozu: "Gespräche festhalten.", + tust: "Call planen, danach „Protokoll schreiben“.", + merke: "To-dos aus dem Protokoll werden Aufgaben." }, + { kachel: "profil", stufe: "regelmaessig", + wozu: "Akte je Creator.", + tust: "Bei jedem neuen Creator: Stammdaten, Ziele und 90-Tage-Plan füllen.", + merke: "Leere Profile erscheinen bei „Was ist dran“." }, + { kachel: "startcheck", stufe: "regelmaessig", + wozu: "Erstanalyse neuer Creator.", + tust: "Bei jedem neuen Creator: 16 Punkte in vier Blöcken bewerten.", + merke: "Jeder neue Creator braucht einen." }, + { kachel: "bereich:schutz", stufe: "regelmaessig", + wozu: "Unterweisungen.", + tust: "Bei jedem neuen Creator: Dokumente durchgehen und bestätigen.", + merke: "Die abgeschlossene Bestätigung ist der Nachweis." }, + { kachel: "report", stufe: "regelmaessig", + wozu: "Rückblick je Creator.", + tust: "Einmal im Monat: Zeitraum wählen, Entscheidung festhalten.", + merke: "Erledigt, blockiert, funktioniert, als Nächstes." }, + + { kachel: "leistung", stufe: "bedarf", + wozu: "Was wirklich passiert ist.", + tust: "Tage eintragen oder Backstage-Datei einlesen, Ziele setzen.", + merke: "In Backstage den Zeitraum auf einen Tag stellen, sonst fehlen Tageswerte." }, + { kachel: "bereich:live", stufe: "bedarf", + wozu: "LIVEs systematisch verbessern.", + tust: "Je Punkt setzen: passt, zu verbessern, passt nicht, kommt nicht vor.", + merke: "Der Creator sieht es sofort." }, + { kachel: "content", stufe: "bedarf", + wozu: "Content planen.", + tust: "Ideen anlegen und durch die fünf Stufen schieben.", + merke: "Fertige Vorschläge lassen sich übernehmen." }, + { kachel: "bereich:community", stufe: "bedarf", + wozu: "Moderation und Konflikte.", + tust: "Festhalten, was vorbereitet ist und was geholfen hat.", + merke: "Drei Blöcke: vorbereiten, aufbauen, wenn es kippt." }, + { kachel: "bereich:technik", stufe: "bedarf", + wozu: "Setup und Fehler je Creator.", + tust: "Aufbau, aktuelles Problem und Lösung dokumentieren.", + merke: "Spart beim nächsten Ausfall Zeit." }, + { kachel: "bereich:agentur", stufe: "bedarf", + wozu: "Events, Schulungen, Anliegen.", + tust: "Einträge anlegen, Rückmeldungen der Creator sehen.", + merke: "DogFather entscheidet am Ende über Vorschläge." }, + { kachel: "dateien", stufe: "bedarf", + wozu: "Ablage und Freigaben.", + tust: "Datei hochladen (bis 25 MB) und wählen, wer sie sehen darf.", + merke: "Niemand ausgewählt heißt: nur DogFather sieht sie." }, + { kachel: "wissen", stufe: "bedarf", + wozu: "Anleitungen zu TikTok und Technik.", + tust: "Nachschlagen und an Creator weitergeben.", + merke: "15 Themenbereiche mit Filtern." }, + { kachel: "support", stufe: "bedarf", + wozu: "Etwas ist kaputt.", + tust: "Beschreiben, Bilder anhängen, abschicken.", + merke: "Geht an DogFather und die rechte Hand." }, + ], + nicht: [ + { titel: "Die volle Betreuungsakte bei jedem Kontakt.", + text: "Sie beginnt erst, wenn aus dem Kontakt ein Creator geworden ist." }, + { titel: "Creator nach der Übergabe.", text: "Ab „Übergeben“ ist der Manager zuständig." }, + { titel: "Personen anlegen und Codes vergeben.", + text: "Das macht ein Manager. Sag ihm Bescheid." }, + { titel: "Die Pipeline der anderen Scouts.", text: "Du siehst nur deine eigene." }, + ], + takt: [ + { titel: "Täglich", text: "Zentrale, Scout-Pipeline (fällige Kontakte), Chat, Aufgaben." }, + { titel: "Wöchentlich", + text: "Dashboard, Calls & Protokolle schreiben, Start-Check und Creator-Profile neuer Creator." }, + { titel: "Monatlich", + text: "Manager-Ziele bis Monatsmitte erledigen, Reports & Review je Creator." }, + ], + fragen: [ + { titel: "Wann zählt ein Kontakt als übergeben?", + text: "Wenn ein Manager übernimmt. Ab da ist es Betreuung, kein Scouting mehr." }, + { titel: "Wer sieht meine Pipeline?", + text: "Du und DogFather. Manager und Spicy Media sehen die Gesamtübersicht." }, + { titel: "Bis wann müssen die Manager-Ziele erledigt sein?", + text: "Jeden Monat neu. Am besten bis Monatsmitte." }, + ], + }, + + manager: { + leitgedanke: + "Betreuung und Freigaben. Du hast dieselben Rechte wie die Leitung, nur ohne " + + "Personenverwaltung. Du legst Creator an und hältst die Betreuung am Laufen.", + saetze: [ + "Dein Tag: Dashboard ansehen, Aufgaben freigeben, im Chat antworten.", + "Die Manager-Ziele sind Pflicht und starten jeden Monat neu.", + "Die Fach-Kacheln (LIVE-Analyse bis Technik) öffnest du je Creator, nicht täglich.", + ], + schritte: [ + { titel: "Mein Steckbrief", ziel: "steckbrief.html", text: "Ausfüllen." }, + { titel: "Personen & Zugänge", ziel: "personen.html", + text: "Ansehen: So legst du einen neuen Creator an und gibst den Code weiter." }, + { titel: "Dashboard", ziel: "uebersicht.html", + text: "Hier siehst du alle Creator und wo es hakt." }, + { titel: "Manager-Ziele", ziel: "manager-ziele.html", + text: "Ansehen: deine vier festen Monatsaufgaben." }, + { titel: "Aufgaben", ziel: "aufgaben.html", + text: "Unter „Wer hat gerade was“ siehst du die Verteilung." }, + ], + karten: [ + { kachel: "uebersicht", stufe: "muss", + wozu: "Alle Creator auf einen Blick.", + tust: "Nachfassen, wo jemand lange nicht da war.", + merke: "Je Creator: Termine, Aufgaben, Zahlen, Start-Check." }, + { kachel: "aufgaben", stufe: "muss", + wozu: "Wer macht was.", + tust: "Zuteilen, freigeben, Rückmeldung geben. Täglich die Spalte „Zur Freigabe“.", + merke: "Tippe eine Person an, um nur ihre Aufgaben zu sehen." }, + { kachel: "chat", stufe: "muss", + wozu: "Nachrichten mit dem Team.", + tust: "Personen, Gruppen, Kanäle.", + merke: "Entscheidungen ins Protokoll übernehmen." }, + { kachel: "manager-ziele", stufe: "muss", + wozu: "Deine Pflichtaufgaben im Monat.", + tust: "Rekrutierungen, Meetings, Schulungen, Werbevideo eintragen.", + merke: "Acht Einträge pro Monat." }, + { kachel: "personen", stufe: "muss", + wozu: "Neue Leute anlegen.", + tust: "Name und Rolle wählen, Code sofort weitergeben.", + merke: "Der Code wird nur einmal gezeigt." }, + { kachel: "steckbrief", stufe: "muss", + wozu: "So sehen dich die anderen.", + tust: "Einmal ausfüllen: Bild, Satz, Kanäle.", + merke: "Jeder pflegt nur seinen eigenen." }, + + { kachel: "kalender", stufe: "regelmaessig", + wozu: "Termine und Calls.", + tust: "Termine und Wiederholungen anlegen.", + merke: "Reviews und Fristen haben eigene Farben." }, + { kachel: "calls", stufe: "regelmaessig", + wozu: "Gespräche festhalten.", + tust: "Call planen, Protokoll schreiben, To-dos vergeben.", + merke: "Kein Call ohne Protokoll." }, + { kachel: "profil", stufe: "regelmaessig", + wozu: "Akte je Creator.", + tust: "Stammdaten, Ziele, 90-Tage-Plan.", + merke: "Die Zahl an der Kachel zeigt leere Profile." }, + { kachel: "startcheck", stufe: "regelmaessig", + wozu: "Erstanalyse.", + tust: "16 Punkte bewerten.", + merke: "Die Zahl an der Kachel zeigt Creator ohne Start-Check." }, + { kachel: "bereich:schutz", stufe: "regelmaessig", + wozu: "Unterweisungen.", + tust: "Mit jedem neuen Creator durchgehen und bestätigen.", + merke: "Danach unveränderlich." }, + { kachel: "scouting", stufe: "regelmaessig", + wozu: "Alle Scouts im Überblick.", + tust: "Nach Scout filtern, Übergaben annehmen.", + merke: "Ab „Übergeben“ bist du zuständig." }, + { kachel: "leistung", stufe: "regelmaessig", + wozu: "Diamanten, LIVE-Tage, Verweildauer.", + tust: "Tage eintragen oder Datei einlesen, Ziele setzen.", + merke: "Quelle ist Backstage." }, + { kachel: "report", stufe: "regelmaessig", + wozu: "Rückblick und Entscheidung.", + tust: "Einmal im Monat: Zeitraum wählen, Entscheidung festhalten.", + merke: "Vergleich mit dem Zeitraum davor." }, + { kachel: "bereich:agentur", stufe: "regelmaessig", + wozu: "Events, Schulungen, Anliegen.", + tust: "Einträge anlegen, vorbereitete Vorschläge übernehmen.", + merke: "Was hier nicht steht, wird vergessen." }, + + { kachel: "bereich:live", stufe: "bedarf", + wozu: "LIVEs verbessern.", + tust: "Feste Punkte bewerten, Notizen schreiben.", + merke: "Vier Stufen von Anfänger bis Meister." }, + { kachel: "content", stufe: "bedarf", + wozu: "Content planen.", + tust: "Ideen durch fünf Stufen führen.", + merke: "61 feste Punkte als Leitfaden." }, + { kachel: "bereich:community", stufe: "bedarf", + wozu: "Moderation und Konflikte.", + tust: "Vorbereitung und Vorfälle dokumentieren.", + merke: "58 feste Punkte." }, + { kachel: "bereich:technik", stufe: "bedarf", + wozu: "Setup und Fehler.", + tust: "Dokumentieren, was klemmt und was geholfen hat.", + merke: "51 feste Punkte." }, + { kachel: "dateien", stufe: "bedarf", + wozu: "Ablage und Freigaben.", + tust: "Hochladen, Sichtbarkeit wählen, freigeben.", + merke: "Entwurf, Zur Freigabe, Freigegeben." }, + { kachel: "wissen", stufe: "bedarf", + wozu: "Anleitungen zu TikTok und Technik.", + tust: "Nachschlagen und weitergeben.", + merke: "Filter nach Stufe, Gerät, Tag." }, + { kachel: "support", stufe: "bedarf", + wozu: "Etwas ist kaputt.", + tust: "Beschreiben, Bilder anhängen.", + merke: "Geht an DogFather und die rechte Hand." }, + ], + nicht: [ + { titel: "Personen sperren, Codes verwalten, Protokoll einsehen.", + text: "Das liegt bei DogFather." }, + { titel: "Kontakte suchen und ansprechen.", + text: "Das machen die Scouts. Du übernimmst ab „Übergeben“." }, + { titel: "Alle Fach-Kacheln für jeden Creator täglich öffnen.", + text: "Die Zentrale und das Dashboard zeigen, wo etwas fehlt." }, + ], + takt: [ + { titel: "Täglich", text: "Zentrale, Dashboard, Aufgaben (Spalte „Zur Freigabe“), Chat." }, + { titel: "Wöchentlich", + text: "Calls & Protokolle, Creator-Profile, Start-Check, Scout-Pipeline (Übergaben)." }, + { titel: "Monatlich", text: "Manager-Ziele, Reports & Review, Agentur-Events." }, + ], + fragen: [ + { titel: "Ein Creator hat seinen Code verloren.", + text: "Unter Personen & Zugänge bei der Person „Neuer Code“. Der alte gilt dann nicht mehr." }, + { titel: "Kann ich Personen sperren?", + text: "Nein. Codes verwalten, sperren und das Protokoll einsehen kann nur DogFather." }, + { titel: "Was passiert bei einer Rollenänderung?", + text: "Der Code bleibt, offene Sitzungen enden, die Person meldet sich neu an." }, + { titel: "Woran erkenne ich, wo es brennt?", + text: "Zentrale: der Satz unter „Zentrale“ und die Liste „Was ist dran“." }, + ], + }, + + spicy: { + leitgedanke: + "Überblick über alle, ohne Automationen. Du siehst das ganze Haus und zusätzlich " + + "die Team-Lage: wer wen betreut und wo sich etwas staut.", + saetze: [ + "Du brauchst den Überblick, nicht die Einzelheiten jedes Creators.", + "Team-Lage und Dashboard zeigen dir, wo sich etwas staut.", + "In der Kachel Agentur kommen die Anliegen der Creator direkt bei dir an.", + ], + schritte: [ + { titel: "Mein Steckbrief", ziel: "steckbrief.html", + text: "Ausfüllen: Bild, Satz, Kanäle." }, + { titel: "Team-Lage", ziel: "team.html", + text: "Auslastung von Managern und Scouts, Creator ohne Betreuung." }, + { titel: "Dashboard", ziel: "uebersicht.html", + text: "Alle Creator mit Terminen, Aufgaben und Start-Check." }, + { titel: "Agentur", ziel: "bereich.html?b=agentur", + text: "Events, Schulungen und Anliegen an die Agentur." }, + { titel: "Kopfleiste", ziel: null, + text: "Oben findest du Teilen, Chat und Suchen." }, + ], + karten: [ + { kachel: "team", stufe: "muss", + wozu: "Manager und Scouts im Blick.", + tust: "Sehen, wer wen betreut, wo jemand fehlt, wer ohne Creator ist.", + merke: "Arbeitslage, keine Überwachung: keine Inhalte aus Terminen, Chats oder Dateien." }, + { kachel: "uebersicht", stufe: "muss", + wozu: "Alle Creator.", + tust: "Je Creator den Stand prüfen.", + merke: "Zeigt auch, wer lange nicht da war." }, + { kachel: "bereich:agentur", stufe: "muss", + wozu: "Offizieller Weg zur Agentur.", + tust: "Events mit Aufgaben anlegen, Schulungen eintragen, Anliegen beantworten.", + merke: "Hier schreiben Creator direkt an die Agentur." }, + { kachel: "chat", stufe: "muss", + wozu: "Nachrichten.", + tust: "Personen, Gruppen, Kanäle.", + merke: "Kanäle für Ankündigungen nutzen." }, + { kachel: "steckbrief", stufe: "muss", + wozu: "So sehen dich die anderen.", + tust: "Einmal ausfüllen: Bild, Satz, Kanäle.", + merke: "Steht unter „Leitung“ in der Teamliste." }, + + { kachel: "aufgaben", stufe: "regelmaessig", + wozu: "Wer macht was.", + tust: "Verteilung sehen, freigeben.", + merke: "Filter nach Person." }, + { kachel: "manager-ziele", stufe: "regelmaessig", + wozu: "Feste Monatsaufgaben.", + tust: "Eigene Einträge machen, Stand des Teams verfolgen.", + merke: "Vorgabe der Agentur, jeden Monat neu." }, + { kachel: "scouting", stufe: "regelmaessig", + wozu: "Alle Scouts.", + tust: "Nach Scout filtern, liegengebliebene Kontakte erkennen.", + merke: "Fünf Stufen bis zur Übergabe." }, + { kachel: "kalender", stufe: "regelmaessig", + wozu: "Termine und Calls.", + tust: "Schulungen und Events eintragen.", + merke: "Wiederholungen laufen von allein." }, + { kachel: "report", stufe: "regelmaessig", + wozu: "Rückblick und Entscheidung.", + tust: "Einmal im Monat: Zeitraum wählen, Entwicklung prüfen.", + merke: "7, 30 oder 90 Tage." }, + + { kachel: "calls", stufe: "bedarf", + wozu: "Gespräche festhalten.", + tust: "Protokolle lesen und schreiben.", + merke: "To-dos werden Aufgaben." }, + { kachel: "dateien", stufe: "bedarf", + wozu: "Ablage und Freigaben.", + tust: "Hochladen, Sichtbarkeit wählen, freigeben.", + merke: "Aktuelle Version ist immer erkennbar." }, + { kachel: "profil", stufe: "bedarf", + wozu: "Akte je Creator.", + tust: "Ziele und 90-Tage-Plan einsehen.", + merke: "Leere Profile werden gezählt." }, + { kachel: "leistung", stufe: "bedarf", + wozu: "Leistung je Creator.", + tust: "Wochenzahlen und Zeiträume ansehen.", + merke: "Quelle ist Backstage." }, + { kachel: "startcheck", stufe: "bedarf", + wozu: "Erstanalyse.", + tust: "Stand der Bewertung einsehen, bei Bedarf eintragen.", + merke: "Teil der Betreuungsakte." }, + { kachel: "bereich:live", stufe: "bedarf", + wozu: "LIVEs verbessern.", + tust: "Stand der Bewertung einsehen, bei Bedarf eintragen.", + merke: "Teil der Betreuungsakte." }, + { kachel: "content", stufe: "bedarf", + wozu: "Content planen.", + tust: "Stand einsehen, bei Bedarf eintragen.", + merke: "Teil der Betreuungsakte." }, + { kachel: "bereich:community", stufe: "bedarf", + wozu: "Moderation und Konflikte.", + tust: "Stand einsehen, bei Bedarf eintragen.", + merke: "Teil der Betreuungsakte." }, + { kachel: "bereich:technik", stufe: "bedarf", + wozu: "Setup und Fehler.", + tust: "Stand einsehen, bei Bedarf eintragen.", + merke: "Teil der Betreuungsakte." }, + { kachel: "bereich:schutz", stufe: "bedarf", + wozu: "Unterweisungen.", + tust: "Sehen, wer welche Dokumente bestätigt hat.", + merke: "Bestätigungen sind der Nachweis." }, + { kachel: "personen", stufe: "bedarf", + wozu: "Neue Leute anlegen.", + tust: "Person anlegen, Rolle wählen, Code weitergeben.", + merke: "Codes, Sperren und Protokoll bleiben bei DogFather." }, + { kachel: "wissen", stufe: "bedarf", + wozu: "Anleitungen und PDFs.", + tust: "Nachschlagen.", + merke: "Schulungs-PDFs der Agentur gehören hierher." }, + { kachel: "support", stufe: "bedarf", + wozu: "Etwas ist kaputt.", + tust: "Beschreiben, Bilder anhängen.", + merke: "Geht an DogFather und die rechte Hand." }, + ], + nicht: [ + { titel: "Die tägliche Betreuung einzelner Creator.", + text: "Das machen Manager und Scouts." }, + { titel: "Die Betreuungsakte selbst füllen.", + text: "Du schaust rein, wenn es einen Anlass gibt." }, + { titel: "Automationen (Sicherungen, lokale KI, Hintergrunddienste).", + text: "Die verwaltet nur DogFather." }, + { titel: "Codes sperren und Protokoll.", text: "Bleibt bei DogFather." }, + ], + takt: [ + { titel: "Täglich", text: "Zentrale, Team-Lage, Chat." }, + { titel: "Wöchentlich", + text: "Dashboard, Scout-Pipeline, Agentur (Anliegen beantworten), Aufgaben zur Freigabe." }, + { titel: "Monatlich", + text: "Manager-Ziele des Teams, Reports & Review, neue Events eintragen." }, + ], + fragen: [ + { titel: "Was heißt „ohne Automationen“?", + text: "Sicherungen, lokale KI und Hintergrunddienste verwaltet nur DogFather." }, + { titel: "Sehe ich private Chats oder Termine?", + text: "Nein. Die Team-Lage zeigt nur die Arbeitslage." }, + { titel: "Wo beantworte ich Anliegen der Creator?", + text: "In der Kachel Agentur unter „Anliegen an die Agentur“." }, + { titel: "Wo trage ich ein neues Event ein?", + text: "Agentur, „Neuer Eintrag“, Art „Agentur-Events“, mit Aufgaben und Frist." }, + ], + }, }; /* ===================================================================== diff --git a/server/mess-anleitung.mjs b/server/mess-anleitung.mjs index 4bdc3c86..89041d59 100644 --- a/server/mess-anleitung.mjs +++ b/server/mess-anleitung.mjs @@ -64,6 +64,8 @@ function anlegen(name, rolle, code) { } anlegen("Luna", "creator", "CODE-LUNA-0001"); anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Cigdem", "spicy", "CODE-SPICY-0001"); +anlegen("Neu Hier", "creator", "CODE-NEU-0001"); d.close(); function anfrage(pfad, methode, koerper, keks) { @@ -95,6 +97,8 @@ async function keksFuer(rolle, code) { } const keksCreator = await keksFuer("creator", "CODE-LUNA-0001"); const keksDogi = await keksFuer("admin", "CODE-DOGI-0001"); +const keksSpicy = await keksFuer("spicy", "CODE-SPICY-0001"); +const keksNeu = await keksFuer("creator", "CODE-NEU-0001"); /* ---------- Bilder ---------------------------------------------------- */ const pw = await import( @@ -265,6 +269,75 @@ console.log(` await startBild("start-ohne-schritte", keksCreator, 412); await bild("creator-fertig", keksCreator, 412, 950); +/* ===================================================================== + DIE PFLEGE (Etappe 4) -- und ob sie LEISE ist, solange sie aus ist + ===================================================================== */ +await bild("spicy-pflege-aus", keksSpicy, 1440, 1000); +await bild("spicy-pflege-an", keksSpicy, 1440, 1000, async (seite) => { + await seite.click("#pflege-an"); + await seite.waitForTimeout(500); +}); + +/* ===================================================================== + DER RUNDGANG (Etappe 6) + ===================================================================== + Eine FRISCHE Person -- "Neu Hier" hat die Einweisung noch nie + gesehen. Gemessen wird nicht nur, dass das Fenster kommt, sondern + auch, dass die Seite danach wieder NORMAL ist: Ein Rundgang, der + die Startseite gedaempft zuruecklaesst, waere schlimmer als keiner. + ===================================================================== */ +async function rundgangBild(name, keks, breite, schritte) { + const kontext = await browser.newContext({ + viewport: { width: breite, height: 1000 }, hasTouch: breite <= 860, + }); + await kontext.addCookies(keks.split("; ").map((k) => { + const [n, ...r] = k.split("="); + return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (e) => { + if (e.type() === "error" && !/favicon|net::ERR/i.test(e.text())) konsole.push(e.text().slice(0, 160)); + }); + seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); + + await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" }); + /* Das Skript wartet absichtlich 1,2 s, damit die Kacheln stehen. */ + await seite.waitForTimeout(2600); + + const gruss = await seite.$(".rg-gruss"); + console.log(`\n--- ${name} (${breite}px) ---`); + console.log(` Begruessung da: ${gruss ? "ja" : "NEIN"}`); + if (gruss) { + await seite.screenshot({ path: `bild-anleitung-${name}-gruss.png` }); + await seite.click(".rg-gruss .knopf"); + await seite.waitForTimeout(700); + } + for (let i = 1; i <= schritte; i++) { + const stand = await seite.evaluate(() => ({ + zaehler: document.querySelector(".rg__zaehler")?.textContent || "(kein Kasten)", + titel: document.querySelector(".rg__titel")?.textContent || "", + gedaempft: document.body.dataset.rundgang === "ja", + ziele: document.querySelectorAll("[data-rundgang-ziel]").length, + mussKacheln: document.querySelectorAll('[data-rundgang-muss="ja"]').length, + })); + console.log(` ${stand.zaehler.padEnd(18)} ${stand.titel.padEnd(28)} ` + + `Ziel ${stand.ziele} Muss-Kacheln ${stand.mussKacheln}`); + if (i === schritte) await seite.screenshot({ path: `bild-anleitung-${name}-letzter.png` }); + const weiter = await seite.$(".rg__weiter"); + if (!weiter) break; + if (i < schritte) { await weiter.click(); await seite.waitForTimeout(600); } + } + if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`); + await kontext.close(); +} + +await rundgangBild("rundgang", keksNeu, 412, 5); + +/* Und: Wer die Begruessung schon gesehen hat, bekommt sie nicht noch + einmal. Dieselbe Person, zweiter Aufruf. */ +await rundgangBild("rundgang-zweites-mal", keksNeu, 412, 1); + await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log("\nBilder geschrieben: bild-anleitung-*.png"); diff --git a/server/pruef-anleitung.mjs b/server/pruef-anleitung.mjs index 5e1379cd..c2234f9d 100644 --- a/server/pruef-anleitung.mjs +++ b/server/pruef-anleitung.mjs @@ -235,6 +235,9 @@ melde("\n=== Die Stufen ==="); const jeStufe = {}; for (const k of creator.j.karten) jeStufe[k.stufe] = (jeStufe[k.stufe] || 0) + 1; const bekannt = new Set((creator.j.stufen || []).map((s) => s.schluessel)); +/* Dieselbe Menge weiter unten fuer alle vier Rollen -- der Server + nennt die Stufen, diese Datei kennt keine eigene Liste. */ +const STUFEN_DA = bekannt; ok([...Object.keys(jeStufe)].every((s) => bekannt.has(s)), `jede Karte traegt eine bekannte Stufe (${Object.keys(jeStufe).join(", ")})`); ok(Object.values(jeStufe).reduce((a, b) => a + b, 0) === creator.j.karten.length, @@ -388,6 +391,94 @@ ok(nachher?.merke === "VON HAND GEAENDERT", `der geaenderte Text steht noch da ("${(nachher?.merke || "").slice(0, 24)}")`); ok(anzahlNachher === 18, `und es sind keine Karten dazugekommen (${anzahlNachher})`); +/* ===================================================================== + ALLE VIER FASSUNGEN — JEDE GEGEN IHRE EIGENEN KACHELN + ===================================================================== + + Oben ist der Creator Zeile fuer Zeile geprueft. Hier laufen alle vier + durch dieselbe Zuordnung, und die ZAHLEN WERDEN ABGELEITET, nicht + abgeschrieben: Wie viele Karten eine Rolle haben muss, sagt ihre + Kachelliste -- nicht die Abnahmeliste im Bauplan. + + Der Bauplan nennt 18/21/22/23. Die stimmen heute. Am Tag, an dem + eine Kachel dazukommt, waere eine feste Zahl hier eine Luege, die + erst auffaellt, wenn jemand sie nachzaehlt. Also zaehlt die + Pruefung selbst -- und meldet die Zahl, damit man sie lesen kann. + ===================================================================== */ +melde("\n=== Jede Rolle erklaert genau ihre eigenen Kacheln ==="); +const FASSUNGEN_HIER = ["creator", "scout", "manager", "spicy"]; +for (const rolle of FASSUNGEN_HIER) { + const a = await holen(rolle); + if (a.code !== 200 || !a.j?.hat_fassung) { + ok(false, `${rolle.padEnd(8)} hat keine Fassung (HTTP ${a.code})`); + continue; + } + const soll = kachelnFuer(rolle); + soll.delete("anleitung"); // die Seite erklaert sich nicht selbst + const ist = new Set(a.j.karten.map((k) => k.kachel)); + const fehlt = [...soll.keys()].filter((x) => !ist.has(x)); + const zuviel = [...ist].filter((x) => !soll.has(x)); + + ok(soll.size > 0 && fehlt.length === 0 && zuviel.length === 0, + fehlt.length || zuviel.length + ? `${rolle.padEnd(8)} stimmt nicht -- ohne Karte: ${fehlt.join(", ") || "nichts"}; ` + + `ohne Kachel: ${zuviel.join(", ") || "nichts"}` + : `${rolle.padEnd(8)} ${soll.size} Kacheln, ${ist.size} Karten, eins zu eins`); + + /* Jede Karte vollstaendig. Eine halbe Karte sieht aus wie ein + Ladefehler, und zwar auf jeder der vier Seiten gleich. */ + const halb = a.j.karten.filter((k) => !k.wozu || !k.tust || !k.merke); + ok(halb.length === 0, + halb.length ? `${rolle.padEnd(8)} ${halb.length} unvollstaendige Karten: ` + + halb.map((k) => k.kachel).join(", ") + : `${rolle.padEnd(8)} jede Karte hat alle drei Zeilen`); + + /* Die Stufen -- gezaehlt, nicht vorgegeben. */ + const je = {}; + for (const k of a.j.karten) je[k.stufe] = (je[k.stufe] || 0) + 1; + const summe = Object.values(je).reduce((x, y) => x + y, 0); + ok(summe === a.j.karten.length + && Object.keys(je).every((x) => STUFEN_DA.has(x)), + `${rolle.padEnd(8)} Stufen ${je.muss || 0}/${je.regelmaessig || 0}/${je.bedarf || 0} ` + + `(zusammen ${summe} von ${a.j.karten.length})`); + + /* Die Ersten Schritte duerfen nur auf Kacheln dieser Rolle zeigen -- + oder auf gar nichts ("Installieren", "Kopfleiste" sind Knoepfe in + der Kopfleiste, keine Seiten). */ + const alleKacheln = kachelnFuer(rolle); + const ins_leere = (a.j.schritte || []).filter((x) => x.ziel) + .filter((x) => !alleKacheln.has(B.schluesselVonZiel(x.ziel))); + ok((a.j.schritte || []).length === 5 && ins_leere.length === 0, + ins_leere.length + ? `${rolle.padEnd(8)} Schritte ins Leere: ${ins_leere.map((x) => x.ziel).join(", ")}` + : `${rolle.padEnd(8)} fuenf Erste Schritte, alle mit gueltigem Ziel`); + + /* Und der Rest der Fassung ist da. Eine Rolle ohne Rhythmus oder + ohne "Nicht deins" waere eine halbe Einweisung. */ + ok(!!a.j.leitgedanke && (a.j.saetze || []).length === 3 + && (a.j.nicht || []).length >= 3 && (a.j.takt || []).length >= 3 + && (a.j.fragen || []).length >= 3, + `${rolle.padEnd(8)} Leitgedanke, 3 Saetze, ${(a.j.nicht || []).length} x Nicht deins, ` + + `${(a.j.takt || []).length} Takt-Zeilen, ${(a.j.fragen || []).length} Fragen`); +} + +/* GEGENPROBE ZUR SCHLEIFE: Sie darf nicht leer durchgelaufen sein. + Vier Rollen, und jede musste wirklich etwas liefern. */ +ok(FASSUNGEN_HIER.length === 4, `vier Fassungen durchgegangen`); + +/* Und: Die Fassungen muessen sich UNTERSCHEIDEN. Lieferte der Server + allen dieselbe Liste, waeren alle Zeilen oben trotzdem gruen -- + solange die Kachellisten zufaellig passten. */ +{ + const koepfe = []; + for (const rolle of FASSUNGEN_HIER) { + const a = await holen(rolle); + koepfe.push(a.j?.leitgedanke || ""); + } + ok(new Set(koepfe).size === 4, + `Gegenprobe: vier verschiedene Leitgedanken (${new Set(koepfe).size} verschiedene)`); +} + /* ===================================================================== ETAPPE 2 — DIE ERSTEN SCHRITTE ZUM ABHAKEN ===================================================================== */ @@ -522,6 +613,199 @@ ok(hWieder.liste.some((h) => h.art === "anl_schritte" && h.anzahl === 1), sehen -- der Haken auf Schritt 2 und die vier von eben. */ for (const s of schritte0) await haken("creator", s.id, false); +/* ===================================================================== + ETAPPE 4 — DIE PFLEGE + ===================================================================== */ +melde("\n=== Pflege: wer darf ==="); +const pput = (rolle, pfad, rumpf, host = null) => + roh(pfad, host, rumpf, kekse[rolle], "PUT"); + +const eineKarte = (await holen("creator")).j.karten.find((k) => k.kachel === "chat"); +braucht(!!eineKarte?.id, "keine Karte zum Bearbeiten gefunden"); + +const pCreator = await pput("creator", `/workspace/api/anleitung/karte/${eineKarte.id}`, + { stufe: "bedarf", wozu: "x", tust: "y", merke: "z" }); +ok(pCreator.code === 403, `ein Creator darf nicht pflegen (HTTP ${pCreator.code}, erwartet 403)`); +const pScout = await pput("scout", `/workspace/api/anleitung/karte/${eineKarte.id}`, + { stufe: "bedarf", wozu: "x", tust: "y", merke: "z" }); +ok(pScout.code === 403, `ein Scout auch nicht (HTTP ${pScout.code}, erwartet 403)`); +const pModi = await pput("modi", `/workspace/api/anleitung/karte/${eineKarte.id}`, + { stufe: "bedarf", wozu: "x", tust: "y", merke: "z" }, CREW_HOST); +ok(pModi.code === 404, `ein Modi bekommt die Pflege gar nicht (HTTP ${pModi.code}, erwartet 404)`); + +melde("\n=== Pflege: Spicy aendert eine Stufe, und sie gilt sofort ==="); +const vorher = eineKarte.stufe; +const pSpicy = await pput("spicy", `/workspace/api/anleitung/karte/${eineKarte.id}`, + { stufe: "bedarf", wozu: "Nachrichten mit deinem Team.", + tust: "Mit „Neu“ ein Gespräch beginnen.", merke: "GEAENDERT VON SPICY" }); +ok(pSpicy.code === 200, `Spicy Media darf (HTTP ${pSpicy.code})`); +const nachSpicy = await holen("creator"); +const chatNeu = nachSpicy.j.karten.find((k) => k.kachel === "chat"); +ok(chatNeu?.stufe === "bedarf", + `die Stufe steht sofort in der Creator-Fassung (${vorher} -> ${chatNeu?.stufe})`); +ok(chatNeu?.merke === "GEAENDERT VON SPICY", "…und der Text auch"); + +/* DIE MARKE "NEU". Der Creator hat die Seite gerade geladen -- damit + ist sein Besuch juenger als die Aenderung davor, aber AELTER als die + von eben. Die Reihenfolge ist der ganze Beweis. */ +ok(chatNeu?.neu === true, "die geaenderte Karte traegt die Marke „Neu“"); +const nochmal = await holen("creator"); +ok(nochmal.j.karten.find((k) => k.kachel === "chat")?.neu === false, + "…und beim zweiten Hinsehen nicht mehr"); + +/* Zurueck auf Anfang, damit die Zuordnungspruefung oben weiter stimmt. */ +await pput("spicy", `/workspace/api/anleitung/karte/${eineKarte.id}`, + { stufe: vorher, wozu: "Nachrichten mit deinem Team.", + tust: "Mit „Neu“ ein Gespräch beginnen.", + merke: "Fehler an der Seite gehören in Support, nicht in den Chat." }); + +melde("\n=== Pflege: was nicht durchgehen darf ==="); +const pUnsinn = await pput("admin", `/workspace/api/anleitung/karte/${eineKarte.id}`, + { stufe: "gibtesnicht", wozu: "", tust: "", merke: "" }); +ok(pUnsinn.code === 400, `eine erfundene Stufe wird abgelehnt (HTTP ${pUnsinn.code})`); +const pWeg = await pput("admin", "/workspace/api/anleitung/karte/999999", + { stufe: "muss", wozu: "", tust: "", merke: "" }); +ok(pWeg.code === 404, `eine Karte, die es nicht gibt (HTTP ${pWeg.code})`); + +melde("\n=== Pflege: fehlenden Eintrag anlegen und wieder entfernen ==="); +/* Erst loeschen, dann ist er "fehlend" -- genau der Fall, den der + Bauplan "Fuer diese Kachel fehlt ein Eintrag" nennt. */ +const weg = await roh(`/workspace/api/anleitung/karte/${eineKarte.id}`, null, null, + kekse.admin, "DELETE"); +ok(weg.code === 200, `DogFather kann eine Karte entfernen (HTTP ${weg.code})`); +const ohne = await holen("creator"); +ok(ohne.j.karten.length === 17 && !ohne.j.karten.some((k) => k.kachel === "chat"), + `danach 17 Karten, die Chat-Karte fehlt (${ohne.j.karten.length})`); + +const neu1 = await roh("/workspace/api/anleitung/karte", null, + { rolle: "creator", kachel: "chat", stufe: "muss", + wozu: "Nachrichten mit deinem Team.", tust: "Mit „Neu“ ein Gespräch beginnen.", + merke: "Fehler an der Seite gehören in Support, nicht in den Chat." }, + kekse.admin, "POST"); +ok(neu1.code === 200, `…und wieder anlegen (HTTP ${neu1.code})`); +const neu2 = await roh("/workspace/api/anleitung/karte", null, + { rolle: "creator", kachel: "chat", stufe: "muss", wozu: "", tust: "", merke: "" }, + kekse.admin, "POST"); +ok(neu2.code === 409, + `ein zweites Mal nicht -- eine Kachel hat genau eine Karte (HTTP ${neu2.code})`); +const zurueck = await holen("creator"); +ok(zurueck.j.karten.length === 18, + `wieder 18 Karten (${zurueck.j.karten.length})`); + +melde("\n=== Pflege: eine Zeile aendern ==="); +const einSchritt = zurueck.j.schritte[0]; +const zAend = await pput("admin", `/workspace/api/anleitung/zeile/${einSchritt.id}`, + { titel: einSchritt.titel, text: "GEAENDERTER TEXT", ziel: einSchritt.ziel }); +ok(zAend.code === 200, `eine Listenzeile laesst sich aendern (HTTP ${zAend.code})`); +const nachZeile = await holen("creator"); +ok(nachZeile.j.schritte[0]?.text === "GEAENDERTER TEXT", "…und der Text steht da"); +await pput("admin", `/workspace/api/anleitung/zeile/${einSchritt.id}`, + { titel: einSchritt.titel, text: einSchritt.text, ziel: einSchritt.ziel }); + +melde("\n=== Pflege: wer ist wie weit ==="); +const standR = await roh("/workspace/api/anleitung/stand", null, null, kekse.admin); +let standJ = null; try { standJ = JSON.parse(standR.text); } catch { /* bleibt null */ } +ok(standR.code === 200 && Array.isArray(standJ?.leute), + `DogFather sieht den Stand (HTTP ${standR.code}, ${standJ?.leute?.length ?? 0} Leute)`); +ok((standJ?.leute || []).some((p) => p.rolle === "creator" && p.gesamt === 5), + "…und der Creator steht mit fuenf Schritten drin"); +const standC = await roh("/workspace/api/anleitung/stand", null, null, kekse.creator); +ok(standC.code === 403, `ein Creator sieht ihn nicht (HTTP ${standC.code}, erwartet 403)`); + +/* ===================================================================== + ETAPPE 5 — DIE SUCHE + ===================================================================== */ +melde("\n=== Suche ==="); +async function suchen(rolle, wort, host = null) { + const r = await roh(`/workspace/api/suche?q=${encodeURIComponent(wort)}`, host, null, kekse[rolle]); + let j = null; try { j = JSON.parse(r.text); } catch { /* bleibt null */ } + const g = (j?.gruppen || []).find((x) => x.titel === "Anleitung"); + return { code: r.code, treffer: g?.treffer || [], gruppen: (j?.gruppen || []).length }; +} + +/* Ein Wort, das NUR in der Creator-Fassung steht. */ +const sCreator = await suchen("creator", "Jemand schaut noch drauf"); +ok(sCreator.code === 200 && sCreator.treffer.length > 0, + `der Creator findet seine eigene Karte (${sCreator.treffer.length} Treffer)`); +/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. `every()` liefert auf einer LEEREN + Liste `true` -- diese Zeile war beim ersten Lauf gruen, ohne einen + einzigen Treffer angesehen zu haben. Genau der Fehler vom 01.09.2026 + im Shop, nur mit anderen Namen. */ +ok(sCreator.treffer.length > 0 + && sCreator.treffer.every((t) => t.ziel && !t.ziel.includes("undefined")), + `…und jeder der ${sCreator.treffer.length} Treffer hat ein Ziel ` + + `(${sCreator.treffer.map((t) => t.ziel).join(", ") || "keine"})`); + +/* Ein Wort, das NUR in der Manager-Fassung steht. */ +const sFremd = await suchen("creator", "Zuteilen, freigeben"); +ok(sFremd.treffer.length === 0, + `der Creator findet die Manager-Karte NICHT (${sFremd.treffer.length} Treffer)`); +const sManager = await suchen("manager", "Zuteilen, freigeben"); +ok(sManager.treffer.length > 0, + `Gegenprobe: der Manager findet sie sehr wohl (${sManager.treffer.length} Treffer)`); + +const sModi = await suchen("modi", "Freigabe heißt", CREW_HOST); +ok(sModi.code === 200 && sModi.treffer.length === 0, + `ein Modi findet nichts davon (HTTP ${sModi.code}, ${sModi.treffer.length} Treffer)`); + +/* Die Adresse im Treffer muss den Zusatz hinter dem Fragezeichen + tragen -- sonst landen fuenf Karten auf derselben Seite. */ +const sBereich = await suchen("creator", "Ruhig statt Drama"); +ok(sBereich.treffer.some((t) => t.ziel === "bereich.html?b=community"), + `ein Bereichs-Treffer traegt seinen Zusatz (${sBereich.treffer.map((t) => t.ziel).join(", ")})`); + +/* ===================================================================== + ETAPPE 6 — BEGRUESSUNG UND RUNDGANG + ===================================================================== */ +melde("\n=== Einweisung beim ersten Anmelden ==="); +async function einweisung(rolle, host = null) { + const r = await roh("/workspace/api/anleitung/einweisung", host, null, kekse[rolle]); + let j = null; try { j = JSON.parse(r.text); } catch { /* bleibt null */ } + return { code: r.code, j }; +} + +const e1 = await einweisung("creator"); +ok(e1.code === 200 && e1.j?.zeigen === true, + `beim ersten Mal wird sie gezeigt (HTTP ${e1.code}, zeigen ${e1.j?.zeigen})`); +ok(Array.isArray(e1.j?.muss) && e1.j.muss.length === 5, + `…mit den fuenf Muss-Kacheln (${e1.j?.muss?.length})`); + +const gesehen = await roh("/workspace/api/anleitung/einweisung/gesehen", null, {}, kekse.creator, + "POST"); +ok(gesehen.code === 200, `„gesehen“ laesst sich merken (HTTP ${gesehen.code})`); +const e2 = await einweisung("creator"); +ok(e2.j?.zeigen === false, "beim zweiten Mal nicht mehr"); + +/* DIE CREW BEKOMMT 200 UND NICHT 404. Das Skript laeuft auf JEDER + Startseite; ein Fehlschlag waere eine rote Zeile in der Konsole bei + jedem Aufruf -- und die verdeckt die naechste, die echt ist. */ +const eModi = await einweisung("modi", CREW_HOST); +ok(eModi.code === 200 && eModi.j?.zeigen === false, + `ein Modi bekommt eine ruhige Absage (HTTP ${eModi.code}, zeigen ${eModi.j?.zeigen})`); +const eDogi = await einweisung("admin"); +ok(eDogi.code === 200 && eDogi.j?.zeigen === false, + `DogFather auch -- er braucht keine Einweisung (zeigen ${eDogi.j?.zeigen})`); + +/* DER ROLLENWECHSEL. Der Bauplan verlangt, dass die Einweisung dann + einmal neu startet. Gebaut ist das NICHT als Sonderregel, sondern + ueber den Schluessel (Person, Rolle) -- die neue Rolle hat schlicht + noch keine Zeile. Genau das wird hier nachgemessen. */ +{ + const d4 = new DatabaseSync(process.env.WORKSPACE_DB); + d4.prepare("UPDATE personen SET rolle = 'scout' WHERE rolle = 'creator'").run(); + d4.close(); + const neuAn = await anmelden("scout"); + const alt = kekse.scout; + kekse.scout = neuAn || alt; + const eNach = await einweisung("scout"); + kekse.scout = alt; + ok(eNach.j?.zeigen === true, + `nach einem Rollenwechsel startet sie einmal neu (zeigen ${eNach.j?.zeigen})`); + const d5 = new DatabaseSync(process.env.WORKSPACE_DB); + d5.prepare("UPDATE personen SET rolle = 'creator' WHERE name = 'Luna'").run(); + d5.close(); +} + /* ---------- 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-eventkarte.mjs b/server/pruef-eventkarte.mjs index 7b58fc42..314904a9 100644 --- a/server/pruef-eventkarte.mjs +++ b/server/pruef-eventkarte.mjs @@ -31,6 +31,7 @@ import { mkdtempSync, rmSync, appendFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { tagLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-evk-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -85,8 +86,13 @@ function anlegen(name, rolle, code) { const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001"); const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001"); -const heute = new Date(); -const tag = (v) => new Date(heute.getTime() + v * 86400000).toISOString().slice(0, 10); +/* ORTSZEIT, NICHT UTC (berichtigt 06.10.2026, gefunden von pruef-struktur). + Hier stand `toISOString().slice(0, 10)`. Zwischen 00:00 und 02:00 + deutscher Zeit liegt UTC noch im Vortag -- ein Lauf um Mitternacht + haette Termine einen Tag zu frueh angelegt und dann an der falschen + Stelle gesucht. `tagLokal` ist die Stelle, an der das Haus das + einmal richtig macht. */ +const tag = (v) => tagLokal(v); function evt(titel, von, bis, auf, reg) { d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum, event_ende," + " event_aufgaben, event_regeln, dringlichkeit, erstellt, erstellt_von)" diff --git a/server/pruef-haus-luecke.mjs b/server/pruef-haus-luecke.mjs index 959fa1ec..59dee281 100644 --- a/server/pruef-haus-luecke.mjs +++ b/server/pruef-haus-luecke.mjs @@ -37,6 +37,7 @@ import { mkdtempSync, rmSync, appendFileSync, readFileSync, readdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-hl-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -142,7 +143,10 @@ function anlegen(name, rolle, code) { return d.prepare("SELECT last_insert_rowid() AS id").get().id; } anlegen("Dogfather", "admin", "CODE-DOGI-0001"); -const heute = new Date().toISOString().slice(0, 10); +/* ORTSZEIT statt UTC -- siehe helfer-zeit.mjs. Nachts zwischen + 00:00 und 02:00 liegt UTC einen Tag zurueck, und dann sucht diese + Pruefung an einem Datum, das sie selbst nicht geschrieben hat. */ +const heute = heuteLokal(); /* Drei Marken auf demselben Brett: eine je Haus und eine ohne. */ for (const [titel, haus] of [["Marke-CREW", "crew"], ["Marke-AGENTUR", "agentur"], ["Marke-OHNE", null]]) { d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum, dringlichkeit, erstellt, erstellt_von, haus)" diff --git a/server/pruef-rollen.mjs b/server/pruef-rollen.mjs index 9b63b9b6..dec79b66 100644 --- a/server/pruef-rollen.mjs +++ b/server/pruef-rollen.mjs @@ -133,6 +133,17 @@ const idDogi = person("Filipe", "admin", "CODE-DOGI-0001"); const idCigdem = person("Cigdem", "manager", "CODE-CIGDEM-01"); const idBen = person("Ben", "scout", "CODE-BEN-00001"); const idTili = person("Tili", "creator", "CODE-TILI-0001"); +/* SPICY MEDIA HAT HIER VIER WOCHEN GEFEHLT (06.10.2026). + + Von den fuenf Rollen des Agenturhauses liefen fuenf Durchgaenge -- + aber zweimal Manager und zweimal Scout, und Spicy Media gar nicht. + Gemerkt habe ich es erst, als die Anleitung ihr eine eigene + Fassung gab und ich wissen wollte, ob die Seite fuer sie aufgeht. + + Sie ist nicht "noch eine Rolle": Ihr gehoert die Gruppe "Rund um + das Team" samt Team-Lage, die sonst niemand ausser DogFather hat. + Diese Kachel ist also auf keinem Durchgang je geoeffnet worden. */ +person("Spicy", "spicy", "CODE-SPICY-00001"); /* Zwei Neue ohne jede Zuteilung -- der Zustand am ersten Tag. */ person("Neu-Scout", "scout", "CODE-NEU-000001"); person("Neu-Manager", "manager", "CODE-NEUM-00001"); @@ -217,6 +228,7 @@ const ROLLEN = [ { rolle: "manager", code: "CODE-CIGDEM-01", name: "Manager (mit Creator)" }, { rolle: "scout", code: "CODE-BEN-00001", name: "Scout (mit Creator)" }, { rolle: "creator", code: "CODE-TILI-0001", name: "Creator (Tili)" }, + { rolle: "spicy", code: "CODE-SPICY-00001", name: "Spicy Media" }, { rolle: "scout", code: "CODE-NEU-000001", name: "Scout (OHNE Creator)" }, { rolle: "manager", code: "CODE-NEUM-00001", name: "Manager (OHNE Creator)" }, /* ==== DIE BEIDEN AUS DEM ANDEREN HAUS (berichtigt 06.10.2026) ==== diff --git a/server/workspace-anleitung.js b/server/workspace-anleitung.js index a0752f9a..c9cf5f22 100644 --- a/server/workspace-anleitung.js +++ b/server/workspace-anleitung.js @@ -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" }); + } +}); diff --git a/server/workspace-suche.js b/server/workspace-suche.js index b22e36e0..98c45725 100644 --- a/server/workspace-suche.js +++ b/server/workspace-suche.js @@ -35,6 +35,10 @@ import { sichtbar as sichtbarDateien } from "./workspace-dateien.js"; vor ihm steht. Eine Suche, die weniger findet als die Seite zeigt, liest sich wie "gibt es nicht". */ import { sichtbarEintrag as sichtbarEintraege, BEREICHE } from "./workspace-bereiche.js"; +/* Welche Rollen eine Einweisung haben -- aus dem Fachmodul, nicht + danebengeschrieben. Eine zweite Liste hier waere die, die beim + naechsten Haus-Umbau stehen bleibt. */ +import { FASSUNGEN as ANLEITUNG_ROLLEN } from "./anleitung-tabellen.js"; export const sucheRouter = express.Router(); @@ -278,6 +282,53 @@ sucheRouter.get("/workspace/api/suche", (req, res) => { } } + /* ---------- Die Anleitung (06.10.2026, Etappe 5) ---------------- + + NUR DIE EIGENE FASSUNG. Jede Rolle hat ihre eigenen Karten mit + eigenen Texten; faende ein Creator die Karte des Managers, + stuende dort "Zuteilen, freigeben" neben einer Kachel, die er + gar nicht hat. Deshalb `rolle = ?` in der Bedingung und nicht + nur die Adresspruefung weiter unten -- die trennt die Haeuser, + nicht die Rollen. + + DER NAME DER KACHEL STEHT NICHT IN DER DATENBANK (siehe + anleitung-tabellen.js), also kann die Suche ihn auch nicht + durchsuchen. Gefunden wird ueber die drei Saetze, und als Titel + steht der Kachel-Schluessel -- aus `bereich:schutz` wird + "Schutz". Das ist ehrlicher, als einen Namen zu erfinden, der + dann nicht mehr zu der Kachel passt, die inzwischen anders + heisst. */ + { + const rolle = String(person?.rolle || ""); + if (ANLEITUNG_ROLLEN.includes(rolle)) { + quelle("Anleitung", ` + SELECT kachel, stufe, wozu, tust, merke + FROM anl_karte + WHERE haus = 'agentur' AND rolle = ? + AND (wozu LIKE ? ESCAPE '\\' OR tust LIKE ? ESCAPE '\\' + OR merke LIKE ? ESCAPE '\\' OR kachel LIKE ? ESCAPE '\\') + ORDER BY reihe LIMIT ${JE_BEREICH}`, + [rolle, m, m, m, m], + (z) => { + const felder = [["Wozu", z.wozu], ["Was du hier tust", z.tust], ["Merke", z.merke]]; + const treffer = felder.find(([, w]) => w && w.toLowerCase().includes(q.toLowerCase())); + /* Aus dem Schluessel wieder etwas Lesbares machen: + `bereich:schutz` -> `Schutz`, `manager-ziele` -> `Manager-ziele`. */ + const name = String(z.kachel).split(":").pop().replace(/^./, (c) => c.toUpperCase()); + return { + titel: name, + text: treffer ? ausschnitt(treffer[1], q) : null, + zusatz: ["Einweisung", treffer ? treffer[0] : null].filter(Boolean).join(" · "), + /* Mit dem Zusatz hinter dem Fragezeichen -- sonst landen + fuenf Karten auf derselben Seite. */ + ziel: z.kachel.startsWith("bereich:") + ? `bereich.html?b=${z.kachel.slice(8)}` + : `${z.kachel}.html`, + }; + }); + } + } + /* UND HIER GILT SIE AUCH (15.09.2026). Ein Suchtreffer ist ein Verweis auf eine Seite. Zeigt er auf eine, diff --git a/workspace/anleitung.html b/workspace/anleitung.html index bda70a39..89150019 100644 --- a/workspace/anleitung.html +++ b/workspace/anleitung.html @@ -7,17 +7,17 @@ - - - - - + + + + + - - + + @@ -63,11 +63,49 @@ werden muss. Der Satz daneben steht trotzdem da – eine Zahl, die man nur sieht, wenn man die Maus darüberhält, ist keine. ================================================================== --> + +

+ Rundgang noch einmal starten +

+ + + + + + +
+

In 60 Sekunden

    @@ -148,6 +186,74 @@ + + +
    +
    +

    Bearbeiten

    +

    +
    + + + + + + + + + + + + + +
    + + +
    +
    +
    + + + +
    +
    +

    Wer ist wie weit?

    +

    + Nur die Ersten Schritte. Wer durch ist, braucht nichts mehr von dir. +

    +
    +
    +
    + +
    +
    +
    + - + - - - - - - + + + + + + diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index ccddf314..9e0317b1 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`. --> - - + + - +