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>
44 KiB
Ö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
k24AtheißteinschaltMailAt. 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
- Register-Eintrag
zeiteninconfig.features(TP2): KatalogpreiszeitenSetup(3000 Cent, einmalig),requiresWartungsAbo: false,ticketFreischaltung: true, neuaboInklusive: true,consentFrei(Zustimmung für den Gratisweg),teilweiseLive: ['setup', 'active']; Eintrag inSTANDARD_HAKEN(featureRegistry.js) mitverfuegbar(Schalter/Pilot) undfreischaltbar(4.4). - Collections
oeffnungszeitenundzeitenErinnerungen; neue Felder anportalFeatures. - Portal-Server: Dienst
server/services/zeiten/(Zeitlogik, Speichern, öffentliche Antwort, Freischalten, Abgleich, Erinnerung), Kundenroutenserver/routes/zeiten.js, öffentliche Routeserver/routes/publicZeiten.js, GratiswegPOST /api/funktionen/zeiten/einschalten, Behalten-Checkout. - Portal-Oberfläche: Kachel im Funktionen-Tab (
zeitenAngebotinGET /api/funktionen) und Editor-Ansichtpublic/zeiten*.js. - Website-Skript
public/embed/zeiten.js(ohne Abhängigkeiten). - Runner: zweite Einsetzung neben der Analyse, Feld
zeitenim Build-Auftrag. - WOMS: Funktionen-Register, Team-Aktionen und -Einstellungen, Protokoll-Label, Rechtliches-Karte.
- Mails K24–K27, Register-Texte für K15/K16/T16, Team-Ticket „Ersatz-Tabelle aktualisieren“.
- 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:MMin 5-Minuten-Schritten; je Tag ≤ 2 Fenster ohne Überschneidung;bis == vonungü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“ brauchtbis. Vergangene Ausnahmen/Hinweise und vergangenefeiertage-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.zielbeilink:new URLmuss gelingen, nurhttps:, ohne Benutzer/Passwort; beianruf: nur Ziffern, Leerzeichen,+,/,-. Telefon und WhatsApp-Nummer werden auf Ziffern (mit führendem49fü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.unlockedByPurchasebleibt 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).featureViewliefert zusätzlichunlockedBy,aboEndeAt,einbau,loeschungAm(auch fürzeiten).
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; beiabo_endebis 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:
- 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. - Steht das Dokument schon auf
setup/activemitunlockedBy: 'abo'⇒ idempotent: fehlende Schritte (Ticket, K24) nachholen, 200 mit aktuellem Zustand. Andere Zustände außeroff⇒ 409orderedConflictMessage. - Zustimmung speichern (
speichereZustimmungohnestripeSessionId). - Feature-Dokument auf
setup,unlockedBy: 'abo'. - Neubau anstoßen (4.6) und
einbausetzen. - Einbau-Ticket anlegen (Register-Text mit Ergebnis aus Schritt 5), dann K24 (Text nach
einbau), dannk24At– 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 wennaboEndeAtgesetzt oderdisabledmitabo_endeund Daten vorhanden): gleiche Zustimmung wie die Neubestellung, Stripe Checkout mit Metadatazweck: 'behalten',behaltenSessionIdam Dokument. Der Zustand bleibt unverändert (Leisten bleiben während der Zahlung live).- Webhook-Zweig
zweck: 'behalten'unterfeature-admin:<projectId>: Session passt zubehaltenSessionId⇒unlockedBy: 'kauf',aboEndeAtleer,behaltenSessionIdleer; war das Dokumentdisabled, zurück aufactive(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:
- 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“. - 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
feiertagejslandesweit führt, die aber nur örtlich gelten (Mariä Himmelfahrt in BY, Fronleichnam in SN und TH). Hinweis: Gemeinde-Feiertage sind nicht enthalten. - 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).
- 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. - 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 wieroutes/termine.js). - 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/bisrechnet 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öchstenrang(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
plaeneleer: kein Status, kein JSON-LD, Platzhalter behalten ihr Ersatz-HTML; Leisten erscheinen nur, wenn eingerichtet. whatsappUrl=https://wa.me/<ziffern>?text=<encodeURIComponent(starttext)>;routeUrlnach 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
- Hinweis-Leiste: Shadow-DOM-Host als erstes Kind von
body, schiebt den Inhalt. Feste/klebende Elemente mittop: 0(oderteam.navSelektor) werden, solange die Leiste sichtbar ist, ummax(0, Leistenhöhe − scrollY)nach unten gerückt (Inline-top, beim Entfernen zurückgesetzt);scroll-padding-topwird um die Höhe erhöht. Farben je Art mit Kontrast ≥ 4,5:1;role="region",aria-label="Hinweis". Aus beiteam.hinweisleisteAus. - Kontakt-Leiste (nur bei
matchMedia('(max-width: 768px)')): Shadow DOM, unten fest,z-index: 40, Abstandhalter am Ende vonbodyin Leistenhöhe,scroll-padding-bottom; Knöpfe Anrufen (tel:), Route, WhatsApp – erst der Klick öffnet den fremden Dienst. Aus, wennteam.kontaktleisteAusoder die Seite schon ein festes Element am unteren Rand mita[href^="tel:"]hat. - 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 nuropeningHoursSpecificationundspecialOpeningHoursSpecification(Text des Blocks perJSON.stringifyneu, unparsbare Blöcke bleiben unberührt); sonst legt es einen Block{ @type: LocalBusiness, name, address?, url, … }an. Regeln: Fenster über Mitternacht mitcloses < opens; „rund um die Uhr“ 00:00–23:59; „nach Vereinbarung“ wird ausgelassen; Sonderzeiten der nächsten 60 Tage mitvalidFrom/validThrough, geschlossen als00:00–00:00. - 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öpfeth scope="row", heutiger Tag mitaria-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. Unbekannterdata-plan: Platzhalter bleibt beim Ersatz-HTML, Konsole-Warnung (kein stilles Umschalten auf den Hauptplan). Beiaktiv: falsebleibt das Ersatz-HTML unverändert stehen.
- Tabelle: echtes
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 (ä, –); 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:
previewDeployliefert im Auftragzeiten: { siteKey } | nullüberbuildZeiten(project)(nur beisetup/active; nie im Dev-Pfad – dort wieanalyticsaufundefined;nullfür Projekte auf der Sperrliste;isPublicnicht nötig; wirft nie). Der Runner prüft das Feld (Whitelist invalidate.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-VariableZEITEN_PUBLIC_BASE, Vorgabehttps://project.webklar.com/embed/;injectIntoHtmlwird auf eine Marke je Tag verallgemeinert, heute festdata-wk-analytics). - Neubau anstoßen bei jedem Übergang in einen ausgebenden Zustand (
setup/active) ausoff,orderedoderdisabled(Gratisweg, Zahlung, Team-Aktivierung, Abo-Rückkehr, Behalten nach Abschaltung):triggerRebuild(MusteranalyseDeps.rebuild), serialisiert über eine Portal-weite Sperrezeiten-neubau(höchstens ein angestoßener Neubau gleichzeitig). Ergebnis ineinbau:angestossen(Auftrag angenommen) odermanuell(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 antwortetaktiv: 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, Ergebniseinbau, 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 (sonstnavSelektor), 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
setupundactivedasselbe (teilweiseLive);findActiveBySiteKeybekommt eine Zustandsliste.activeheiß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 überadminActivate(setzt heute direktactive). „Abschalten“ ⇒disabled, Grundteam, ohne Löschfrist.activate()und jede Wiederherstellung räumenaboEndeAtundbehaltenSessionIdab; eine Neubestellung nach Datenlöschung setztticketId,einbau,k24At,ersatzHashzurück. - Abo-Abgleich (eigener Tagesjob, nicht an
TERMINE_ENABLEDgekoppelt), nurunlockedBy === 'abo', Abo-Lage überaboLage()(Fehler ⇒ Kunde überspringen, nie abschalten):- Abo läuft nicht mehr und
aboEndeAtleer ⇒aboEndeAt = jetzt, K26 (Variante nachart: beendet / Wechsel zu Hosting / Wechsel widerrufen; Behalten-Link, Frist =aboEndeAt+ 30 Tage). aboEndeAt+ 30 Tage erreicht ⇒disabled,disabledReason: 'abo_ende', K27, ggf. Ticket „Ersatz-Tabelle aktualisieren“ (4.5).- Abo läuft wieder, solange die Daten noch da sind (also bis
aboEndeAt+ 60 Tage) ⇒aboEndeAtleer, Zustand zurück (active, wenn das Ticket geschlossen ist, sonstsetup), Neubau bei Rückkehr ausdisabled, K24-Variante „wieder eingeschaltet“. disabledmitabo_ende+ 30 Tage ⇒daten,teamund Merker gelöscht, Feature-Dokumentstate: 'off'. Bei Grundteamkeine automatische Löschung. K16 kann nach K26 noch kommen (Ticket wird während der Frist geschlossen); das ist gewollt.
- Abo läuft nicht mehr und
- Kunde gelöscht (
customerAdmin.js, Löschpfad):zeitenwird wietermineabgeschaltet;oeffnungszeiten,zeitenErinnerungenund der Cache-Eintrag werden gelöscht.retention.jsbekommtzeiten(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 < vonheiß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.
zuUtcliefert in der März-Lückenull; 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 vonzuUtc). - 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: Eintragzeiten(Name, Zustandstexte, „Leisten live – Einbau läuft“ bzw. „wartet auf die Zeiten des Kunden“ fürsetup, Abo-Ende-Hinweis mit Frist,loeschungAm),siteKeyOfumzeitenerweitert, Test-Pin angepasst.- Funktionen-Panel: Aktionen „Aktivieren ohne Zahlung“ / „Abschalten“; Team-Einstellungen
navSelektor,kontaktleisteAus,hinweisleisteAus(PATCH /api/team/funktionen/:customerId/zeiten/teammitteamVersion); Knopf „Ersatz-HTML kopieren“ je Plan. viewLog.js: Label fürzeiten_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).
- 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.
- 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.
- Datenschutz Portal: neuer § 6d (gespeicherte Angaben, Erinnerungsmail, Art. 6 Abs. 1 lit. b, Löschfristen); § 6 um „Öffnungszeiten“ im Kundenauftrag erweitert.
- 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. - 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?).
- 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.tsxFUNKTIONEN: 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) nachbaeckerei/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 ausdev-zeiten, Profilbaeckereimitfeatures.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; keinehttps://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 undmeldeMarke-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“ (Statusnull, 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 409inklusive, Team-Aktivierung über den Gratis-Pfad, Ausgabe insetupwie inactive, 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 beiabo_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 trotzstand, LRU-Grenze, Rate-Limit, keine internen IDs,firmaohne 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), unbekannterdata-plan, keine Cookies/kein Storage, doppeltes Laden, Handy- und Desktop-Breite, reduzierte Bewegung, ungültigernavSelektor. - 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 testinkl. Mail-Pin undcustomerViewLog-Pin, WOMSnpm testvite build, Relaunch-Tests).
13. Reihenfolge und Livegang
- Spec (diese Datei) → Durchsicht durch den Inhaber.
- Umsetzungsplan (
docs/superpowers/plans/2026-10-04-oeffnungszeiten-live.md) → Freigabe. - Umsetzung in
dev-zeiten(Portal) und einem WOMS-Worktree auffeature/terminplaner-tp2mit Sonnet-Agenten; regelmäßig auf die TP2-Branches rebasen. Die TP2-Worktreesdev-terminewerden nicht angefasst. - Relaunch-Station auf eigenem Branch im Relaunch-Repo.
- Rechtsrunde gemeinsam mit TP2 (Abschnitt 9).
- 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-VariableZEITEN_PUBLIC_BASEund Sperrliste setzen, Inhaber pusht,ZEITEN_ENABLED=pilot(Testkonto), Probelauf auf einer Testseite, dannon; Relaunch-Stationbald: 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).