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`. --> - - + + - +