Files
Webklar-Kundenbereich/docs/superpowers/specs/2026-10-04-oeffnungszeiten-live-design.md
T
ClaudeandClaude Opus 5.5 c7cd436ec2 Öffnungszeiten: Mails K24–K27 heißen jetzt K27–K30, Feld k24At heißt einschaltMailAt
K24–K26 sind seit dem Webshop-Livegang (main) vergeben; der Mail-Lader sucht die Vorlage über den
Dateianfang, doppelte Nummern hätten die falsche Mail geschickt. Das Feld ist noch nicht in der
Produktion angelegt (zeiten-setup.mjs lief noch nicht), daher jetzt ein sprechender Name.
Spec und Plan behalten die alten Nummern (Hinweis oben im Dokument).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:00:17 +00:00

44 KiB
Raw Blame History

Öffnungszeiten live (Funktion im Kundenportal) – Design

Hinweis 2026-10-06: Die Mails dieser Funktion heißen inzwischen K27 (eingeschaltet), K28 (Feiertage prüfen), K29 (Abo beendet) und K30 (abgeschaltet), weil K24–K26 seit dem Webshop-Livegang vergeben sind; das Feld k24At heißt einschaltMailAt. Dieses Dokument behält die alten Nummern.

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
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:<staffId>). Rechte wie die übrigen Funktions-Collections (nur Server-Key, Label-Team). Alle Schreibzugriffe laufen unter withLocks(['zeiten:<projectId>']); 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:<projectId>: 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 <input type="time" step="300">-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: <ISO>,
  plaene: [{ kurz, name, haupt,
             woche: [{ tag: 1, modus, fenster }],
             tage: [{ datum, art: 'regulaer'|'feiertag'|'urlaub'|'sondertag', name, modus, fenster }],  // 60 Tage
             wechsel: [{ ab: <epoch-ms>, offen: true|false|null, text, kurz }] }],                    // 8 Tage
  hinweise: [{ art, text, knopf, ab: <epoch-ms>, bis: <epoch-ms>, 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/<ziffern>?text=<encodeURIComponent(starttext)>; 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): <div data-webklar-zeiten data-modus="tabelle|status|badge" data-plan="<kurz>">Ersatz-HTML</div>.
    • Tabelle: echtes <table> 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=<kurz> liefert eine schlichte Wochentabelle des Plans als ASCII mit Entities (&auml;, &#8211;); 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 </head> jeder HTML-Datei ein: <script data-wk-zeiten src="<ZEITEN_PUBLIC_BASE>zeiten.js" data-site="<siteKey>" defer></script> (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 <input type="range"> 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).