diff --git a/docs/superpowers/specs/2026-10-04-oeffnungszeiten-live-design.md b/docs/superpowers/specs/2026-10-04-oeffnungszeiten-live-design.md new file mode 100644 index 0000000..bb1198c --- /dev/null +++ b/docs/superpowers/specs/2026-10-04-oeffnungszeiten-live-design.md @@ -0,0 +1,585 @@ +# Öffnungszeiten live (Funktion im Kundenportal) – Design + +Stand: 2026-10-04 · Status: vom Inhaber im Chat freigegeben (Abschnitte 1–5), Spec zur Durchsicht +Branch `feature/oeffnungszeiten` (Worktree `dev-zeiten`), aufgesetzt auf `feature/terminplaner-tp2` (6ae0f67). +WOMS: eigener Worktree auf `feature/terminplaner-tp2` des WOMS-Repos (6f03712), Branch `feature/oeffnungszeiten`. +Ideenfindung und Bewertung: `/root/reports/funktionen-ultra-update-2026-10-04/ideen.md` (21 Ideen, 3 Jurys), +Code-Fakten: `/root/reports/funktionen-ultra-update-2026-10-04/design-fakten.md`; Spec-Prüfung gegen den Code +(19 Punkte) eingearbeitet. +Keine Rechtsberatung – Texte gehen vor Livegang in die Rechtsrunde (Abschnitt 9). + +## Ziel + +Die „Killer-Funktion“ des Ultra-Updates im Funktionen-Tab. Der Kunde trägt seine Öffnungszeiten **einmal** +im Portal ein. Seine Website zeigt daraufhin überall dieselben Zeiten und einen Live-Status („Jetzt geöffnet · +schließt um 18 Uhr“). Feiertage des Bundeslands sind schon eingerechnet, Betriebsurlaub und Sondertage +ebenso. Dazu kommen eine zeitgesteuerte Hinweis-Leiste oben auf der Seite, eine Kontakt-Leiste auf dem Handy +(Anrufen, Route, WhatsApp) und Google-Daten im Seitencode. Eine Mail erinnert vor Feiertagen, und ein Knopf +gibt die Zeiten für das Google-Unternehmensprofil aus. Alles läuft auf dem eigenen Server, ohne +Google-Cloud-Projekt und ohne Cookies. + +## Entscheidungen (vom Inhaber) + +| # | Entscheidung | +|---|---| +| Z1 | Eine einzelne Killer-Funktion statt eines Pakets: **Öffnungszeiten live** (mit Hinweis-Leiste). | +| Z2 | Umfang V1: Kern (Wochenplan mit bis zu 2 Fenstern/Tag, Live-Status, Feiertage des Bundeslands, Ausnahmen) **plus** Hinweis-Leiste, mobile Kontakt-Leiste, Feiertags-Erinnerung mit Google-Hilfe, JSON-LD. | +| Z3 | Google nur ohne Google Cloud. Automatisches Schreiben ins Unternehmensprofil braucht die Business-Profile-API (Cloud-Projekt, Zugangsantrag mit bis zu 14 Tagen Prüfung, vorher 0 Anfragen/Minute, Profil ≥ 60 Tage bestätigt mit Website). Deshalb V1 als Umweg: Kopieren in Googles Reihenfolge + Link zur Profilverwaltung + Erinnerung. Echter Abgleich ist eine mögliche spätere Stufe. | +| Z4 | Preis: **30 € einmalig** für jeden Kunden; mit **laufendem Wartungs-Abo ohne Aufpreis enthalten**. | +| Z5 | Einbau **hybrid**: Hinweis-Leiste, Kontakt-Leiste und JSON-LD kommen über das Skript, das der Runner beim Deploy einsetzt; Zeiten-Tabelle/Status/Badge setzt das Team per WOMS-Ticket an die richtige Stelle und ersetzt die fest eingetragenen Zeiten. | +| Z6 | **Mehrere benannte Zeitpläne** je Website; der Hauptplan steuert Live-Status und Leisten. | +| Z7 | Abo-Kunden **schalten selbst ein**; der Klick legt das Einbau-Ticket an. | +| Z8 | Launch-Zugabe: nur eine **Live-Station auf der Relaunch-Seite**, keine Ankündigungsmail, kein Portal-Banner. | +| Z9 | Ansatz A: **eigenes Modul** (eigene Collection, eigene öffentliche Schnittstelle, eigenes Skript), kein allgemeiner Inhalts-Baukasten, nicht aus den Terminplaner-Zeiten abgeleitet. | +| Z10 | Gebaut **auf dem TP2-Branch** (Funktions-Register `config.features`); live zusammen mit oder direkt nach TP2. | +| Z11 | Abo endet bei Gratis-Freischaltung: Mail mit „für 30 € behalten“, nach **30 Tagen** ohne Zahlung abgeschaltet, Daten noch 30 Tage, auf der Seite bleibt die Ersatz-Tabelle. | + +Vom Inhaber in den Abschnitten 1–5 mitgetragene Festlegungen, die ich vorgeschlagen habe: + +| # | Festlegung | +|---|---| +| Z12 | Bezahlen ist gesperrt, solange ein Wartungs-Abo läuft und nicht gekündigt ist; ein gekündigtes Abo führt auf den 30-€-Weg. | +| Z13 | Der Editor ist ab Bestellen/Einschalten offen und wird nie gesperrt. | +| Z14 | Hinweis-Leiste **schiebt den Inhalt** (liegt nicht darüber); kein Schließen-Knopf (keine Speicherung im Browser). | +| Z15 | Kontakt-Leiste bleibt automatisch aus, wenn die Seite schon eine eigene feste Anruf-Leiste hat. | +| Z16 | JSON-LD erzeugt das Skript im Browser: vorhandener LocalBusiness-Knoten wird ergänzt, sonst ein kleiner Block angelegt. Kein Neubau je Zeitenänderung. | +| Z17 | In jedem Platzhalter bleibt eine **statische Ersatz-Tabelle** stehen (ohne JavaScript, nach Abschaltung). Sie zeigt den Stand beim Einbau; Pflege siehe 4.5. | +| Z18 | Erinnerung **7 Tage** vorher, eine Mail für alle Tage im Fenster; Heiligabend, Silvester und landesweit geführte, aber örtlich nicht überall geltende Feiertage als „bitte prüfen“. | +| Z19 | „Im Wartungs-Abo enthalten“ ist ein **freiwilliges Extra**, keine Änderung der Abo-Bedingungen. | +| Z20 | Im Preis (und im Abo ohne Anrechnung aufs Stundenkontingent) ist **ein Einbau je Website** enthalten; Sonderwünsche nach Angebot/Stunden. | +| Z21 | Relaunch-Station: 4. Eintrag der Funktionen-Gruppe, „Bald verfügbar“ bis zum Portal-Livegang, kein Preis, Uhr-Regler bedienbar mit Automatiklauf. | +| Z22 | Schalter `ZEITEN_ENABLED=off|pilot|on` mit Pilotliste `ZEITEN_PILOT_EMAILS` wie beim Terminplaner. | +| Z23 | „Route“ öffnet Google Maps (`https://www.google.com/maps/dir/?api=1&destination=…`, erst beim Klick). | + +## 1. Bausteine + +1. **Register-Eintrag `zeiten`** in `config.features` (TP2): Katalogpreis `zeitenSetup` (3000 Cent, einmalig), + `requiresWartungsAbo: false`, `ticketFreischaltung: true`, neu `aboInklusive: true`, `consentFrei` + (Zustimmung für den Gratisweg), `teilweiseLive: ['setup', 'active']`; Eintrag in `STANDARD_HAKEN` + (`featureRegistry.js`) mit `verfuegbar` (Schalter/Pilot) und `freischaltbar` (4.4). +2. **Collections** `oeffnungszeiten` und `zeitenErinnerungen`; neue Felder an `portalFeatures`. +3. **Portal-Server:** Dienst `server/services/zeiten/` (Zeitlogik, Speichern, öffentliche Antwort, Freischalten, + Abgleich, Erinnerung), Kundenrouten `server/routes/zeiten.js`, öffentliche Route + `server/routes/publicZeiten.js`, Gratisweg `POST /api/funktionen/zeiten/einschalten`, Behalten-Checkout. +4. **Portal-Oberfläche:** Kachel im Funktionen-Tab (`zeitenAngebot` in `GET /api/funktionen`) und + Editor-Ansicht `public/zeiten*.js`. +5. **Website-Skript** `public/embed/zeiten.js` (ohne Abhängigkeiten). +6. **Runner:** zweite Einsetzung neben der Analyse, Feld `zeiten` im Build-Auftrag. +7. **WOMS:** Funktionen-Register, Team-Aktionen und -Einstellungen, Protokoll-Label, Rechtliches-Karte. +8. **Mails** K24–K27, Register-Texte für K15/K16/T16, Team-Ticket „Ersatz-Tabelle aktualisieren“. +9. **Rechtstexte** (Abschnitt 9) und **Relaunch-Station** (Abschnitt 10, eigenes Repo). + +## 2. Daten + +### 2.1 `oeffnungszeiten` (neu) + +Dokument-ID = ID des zugehörigen `portalFeatures`-Dokuments (TP2-Konvention, `featureDocId(projectId, +'zeiten')`). Felder: `customerId`, `projectId`, `featureId`, `version` (Integer), `daten` (JSON-String, +≤ 32 KB), `teamVersion` (Integer), `team` (JSON-String, nur über WOMS änderbar), `ersatzHash` (String, Hash des +Hauptplan-Wochenplans beim letzten Ersatz-HTML), `updatedAt`, `updatedBy` (`kunde` | `team:`). +Rechte wie die übrigen Funktions-Collections (nur Server-Key, Label-Team). Alle Schreibzugriffe laufen unter +`withLocks(['zeiten:'])`; Versionen werden innerhalb der Sperre geprüft und hochgezählt. + +`daten`: + +``` +{ + land: 'RP', // Bundesland-Kürzel (16); Vorgabe RP + plaene: [{ id, kurz, name, haupt: true|false, // kurz = unveränderlicher Kurzname [a-z0-9-]{1,24}, im Editor sichtbar + tage: { 1..7: { modus: 'zu'|'zeiten'|'rund'|'vereinbarung', + fenster: [['09:00','12:30'], ['13:30','18:00']] } } }], + feiertage: { 'YYYY-MM-DD': { modus, fenster } }, // nur Abweichungen von der Vorgabe + ausnahmen: [{ id, art: 'urlaub'|'sondertag', von, bis, text, modus, fenster, plan: null|planId }], + hinweise: [{ id, art: 'info'|'urlaub'|'aktion'|'wichtig', text, knopf: null|{ art: 'link'|'anruf', ziel, text }, + von, bis }], // von/bis = Kalendertage (YYYY-MM-DD), Berliner Zeit + kontakt: { an: bool, telefon, adresse, whatsapp: null|{ nummer, starttext, bestaetigtAt } } +} +``` + +`team`: `{ navSelektor: null|string, kontaktleisteAus: bool, hinweisleisteAus: bool }`. + +**Grenzen und Prüfregeln** (Server, Fehler je Feld): +- Höchstens 5 Pläne, genau ein Hauptplan, der letzte Plan ist nicht löschbar. +- Zeiten `HH:MM` in 5-Minuten-Schritten; je Tag ≤ 2 Fenster ohne Überschneidung; `bis == von` ungültig; + `bis < von` (über Mitternacht) nur beim letzten Fenster des Tages und nur, wenn es nicht in das erste + Fenster des Folgetags hineinragt. +- Ausnahmen ≤ 100, Hinweise ≤ 20; Urlaub `von ≤ bis`, höchstens 366 Tage; „Aktion“ braucht `bis`. + Vergangene Ausnahmen/Hinweise und vergangene `feiertage`-Schlüssel (> 30 Tage) entfernt der Server beim + Speichern. +- Längen: Hinweistext ≤ 140, Plan-/Ausnahmetext ≤ 60, Knopftext ≤ 30, Starttext ≤ 200, Adresse ≤ 200, + Telefon ≤ 30, Link ≤ 300 Zeichen. +- `knopf.ziel` bei `link`: `new URL` muss gelingen, nur `https:`, ohne Benutzer/Passwort; bei `anruf`: nur + Ziffern, Leerzeichen, `+`, `/`, `-`. Telefon und WhatsApp-Nummer werden auf Ziffern (mit führendem `49` + für deutsche Nummern) normalisiert. +- `navSelektor` (nur Team): ≤ 120 Zeichen, nur `[A-Za-z0-9 #.\-_>:\[\]="']`, kein `<`, `{`, `}`, `;`. +- Schreib-Limit 60 Speichervorgänge/Stunde je Kunde (`writeLimiter`). + +### 2.2 `zeitenErinnerungen` (neu) + +Dokument-ID = sha1(`featureId|datum`) gekürzt; Felder `featureId`, `datum`, `createdAt`. Anlegen mit 409 = +„schon erinnert“. Der Tagesjob löscht Merker, deren Datum > 30 Tage zurückliegt. + +### 2.3 `portalFeatures` (Erweiterung, Schema-Skript `feature-setup.mjs`) + +- `unlockedBy` (String 8): `kauf` | `abo` | `team`. `unlockedByPurchase` bleibt und wird abgeleitet + (`kauf` ⇒ true, sonst false). +- `aboEndeAt` (Datetime): Zeitpunkt, an dem der Abgleich das Ende des Abos erkannt hat. +- `einbau` (String 12): `angestossen` | `manuell` (Ergebnis des Neubau-Anstoßes, 4.6). +- `behaltenSessionId` (String 128): offene Behalten-Zahlung (3.3). +- `k24At` (Datetime): Marker der K24 (Selbstheilung, 3.2). +- `featureView` liefert zusätzlich `unlockedBy`, `aboEndeAt`, `einbau`, `loeschungAm` (auch für `zeiten`). + +## 3. Portal + +### 3.1 Kachel „Öffnungszeiten live“ + +`GET /api/funktionen` liefert `zeitenAngebot` je Projekt (analog `termineAngebot`), mit +`bestellbar: null` bei einem Abo-Lesefehler (Kachel zeigt dann „Gerade nicht prüfbar – bitte später erneut“). + +- **off, Wartungs-Abo läuft, nicht gekündigt, `active`:** „Im Wartungs-Abo enthalten“ · Haken + Leistungsbeschreibung · Knopf **„Einschalten“** (3.2). +- **off, Wartungs-Abo läuft, nicht gekündigt, aber nicht `active` (Probezeit, Zahlung offen):** „Im + Wartungs-Abo enthalten – einschaltbar, sobald dein Abo aktiv ist“; kein Bezahlen. +- **off, sonst** (kein Wartungs-Abo, Hosting-Abo, Kündigung vorgemerkt): „30 € einmalig“ · Bestellseite aus + dem Register (Haken Leistungsbeschreibung und Beginn-Verlangen) · **„Zahlungspflichtig bestellen · 30 €“**. +- **ordered:** „Zahlung wird bestätigt …“. +- **setup:** je nach `einbau`: „Hinweis- und Kontakt-Leiste erscheinen nach dem nächsten Veröffentlichen + (meist in wenigen Minuten)“ bzw. „Unser Team baut die Funktion ein (Ticket #…)“; bei leeren Plänen + zusätzlich „Trag jetzt deine Zeiten ein – erst dann kann das Team die Tabelle einbauen“ · + **„Zeiten bearbeiten“**. +- **active:** Kurzstatus der Website („Jetzt geöffnet · schließt um 18 Uhr“) · **„Zeiten bearbeiten“**. +- **active/setup mit `aboEndeAt`:** zusätzlich „Dein Wartungs-Abo ist beendet – behalte die Funktion bis + für einmalig 30 €“ · Knopf zum Behalten-Checkout (3.3). +- **disabled:** Grundtext aus `disabledMessage`; bei `abo_ende` bis zur Löschung Knopf + „Für 30 € wieder einschalten“ (3.3). + +Bei `ZEITEN_ENABLED=off` erscheint die Kachel nicht; bei `pilot` nur für die Pilotliste. + +### 3.2 Gratisweg `POST /api/funktionen/zeiten/einschalten` + +Vorbild `activateAnalyse` (`server/services/analyse.js`). Stufen in dieser Reihenfolge, gleiche Fehlertexte +wie der Checkout: Kundenansicht (viewAs ⇒ 403, Mitarbeiter schalten nicht für den Kunden ein) → Zustimmung +(`consentFrei`, Fassung) → eigenes, nicht archiviertes Projekt → Schalter/Pilot → Unterlagen bereit. Dann unter +der Sperre `feature-order`: + +1. Abo frisch prüfen: `bestellbar()` direkt (ohne Memo). `kein_abo`/`gekuendigt` ⇒ 409 mit Hinweis auf den + 30-€-Weg; `nicht_aktiv` ⇒ 409 „sobald dein Abo aktiv ist“; Lesefehler ⇒ 503, nie stillschweigend erlaubt. +2. Steht das Dokument schon auf `setup`/`active` mit `unlockedBy: 'abo'` ⇒ **idempotent**: fehlende Schritte + (Ticket, K24) nachholen, 200 mit aktuellem Zustand. Andere Zustände außer `off` ⇒ 409 + `orderedConflictMessage`. +3. Zustimmung speichern (`speichereZustimmung` ohne `stripeSessionId`). +4. Feature-Dokument auf `setup`, `unlockedBy: 'abo'`. +5. Neubau anstoßen (4.6) und `einbau` setzen. +6. Einbau-Ticket anlegen (Register-Text mit Ergebnis aus Schritt 5), dann K24 (Text nach `einbau`), dann + `k24At` – jeweils erst nach Erfolg. + +**Selbstheilung:** Der 5-Minuten-Job (`runActivation`) legt für `zeiten` in `setup` ohne `ticketId` das +Ticket nach und sendet K24 nach, wenn `unlockedBy: 'abo'` und `k24At` leer ist. Ein erneuter Klick heilt +ebenso (Schritt 2). + +### 3.3 Bezahlweg und Behalten + +**Neubestellung** über den TP2-Checkout je Funktion; der Register-Eintrag liefert Preis, Zustimmung, Ticket- +und Mailtexte. Zusatz: Wartungs-Abo läuft und ist nicht gekündigt (`bestellbar` ok oder Grund `nicht_aktiv`) +⇒ 409 `inklusive` („Im Wartungs-Abo bereits enthalten – einfach einschalten“). + +**Behalten** (neuer Modus, nicht über `markOrdered`/`markPaid` der Neubestellung, weil diese bei +`setup`/`active`/`disabled` mit 409 bzw. Anomalie antworten): + +- `POST /api/funktionen/zeiten/behalten` (eigener Kunde, kein viewAs; nur wenn `aboEndeAt` gesetzt oder + `disabled` mit `abo_ende` und Daten vorhanden): gleiche Zustimmung wie die Neubestellung, Stripe Checkout + mit Metadata `zweck: 'behalten'`, `behaltenSessionId` am Dokument. **Der Zustand bleibt unverändert** + (Leisten bleiben während der Zahlung live). +- Webhook-Zweig `zweck: 'behalten'` unter `feature-admin:`: Session passt zu + `behaltenSessionId` ⇒ `unlockedBy: 'kauf'`, `aboEndeAt` leer, `behaltenSessionId` leer; war das Dokument + `disabled`, zurück auf `active` (Ticket war geschlossen) bzw. `setup`, Neubau anstoßen (4.6). + **Kein neues Einbau-Ticket.** K15 mit Behalten-Wortlaut. Passt die Session nicht (schon behalten, Daten + gelöscht) ⇒ T19 „Erstattung fällig“ wie bei anderen Anomalien. + +### 3.4 Editor (Ansicht `#zeiten`) + +Oben eine **Live-Vorschau**: „So steht es gerade auf deiner Seite: *Jetzt geöffnet · schließt um 18 Uhr*“ +(dieselbe Serverlogik) und ein Regler **„Uhrzeit testen“** (Tag + Uhrzeit, `GET /api/zeiten/:projectId/ +vorschau?zeit=…`). Vor dem ersten Speichern: leerer Hauptplan und „Trag deine Öffnungszeiten ein“. +Darunter sechs Bereiche: + +1. **Zeitpläne:** Hauptplan „Öffnungszeiten“ (Kurzname `oeffnungszeiten`), „Plan hinzufügen“ (Name, der + Kurzname wird einmalig daraus gebildet und angezeigt), Hauptplan wechseln, Plan löschen (nicht den letzten, + nicht den Hauptplan). Je Wochentag: Haken „geöffnet“, zwei ``-Paare, + Auswahl „rund um die Uhr“ / „nach Vereinbarung“. +2. **Feiertage:** Bundesland-Auswahl; Liste der Feiertage der nächsten 12 Monate mit Namen; je Eintrag + „geschlossen“ (Vorgabe) oder eigene Zeiten. „Bitte prüfen“ (Vorgabe: Wochenplan, nicht „geschlossen“): + Heiligabend, Silvester sowie Feiertage, die `feiertagejs` landesweit führt, die aber nur örtlich gelten + (Mariä Himmelfahrt in BY, Fronleichnam in SN und TH). Hinweis: Gemeinde-Feiertage sind nicht enthalten. +3. **Ausnahmen:** Betriebsurlaub (von–bis, Text) und Sondertag (Datum, geschlossen oder Zeiten); Geltung + „alle Pläne“ (Vorgabe) oder ein Plan; Liste kommend/vergangen. Beim Speichern eines Urlaubs ≥ 2 Tage: + „Hinweis-Leiste dazu anlegen?“ (füllt Text und Zeitraum vor; sichtbar ab 7 Tage vor Beginn bis Urlaubsende). +4. **Hinweis-Leiste:** Text, Art (Info/Urlaub/Aktion/Wichtig mit festen Farben), optionaler Knopf + (Link `https://…` oder Anrufen), Zeitraum von–bis (Kalendertage); „Aktion“ verlangt ein Enddatum. Enthält + der Text „%“ oder „statt“: Hinweis zu Preisangaben (Endpreise, bei Waren niedrigster Preis der letzten + 30 Tage). Liste geplanter Hinweise. +5. **Kontakt-Leiste (Handy):** Ein/Aus; Telefon; Adresse für „Route“; WhatsApp optional mit Nummer, + Starttext und Pflicht-Haken „Ich habe den WhatsApp-Hinweis in meine Datenschutzerklärung aufgenommen“ + (Zeitpunkt in `bestaetigtAt`; in der Kundenansicht setzen Mitarbeiter den Haken **nicht** – 403 wie + `routes/termine.js`). +6. **Google:** Knopf **„Für Google kopieren“** (Zwischenablage: Montag–Sonntag im Format + „Montag: 09:00–12:30, 13:30–18:00“, „geschlossen“, danach Sonderzeiten der nächsten 60 Tage) und Link + „Google-Profil öffnen“ (`https://business.google.com/`). Text: Die Google-Daten im Seitencode laufen + automatisch. + +Speichern: `PUT /api/zeiten/:projectId` mit `version`; veraltete Version ⇒ 409 „Inzwischen geändert – bitte +neu laden“. Mitarbeiter können über die Kundenansicht speichern; Protokoll +`VIEW_ACTIONS += 'zeiten_gespeichert'` (Test-Pin und WOMS-`viewLog.js`-Label anpassen). + +## 4. Website + +### 4.1 Öffentliche Schnittstelle `GET /api/public/zeiten/v1/:siteKey` + +CORS `*`, nur GET, `Cache-Control: public, max-age=60`, weiches ETag über den Body **ohne** `stand`, +Speichercache 30 s je siteKey als **LRU mit höchstens 500 Einträgen** (auch für „unbekannt“), 120 +Anfragen/Minute je IP, `Access-Control-Expose-Headers: Date`. siteKey-Format wird vor jedem Appwrite-Zugriff +geprüft. Unbekannter Schlüssel, Schalter aus, Kunde gelöscht oder Zustand nicht in `setup`/`active` ⇒ +`{ aktiv: false }`. Keine internen IDs, keine Kundendaten außer den veröffentlichten Kontaktangaben. + +Antwort (gekürzt): + +``` +{ aktiv: true, stand: , + plaene: [{ kurz, name, haupt, + woche: [{ tag: 1, modus, fenster }], + tage: [{ datum, art: 'regulaer'|'feiertag'|'urlaub'|'sondertag', name, modus, fenster }], // 60 Tage + wechsel: [{ ab: , offen: true|false|null, text, kurz }] }], // 8 Tage + hinweise: [{ art, text, knopf, ab: , bis: , rang }], // aktuell gültige + Beginn in 8 Tagen + kontakt: null|{ telefon, routeUrl, whatsappUrl }, + jsonld: { openingHoursSpecification: [...], specialOpeningHoursSpecification: [...] }, + firma: { name, adresse }, + team: { navSelektor, kontaktleisteAus, hinweisleisteAus } } +``` + +- `hinweise[].ab/bis` rechnet der Server aus den Kalendertagen in Berliner Zeit (Beginn 00:00, Ende 24:00). + Der Browser zeigt zu jedem Zeitpunkt den gültigen Hinweis mit dem höchsten `rang` + (Wichtig > Urlaub > Aktion > Info; bei Gleichstand späterer Beginn). +- `firma.name` = Firmenname des Kunden; fehlt das Kundendokument, Projektname, sonst leer (nie 500). + `firma.adresse` = `kontakt.adresse`. +- Solange keine Zeiten gespeichert sind, ist `plaene` leer: kein Status, kein JSON-LD, Platzhalter behalten + ihr Ersatz-HTML; Leisten erscheinen nur, wenn eingerichtet. +- `whatsappUrl` = `https://wa.me/?text=`; `routeUrl` nach Z23. + +### 4.2 Skript `zeiten.js` + +IIFE ohne Abhängigkeiten, Schutz gegen doppeltes Laden (`window.__wkZeiten`). siteKey aus `data-site` des +eigenen Script-Tags oder aus `[data-webklar-zeiten]`; API-Basis aus `data-api` oder der Herkunft des Skripts. +**Ein** Abruf je Seite für alle Teile. Der Browser rechnet keine Feiertage und keine Zeitzonen: Er wählt aus +`wechsel` den letzten Eintrag mit `ab ≤ jetzt`, stellt einen Timer auf den nächsten Wechsel (Statuswechsel +oder Hinweis-Beginn/-Ende), lädt höchstens alle 10 Minuten und bei `visibilitychange` neu. „Jetzt“ = +`Date.now()`; weicht die Uhr laut `Date`-Header um mehr als 5 Minuten ab, rechnet das Skript mit der +Serverzeit. Schlägt der Abruf fehl oder sind die Daten älter als 24 h: Leisten aus, Platzhalter behalten ihr +Ersatz-HTML. Keine Cookies, kein Storage, keine Anfragen an Dritte; Kundentexte nur über `textContent`; +Links aus der API werden im Browser erneut geprüft (`https:`/`tel:`), `rel="noopener noreferrer"`; +`querySelectorAll(navSelektor)` in try/catch. + +### 4.3 Teile auf der Seite + +1. **Hinweis-Leiste:** Shadow-DOM-Host als erstes Kind von `body`, schiebt den Inhalt. Feste/klebende + Elemente mit `top: 0` (oder `team.navSelektor`) werden, solange die Leiste sichtbar ist, um + `max(0, Leistenhöhe − scrollY)` nach unten gerückt (Inline-`top`, beim Entfernen zurückgesetzt); + `scroll-padding-top` wird um die Höhe erhöht. Farben je Art mit Kontrast ≥ 4,5:1; `role="region"`, + `aria-label="Hinweis"`. Aus bei `team.hinweisleisteAus`. +2. **Kontakt-Leiste** (nur bei `matchMedia('(max-width: 768px)')`): Shadow DOM, unten fest, `z-index: 40`, + Abstandhalter am Ende von `body` in Leistenhöhe, `scroll-padding-bottom`; Knöpfe Anrufen (`tel:`), Route, + WhatsApp – erst der Klick öffnet den fremden Dienst. Aus, wenn `team.kontaktleisteAus` oder die Seite + schon ein festes Element am unteren Rand mit `a[href^="tel:"]` hat. +3. **JSON-LD:** Gibt es einen `application/ld+json`-Block mit einem Knoten vom Typ LocalBusiness oder einem + Untertyp (auch in `@graph`), ersetzt das Skript dort nur `openingHoursSpecification` und + `specialOpeningHoursSpecification` (Text des Blocks per `JSON.stringify` neu, unparsbare Blöcke bleiben + unberührt); sonst legt es einen Block `{ @type: LocalBusiness, name, address?, url, … }` an. Regeln: + Fenster über Mitternacht mit `closes < opens`; „rund um die Uhr“ 00:00–23:59; „nach Vereinbarung“ wird + ausgelassen; Sonderzeiten der nächsten 60 Tage mit `validFrom`/`validThrough`, geschlossen als + `00:00`–`00:00`. +4. **Platzhalter** (vom Team gesetzt; das Skript füllt sie ab `setup`, damit das Team vor dem Schließen des + Tickets prüfen kann): + `
Ersatz-HTML
`. + - *Tabelle:* echtes `` mit Caption, Zeilenköpfe `th scope="row"`, heutiger Tag mit + `aria-current="date"`, darunter „Abweichungen in den nächsten 14 Tagen“. + - *Status:* Punkt + Text („Jetzt geöffnet · schließt um 18 Uhr“), kein `aria-live`. + - *Badge:* Kurzform („Geöffnet“ / „Geschlossen“ / „Nach Vereinbarung“). + Light DOM mit Präfix `wkz-`, `@layer wkz`, Variablen `--wkz-*`; erbt Schrift und Farben der Seite. + Unbekannter `data-plan`: Platzhalter bleibt beim Ersatz-HTML, Konsole-Warnung (kein stilles Umschalten + auf den Hauptplan). Bei `aktiv: false` bleibt das Ersatz-HTML unverändert stehen. + +Barrierefreiheit: Tippflächen ≥ 44 px, sichtbarer Fokus, `prefers-reduced-motion` (keine Einblend-Animation). + +### 4.4 Freischalten (Ticket schließen ⇒ `active`) + +Der vorhandene 5-Minuten-Job setzt `active`, wenn das Ticket geschlossen ist **und** `freischaltbar` +(Register-Haken) erfüllt ist: Hauptplan gespeichert und mindestens ein Tag nicht „zu“. Sonst bleibt `setup`, +WOMS zeigt „wartet auf die Zeiten des Kunden“. Bei `active`: K16, `ersatzHash` setzen. + +### 4.5 Ersatz-HTML + +`GET /api/team/funktionen/:customerId/zeiten/ersatz?projectId=…&plan=` liefert eine schlichte +Wochentabelle des Plans als ASCII mit Entities (`ä`, `–`); bei leeren Plänen 409. Das Team setzt sie +beim Einbau in jeden Platzhalter. Ändert sich der Wochenplan des Hauptplans später, bleibt das Ersatz-HTML +zunächst stehen (mit Skript unsichtbar). Wird die Funktion **abgeschaltet** und weicht der aktuelle Wochenplan +von `ersatzHash` ab, legt der Abschalt-Pfad ein WOMS-Ticket „Ersatz-Tabelle aktualisieren“ mit dem aktuellen +Ersatz-HTML an (gehört zum enthaltenen Einbau). K27 sagt dem Kunden, dass ab jetzt wieder feste Zeiten auf der +Seite stehen. Statisches Vorbefüllen durch den Runner bei jedem Deploy bleibt V1.1. + +### 4.6 Einbau des Skripts + +- **Seite läuft über den Runner:** `previewDeploy` liefert im Auftrag `zeiten: { siteKey } | null` über + `buildZeiten(project)` (nur bei `setup`/`active`; nie im Dev-Pfad – dort wie `analytics` auf `undefined`; + `null` für Projekte auf der Sperrliste; `isPublic` nicht nötig; wirft nie). Der Runner prüft das Feld + (Whitelist in `validate.js`, siteKey-Regex) und setzt in derselben Runde wie die Analyse vor `` jeder + HTML-Datei ein: + `` + (neue Runner-Variable `ZEITEN_PUBLIC_BASE`, Vorgabe `https://project.webklar.com/embed/`; + `injectIntoHtml` wird auf eine Marke je Tag verallgemeinert, heute fest `data-wk-analytics`). +- **Neubau anstoßen** bei jedem Übergang in einen ausgebenden Zustand (`setup`/`active`) aus `off`, `ordered` + oder `disabled` (Gratisweg, Zahlung, Team-Aktivierung, Abo-Rückkehr, Behalten nach Abschaltung): + `triggerRebuild` (Muster `analyseDeps.rebuild`), **serialisiert** über eine Portal-weite Sperre + `zeiten-neubau` (höchstens ein angestoßener Neubau gleichzeitig). Ergebnis in `einbau`: `angestossen` + (Auftrag angenommen) oder `manuell` (kein Repo, Preview aus, Dev-Modus, Sperrliste, Fehler oder Runner + ausgelastet). Änderungen an Zeiten/Leisten brauchen **keinen** Neubau. Beim Abschalten kein Pflicht-Neubau; + die API antwortet `aktiv: false`, das Tag verschwindet beim nächsten Deploy. +- **Sperrliste** `ZEITEN_KEIN_NEUBAU` (Subdomains; Vorgabe: Emmas Seiten, deren Dateien teilweise nicht in + Git liegen): weder angestoßener Neubau noch Einsetzen bei normalen Deploys (`buildZeiten` ⇒ `null`); das + Team setzt die Zeile von Hand ins Repo. +- **Seite liegt woanders** oder `einbau: manuell`: Das Ticket enthält die Einbau-Zeile; das Team setzt sie ein. +- **Ticket-Inhalt** (Register `ticket.details`): siteKey, Einbau-Zeile, Ergebnis `einbau`, Beispiele der + Platzhalter mit Plan-Kurznamen, Link auf das Ersatz-HTML (nach dem ersten Speichern des Kunden), Prüfliste: + feste Zeiten im Repo suchen und ersetzen (`grep -ril "öffnungszeit"` u. Ä., auch Footer, Kontaktseite, + eigene Status-Skripte), Header-Überlappung der Hinweis-Leiste prüfen (sonst `navSelektor`), eigene + Anruf-Leiste vorhanden?, vorhandenes JSON-LD, Datenschutz-Absatz auf der Website ergänzt (Abschnitt 9). + +## 5. Zustände und Abläufe + +``` +off ──Checkout──▶ ordered ──Zahlung──▶ setup ──Ticket zu + freischaltbar──▶ active + ├──Einschalten (Abo)───────────────▶ setup + └──Team „Aktivieren ohne Zahlung“───▶ setup +setup/active ──Abo-Ende + 30 Tage ohne Zahlung (nur abo) / Team „Abschalten“──▶ disabled +disabled (abo_ende) ──30 Tage──▶ Daten gelöscht, state off +``` + +- Die öffentliche Schnittstelle liefert in `setup` und `active` dasselbe (`teilweiseLive`); + `findActiveBySiteKey` bekommt eine Zustandsliste. `active` heißt „Einbau abgeschlossen“ (Ticket zu, K16). +- **Team** (WOMS): „Aktivieren ohne Zahlung“ läuft für `zeiten` über denselben Pfad wie der Gratisweg + (`setup`, `unlockedBy: 'team'`, Neubau, Ticket; Zustimmung entfällt, Aktion protokolliert) statt über + `adminActivate` (setzt heute direkt `active`). „Abschalten“ ⇒ `disabled`, Grund `team`, ohne Löschfrist. + `activate()` und jede Wiederherstellung räumen `aboEndeAt` und `behaltenSessionId` ab; eine Neubestellung + nach Datenlöschung setzt `ticketId`, `einbau`, `k24At`, `ersatzHash` zurück. +- **Abo-Abgleich** (eigener Tagesjob, nicht an `TERMINE_ENABLED` gekoppelt), nur `unlockedBy === 'abo'`, + Abo-Lage über `aboLage()` (Fehler ⇒ Kunde überspringen, nie abschalten): + 1. Abo läuft nicht mehr und `aboEndeAt` leer ⇒ `aboEndeAt = jetzt`, **K26** (Variante nach `art`: + beendet / Wechsel zu Hosting / Wechsel widerrufen; Behalten-Link, Frist = `aboEndeAt` + 30 Tage). + 2. `aboEndeAt` + 30 Tage erreicht ⇒ `disabled`, `disabledReason: 'abo_ende'`, **K27**, ggf. Ticket + „Ersatz-Tabelle aktualisieren“ (4.5). + 3. Abo läuft wieder, solange die Daten noch da sind (also bis `aboEndeAt` + 60 Tage) ⇒ `aboEndeAt` leer, + Zustand zurück (`active`, wenn das Ticket geschlossen ist, sonst `setup`), Neubau bei Rückkehr aus + `disabled`, K24-Variante „wieder eingeschaltet“. + 4. `disabled` mit `abo_ende` + 30 Tage ⇒ `daten`, `team` und Merker gelöscht, Feature-Dokument + `state: 'off'`. Bei Grund `team` keine automatische Löschung. + K16 kann nach K26 noch kommen (Ticket wird während der Frist geschlossen); das ist gewollt. +- **Kunde gelöscht** (`customerAdmin.js`, Löschpfad): `zeiten` wird wie `termine` abgeschaltet; + `oeffnungszeiten`, `zeitenErinnerungen` und der Cache-Eintrag werden gelöscht. `retention.js` bekommt + `zeiten` (Löschfristen wie oben). + +## 6. Zeitlogik (Server, `server/services/zeiten/zeitlogik.js`) + +Reine Funktionen auf Basis von `termine/zeit.js` (Europe/Berlin über `Intl`) und einem erweiterten +`termine/feiertage.js` (neu: `feiertagName(tag, land)` und `feiertageImZeitraum(von, bis, land)` über +`feiertagejs.getHolidays(jahr, land)` und die Methode `translate('de')` der Feiertags-Objekte). Der Browser +rechnet nichts davon nach. + +- **Tagesauflösung** je Plan und Datum, Vorrang (spezifisch vor allgemein): + Sondertag > Betriebsurlaub > Feiertag (Abweichung oder Vorgabe) > Wochenplan. Bei gleicher Art schlägt eine + planbezogene Ausnahme eine für alle Pläne; zwei gleichrangige überlappende Ausnahmen: die später angelegte. + Bewusst anders als `slots.js` (dort schlägt Abwesenheit die Sonderöffnung), weil ein einzelner geöffneter + Tag im Urlaub hier gewollt ist. +- **Fenster** gehören zum Starttag; `bis < von` heißt „bis zum Folgetag“. Ein über Mitternacht laufendes + Fenster folgt der Regel seines **Starttags**, auch wenn der Folgetag Feiertag, Urlaub oder geschlossen ist + (Bar hat Samstag 18–02 offen, Sonntag ist Feiertag ⇒ bis 02:00 offen). Der Status zu einem Zeitpunkt prüft + die Fenster des Tages **und** das über Mitternacht laufende Fenster des Vortags. Aneinander grenzende Fenster + (auch über Tage, z. B. zweimal „rund um die Uhr“) werden vor der Textbildung zusammengefügt. +- **Sommerzeit:** Fenster sind Wanduhrzeiten. `zuUtc` liefert in der März-Lücke `null`; die Zeitlogik + rückt dann selbst auf die erste existierende Minute danach vor (03:00). In der doppelten Oktober-Stunde gilt + das erste Vorkommen (Verhalten von `zuUtc`). +- **Texte** (ohne Anrede): + - offen: „Jetzt geöffnet · schließt um 18 Uhr“ / „… um 18:30 Uhr“; Schluss um 00:00 des Folgetags: + „… schließt um Mitternacht“; Schluss mehr als 24 h entfernt: „Jetzt geöffnet · bis Dienstag 14 Uhr“; + durchgehend ≥ 7 Tage: „Rund um die Uhr geöffnet“. Nie „24 Uhr“. + - zu: „Geschlossen · öffnet heute um 14 Uhr“ / „morgen um 9 Uhr“ / „Montag um 9 Uhr“ (≤ 6 Tage) / + „am 18.08. um 9 Uhr“; keine Öffnung in 60 Tagen: „Derzeit geschlossen“. Grund an geschlossenen Tagen: + „Heute geschlossen (Betriebsurlaub bis 17.08.)“, „(Feiertag: Tag der Deutschen Einheit)“. + - nach Vereinbarung: „Heute nach Vereinbarung“ (`offen: null`). + - kurz: „Geöffnet“ / „Geschlossen“ / „Nach Vereinbarung“. +- **`wechsel`**: alle Statuswechsel der nächsten 8 Tage ab Beginn der aktuellen Minute; zusätzlich ein Eintrag + zu jeder Tagesgrenze, an der sich der Text ändert („morgen“ → „heute“). + +## 7. Erinnerung und Mails + +**Feiertags-Erinnerung** (Job in `server/index.js`, stündlich, nur 08:00–20:00 Berliner Zeit, +Überlappsperre, nur bei Schalter `pilot`/`on` und nur für freigegebene Kunden): Für jedes Dokument in +`setup`/`active` alle Tage in 1–7 Tagen, die ein Feiertag des Landes oder ein „bitte prüfen“-Tag (3.4) sind. +Nicht erinnert wird, wenn der Tag im Hauptplan ohnehin geschlossen ist, eine Ausnahme ihn abdeckt oder der +Kunde für den Tag schon eine Abweichung gespeichert hat. Für die übrigen Tage: erst Merker anlegen (409 ⇒ +überspringen), dann **eine** Mail **K25** mit allen neuen Tagen („Stimmen deine Zeiten an diesen Tagen?“, +Knopf zum Editor, Hinweis „bei Google auch eintragen“). Lieber einmal zu wenig als doppelt; kein BCC ans Team. + +**Mails** (im Design der Mailvorlagen vom 27.09., Texte in die Mailrunde des Inhabers): + +| ID | Anlass | +|---|---| +| K15/K16/T16 | Wie gehabt, Texte aus dem Register (Kauf, bereit, Team); T16 beim Gratis-/Team-Weg mit Zusatz „im Wartungs-Abo enthalten“ bzw. „vom Team freigeschaltet“; K15 mit Behalten-Wortlaut (3.3). | +| K24 (neu) | Im Abo eingeschaltet; Text nach `einbau` („Leisten erscheinen nach dem nächsten Veröffentlichen“ / „das Team baut sie ein“), Tabelle folgt per Ticket; Variante „wieder eingeschaltet“. | +| K25 (neu) | Feiertags-Erinnerung. | +| K26 (neu) | Wartungs-Abo beendet (Varianten nach `art`): Funktion läuft noch bis , „für einmalig 30 € behalten“. | +| K27 (neu) | Abgeschaltet: Leisten sind weg, auf der Seite stehen wieder feste Zeiten; Daten noch 30 Tage, Wiedereinschalten für 30 €. | + +Mail-Pin-Test (K01–K27) und der Block `funktionen` in K02/K03 werden angepasst. + +## 8. WOMS (eigener Worktree auf WOMS `feature/terminplaner-tp2`) + +- `src/lib/funktionenView.js`: Eintrag `zeiten` (Name, Zustandstexte, „Leisten live – Einbau läuft“ bzw. + „wartet auf die Zeiten des Kunden“ für `setup`, Abo-Ende-Hinweis mit Frist, `loeschungAm`), `siteKeyOf` um + `zeiten` erweitert, Test-Pin angepasst. +- Funktionen-Panel: Aktionen „Aktivieren ohne Zahlung“ / „Abschalten“; Team-Einstellungen `navSelektor`, + `kontaktleisteAus`, `hinweisleisteAus` (`PATCH /api/team/funktionen/:customerId/zeiten/team` mit + `teamVersion`); Knopf „Ersatz-HTML kopieren“ je Plan. +- `viewLog.js`: Label für `zeiten_gespeichert`. +- Rechtliches: Karte „Leistungsbeschreibung Öffnungszeiten live“ über `scripts/setup-legal.mjs` (nach den + Terminplaner-Karten aus TP2 F14; setzt TP2 Block F voraus). + +## 9. Rechtstexte + +Grundsatz Z19: freiwilliges Extra, Abo-Bedingungen bleiben unverändert (kein § 15-Verfahren, kein P10-Risiko). + +1. **Leistungsbeschreibung „Öffnungszeiten live“** (eigenständig, Gerüst der Analyse-Leistungsbeschreibung): + Leistung; automatischer Einbau nur bei WEBklar-Hosting, sonst durch das Team; Preis 30 € ohne + Umsatzsteuer nach § 19 UStG, mit laufendem Wartungs-Abo ohne Aufpreis und ohne Anrechnung aufs + Stundenkontingent („gehört nicht zum Leistungsumfang der Abo-Bedingungen; Leistungsbeschreibung B Nr. 3 + gilt dafür nicht; der Abo-Preis ändert sich nicht“); ein Einbau je Website inkl. Aktualisierung der + Ersatz-Tabelle beim Abschalten, Sonderwünsche nach Angebot/Stunden; nicht enthalten: Übertragung zu + Google, Gemeinde-Feiertage; Pflichten des Kunden (Zeiten, Feiertage und Hinweistexte prüfen, + Preise/Rabatte rechtmäßig angeben, Datenschutz-Absatz auf der Website); **Haftung** (Angaben vom Kunden, + Live-Anzeige als Hilfsmittel, Softwarefehler inkl. Feiertagsdaten nur im Rahmen der üblichen Grenzen – + spiegelt Abo § 13 Abs. 1 und 2, damit Kunden ohne Abo abgedeckt sind); Ende der Leistung (Abo-Ende nach Z11) + und Einstellung/Änderung nach Vorbild Analyse Nr. 8. +2. **Zustimmung:** bezahlt mit Beginn-Verlangen (§ 356 Abs. 5 Nr. 2 BGB, Belehrung über TP2-Baustein F3); + gratis nur Haken für die Leistungsbeschreibung, Knopf „Einschalten“; Nachweis in beiden Fällen. +3. **Datenschutz Portal:** neuer § 6d (gespeicherte Angaben, Erinnerungsmail, Art. 6 Abs. 1 lit. b, + Löschfristen); § 6 um „Öffnungszeiten“ im Kundenauftrag erweitert. +4. **Absatz für die Kundenseite** (`docs/funktionen/website-datenschutz-absatz.md`, Varianten A/B): + Abruf von project.webklar.com (IP 14 Tage im Server-Log, keine Cookies, nichts im Browser gespeichert); + WhatsApp (Meta) und Google Maps erst nach Klick. +5. **AVV:** mit Abo reicht Anlage 1 (Server-Protokolle); ohne Abo/bei Fremdhosting gleiche Lücke wie bei + der News-Pinnwand ⇒ Frage ins Kanzlei-Paket (⚖ „AVV Funktionen“ per Haken?). +6. **Mitkorrektur News** (TP2 F2): „inkl. USt.“ → § 19 UStG, § 356 Abs. 4 → Abs. 5. + +Reihenfolge der Rechtsrunde (gemeinsam mit TP2): gemeinsame Bausteine (TP2 Block F) → Leistungsbeschreibung → +Datenschutz § 6d/§ 6 und Website-Absatz → Oberflächen- und Mailtexte (Mailrunde) → ⚖-Fragen ans Kanzlei-Paket +→ Inhaber lädt Fassungen in WOMS „Rechtliches“ hoch → Schalter `on`. + +## 10. Relaunch-Station (Repo webklar.com, eigener Branch ab `main`) + +`main` des Relaunch-Repos ist live (Push = Deploy) – Arbeit auf eigenem Branch im eigenen Worktree, der Inhaber +pusht. + +- `Kundenportal.tsx` `FUNKTIONEN`: 4. Eintrag nach der News-Pinnwand, `demo: 'zeiten'`, `bald: true` + (bis Portal-Livegang), kein Preis. Indizes 0–6 bleiben, „Als App“ rückt auf 8. +- Neuer Ordner `src/components/home/demos/zeiten/`: `ablauf.ts`, `ZeitenSeite.tsx`, `ZeitenHandy.tsx`, + `ZeitenRegler.tsx`, `zeiten.css`. Gemeinsame Bäckerei-Teile (`Logo`, `Pfeil`, `Stempel`) nach + `baeckerei/teile.tsx`; eigenes Wurzelelement ohne `.bk-inhalt` (sonst greift `--kp-v`). +- **MacBook:** Bäckerei Sonnenschein mit großer Status-Pille (≥ 30 pt) und Hinweis-Leiste + „Betriebsurlaub 12.–26. Oktober“ (≥ 24 pt, nicht unter dem iPhone). Demo-Datum Mo 5. Okt. 2026 (liegt im + 7-Tage-Vorlauf des Urlaubs). +- **Handy:** Portal-Editor-Shot `phone-12-zeiten` (Fixture-Kopie mit Portal aus `dev-zeiten`, Profil + `baeckerei` mit `features.zeiten`, fester Uhrzeit, freier Port aus 5326/5327/5329), Finger tippt + „Hinweis-Leiste anlegen“, danach gleitet die Leiste auf der Seite ein. +- **Uhr-Regler:** echtes `` mit Label, Tippfläche ≥ 44 px; Automatiklauf + Mo 05:50 → 18:10 („Jetzt geöffnet“ → „Geschlossen · öffnet morgen um 6 Uhr“), danach Hinweis-Leiste; stoppt + bei Bedienung; bei reduzierter Bewegung fester Endzustand. +- Animation nur über `transform`/`opacity`; keine `https://` in Demo-Dateien; kein „24 h“; kein + Google-Ranking-Versprechen; WhatsApp nur als Text/Piktogramm ohne Logo. +- Budget: Haupt-JS ≤ +0,7 KB gz (Grenze 130 KB), neue Chunks ≤ 8 KB gz, Handy-Bild ≤ 60 KB. +- Tests: neuer `demo-zeiten.test.tsx` (Endzustand, nichts fokussierbar außer Regler, Status zu jeder Uhrzeit); + `relaunch-kundenportal.test.tsx`: `TITEL`, Karten-Obergrenze, `ziele`-Länge, Bilderzahl („2 + n“), + Handy-Bildzählung und `meldeMarke`-Indizes („Als App“ 7 → 8) aus der Stationsliste ableiten bzw. anpassen; + Spec-Text „höchstens 7 Karten“ anpassen. +- Das Handy-Bild entsteht erst, wenn der Portal-Editor steht. + +## 11. Fehlerfälle + +| Fall | Verhalten | +|---|---| +| API nicht erreichbar / Daten > 24 h alt | Leisten aus, Platzhalter zeigen Ersatz-HTML. | +| Browser-Uhr falsch (> 5 min laut `Date`) | Serverzeit wird verwendet. | +| Gleichzeitiges Speichern (Kunde + Team) | 409, Editor lädt neu und zeigt die neue Fassung. | +| Neubau nicht möglich / Sperrliste / Fremdhosting | `einbau: manuell`, Ticket enthält Einbau-Zeile; Kachel und K24 sagen „das Team baut ein“. | +| Ticket-Anlage oder K24 scheitert beim Gratisweg | Zustand bleibt `setup`; 5-Minuten-Job bzw. erneuter Klick holt nach. | +| Ticket geschlossen, Kunde hat keine Zeiten eingetragen | bleibt `setup`, WOMS „wartet auf die Zeiten des Kunden“. | +| Abo endet während `setup` | Abgleich wie 5 (Frist läuft unabhängig vom Einbau). | +| Behalten-Zahlung nach Datenlöschung | T19 „Erstattung fällig“. | +| Abo-Lesefehler | Einschalten 503, Kachel „gerade nicht prüfbar“, Abgleich überspringt den Kunden. | +| Hauptplan löschen | nicht möglich; erst anderen Plan zum Hauptplan machen. | +| Platzhalter mit unbekanntem `data-plan` | Ersatz-HTML bleibt, Konsole-Warnung. | +| Skript zweimal eingebunden (Runner + manuell) | zweiter Lauf beendet sich sofort. | +| Beliebige siteKeys (Abtasten) | Format-Prüfung, LRU-Cache mit Obergrenze, Rate-Limit. | +| Kunde gelöscht | API `aktiv: false`, Daten gelöscht (5). | +| Projekt archiviert | Funktion läuft weiter, solange `setup`/`active` (wie News); Neubestellung gesperrt. | + +## 12. Tests + +- **Zeitlogik** (`test/zeitenZeitlogik.test.js`): 29.03.2026 (Fenster 02:00–04:00, Öffnung 02:30 ⇒ 03:00), + 25.10.2026 (Fenster 02:00–06:00, „schließt um 02:30“); Fenster 18:00–02:00 (Status um 01:00 am Folgetag; + Feiertag/Urlaub am Folgetag ⇒ trotzdem offen bis 02:00; Sondertag am Folgetag); Mo+Di rund um die Uhr (kein + „24 Uhr“, Wechsel am Ende von Dienstag); Feiertag an geschlossenem Tag; Feiertag mit Abweichung; „bitte + prüfen“-Tage (Heiligabend, Mariä Himmelfahrt BY); Urlaub über Feiertag; Sondertag im Urlaub (Sondertag + gewinnt); überlappende Urlaube; Urlaub über den Jahreswechsel; „nach Vereinbarung“ (Status `null`, nicht im + JSON-LD); mehrere Pläne mit planbezogenen Ausnahmen; Hauptplan wechseln; Texte + „heute/morgen/Wochentag/Datum/Derzeit geschlossen/Mitternacht“; `wechsel` über 8 Tage; Hinweis-`ab/bis` + über Sommerzeitgrenzen. +- **Prüfregeln:** Fenster-Regeln, Längen, Link-/Telefon-/Selektor-Regeln, Aufräumen vergangener Einträge. +- **Abläufe:** Gratisweg (alle Stufen, `kein_abo`/`gekuendigt`/`nicht_aktiv`, Lesefehler 503, Doppelklick + idempotent, Selbstheilung Ticket/K24 im Job), Bezahlweg mit 409 `inklusive`, Team-Aktivierung über den + Gratis-Pfad, Ausgabe in `setup` wie in `active`, leere Pläne, `freischaltbar`-Gate, Neubau bei jedem + Übergang in einen ausgebenden Zustand und Serialisierung, Abo-Abgleich (K26-Varianten, 30 Tage, K27, Ticket + Ersatz-Tabelle nur bei Abweichung, Rückkehr bis Tag 60, Löschung nur bei `abo_ende`), Behalten (Zustand + bleibt während der Zahlung, kein neues Ticket, Session-Abgleich, T19 nach Löschung), Kunde gelöscht, + Erinnerung genau einmal (Neustart zwischen Merker und Mail, Bundeslandwechsel, mehrere Tage in einer Mail, + keine Mail bei geschlossenem Tag/Ausnahme/Abweichung). +- **Schnittstelle:** `aktiv:false`-Fälle, ETag/304 trotz `stand`, LRU-Grenze, Rate-Limit, keine internen IDs, + `firma` ohne Kundendokument. +- **Runner:** Einsetzen beider Tags in einem Durchlauf, idempotent, Whitelist, Dev-Pfad ohne `zeiten`, + Sperrliste ohne Einsetzen. +- **Skript** (Playwright, Testseiten): fester Header (Rücken), klebender Header, eigene Anruf-Leiste + (Kontakt-Leiste aus), vorhandenes JSON-LD (ergänzt, kein Duplikat, unparsbarer Block unberührt), kein JSON-LD + (Block neu), `aktiv:false` (Ersatz-HTML bleibt), unbekannter `data-plan`, keine Cookies/kein Storage, + doppeltes Laden, Handy- und Desktop-Breite, reduzierte Bewegung, ungültiger `navSelektor`. +- **Editor** (UI-Test mit Fixture): Pläne mit Kurznamen, Fenster über Mitternacht, Validierungsfehler je Feld, + Urlaub ⇒ Hinweis-Vorschlag, „Für Google kopieren“, Vorschau-Regler, Kundenansicht ohne WhatsApp-Haken. +- **WOMS:** Register-Pin, Zustandstexte, Team-Einstellungen, viewLog-Label. +- Bestehende Suiten bleiben grün (Portal `npm test` inkl. Mail-Pin und `customerViewLog`-Pin, WOMS `npm test` + + `vite build`, Relaunch-Tests). + +## 13. Reihenfolge und Livegang + +1. Spec (diese Datei) → Durchsicht durch den Inhaber. +2. Umsetzungsplan (`docs/superpowers/plans/2026-10-04-oeffnungszeiten-live.md`) → Freigabe. +3. Umsetzung in `dev-zeiten` (Portal) und einem WOMS-Worktree auf `feature/terminplaner-tp2` mit + Sonnet-Agenten; regelmäßig auf die TP2-Branches rebasen. Die TP2-Worktrees `dev-termine` werden nicht + angefasst. +4. Relaunch-Station auf eigenem Branch im Relaunch-Repo. +5. Rechtsrunde gemeinsam mit TP2 (Abschnitt 9). +6. Livegang nach TP2: Rebase auf `main` (mit TP2), Schema-Skripte (`feature-setup.mjs`, neue Collections), + Stripe-Katalog prüfen (`scripts/stripe-products.mjs --pruefen`, `PRODUCT_METADATA`), Runner-Variable + `ZEITEN_PUBLIC_BASE` und Sperrliste setzen, Inhaber pusht, `ZEITEN_ENABLED=pilot` (Testkonto), Probelauf auf + einer Testseite, dann `on`; Relaunch-Station `bald: false`. + +## 14. Nicht im Umfang (V1) + +Automatischer Abgleich mit dem Google-Unternehmensprofil (API/Cloud); statisches Vorbefüllen der Platzhalter +durch den Runner bei jedem Deploy (V1.1); Gemeinde-Feiertage; Schließen-Knopf der Hinweis-Leiste; Übernahme +der Zeiten in den Terminplaner; mehrere Hinweise gleichzeitig; Mehrsprachigkeit; Ankündigungsmail/Portal-Banner +(Z8).