Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,450 @@
# HiveGrid 2.0 — Architektur-Entscheidungen
> Entscheidungen aus dem Boilerplate-Abgleich (Stand 2026-06-04). Diese ADR-Liste
> ergänzt `AGENTS.md` und ist verbindlich für die Umsetzung. Grundlage:
> `docs/boilerplate-mapping/_SUMMARY.md` + die Modul-Reports.
> Legende: ✅ entschieden · 🅿️ Backlog (Entscheidung später).
---
## AD-1 — Mandanten-Trennung: echtes Postgres-RLS ✅
**Entscheidung:** Row-Level-Security in Postgres als bauliche Vorgabe (nicht nur App-Level-Scoping).
- RLS-Policies (USING/WITH CHECK) als Forward-Migration mit Inverse-SQL-Kommentar
- Request-Middleware setzt `SET LOCAL app.tenant_id`
- App-DB-Rolle **ohne** `BYPASSRLS`
- Worker-/Job-Tenant-Context-Wrapper
- Cache-Präfix `tenant:{id}:`, S3-Pfad `tenants/{id}/`
- Ausnahmen ohne RLS: `users`, `subscription_*`, `feature_flags`, Branchen-Templates
**Warum:** Spec A.4 + M019-Erfolgskriterium (Pen-Test). Defense-in-depth — ein vergessener
Query-Filter kann keine Fremddaten mehr leaken. Boilerplate hat **nur** App-Level-Scoping.
**Betrifft:** M019 (+ jedes tenant-scoped Modul).
## AD-2 — Daten-Modell: typisierte Tabellen, Entity-Builder raus ✅
**Entscheidung:** HiveGrid-Fachentitäten (CRM, Deals, Aufträge, Tasks, …) als dedizierte,
typisierte First-Class-Prisma-Tabellen. Das Entity-Builder-CRM der Boilerplate
(`Entity`+`Row`+`RowValue`, `apps/web/app/modules/crm`) wird **entfernt**, nicht angepasst.
Custom-Felder über purpose-built `CustomFieldDefinition` + `CustomFieldValue` (JSONB-Wert),
nicht über die polymorphe Row-Engine.
**Warum:** Spec M01 verlangt typisierte Tabellen mit Constraints, Indizes (`pg_trgm`,
`tenant_id, archived_at, last_activity_at`), `ContactEmail`/`ContactPhone`, M:N-Relationen,
zentrale `Activity`-Tabelle. Polymorphes `RowValue` ist damit unvereinbar (Performance bei
10k+, Indizes, Permissions, RLS). Zwei Architekturen parallel = Fremd-Architektur sickert ein.
**Betrifft:** M01, M02, M03, M04, M26 (+ alle Fachmodule).
> **🔄 REVIDIERT 2026-06-13 (Modell-Pivot, siehe AD-20):** Deal + Project verschmelzen zu EINER
> typisierten `Projekt`-Tabelle (Superset der Felder) + `CustomFieldDefinition/Value` + einem
> neuen **Pipeline-Feld-Config-Layer** (welche Felder pro Pipeline+Status sichtbar/pflichtig sind;
> nicht sichtbare Felder bleiben im Hintergrund erhalten). Die Konfigurierbarkeit ist **Metadaten
> über typisierten Spalten** — der generische Entity-Builder (Entity/Row/RowValue) bleibt entfernt.
> AD-2-Prinzip („typisiert, kein Generikum") bleibt gewahrt. Anker: `.planning/ENTITY-MODEL.md`.
## AD-3 — Plattform-Admin-Identität: getrennt, Email+PW+TOTP, optional Authentik-SSO ✅
**Entscheidung:** Vollständig getrennte Admin-Welt — eigene `AdminUser`-Tabelle (eigenes
`passwordHash`/`totpSecret`/`status`), eigene `AdminSession` (server-side, IP-gebunden),
eigene `AdminRole`/`AdminPermission`-RBAC. Primärer Login: **Email + Passwort + TOTP**
(Magic-Link verboten). **Zusätzlich** optionaler **Authentik-OIDC/SSO**-Login, gemappt auf
dieselbe getrennte Admin-Identität. **Kein** Generalschlüssel zwischen Plattform und Mandanten.
- `AdminAuditLog` braucht **kein** neues Schema: vorhandener `AuditEvent` (Hash-Kette,
`tenantId=null` = plattformweit) deckt das ab.
- Boilerplate-Admin-Auth (Login-Flow, MFA-Step-up-Gate, Rate-Limit, Session-Cookie-Policy)
dient als **Vorlage**, nicht 1:1 (BP hat AdminUser nur als `User`-Extension).
**Warum:** Spec 37.1 / M034 verbietet den geteilten Identity-/RBAC-Pfad explizit.
**Betrifft:** M034–M041 (M37).
## AD-4 — Sessions: Postgres als Source-of-Truth + Redis-Cache ✅
**Entscheidung:** Server-side Session-Tabelle in Postgres (Geräte-Liste mit Browser/IP/
Standort/letzter Aktivität, Revoke, „von allen Geräten abmelden", 4h-Inaktivität,
30-Tage-Sliding-Window, Auto-Logout bei Passwort-/2FA-/Email-Änderung) als Quelle der
Wahrheit; **Redis-Cache** für schnelle Pro-Request-Validierung. Cookie hält nur die
Session-ID-Referenz.
**Warum:** Spec B.5 braucht dauerhafte, abfragbare Geräte-Liste + verlässliches Revoke-all;
reine Cookies (BP-Default) können nicht serverseitig widerrufen werden; reines Redis verliert
Geräte-Historie bei Flush.
**Betrifft:** M021 (Mandanten-User), M034 (`AdminSession`), M141 (Customer-Portal).
## AD-5 — Such-Engine: Meilisearch (self-hosted) + pgvector-Hybrid ✅
**Entscheidung:** Keyword-Suche über das **selbst gehostete Meilisearch** (BP-`@toolctrl/search`
weitgehend 1:1), semantische Suche über **pgvector + EU-Embeddings** (Scaleway 1024-dim, BP 1:1),
kombiniert via **RRF-Fusion**. Embedding-Pipeline + Outbox + Indexer-Worker aus der BP übernehmen.
**Warum:** Meilisearch wird ohnehin selbst gehostet (EU-Datenhoheit + Ops bereits gelöst) →
bessere Tippfehler-Toleranz/Relevanz + maximaler Reuse. **Weicht bewusst von M056
„Postgres-FTS, kein Extra-Service" ab → Change-Request gegen M056 nötig.**
**Betrifft:** M24 / M056–M058.
## AD-6 — EU-Email-Provider ✅
**Entscheidung (2026-06-16, EXT-02):** **Mailjet** (EU/FR) als EU-Email-Provider für
transaktionale **und** Marketing-Mails. MailerSend (BP-Default) wird **verworfen** — MailerSend,
Inc. ist US-Entität (Transfers via EU-US Data Privacy Framework), unter strikter LP-13-Lesart
(„keine US-Gesellschaft im Boot", wie bei OpenAI/Anthropic) problematisch.
**Warum:** EU-Datenhoheit ohne US-Gesellschaft im Boot; feature-reich (transaktional + Marketing
+ A/B in P14). Boilerplate ist bereits auf `MAILJET_*` verdrahtet.
**Verifikationsstand (EXT-02):** Sende-Domain `hivegrid.de` bei Mailjet verifiziert — SPF
(`include:spf.mailjet.com`) + DKIM (Selector `mailjet`) Auto-Probe **grün**, Sender `*@hivegrid.de`
aktiv; DMARC `p=none` (Monitoring während Warm-up).
**Cleanup-Follow-ups:** (a) `include:_spf.mailersend.net` aus dem SPF-Record entfernen, sobald die
Migration vollständig ist; (b) DMARC nach Warm-up von `p=none` auf `p=quarantine` ziehen.
**Betrifft:** M09, M10, M22, M102.
## AD-7 — Captcha / Bot-Schutz 🅿️ (Backlog)
**Status:** Entscheidung später. **Tendenz: EU** — Friendly Captcha (DE, GDPR-first) oder
self-hosted mCaptcha (passt zum Meilisearch-Self-Host-Ansatz).
- Turnstile (BP) und hCaptcha (Spec) sind beide US-Firmen → bei strikter Linie Change-Request.
**Betrifft:** Login (M020), Public-Forms (M05/M125), Booking (M06).
## AD-8 — Verschlüsselung: Per-Tenant-DEK + Envelope ✅
**Entscheidung:** Pro Mandant ein Data-Encryption-Key (DEK), verschlüsselt mit einem Master-Key
(Scaleway Key Manager o.ä.) — Envelope-Encryption. Crypto-Shredding bei Mandanten-Löschung
(M165: Tenant-Key wegwerfen → alle verschlüsselten Daten unlesbar).
**Warum:** Spec verlangt Per-Tenant für Passwort-Safe (M134) + OAuth-Tokens; globaler Key
(BP-Default) exponiert bei Kompromiss alle Mandanten und erlaubt kein Crypto-Shredding.
**Betrifft:** M134 (HR-Passwort-Safe), OAuth-Tokens (M09/M25/M12), M165 (DSGVO-Löschung).
## AD-9 — Realtime: WebSocket für Chat + SSE für Read-Updates ✅
**Entscheidung:** Generischer WebSocket-Room-Layer für bidirektionale Echtzeit (Team-Chat M13:
Rooms, Presence, Typing, Read-Receipts). SSE (BP-Default) bleibt für einseitige Live-Updates
(Kanban M03, Notifications M22). `hocuspocus` bleibt CRDT-only (Kollab-Dokumente).
**Warum:** SSE ist einseitig → für Chat ungeeignet; hocuspocus ist CRDT, kein Chat-Broadcast.
**Betrifft:** M13 (M110–M114); SSE-Pfad: M03, M22.
## AD-10 — Naming-Konvention: `@toolctrl/*` behalten, Docs anpassen ✅
**Entscheidung:** Die geteilten Boilerplate-Pakete behalten ihre Namen (`@toolctrl/*`,
`packages/ui-app`) — **kein** flächiger Rename zu `@hivegrid/*`/`ui-hivegrid`.
- `@toolctrl/*` + `ui-app`/`ui-subframe`/`ui-mock` = **geteilte Boilerplate-Pakete** (kommen
von upstream, bleiben mergebar).
- `@hivegrid/*` = **nur für genuin neue HiveGrid-Pakete**, die upstream nie existieren
(z.B. das neue Activity-Paket, HiveGrid-Domänen-Pakete) → keine Merge-Konflikte.
- HiveGrid-Doku (`AGENTS.md`, `docs/INTEGRATION-CONTRACTS.md`, PLANs, Reports) wird auf diese
Realität gezogen; wo Docs `ui-hivegrid`/`@hivegrid/foundation` sagen, ist `ui-app`/
`@toolctrl/foundation` gemeint.
**Warum:** Es sind 901 Dateien / 1649 `@toolctrl`-Vorkommen. Ein Rename würde **jeden** künftigen
`git merge upstream/main` (AD-Setup: Git-Downstream) massiv kollidieren lassen und die
Update-Fähigkeit zerstören. ToolCTRL ist die Firma, HiveGrid das Produkt — geteilte Pakete
unter `@toolctrl` sind korrekt.
**Betrifft:** Querschnitt (alle Module), Phase 0.
---
# Delivery- & Scope-Entscheidungen
## AD-11 — Bau-Strategie: Hybrid (Fundament-Kern, dann Geerkens-Kernpfad) ✅
**Entscheidung:** Nach Phase 0 zuerst der Fundament-Kern (M27 Auth + M33a Apps-Engine + M01 CRM-Datenmodell),
dann sofort die Geerkens-Wertschöpfungskette **Lead → Projekt (konfigurierbare Pipelines) → KWP**, danach die Breite
(HR, Portal, Marketing, Mobile, Billing-Feinheiten). Balance aus sauberem Fundament und früh sichtbarem Nutzen.
> **🔄 REVIDIERT 2026-06-13 (Modell-Pivot, AD-20):** Kette ist nicht mehr „Lead → Sales(Deal) → Auftrag(Project)",
> sondern **Lead → Projekt → KWP**. Sales und Auftrag sind konfigurierbare Pipeline-Views auf denselben `Projekt`-Kern.
## AD-12 — SHK-Domänen-Defaults: aus Spec als Seed, Noah validiert ✅
**Entscheidung:** Die SHK-Inhalte (10 Standard-Rollen + Permission-Bundles [27.7/N1], SHK-Sales-Pipeline-Stufen [M59],
Standard-Custom-Felder + Tags [M44], Provisionierungs-Daten [M158]) werden aus Spec/Pflichtenheft als
**deklaratives, leicht änderbares Seed** umgesetzt; Noah/Geerkens validieren iterativ im Klick-Pfad. Blockiert den Bau nicht.
> **🔄 AMENDMENT 2026-06-19 (Owner-Entscheidung Raffael, Partner-validiert):** Die spekulativen „10 Standard-Rollen"
> aus 27.7/N1 sind auf die **6 vom Partner gelieferten Rollen** festgelegt: **Geschäftsführer, Projektleiter,
> Verwaltung, Vertriebler, Kundensupporter, Monteur** (Seed `apps/web/app/utils/services/shkRoles.server.ts`,
> idempotent, `is_demo_data`). Das deklarative-Seed-Prinzip bleibt unverändert; nur die Anzahl/Liste ist fixiert.
> R-P2b-DoD + Planungs-Docs (.planning/, Milestone-Layer) entsprechend von 10 auf 6 nachgezogen.
## AD-13 — Go-Live-Scope: breiter Launch ✅
**Entscheidung:** Der erste Geerkens-Go-Live umfasst nicht nur die Vertriebskette, sondern auch **HR-Hub,
Customer-Portal, Marketing und Mobile**. Scope ≈ Großteil der 35 Module. (Sequenzierung trotzdem nach AD-11.)
## AD-14 — Migration: sauberer Start + KWP-Sync füllt nach ✅
**Entscheidung:** Geerkens startet weitgehend leer; KWP-Sync (M089ff) bringt Auftrags-/Stammdaten automatisch.
HubSpot/Quiply-Altdaten bleiben **extern als Read-only-Archiv**; nur punktuelle CSV-Importe via `packages/imports`.
Keine vollen Migrations-Pipelines pro Quelle zum Launch.
## AD-15 — Pricing/Billing: Engine bauen, Preise geparkt ✅
**Entscheidung:** Stripe-Integration + Seat-/Plan-Engine + Verbrauchs-Metering (M153–M156) werden **technisch gebaut**,
die konkreten Preis-Stufen (Cloud/Cloud+/Enterprise, Seat-Preise, Trigger-Volumen, Setup-Fee) bleiben **geparkt**.
Geerkens läuft auf manueller Sonderkondition/Trial. Self-Service-Checkout-Preise = eigene spätere Pricing-Runde.
## AD-16 — USP-Säulen (Automation + KI): volle Tiefe zum Launch ✅ (⚠ KI-Hälfte abgelöst von AD-26, 2026-06-22)
**Entscheidung:** Kompletter visueller Automation-Editor (M075–M082, alle Trigger/Aktionen, 24h-Undo, Guards) **und**
voller KI-Layer (M083–M088, EU-Provider, RAG, Sprach-zu-Automation, Pay-as-you-go) sind day-1 dabei — nicht nur Fundament.
> **⚠ KI-Hälfte abgelöst durch [AD-26] (2026-06-22):** Kein selbst-gehostetes LLM/RAG/HiveMind mehr — KI kommt via **MCP-Server** (externe Clients), M083–M088 gestrichen. Die **Automation-Säule bleibt** unverändert day-1.
## AD-17 — Mobile: PWA-only (v1 + v2), kein nativer Client ✅ (revidiert 16.06.)
**Entscheidung (revidiert 2026-06-16):** Mobile läuft **ausschließlich als installierbare PWA / responsive Web** — in v1 **und** v2. Monteur kann auf der Baustelle offline Aufträge bearbeiten, Abnahmen erfassen, Plantafel sehen; alles wird über Service-Worker + IndexedDB-Offline-Queue + Background-Sync gequeued und bei Sync mit „KWP gewinnt" aufgelöst (KWP-Sync = v2). **Kein Expo, kein Capacitor, keine native iOS/Android-App, keine App-Store-Einreichung.** Push = **Web-Push (VAPID)** — aktiviert in v2 den in P1 ruhenden `notify()`-Push-Kanal. Native-App + App-Store → frühestens **v3** (Backlog, nicht beauftragt).
**Historie:** 10.05. voller Offline-Betrieb (Capacitor, M167–M171) · 11.06. Capacitor→Expo + native Hülle nach v2 · **16.06. komplett auf PWA-only reduziert** (kein nativer Client in v1/v2; M167–M171 entfallen, „Mobile-App"-Arbeit = PWA-Ausbau: Installierbarkeit, Service-Worker, Offline-Härtung, Web-Push).
## AD-18 — Customer-Portal: voll inkl. Skalierungs-Sicht ✅
**Entscheidung:** Komplettes Portal day-1: Projekt-Sicht + Upload + Reklamation + Subunternehmer-Zugang +
Skalierungs-Sicht (Service-Inbox + Routing + Pro-Kunden-Lebensdauer), M141–M146 inkl. [N19].
> **Scope-Muster (historisch):** AD-13/16 wählen den maximalen Umfang → der erste Go-Live ist faktisch
> das **vollständige Phase-1-Produkt** in großer Tiefe. Sequenzierung nach AD-11. **AD-17 (Mobile) und
> AD-18 (Portal) wurden später revidiert** (AD-18 → v2; AD-17 → PWA-only v1/v2) und folgen diesem Maximal-Muster
> nicht mehr. Dies bleibt eine bewusst große Scope-Wahl (siehe Scope-Check in der Session + SCOPE-V1-CUTS.md).
## AD-19 — Go-Live-Modus: Big-Bang ✅
**Entscheidung:** Geerkens geht erst live, wenn das vollständige Phase-1-Produkt steht — **ein großer Launch**,
kein phasiertes Produktiv-Go-Live. Interne Bau-Reihenfolge bleibt AD-11 (Fundament → Kernpfad → Breite),
aber es gibt kein inkrementelles Produktiv-Schalten einzelner Module für Geerkens.
**Bewusst akzeptiert:** längste Zeit bis zum ersten echten Nutzer-Feedback, höchstes „am-Alltag-vorbei"-Risiko.
**Gegenmaßnahme empfohlen:** Noah/Geerkens-Validierung pro Modul über den PROGRESS-TRACKER-Klick-Pfad
(READY-TO-VERIFY → Sign-Off) als Feedback-Ersatz vor dem Big-Bang.
## AD-20 — Projekt-zentrisches CRM/Pipeline-Modell ✅ (NEU 2026-06-13)
**Entscheidung:** Das CRM/Pipeline-Modell wird **Projekt-zentrisch** (auf Basis von Davids UX-Briefing,
voll adoptiert). Statt getrennter Deal- und Project-Entitäten gibt es **EINEN `Projekt`-Kern**, auf den
**frei konfigurierbare Pipelines als Views** schauen. „Deal" und „Auftrag" sind nur Pipeline-Namen/-Typen,
keine eigenen Objektarten.
- **Pipeline = View:** Admin legt Pipelines an/benennt sie, steuert Zugriff (wer sieht), Feld-Sichtbarkeit,
Pflichtfelder pro Pipeline/Status und die Statuswerte. Ein Projekt ist genau **einer** Pipeline zugeordnet;
`Status` ist pipeline-intern (kein globaler Projektstatus). Nicht sichtbare Felder bleiben im Hintergrund erhalten.
- **Lead-Flow:** Lead/LeadInput ist Vorstufe mit Datenpunkt `Weiterverarbeitungsfreigabe` (Ja/Nein/Unklar);
bei Freigabe → Vererbung ins Projekt. **Lead → Projekt** (das frühere Lead→Contact-Promotions-Modal entfällt;
Kontakt wird separat verknüpft).
- **Kontaktmodell:** `Kontakt` ist Oberbegriff mit `Kontakttyp` (Privatperson/Organisation), `Kontaktstatus`
(Kontakt/Kunde/Bestandskunde) und `Kontaktart` (Haupt-/Sekundärkontakt als Rolle/Relation).
- **Scope-Calls (13.06.):** Abnahme bleibt **v1** als Checklisten/Protokoll-Sub-Area am Projekt (Mängel = Checkliste);
Material/Zeiterfassung/Leistungspositionen/Baustelle/Support → v2-Backlog. **Rechnung** = einfaches Sub-Entity am Projekt in v1.
KWP-Felder bleiben v2-Sync, v1 nur Feld-Halter.
**Warum:** Davids konsolidiertes UX-Modell wird zur Bau-Grundlage gemacht; Raffael richtet die Planung daran aus
(statt David einzunormen). Höhere Konfigurierbarkeit (SHK-Betriebe brauchen frei benennbare Pipelines) bei
erhaltenem typisiertem Kern.
**Betrifft:** AD-2 (revidiert), AD-11 (Kette revidiert), M01–M04, ENTITY-MODEL, ROADMAP P6–P9, REQUIREMENTS, SCREEN-MAP.
**Anker:** `.planning/ENTITY-MODEL.md`.
## AD-21 — Prisma: Schema-Folder + Migrations-Disziplin für parallele Branches ✅ (NEU 2026-06-16)
**Kontext:** 2 Devs (Raffael + Akeshan) bauen Modul-Phasen parallel in eigenen Branches. Risiko: Kollisionen im Datenmodell und in der Migrations-Historie.
**Befund (16.06.):** Prisma **7.8.0**; das Schema ist **bereits als Ordner** organisiert (`prisma/schema/` mit 29 Domänen-Dateien: `core`, `accounts`, `permissions`, `appsEngine`, `email`, `workflows`, `notifications`, `marketingLeads`, `ai` …). Die **Datei-Ebene ist damit gelöst** — Module in verschiedenen Domänen-Dateien kollidieren nicht. Es existieren bereits 153 Migrations (lineare Timestamp-Historie). Connection-URLs in `prisma.config.ts` (Prisma 7): `DIRECT_URL` für Migrate, `DATABASE_URL` via PgBouncer zur Laufzeit.
**Verbleibende Risiken:** (1) `core.prisma`/`accounts.prisma` sind der geteilte Kern (Tenant/User/Audit/File/ApiKey/Credential) — gleichzeitige Relations-Edits kollidieren *in dieser Datei*. (2) Zwei Branches mit je `migrate dev` erzeugen divergierende Migrations-Ordner → „applied out of order" beim Merge.
**Entscheidung — drei verbindliche Konventionen:**
1. **Ein Modul = eine eigene `prisma/schema/<modul>.prisma`.** Neue Modelle **nie** in `core.prisma`/`accounts.prisma`. Diese nur für Relationen zu `Tenant`/`User` anfassen, Rück-Relationsfelder dort minimal halten.
2. **Migrationen erst beim Merge.** Auf dem Feature-Branch mit `prisma db push` iterieren (kein Migrations-File). Die **eine** Migration pro Phase erzeugt der Dev, der zuerst auf `main` merged — Forward-only mit Inverse-SQL als Kommentar (AGENTS.md DoD 28). Tägliche Integration hält Diffs klein.
3. **CI-Gate vor Merge:** `prisma validate` + `prisma migrate status` grün (pre-push-Hook schärfen).
**Warum:** Schema-Splittung erledigt die Datei-Konflikte bereits; was bleibt (core-Hotspot + lineare Migrations-Historie) löst Konvention/Disziplin, nicht neue Technik.
**Betrifft:** alle parallel gebauten Modul-Phasen (P2–P21, v2), `AGENTS.md` §Prisma-Schema, lefthook pre-push.
**Anker:** `prisma/schema/`, `AGENTS.md`.
## AD-22 — GeoIP lokal + IP-Anonymisierung (EXT-12) ✅
**Entscheidung:** Geo-Auflösung von Login-/Session-IPs über eine **lokale GeoIP-Datenbank**
(DB-IP Lite Country, CC-BY-4.0, MMDB-Format) statt eines externen Dienstes. Die gespeicherte IP wird
**pseudonymisiert** (HMAC-SHA256 mit geheimem Pepper); persistiert wird nur das **Land**
(`countryCode`/`countryName`). **Kein** EU-SaaS-Geo-Dienst, **kein** ipstack (US/apilayer, LP-13).
**Warum:**
- Ein externer Geo-Dienst — auch ein EU-gehosteter — bekäme die Endnutzer-IP (personenbezogen)
übermittelt → DPA + Subprozessor (M166) + Latenz + Abhängigkeit. Der **Transfer selbst** ist das Risiko.
- Lokale MMDB: Lookup passiert auf unserem Server, **die IP verlässt unsere Infra nie**. Kein DPA,
kein Subprozessor. Passt zur Self-Host-Linie (Meilisearch/mCaptcha).
**Wofür Geo-IP (Zweck):** „Login aus Land X" + Anomalie „Login aus neuem Land" in der Session-/
Security-Sicht (P2); optionale Analytics-Anreicherung nach Land (Default aus); Feature-Flag-Targeting
nach Land. Der HIBP-Breach-Check (EXT-12, `password-policy.server.ts`) ist davon unabhängig und bereits aktiv.
**Speicher-/Minimierungs-Regeln:**
- IP nie im Klartext: `IpAddress.ip`/`IpAddressLog.ip` speichern `HMAC-SHA256(IP, IP_HASH_PEPPER)`.
Der Pepper liegt in den Coolify-Secrets, **nicht** in der DB → ein DB-Leak allein ist wertlos; der
Gleichheits-Vergleich („gleicher Client?") bleibt möglich (Pseudonymisierung statt Anonymisierung).
- Geo nur Land: `regionCode`/`city`/`zipCode`/`latitude`/`longitude` bleiben leer/null.
- Der Blacklist-Abgleich nutzt die Roh-IP nur zur Laufzeit (Security-Liste), persistiert sie nicht im Log.
**Provisionierung:** Die MMDB ist ein **Deploy-Artefakt** (nicht im Git), Pfad via `GEOIP_DB_PATH`
(Default `./data/geoip/dbip-country-lite.mmdb`); `scripts/update-geoip-db.sh` lädt die Monatsversion.
Fehlt die Datei, ist die Land-Auflösung ein No-op (Stub) ohne Fehler. Reader: npm `maxmind` (MIT).
**Env:** `IP_HASH_PEPPER` (Secret), `GEOIP_DB_PATH` (optional).
**Folge-Arbeit (offen):** monatlicher Worker-Cron (BullMQ) statt Shell-Skript; `IpAddress`/`IpAddressLog`
perspektivisch auf Land + Hash-IP eindampfen (Spalten entfernen, sobald Session-Filter/Feature-Flag-
Konsumenten umgestellt sind); UI, die bisher die Roh-IP zeigte, auf Land umstellen.
**Betrifft:** EXT-12, P2 (Sessions/Security), P21 (DSGVO/Retention), `modules/ipAddress`, `services/auth/password-policy`.
**Anker:** `apps/web/app/modules/ipAddress/`, `docs/ARCHITECTURE-DECISIONS.md`.
## AD-23 — Media-Auslieferung: privater Bucket + authentifizierter App-Proxy (kein public-read) ✅
**Entscheidung (2026-06-17):** Hochgeladene Medien werden **nicht** über public-read S3 ausgeliefert.
Der Bucket bleibt **privat**; die App liefert Medien über eine **authentifizierte Proxy-Route** aus
(Session + Tenant-Scope + Permission), die das Objekt aus dem privaten Bucket streamt bzw. auf eine
kurzlebige signierte Download-URL weiterleitet. `STORAGE_PUBLIC_BASE_URL` bleibt leer; der `publicUrl`-
Pfad (`RowHelper`/`MediaHelper`/`ApiHelper`, `publicUrl ?? file`) zeigt auf die Proxy-Route.
**Warum:** CRM-Medien enthalten Kundendaten/Dokumente — world-readable Buckets sind DSGVO-untragbar
(jeder mit der URL käme ohne Auth/Tenant-Check ran). Weg A (public-read) ist damit verworfen.
**Für P4 (Files, `AK-P4a/b`) umzusetzen:**
- Proxy-Route (z.B. `/media/…` bzw. `/storage/…`) mit Auth + `withTenantScope()` + `withPermission()`,
streamt aus dem privaten Bucket oder redirected auf signierte Download-URL (kurze TTL).
- `publicUrl` in den Media-Helpern auf die Proxy-Route verdrahten (statt null/rohem Key).
- **Genuin öffentliche Assets** (E-Mail-Bilder, öffentliche Formular-/Booking-/Portal-Seiten,
Branding-Logos) können keinen Auth-Proxy nutzen → signierte URLs mit TTL **oder** ein separater
`public/`-Prefix mit eng begrenztem public-read. Pro Asset-Typ entscheiden.
- DSGVO-Export-Artefakte nutzen bereits signierte URLs (unverändert).
**Betrifft:** P4 (Files), EXT-06 (nur Bucket/Infra), `packages/storage`, `apps/web` Media-Helper.
**Anker:** AD-8 (Verschlüsselung), `packages/storage`, `apps/web/app/utils/helpers/MediaHelper.ts`.
---
## Phase-0-Pflichten (kein „Entscheid", sondern Voraussetzung — siehe `_SUMMARY.md`)
Diese Punkte sind vor der ersten Fachmodul-Umsetzung abzuarbeiten:
1. **Boilerplate als Code-Basis einziehen** (Repo ist heute Docs-only).
2. **Naming-Drift beheben:** `@toolctrl/*` → `@hivegrid/*`, `ui-app`-Stub → `ui-hivegrid`,
Sidebar-Konvention, falsche AGENTS.md-Pfade (`indexed-entities.ts`, `sidebar.config.ts`).
3. **EU-Sterilisation:** GitHub/Google-OAuth-Reste + `User.githubId/googleId` entfernen.
4. **M00 aktivieren** + 4 echte Neubauten: `logActivity()`/Activity-Stream, Search-`indexed-entities`-Registry,
**Subframe-Adapter-Code** (Quer-Voraussetzung für jede UI-Route), Mass-Action-Schutz.
5. **Fehlende Dependencies** ergänzen (PLAN.md teils falsch): `signature_pad`,
`browser-image-compression`, `react-grid-layout`, `@dnd-kit` (statt react-beautiful-dnd),
`libphonenumber`, `ics`/`ical-generator`, `btree_gist`/`EXCLUDE`-Constraint u.a.
## AD-24 — Prod unter eigenem Hetzner-Account (dediziert, self-contained) ✅
**Entscheidung (2026-06-19, Owner Raffael):** Produktion läuft in einem **eigenen, dedizierten
Hetzner-Cloud-Account**, vollständig getrennt von Staging/dev. Topologie — **5 VMs** (fsn1, x86 cx-Linie):
- **control** — eigenes **Coolify** + Orchestrierung/Bastion (cx33: 4 vCPU/8 GB)
- **web**, **api** (je cx33)
- **worker + services** — 5 Worker-Queue-Gruppen + **self-hosted Meilisearch + ClamAV + mCaptcha** (cx43: 8 vCPU/16 GB)
- **db** — Postgres + PgBouncer + Redis + Daten-Volume (cx33 + Volume)
mCaptcha nutzt die Postgres/Redis der db-VM (eigene DB), keine separate VM. Self-contained: eigene
Zentral-Dienste (kein Zugriff aufs Staging-Privatnetz), eigener **DEK-Master-Key** (AD-8) + eigener
**S3-Bucket** (`hivegrid-prod`, fsn1), eigenes Coolify.
**Ingress (Variante „Cloudflare-Proxy"):** Cloudflare **proxied** vor `app./api./captcha.` (Edge-TLS,
DDoS/WAF, Origin-IP versteckt) → **Coolify/Traefik pro Host** (Let's-Encrypt am Origin). `admin.`
restriktiv (dns-only + Firewall bzw. Cloudflare Access). **Kein Hetzner-Load-Balancer** bis zur
horizontalen Skalierung — die App ist über serverseitige Sessions in Postgres+Redis (AD-4) dafür bereit;
LB + 2+ web/api-Instanzen kommen nach Anhebung des VPS-Limits.
**Netzwerk:** privates Hetzner-Network + Firewall — `db`/`services` nur privat erreichbar, öffentlich
nur 80/443 (web/api/control), SSH eingeschränkt.
**Warum:** Blast-Radius/Isolation (getrennte Tokens/Billing, kein Cross-Deploy-Unfall Staging↔Prod),
saubere DSGVO-/Audit-Grenze, Crypto-Shredding-Grenze pro Account. Revidiert/erweitert die Infra-ADR-009
(Hetzner+Coolify) um die Account-Trennung.
**Grenzen:** Hetzner-Default-Limit 5 VPS/Projekt → echtes HA (web1/web2 + api1/api2 hinter LB) erst nach
Limit-Anhebung (Support). Provisionierungs-Schritte + Go-Gates: `docs/ops/prod-rollout-hivegrid.md`.
**Betrifft:** Prod-Deploy, Ops, alle Module.
## AD-25 — E-Signatur: gehärtete SES-Eigenbau, kein externer Provider (v1); QES-Add-on via DE-QTSP (v2) ✅ (NEU 2026-06-21)
**Entscheidung (2026-06-21, Owner Raffael):** HiveGrid baut die elektronische Signatur (Angebote/Aufträge/Abnahmen)
für v1 **selbst als gehärtete SES** und macht das zum **Default für alle Tenants** — **kein externer Provider als
Zwang**. **PandaDoc bleibt als optionale BYOK-Anbindung** erhalten: Tenants mit eigenem PandaDoc-Account tragen
ihren **eigenen API-Key** ein und signieren über *ihren* Account (Opt-in). Damit ist die Annahme „PandaDoc ist der
*zentrale/erzwungene* Provider" abgelöst (Abweichung vom Pflichtenheft V.4.4), PandaDoc selbst aber **nicht** —
es wird vom Default zum **Opt-in-Adapter**. Betrifft AK-P16d, EXT-09, EXT-17, VR-P16.
**BYOK-Vorteil (CLOUD Act):** Bei BYOK nutzt der Tenant bewusst seinen *eigenen* PandaDoc — die CLOUD-Act-/Drittland-
Verantwortung liegt damit beim Tenant (dessen Account, dessen Entscheidung), nicht bei HiveGrid. Das „100 % EU"-
Default (SES-Eigenbau) bleibt unangetastet; PandaDoc ist eine transparente Opt-in-Wahl (Subprozessor-Liste/AVV).
**Begründung (dreifach belegt):**
- **Rechtlich:** SHK-Dokumente sind formfrei oder Textform (§§ 631, 650i Abs. 2, 640 BGB) → **SES genügt** (auch
beim Verbraucherbauvertrag, der nur Textform verlangt — keine QES). Einziger zwingender QES-Fall im ganzen SHK:
**Kündigung eines Bauvertrags (§ 650h BGB)** — kein Tagesgeschäft, selten.
- **Praxis:** Marktstandard im DE-Handwerk ist die **SES-Touch-Unterschrift vor Ort** (+ Fern-Link). Alle
etablierten Tools (HERO, plancraft, Craftnote, ToolTime …) bieten SES; AES/QES spielt im Tagesgeschäft keine
Rolle, **kein Marktdruck**. Wettbewerbsfaktor ist der Prozess (papierlos → schneller zur Rechnung), nicht die Stufe.
- **Souveränität:** PandaDoc/DocuSign = US-Firmen → **CLOUD Act §2713**, bricht das „100 % EU"-Versprechen (A.12.4).
EU-Provider geprüft (Yousign FR am besten; Skribble CH hat US-Subprozessoren/kein echtes Embedding/per-Seat). OSS
geprüft (Documenso = nur SES + AGPL). **Eigenbau ist souveränster + günstigster + rechtlich ausreichend.**
**Umsetzung v1 (P16 / AK-P16d) — „gehärtete SES" (praktisch auf AES-Beweisniveau, ohne PKI-pro-Person/QTSP):**
1. **Touch-Signatur vor Ort** (Canvas → PDF) + **Fern-Link-Signatur** (Angebot remote annehmen)
2. **OTP-Identifizierung** (E-Mail; SMS als stärkere Zwei-Kanal-Variante)
3. **PDF-Versiegelung** (PAdES mit HiveGrid-Organisations-Zertifikat → jede Änderung bricht die Signatur)
4. **Zeitstempel** (TSA, RFC 3161; nicht-qualifiziert genügt für SES/AES)
5. **Audit-Trail** über M001 (Link/Öffnen/OTP/Signatur: wer, wann, IP, Gerät, Geo, PDF-Hash; append-only)
6. **Completion-Certificate** als PDF-Anhang (sichtbare Beweis-Doku)
7. **Einmal-Link** (personengebunden) + expliziter **„Intent to sign"**
> Beweiskraft: gehärtete SES wie AES unter freier Beweiswürdigung (§ 286 ZPO) — der Audit-Trail trägt, nicht das Stufen-Etikett. Anscheinsbeweis (§ 371a) hat nur QES.
**Architektur:** Signatur über `defineAdapter` (Apps-Engine, provider-agnostisch). Adapter:
- **`ses-selfhosted`** — Default (v1), die gehärtete Eigenbau-SES.
- **`pandadoc-byok`** — optional (v1), Tenant bringt eigenen PandaDoc-API-Key (DEK-verschlüsselt im Credential-Store); Mandanten-Trennung via eigener Account + Webhook-Routing (pro-Tenant-URL + `metadata.hivegrid_tenant_id`). Anbindungs-Spec: [[EXT-17]].
- Spätere Optionen ohne Fachcode-Umbau: **AES** (Yousign managed oder EU-DSS-Library self-hosted, Java-Sidecar), **QES** (DE-QTSP **D-Trust sign-me**, Hash-only — Dokument bleibt im Perimeter) für § 650h-Bauvertragskündigung.
**Betrifft:** P16 (AK-P16d), EXT-09 (SES-Eigenbau-Default), EXT-17 (PandaDoc-BYOK-Adapter), VR-P16.
PandaDoc bleibt als BYOK-Opt-in. Vor Produktivschaltung der Signatur: anwaltliche Bestätigung der Form-/Beweis-Einordnung (SES genügt, § 650h→QES).
## AD-26 — KI-Pivot: MCP-only, kein selbst-gehostetes LLM/RAG (löst die KI-Hälfte von AD-16 ab) ✅ (NEU 2026-06-22)
**Entscheidung (2026-06-22, Owner Raffael):** HiveGrid hostet **kein** eigenes LLM/RAG/HiveMind mehr. Statt eine KI-App
zu sein, wird HiveGrid **KI-anschließbar** über einen **MCP-Server** (Model Context Protocol): externe KI-Clients
(Claude Desktop/Cursor/ChatGPT) docken an, **Inferenz liegt beim Client**. Damit entfällt der EU-LLM-Provider-/DSGVO-
Zwang für HiveGrid selbst (kein Mistral/Scaleway/AlephAlpha-Hosting mehr). Löst die **KI-Hälfte von AD-16** ab — die
**Automation-Säule bleibt**.
**Begründung:** Kleines Team; der teure/harte Teil (LLM-Hosting, RAG-Infra, Promptfoo-Eval, AiCredits-Abrechnung,
EU-Provider-Compliance) fällt weg. MCP nutzt **dieselbe mandanten-gescopte Daten-/Aktions-Fläche** wie die geplante
Public-API ([[VR-P22a]]/[[VR-P22b]]) → keine Doppelarbeit.
**v1 (KI komplett gestrichen):** R-P13 (KI-Layer/Mistral-Gateway) + D-P13 entfernt, EXT-11 + Mistral-Key gelöscht,
Milestones **M083–M088 gelöscht**, R-P12b verliert den Sprach→Automation-Teil (bleibt reiner visueller Flow-Editor).
Neues letztes v1-Glied = R-P12b (~1 Tag Puffer → Härtung). Freeze/Launch **09./10.07.** bleiben fix.
**v2:** VR-P25/VD-P25 (HiveMind/RAG/Whisper) **gestrichen** (Plane: Cancelled) → neu **VR-MCP + VD-MCP** im Slot
**18.–20.07.**, `blocked-by` VR-P22a/b (Public-API, fertig 17.07. → kein Konflikt).
**MCP-Design (Best-Practice, MCP-Spec-Rev 2025-11-25):** Remote-Server über **Streamable HTTP** (stateless Start),
SDK **`@modelcontextprotocol/sdk`** (Gerüst `apps/web/app/modules/mcp/` existiert bereits). **Auth Stufe 1 =
API-Key-Bearer** (aus VR-P22), **Stufe 2 = OAuth 2.1** (PKCE/RFC 9728/8707) später. **Tenant aus dem Token** (nie aus
Tool-Argumenten) → `withTenantScope()` + RLS. **Read-Tools + Write-Tools** (`withPermission()` + Client-Confirmation +
`auditEvent`/`logActivity`). Annotations (`readOnlyHint`/`destructiveHint`) nur als Hint — echte Guards serverseitig.
Rate-Limit + Output-Limits + Pagination; Tenant-Daten sind **Daten, nie Instruktionen** (Prompt-Injection).
**Code-Rip-out (Variante a):** raus = `packages/ai`, `packages/vector-store`, Module ai/ai-gen/ai-cost/chatbot/
ai-elements, AI-Routen, Worker-AI-Jobs, `ai.prisma` — UND die Legacy-PromptFlow-Engine (promptBuilder/admin/prompts/
`promptFlows.prisma`). **Bleiben:** `@modelcontextprotocol/sdk`, `modules/mcp/`, der EU-Sterilisations-Guard.
**Pflichtenheft:** A.12.3 (EU-LLM-Provider) und V.2.2 („KI als Bedienoberfläche der Automation") widersprechen jetzt →
**Change-Request (M176) nötig**; bis dahin gilt diese AD vor dem Pflichtenheft (Pivot-Vorrang). Anti-Liste V.3.2.7
(HiveMind autonom) bleibt erfüllt (wird gar nicht gebaut).
**Betrifft:** R-P13/D-P13/EXT-11/M083–M088 (entfernt), R-P12b (Trim), VR-P25/VD-P25 (gestrichen), VR-MCP/VD-MCP
(neu), R-P11a/R-P11b/D-P3/R-P5/D-P5/D-P20 (KI-Andock umdeuten/trimmen), VA-P24b/VA-P29/VR-P31/VR-P33 (KI-Teile entfallen).
## KWP (separater Strang — siehe `docs/boilerplate-mapping/M19-kwp-integration.md`)
KWP bekommt eine offizielle **API-Anbindung** → M089 (Windows-Sync-Programm) entfällt,
M090/M091/M092/M094/M095 vereinfachen sich, M093 bleibt. KWP als `DataConnection type='rest-api'`
ins Apps-Engine-Muster (M33a). M089–M095 vor Umsetzung neu schneiden.
@@ -0,0 +1,238 @@
# HiveGrid Integration-Contracts
**Zweck:** Verbindliche Docking-Points, an denen jedes neue Modul anschließt. Wenn ein AI-Coding-Agent eine Funktion baut, ohne sie hier anzudocken, ist die Funktion **nicht erreichbar** und das Milestone gilt als nicht erledigt.
Diese Datei ist Quelle der Wahrheit für: Sidebar-Gruppen, Route-Patterns, Breadcrumbs, Permissions, Search-Index, Activity-Stream, Audit, i18n.
> **Naming-/Pfad-Realität (Stand 2026-06-04, siehe `docs/ARCHITECTURE-DECISIONS.md` AD-10):**
> Das HiveGrid-UI-Paket heißt real **`packages/ui-app`** (nicht `packages/ui-hivegrid`). Die
> Sidebar-Konfiguration liegt in **`apps/web/app/application/sidebar/`** (nicht `sidebar.config.ts`).
> Der Such-Index / `indexedEntities` gehört nach **`packages/search/`** (nicht
> `packages/db/src/search/indexed-entities.ts`). Geteilte Pakete bleiben `@toolctrl/*`; neue
> HiveGrid-Querschnittsdienste (z.B. Activity/`logActivity`) werden als neues `@hivegrid/*`-Paket
> angelegt. Lies die `ui-hivegrid`/`@hivegrid/*`/`sidebar.config.ts`-Erwähnungen unten entsprechend.
## 1. App-Shell-Sidebar (Mandanten-App `app.hivegrid.de`)
Sidebar ist in 6 feste Gruppen geteilt. Jede neue Funktion gehört in **genau eine** Gruppe:
```
🏢 ARBEITEN
├─ Dashboard → M147-M150
├─ Globale Suche (Cmd-K) → M058
├─ Lead-Inbox → M047-M051
├─ Verkaufsfälle (Sales) → M059-M064
├─ Aufträge (Projekte) → M065-M070
├─ Aufgaben → M071-M074
├─ Termine → M120
└─ Buchungs-Seiten → M126-M129
📇 STAMMDATEN
├─ Kontakte → M042-M046
├─ Firmen → M042-M046
├─ Dateien → M052-M055
├─ Tags / Schlagwörter → M044
└─ Custom-Felder → M044
📨 KOMMUNIKATION
├─ Email-Inbox → M096-M099
├─ Marketing-Kampagnen → M100-M104
├─ Telefonie → M105-M109
├─ Team-Chat → M110-M114
└─ Notification-Center (Glocke) → M115-M117
⚙️ AUTOMATION & KI
├─ Automation-Studio → M075-M082
├─ KI-Sprach-Assistent → M086
└─ Vorlagen / Templates → M082
👥 PERSONAL (HR-Hub)
├─ Schwarzes Brett → M137
├─ Mitarbeiter-Verzeichnis → M133
├─ Urlaub / Krankheit → M135, M136
├─ Lohnabrechnungen → M134
├─ Anträge → M139
└─ Passwort-Safe → M134
🛠️ EINSTELLUNGEN (Mandanten-Admin)
├─ Mandant (Stammdaten) → M018
├─ Mitarbeiter & Rollen → M023, M024
├─ Pipelines → M059, M065
├─ Forms → M121-M125
├─ Integrationen (Apps) → M008-M010
├─ KWP-Verbindung → M089-M095
├─ Audit-Log → M151
├─ Subscription / Abo → M153-M156
├─ Branding / Design → M030
└─ DSGVO / Datenschutz → M164-M166
```
**Plattform-Admin-App `admin.hivegrid.de`** hat eine separate Sidebar-Struktur (siehe M034-M041).
**Customer-Portal** hat eigene minimale Navigation (siehe M141-M146).
**Mobile-App-Tab-Bar** ist UI-Modus-abhängig — siehe M169.
## 2. Sidebar-Wiring-Pflicht
Jeder neue Modul-Bereich muss in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` registriert werden. Das ist die **einzige** Quelle der Wahrheit für Sidebar-Inhalte.
```ts
// packages/ui-hivegrid/src/app-shell/sidebar.config.ts
export type SidebarEntry = {
id: string; // eindeutig, kebab-case
group: 'arbeiten' | 'stammdaten' | 'kommunikation' | 'automation-ki' | 'personal' | 'einstellungen';
label: string; // i18n-Key, kein Hardcoded-String
icon: string; // lucide-react Icon-Name
route: string; // RR7-Route-Pattern
permission: string | string[]; // Permission-Key aus M004; Eintrag nur sichtbar wenn berechtigt
badge?: 'count' | 'dot' | null; // Live-Counter (z.B. ungelesene Mails)
order: number; // Sortierung innerhalb der Gruppe
feature_flag?: string; // optional: Sidebar-Eintrag nur sichtbar wenn Flag aktiv
};
```
**Regel:** Eine neue Funktion ohne Sidebar-Eintrag (oder ohne Inline-Verlinkung aus einem bestehenden Modul) wird **nicht akzeptiert**.
## 3. Route-Patterns
Verbindliche URL-Schemata. Abweichungen erfordern Change-Request.
| Bereich | Pattern | Beispiel |
|---|---|---|
| Mandanten-App | `app.hivegrid.de/<modul>/...` | `app.hivegrid.de/sales/D-2026-0042` |
| Mandant-Settings | `app.hivegrid.de/settings/<modul>/...` | `app.hivegrid.de/settings/integrationen/kwp` |
| Plattform-Admin | `admin.hivegrid.de/<bereich>/...` | `admin.hivegrid.de/mandanten/123` |
| Customer-Portal | `app.hivegrid.de/portal/<tenant>/...` | `app.hivegrid.de/portal/geerkens/projekte/42` |
| Public-Forms | `app.hivegrid.de/f/<tenant>/<slug>` | `app.hivegrid.de/f/geerkens/kontaktformular` |
| Public-Booking | `app.hivegrid.de/b/<tenant>/<slug>` | `app.hivegrid.de/b/geerkens/wartungstermin` |
| Public-Help | `app.hivegrid.de/help/<lang>/<kategorie>/<slug>` | `app.hivegrid.de/help/de/integrationen/kwp-setup` |
| Public-API | `app.hivegrid.de/api/v1/<resource>` | `app.hivegrid.de/api/v1/contacts` |
| **Connector-OAuth-Callback** | `app.hivegrid.de/integrations/<provider>/oauth/callback` | `app.hivegrid.de/integrations/google/oauth/callback` |
Die Tenant-Slug ist **nicht** im Pfad bei der Mandanten-App — Tenant kommt aus Subdomain oder Session.
**Externe OAuth-Connectors (Google-Kalender, KWP, später AirCall/Microsoft/…):** Der Redirect-/Callback-Endpoint
liegt **immer** auf `app.` (nicht `api.`) unter `/integrations/<provider>/oauth/callback` — konsistent zu den
Connector-Settings-Screens (`/settings/integrations/<modul>`, `/integrations/<provider>/…`). Begründung: Der
OAuth-Rückweg ist browser-basiert und braucht die an `app.` gebundene Nutzer-Session, um den Token dem richtigen
Nutzer/Tenant zuzuordnen; `api.hivegrid.de` (token-basiert, ohne Session) ist dafür nicht geeignet. Tenant wird
im OAuth-`state` mitgeführt (eine feste Callback-URL je Umgebung, kein Tenant im Pfad). Pro Umgebung registriert:
`app.hivegrid.de`, `staging-app.hivegrid.de`, `localhost:3000`. Das frühere `/api/apps/<slug>/oauth/callback`
ist abgelöst (2026-06-21).
## 4. Breadcrumbs-Konvention
Jede Detail-Route hat einen Breadcrumb in `packages/ui-hivegrid/src/breadcrumbs/`. Format:
```
[Sidebar-Gruppe] > [Modul] > [Listen-Name] > [Detail-Titel]
```
Beispiel: `Arbeiten > Verkaufsfälle > Anlagen-Verkauf > D-2026-0042 Heizung Schmidt`
## 5. Activity-Stream-Wiring (M045)
Jede schreibende Aktion in jedem Modul muss einen Activity-Stream-Eintrag erzeugen, falls die betroffene Entität einen Stream hat (Kontakt, Firma, Lead, Verkaufsfall, Auftrag).
**Pflicht-Service-Aufruf:**
```ts
import { logActivity } from '@hivegrid/activity';
await logActivity({
tenantId, actorId, actorType: 'user' | 'auto' | 'sync' | 'system',
entityType, entityId,
type: 'contact.created' | 'deal.stage_changed' | ...,
payload: { ... }, // Human-readable Klartext + ggf. Vorher/Nachher
});
```
Der `logActivity`-Aufruf ist NICHT der Audit-Eintrag — der entsteht zusätzlich automatisch via M001 Prisma-Middleware.
## 6. Audit-Wiring (M001)
Audit ist **automatisch** via Prisma-Middleware. Agent muss nichts manuell aufrufen. Wenn etwas im Audit fehlt: Bug in M001-Middleware, nicht im Modul-Code.
**Ausnahme:** Aktionen außerhalb von Prisma (z.B. Datei-Upload zu S3, externe Webhook-Aufrufe) müssen manuell auditiert werden:
```ts
import { auditEvent } from '@hivegrid/audit';
await auditEvent({ ... });
```
## 7. Notification-Wiring (M011)
Wenn ein Modul Mitarbeiter benachrichtigen soll: NIE direkt Email/Push senden. Immer:
```ts
import { notify } from '@hivegrid/notifications';
await notify({
tenantId, recipientId,
category: 'lead.received' | 'deal.won' | ...,
template: 'lead-received', // i18n-Key in M003
payload: { ... },
priority: 'normal' | 'high' | 'urgent',
});
```
Der Notification-Service in M011 entscheidet Kanal (In-App/Email/Push), Bündelung (M117), Stille-Stunden (M116).
## 8. Search-Index-Wiring (M056)
Neue Entität, die in der globalen Suche auftauchen soll, muss in `packages/db/src/search/indexed-entities.ts` registriert sein:
```ts
export const indexedEntities = {
contact: { table: 'Kontakt', searchable: ['displayName', 'email', 'phone'], permission: 'crm.read' },
deal: { table: 'Verkaufsfall', searchable: ['title', 'description'], permission: 'sales.read' },
// ...
};
```
Permission-aware Search (M057) liest diese Map.
## 9. i18n-Wiring (M003)
Alle UI-Texte als i18n-Keys. Locale-Files unter `packages/i18n/src/locales/de/<modul>.json`. ESLint-Rule blockt JSX-Text-Nodes ohne `t()`. Build-Lint blockt Hard-Strings im Build.
Naming: `<modul>.<bereich>.<key>`, z.B. `sales.kanban.empty_state` oder `auth.login.password_too_short`.
## 10. Permission-Keys (M004)
Permission-Definition als TypeScript-Konst in `packages/entitlements/src/permissions.ts`:
```ts
export const Permissions = {
CRM_READ: 'crm.read',
CRM_READ_OWN: 'crm.read_own',
CRM_WRITE: 'crm.write',
SALES_READ: 'sales.read',
SALES_VOLUME_READ: 'sales.volume.read', // separater Key für Volumen-Sichtbarkeit (siehe 3.11.2)
// ...
} as const;
```
Sidebar-Einträge, Routes, View-Models, Actions filtern alle gegen diese Keys.
## 11. View-Model-Pflicht (M013)
JEDE Route in `app/routes/**` baut im Loader ein View-Model. UI-Components in `packages/ui-hivegrid/**` empfangen NUR das VM und Action-Callbacks. Siehe AGENTS.md.
## 12. Smoke-Test pro Route (Pflicht)
Jede neue Route bekommt einen Smoke-Test in `e2e/smoke/<modul>.spec.ts`:
```ts
test('contact-detail loads with view-model', async ({ page }) => {
await loginAs(page, 'geerkens-admin');
await page.goto('/kontakte/test-kontakt-id');
await expect(page.getByRole('heading', { name: /kontakt/i })).toBeVisible();
});
```
Smoke-Tests laufen im Hot-Swap-Drill (M017) gegen `packages/ui-mock` — wenn sie da brechen, ist die Hot-Swap-Architektur verletzt.
## 13. Mobile-Tab-Bar-Wiring (M169)
Neue Funktionen, die in der Mobile-App relevant sind, müssen in `packages/ui-hivegrid/src/mobile/tab-bar.config.ts` für mindestens einen UI-Modus (Baustelle / Büro) registriert sein.
## 14. Customer-Portal-Wiring (M142, M143)
Funktionen, die Endkunden sehen sollen, müssen explizit in `packages/ui-hivegrid/src/customer-portal/portal-shell.config.ts` registriert sein. Default-Sichtbarkeit: **NICHT sichtbar**. Tenant-Admin gibt explizit frei (M142.5).
+101
View File
@@ -0,0 +1,101 @@
# Mid-Stream-Änderungen — Rollback-Playbook
**Zweck:** Wenn ein AI-Coding-Agent ein Milestone falsch baut und du es mittendrin korrigieren musst, ohne dass 5 andere Module brechen.
## Grundsatz: atomare Commits pro Slice
Der ToolController committet pro Slice atomar. Eine Slice rückgängig zu machen heißt: einen Commit-Range zu reverten — nicht einen einzelnen File.
## Eskalations-Stufen
### Stufe 1: Kosmetik / UI falsch
**Anzeichen:** Funktion läuft technisch, aber Layout/Wording/Reihenfolge falsch.
**Vorgehen:**
1. NIEMALS direkt im `app/routes/**` korrigieren — Designer baut neu in Subframe
2. Subframe-Update → AI-Agent integriert in `packages/ui-hivegrid/src/adapters/`
3. Visual-Regression-Gate (M016) zeigt Diff
4. Merge
**Risiko:** sehr niedrig. Layer-Trennung schützt.
### Stufe 2: View-Model falsch (Daten-Form)
**Anzeichen:** Funktion zeigt falsche Daten / fehlende Felder / falsche Aggregation.
**Vorgehen:**
1. View-Model in `packages/view-models/src/<domain>/*.vm.ts` korrigieren
2. Zod-Schema mit-aktualisieren
3. Loader-Funktion im `app/routes/**` korrigieren
4. UI-Component in `packages/ui-hivegrid/**` muss NICHT geändert werden (sonst war die Trennung schlecht)
**Risiko:** mittel. Wenn die UI-Component die VM-Form falsch annimmt: neuer Vertrag.
### Stufe 3: Domain-Logik falsch
**Anzeichen:** Daten werden falsch geschrieben, Permissions stimmen nicht, falsche Side-Effects.
**Vorgehen:**
1. `app/domain/**` korrigieren (NICHT UI, NICHT VM)
2. Bei DB-Schema-Änderung: neue Prisma-Migration als FORWARD-MIGRATION (kein Backward-Edit der alten)
3. Bei Permission-Bug: Permission-Wrapper-Test in M004 erweitern
4. Cross-Tenant-Probe-Test (M005) muss grün bleiben
**Risiko:** hoch. Daten könnten bereits in der DB falsch sein.
### Stufe 4: Komplettes Milestone neu
**Anzeichen:** Architektur-Fehler, falscher Layer-Schnitt, Anti-Pattern in der Foundation.
**Vorgehen:**
1. Branch identifizieren — TC arbeitet pro Milestone in einem Branch
2. Branch nicht mergen → revert ist trivial
3. Wenn schon gemergt: `git revert <merge-commit>` als neuer Commit
4. PLAN.md des Milestones aktualisieren (Lessons Learned als Hinweis für Slice-Planung)
5. TC neu starten auf das Milestone
**Risiko:** sehr hoch wenn andere Milestones bereits darauf bauen. Deshalb: vor jedem Milestone PLAN.md prüfen, ob die Abhängigkeiten klar sind.
## Reversibilitäts-Pflichten pro Milestone
Bevor TC ein Milestone als fertig markiert, müssen folgende Rollback-Pfade existieren:
| Artefakt | Rollback-Pfad |
|---|---|
| **Code-Änderungen** | `git revert <slice-commit>` — atomic per Slice |
| **DB-Migration** | Forward-Only-Migration mit dokumentiertem Inverse-SQL als Kommentar im Migration-File |
| **Sidebar-Eintrag** | Eintrag in `sidebar.config.ts` löschen → Sidebar baut sich neu |
| **Routes** | Route-Datei löschen → RR7 verweigert die URL |
| **Permissions** | Permission-Key in `permissions.ts` als deprecated markieren |
| **Feature-Flag** | Flag aus auf `false` → Modul unsichtbar ohne Code-Deploy |
## Notbremse: Feature-Flag
Jedes Modul, das in Produktion landet, kann via Feature-Flag aus M006 ausgeschaltet werden — ohne Deploy. Wenn etwas live brennt:
1. Plattform-Admin → Feature-Flags
2. Flag des betroffenen Moduls auf `false` für betroffenen Mandanten
3. Modul verschwindet aus der Sidebar (siehe sidebar.config.ts `feature_flag` Feld)
4. Bug in Ruhe fixen
5. Flag wieder auf `true`
## Was du als Mensch nicht selbst korrigieren solltest
- **NICHT direkt in `packages/ui-subframe/**`** — das ist Subframe-Output, wird beim nächsten Designer-Update überschrieben
- **NICHT direkt in `packages/ui-hivegrid/src/adapters/**`** — der AI-Agent generiert das aus dem Subframe-Output
- **NICHT in TC-State (`.tc/`)** ohne Backup
## Was du selbst korrigieren KANNST
- `docs/milestones/M*/PLAN.md` — Pflichtenheft-Bezug, Hinweise, Erfolgskriterien anpassen
- `AGENTS.md` und `docs/INTEGRATION-CONTRACTS.md` — Agent-Regeln verschärfen
- `docs/CHANGE-REQUESTS.md` — Change-Request festhalten
## Eskalationspfad
1. Feature-Flag aus (Notbremse)
2. PLAN.md aktualisieren mit klarer Korrektur-Anweisung
3. TC auf das Milestone neu starten oder gezielt eine Slice via `/tc steer` umsteuern
4. Verify-Gates laufen lassen, bis grün
5. Flag wieder an
+329
View File
@@ -0,0 +1,329 @@
# HiveGrid 2.0 — Progress-Tracker
**Zweck:** Live-Übersicht über alle 176 Milestones. Der Agent updatet diese Datei nach jedem Milestone-Abschluss. Du als User prüfst über den Klick-Pfad und setzt Sign-Off.
**Master-Quellen:**
- Master-Index: [`docs/milestones/README.md`](milestones/README.md)
- Per-Milestone-Plan: `docs/milestones/M???-<slug>/PLAN.md`
- Definition of Done: in jeder PLAN.md am Ende
- Coverage: [`docs/milestones/PFLICHTENHEFT-COVERAGE.md`](milestones/PFLICHTENHEFT-COVERAGE.md)
---
## Status-Legende
| Status | Bedeutung |
|---|---|
| `⚪ NICHT-GESTARTET` | Milestone wartet, Abhängigkeiten ggf. offen |
| `🔵 IN-ARBEIT` | Agent plant oder führt Slices aus |
| `🟡 READY-TO-VERIFY` | Agent fertig, Verify-Gates grün, wartet auf User-Sign-Off via Klick-Pfad |
| `🟢 SIGNED-OFF` | User hat Klick-Pfad durchlaufen und bestätigt |
| `🔴 BLOCKED` | Agent kann nicht weiter, User-Input nötig |
| `🟣 PARKED` | Bewusst zurückgestellt |
| `🟦 BOILERPLATE-PATTERN` | Pattern in Boilerplate vorhanden — HiveGrid-Agent füllt nur Inhalte ein |
---
## Phase 0 — Vorbedingungen (außerhalb der 176)
| Aufgabe | Status | Bemerkung |
|---|---|---|
| EU-Sterilisation der Boilerplate | 🟢 SIGNED-OFF | 2026-05-11 verifiziert: Anthropic/OpenAI/Google-Auth raus; Mistral via `@ai-sdk/mistral` (`packages/ai/src/providers/mistral.ts`), Scaleway + AlephAlpha + compatible-Adapter vorhanden; Multi-Provider-Fallback (`packages/ai/src/fallback.ts`) |
| RR7-Migration zu Ende | 🟢 SIGNED-OFF | 2026-05-11 verifiziert: `apps/web/app` ist authoritative, kein Root-Hybrid mehr (siehe `boilerplate/docs/architecture-migration-status.md`) |
| Phase-C-Foundation in Boilerplate (`packages/foundation`, `view-models`, `ui-app`, `ui-mock`, `ui-subframe`) | 🟢 SIGNED-OFF | 2026-05-11 verifiziert: `pnpm verify:phase-c-boundaries` läuft grün; alle Foundation-Patterns in `@toolctrl/foundation` exportiert |
| Boilerplate-Inhalt in `HiveGrid-Cloud/` übernommen | 🟢 erledigt | 2026-06-04 via **git-merge** (downstream), nicht rsync — siehe `docs/ARCHITECTURE-DECISIONS.md` AD-10 |
| **Phase-0 Pre-Flight im Code umgesetzt** | 🟢 erledigt | 2026-06-05: AGENTS Pre-Flight-Check besteht alle 4 Schritte; Build-Baseline + EU-Steri (OAuth raus) + M00-Neubauten (4/4) committed; offen nur #19 Deps (just-in-time) |
| `AGENTS.md` + `docs/INTEGRATION-CONTRACTS.md` + `docs/MID-STREAM-CHANGES.md` + `docs/TC-PLANNING-BRIEFING.md` im Repo | 🟢 SIGNED-OFF | 2026-05-10 |
| 176 Milestone-Verzeichnisse mit `PLAN.md` | 🟢 SIGNED-OFF | 2026-05-10 |
| `docs/PROGRESS-TRACKER.md` (diese Datei) angelegt | 🟢 SIGNED-OFF | 2026-05-10 |
| Doku-Naming-Anpassung an Boilerplate-Realität (`ui-hivegrid` → `ui-app`, `@hivegrid/foundation` → `@toolctrl/foundation`, AdapterStatus, VMStatus) | 🟢 erledigt | 2026-06-05, AGENTS.md (commit `206e1ea3`); ältere PLANs/Specs via AD-10-Mapping-Notiz abgedeckt (kein flächiges Rename) |
**Boilerplate-Realität ab 2026-05-11:** Die EU-Sterilisation + RR7-Migration + Phase-C-Foundation sind technisch abgeschlossen. Die HiveGrid-Milestones M001-M017 sind in der Boilerplate als **Pattern** umgesetzt — der HiveGrid-Agent muss sie nicht implementieren, sondern nur mit HiveGrid-spezifischen Inhalten befüllen.
**Pattern-Standorte in der Boilerplate (für HiveGrid-Agent):**
| HiveGrid-Pflicht | Pattern in Boilerplate |
|---|---|
| Audit-Log default (M001) | `@toolctrl/foundation` → `withAuditMiddleware`, `AuditEventInput`, `AuditEventClient`; DB: `packages/db/src/audit-events.server.ts` |
| Klartext-Diagnostik (M002) | `@toolctrl/foundation` → `CatalogError`, `catalogRegistry` (leer, HiveGrid füllt), `foundationErrors`, `toPinoLogObject` |
| i18n-Pflicht (M003) | `@toolctrl/foundation` → `assertI18nKey`; CI: `pnpm verify:i18n` (i18n-check.ts + ESLint) |
| Tenant-Scope + Permissions (M004, M005) | `@toolctrl/foundation` → `withTenantScope`, `withPermission`, `withOwnerCheck`, `requirePermission`, `PermissionContext` |
| Feature-Flag-Engine (M006) | `@toolctrl/foundation` → `defineFlag`, `featureFlag`, `FlagDefinition`, `FlagResolutionInput` |
| Idempotenz-Wrapper (M007) | `@toolctrl/foundation` → `IdempotencyClient`, `IdempotencyResponse` |
| Apps-Engine-Adapter + State-Machine (M008, M009, M010) | `@toolctrl/foundation` → `defineAdapter`, `getAdapter`, `AdapterStatus` (`not_configured \| configured \| healthy \| degraded \| unavailable`); Plugin-Loader: `@toolctrl/plugins` |
| Notification-Service (M011) | `@toolctrl/foundation` → `notify`, `notifyMany`, `bundleNotifications`, `isNotificationEnabled`, `NotificationInput` |
| Workspace-Topologie (M012) | Bereits angelegt: `packages/view-models`, `packages/ui-app`, `packages/ui-subframe`, `packages/ui-mock` |
| View-Model-Konvention (M013) | `@toolctrl/view-models` → `VMState<T>` (`idle \| loading \| ready \| empty \| error`), `ActionCallbacks<T>`, `I18nLabel`, `RegistryGate`, `ready`, `empty`, `assertI18nLabel` |
| Action-Callback-Contract (M014) | `@toolctrl/view-models` → `ActionCallbacks<T>` mit `onCreate`, `onUpdate`, `onDelete`, `onRefresh` |
| Subframe-Adapter-Layer (M015) | `packages/ui-subframe` ist als leeres Paket reserviert (README erklärt die Vendor-Import-Boundary) |
| Visual-Regression-Gate (M016) | CI-Script `pnpm verify:ui-visual` läuft + `scripts/verify-ui-mock-visual.ts` |
| Hot-Swap-Drill (M017) | CI-Job „Test UI mock hot-swap" in `.gitea/workflows/ci.yml` + `pnpm test:hot-swap` → `pnpm verify:ui-hot-swap` |
---
## Master-Tabelle (176 Milestones)
| ID | Titel | Status | DoD-Score | Slices | Sign-Off |
|---|---|---|---|---|---|
| **M001** | Audit-Log default | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M002** | Klartext-Diagnostik | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M003** | i18n-Pflicht-Wiring | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M004** | Tenant-Scope-Wrapper + Owner-Check | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M005** | URL-Tampering Hardening + Mass-Action-Schutz | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M006** | Feature-Flag-Engine (Code-Layer) | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M007** | Idempotenz-Wrapper für BullMQ-Steps | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M008** | Apps-Engine: Adapter-Interface + State-Machine | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M009** | Apps-Engine: Setup-Wizard + Sync-Log | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M010** | Apps-Engine: Probe-Scheduler + Webhook-Sturm-Schutz | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M011** | Notification-Service-Layer | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M012** | Workspace-Topologie | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M013** | View-Model-Konvention | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M014** | Action-Callback-Contract | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M015** | Subframe-Adapter-Layer | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M016** | Visual-Regression-Gate | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M017** | Hot-Swap-Drill | 🟦 BOILERPLATE-PATTERN | – | – | HiveGrid füllt Inhalte |
| **M018** | Mandanten-Anlage | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M019** | Mandanten-Trennung Hardening | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M020** | Email/Passwort-Auth + HIBP + Brute-Force | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M021** | Magic-Link + Sitzungs-Verwaltung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M022** | TOTP-2FA + Notfall-Codes | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M023** | Rollen-Engine + 6 SHK-Standard-Rollen + Gruppen | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M024** | Mitarbeiter-Einladung / Deaktivierung / Umverteilung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M025** | Konto-Löschung (User + Admin) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M026** | Mandanten-Lebenszyklus | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M027** | API-Keys + Tarif-Feature-Gates | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M028** | User/Tenant Sprach-Settings + Auflösungs-Reihenfolge | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M029** | Sprach-Wechsler + Fallback-Hinweise | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M030** | Hell/Dunkel-Modus + Capacitor-Status-Bar | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M031** | Feature-Flag Schalter-Definition + Lifecycle | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M032** | Feature-Flag Filter-Editor + Einzel-Ausnahme-Editor | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M033** | Feature-Flag Massen-Funktionen + Statistik + Audit-Historie | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M034** | Plattform-Admin-Auth (admin.<domain>) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M035** | Plattform-Admin-Dashboard | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M036** | Mandanten-Verwaltung (Plattform-Admin) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M037** | Stellvertreter-Anmeldung („Login as") | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M038** | Mandanten-Mitarbeiter-Verwaltung (Plattform-Admin) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M039** | Plattform-Admin-Konten + Rollen | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M040** | Interne Lead-Pipeline (HiveGrid-Verkäufe) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M041** | System-Health + Audit-Anzeige + Datenschutz + Support | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M042** | Kontakt-Stammdaten | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M043** | Firma-Stammdaten + Hierarchie | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M044** | Schlagwörter + Custom-Felder + Datenherkunft | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M045** | Aktivitäts-Stream | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M046** | Listen + Segmente + DSGVO-Felder | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M047** | Lead-Inbox + Quellen-Adapter | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M048** | Lead-Routing pro Quelle [N4] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M049** | Auto-Verkaufsfall-Anlage | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M050** | Spam/Duplikat-Erkennung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M051** | Lead-Status-Pipeline + Verlust-Gründe + Notifications | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M052** | Datei-Upload (Anti-Virus + Format-Whitelist) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M053** | Speicher-Backend (S3 auf Scaleway) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M054** | Datei: Permissions + Sharing + Versionierung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M055** | Datei: Suche + Tagging + Archivierung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M056** | Globale Suche: Such-Index | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M057** | Globale Suche: Permission-aware | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M058** | Globale Suche: UI + Saved-Searches | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M059** | Sales-Pipeline: Konfiguration + SHK-Standard-Pipeline | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M060** | Verkaufsfall-CRUD + Pflichtfeld Vertriebler [N6] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M061** | Sales-Pipeline: Kanban-UI + Drag&Drop | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M062** | Sales-Pipeline: Verlust/Gewinn-Gründe [N8] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M063** | Auto-Auftragsprojekt [N7] + Setter-Closer [N17] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M064** | Sales-Pipeline: Reporting + KWP-Sync | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M065** | Auftragspipeline: Konfiguration | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M066** | Auftrag-CRUD + KWP-Dokumenten-Archiv [N5] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M067** | Plantafel mit Tagen-Granularität [N10] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M068** | Auftrag: Mitglieder-Mgmt + Rollen pro Auftrag | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M069** | Auftrag: Abnahme-Workflow + Mängel-Liste | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M070** | Auftrag: Material-Listen + Lager-Anbindung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M071** | Aufgaben-CRUD + Zuweisung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M072** | Aufgaben-Listen pro Modul (Inline) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M073** | Aufgaben: Erinnerungen + Push + Email | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M074** | Aufgaben-Sync mit ERP-Aufgaben-Welt [N11] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M075** | Automation: Schema + Auslöser-Registry | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M076** | Automation: Aktion-Registry + Steuerungs-Bausteine | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M077** | Automation: Bedingungen | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M078** | Automation: Visueller Editor | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M079** | Automation: Test-Lauf + Schwellenwert-Vorschau | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M080** | Automation: 24h-Undo-Fenster + Reversibilität | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M081** | Automation: Schleifen-Schutz + Webhook-Sturm + Anomalie | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M082** | Automation: Klartext-Logs + Re-Run + Trigger-Volumen-Metering | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M083** | KI: EU-Provider-Layer (Mistral/Scaleway/AlephAlpha) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M084** | KI: Multi-Provider-Fallback + 30-Min-Health-Window | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M085** | KI: Promptfoo-Eval-Suite mit Injection-Vektoren in CI | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M086** | KI: Sprach-zu-Automation | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M087** | KI: RAG-Layer + Aktions-Grenze | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M088** | KI: Token-Limits + Pay-as-you-go [N13] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M089** | KWP-API-Connector (REST/OAuth über Apps-Engine) [Re-Cut] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M090** | KWP-API Connection-Health [Re-Cut] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M091** | KWP-Push-Queue + Replay (Cloud-Outbox) [Re-Cut] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M092** | KWP-API-Contract-Sentinel [Re-Cut] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M093** | KWP: Konflikt-Auflösung „KWP gewinnt" [N14] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M094** | KWP: Bidirektionale Plantafel-Sync [N10] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M095** | KWP: Dokumenten-Archiv-Sync KWP→HiveGrid [N5] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M096** | Email-Inbox: Gmail-OAuth-Integration via Apps-Engine | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M097** | Email-Inbox: Sync + Inbox-UI + Threading + Auto-Verknüpfung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M098** | Email-Versand + Vorlagen + Variable-Resolution | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M099** | Email-Tracking + Delivery-Status + Bounces | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M100** | Email-Marketing: Editor (Block-Style) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M101** | Email-Marketing: Segmente + Empfänger-Auswahl | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M102** | Email-Marketing: Versand-Engine + DKIM/SPF + Frequenz-Schutz | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M103** | Email-Marketing: Reporting (Open/Click + Klick-Karte + Pro-Empfänger) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M104** | Email-Marketing: Unsubscribe + Bounce-Handling + Pflicht-Footer | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M105** | Telefonie: HalloPetra-Adapter [N16] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M106** | Telefonie: AirCall-Adapter | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M107** | Telefonie: Anruf-Eingang + CRM-Verknüpfung + Auto-Match | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M108** | Telefonie: Anruf-Ausgang + Click-to-Call | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M109** | Telefonie: Voicemail + Anruf-Notizen + Aufgaben-Auto-Anlage | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M110** | Team-Chat: Channel-Modell + Permissions | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M111** | Team-Chat: Nachrichten + Threads + Pinned + Reactions | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M112** | Team-Chat: Datei/Foto-Anhänge | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M113** | Team-Chat: Modul-Verknüpfungen | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M114** | Team-Chat: Mentions + Push + Lese-Bestätigungen | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M115** | Notification-Center + Lese-Status | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M116** | Notifications: User-Settings (Kanal + Stille Stunden) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M117** | Notifications: Kategorien + Bündelung + Anti-Spam | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M118** | Calendar: Google + Microsoft via Apps-Engine | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M119** | Calendar: Termin-Sync (bidirektional) + Konflikt-Auflösung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M120** | Calendar: Display + Modul-Verknüpfung + Activity-Stream | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M121** | Form-Builder: Editor | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M122** | Form-Builder: Veröffentlichung + Sharing + Embed | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M123** | Form-Builder: Submission-Eingang + CRM-Verknüpfung + Aktionen | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M124** | Form-Builder: Conditional-Display (kein Branching) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M125** | Form-Builder: Rate-Limit + Captcha + DSGVO-Pflicht | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M126** | Booking: Buchungs-Seite + Time-Slot-Engine | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M127** | Booking: Verfügbarkeits-Berechnung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M128** | Booking: Bestätigungs-Flow + Verknüpfungen | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M129** | Booking: Stornierung + Verschiebung + No-Show-Tracking | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M130** | Angebot: Erstellung (PDF aus Datei-Speicher oder Upload) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M131** | Angebot: PandaDoc/DocuSign-Integration via Apps-Engine | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M132** | Angebot: Status-Tracking + Auto-Stage-Wechsel | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M133** | HR: Mitarbeiter-Stammdaten + Standorte | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M134** | HR: Dokumente + Lohnabrechnungen + Verschlüsselter Passwort-Safe | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M135** | HR: Urlaubsanträge + Genehmigungs-Workflow | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M136** | HR: Krankmeldungen + Atteste | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M137** | HR: Schwarzes Brett (Bekanntmachungen) + Quick-Links + Resources | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M138** | HR: Onboarding-Drittanbieter (Memberspot/Boobora) [N18] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M139** | HR: Generische Anträge + Berichte + Lohn-Export + Surveys | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M140** | HR: Mitarbeiter-Selbstbedienung + Mobile-First | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M141** | Customer-Portal: Auth (separater Login-Flow) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M142** | Customer-Portal: Projekt-Sicht + Status-Mails | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M143** | Customer-Portal: Datei-Upload durch Endkunde + Reklamations-Forms | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M144** | Customer-Portal: Subunternehmer-Zugang [N19] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M145** | Customer-Portal: Skalierungs-Sicht (Service-Inbox + Routing) [N19] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M146** | Customer-Portal: Pro-Kunden-Lebensdauer + DSGVO + Konfig [N19] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M147** | Dashboard: Layout + Widget-Engine | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M148** | Dashboard: 7 Rollen-Standard-Layouts | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M149** | Dashboard: Drill-Down + Custom-Widget-Konfiguration | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M150** | Dashboard: Export + Report-Versand | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M151** | Audit-Log: Anzeige + Filter + Volltext-Suche | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M152** | Audit-Log: Compliance-Export + Aufbewahrung + Legal Hold | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M153** | Subscriptions: Stripe-Integration (EU) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M154** | Subscriptions: Plan-Engine + Seat-Modell (Büro/Monteur) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M155** | Subscriptions: Trial-Phase + Read-Only-Downgrade + Frozen-Modus | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M156** | Subscriptions: Verbrauchs-Erfassung + Pay-as-you-go [N13] | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M157** | Onboarding: Wizard-Framework + State-Machine | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M158** | Onboarding: SHK-Standard-Daten-Provisionierung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M159** | Onboarding: Branchen-Templates deklarativ (A.3) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M160** | Onboarding: Demo-Daten + KI-Walkthrough | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M161** | Public-API: OpenAPI-Spec + Read-Endpoints | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M162** | Public-API: API-Key-Auth + Scopes + Rate-Limit | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M163** | Public-API: Webhooks (signiert, ein- und ausgehend) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M164** | DSGVO: User-Daten-Export + Mandanten-Daten-Export | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M165** | DSGVO: Account-Löschung + Mandanten-Lösch-Workflow | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M166** | DSGVO: AVV-Vertrag + Subprozessoren-Liste + Auskunfts-Workflow | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M167** | Mobile: Capacitor-Setup + Build-Pipeline | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M168** | Mobile: Native-Features (Push, Face/Touch-ID, Kamera) + Universal Links | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M169** | Mobile: Native Tab-Bar + Mobile-First-Routes (Baustelle) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M170** | Mobile: Offline-Modus für Baustelle (Pflicht Phase 1) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M171** | Mobile: App-Store-Compliance (Apple + Google) + Force-Update | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M172** | Help-Center: Markdown-Artikel-Mgmt (Git-PRs) | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M173** | Help-Center: Routes + Suche + SEO + Kontextuelle Hilfe | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M174** | Help-Desk: Email-basiertes Ticket-System + Plattform-Admin-Bearbeitung | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M175** | Help-Desk: In-App-Feedback-Widget („Problem melden") | ⚪ NICHT-GESTARTET | 0/14 | – | – |
| **M176** | Anti-Liste + Change-Request-Workflow als Repo-Doku | ⚪ NICHT-GESTARTET | 0/14 | – | – |
---
## Detail-Sektionen pro Milestone
> **Anweisung an den Agent:** Sobald ein Milestone in IN-ARBEIT geht, ergänze hier eine Detail-Sektion gemäß folgender Vorlage. Nach Abschluss aller Slices: Status auf `🟡 READY-TO-VERIFY`. User trägt nach Sign-Off `🟢 SIGNED-OFF` mit Datum ein.
### Vorlage (zu kopieren pro Milestone)
```markdown
### M??? — <Titel> — Status: 🔵 IN-ARBEIT
**Pflichtenheft-Bezug:** _bmad-output/pflichtenheft/blocks/blockX.md Sektion Y.Z
**Slice-Status:** N von M abgeschlossen
- [x] S01: <slice-title> — Commit: <hash>
- [x] S02: <slice-title> — Commit: <hash>
- [ ] S03: <slice-title>
**Definition of Done:**
*Erreichbarkeit*
- [ ] Sidebar-Eintrag — Datei: `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` — Gruppe: <Gruppe> — Permission: <key>
- [ ] Route — `app/routes/<...>.tsx`
- [ ] Breadcrumb — `packages/ui-hivegrid/src/breadcrumbs/<...>.ts`
- [ ] Mobile-Tab-Bar — falls relevant: `packages/ui-hivegrid/src/mobile/tab-bar.config.ts`
- [ ] Customer-Portal-Sichtbarkeit — falls relevant: `packages/ui-hivegrid/src/customer-portal/portal-shell.config.ts`
*Querschnitt-Wiring*
- [ ] Audit (auto via M001 oder manuell)
- [ ] Activity-Stream via logActivity()
- [ ] Notifications via notify()
- [ ] Search-Index registriert
- [ ] Permission-Filter via withTenantScope() + withPermission()
- [ ] Feature-Flag-fähig
*Code-Layer-Disziplin*
- [ ] Loader baut VM, UI nur Props
- [ ] Keine Prisma-Imports in ui-hivegrid
- [ ] Alle UI-Texte als i18n-Keys
*Tests*
- [ ] Unit-Test (VM-Builder + Zod): `<test-path>`
- [ ] Smoke-Test: `e2e/smoke/<modul>.spec.ts`
- [ ] Hot-Swap-Drill: grün
- [ ] Visual-Regression-Snapshot: <Anzahl> Bilder
*Klartext + Rollback*
- [ ] Fehler-Texte aus M002-Catalog
- [ ] Empty/Loading/Error-States
- [ ] 404 statt 403 bei fehlender Permission
- [ ] Forward-Only-DB-Migration mit Inverse-SQL-Kommentar
- [ ] Feature-Flag zum Abschalten ohne Deploy
**DoD-Score:** N/14 erfüllt
**Klick-Pfad zum Prüfen** (für User):
1. Login als <user-typ> auf `app.hivegrid.de`
2. Sidebar → <Gruppe> → <Eintrag>
3. <Aktion ausführen>
4. <Erwartetes UI / Ergebnis>
**Notes vom Agent:** <was war nicht offensichtlich, was hat länger gedauert, was muss User wissen>
**User-Sign-Off:** [ ] Klick-Pfad durchlaufen, alles wie erwartet — Datum: <YYYY-MM-DD>, User: <Name>
```
---
## Workflow
1. **Agent startet Milestone** → Status auf `🔵 IN-ARBEIT`, Detail-Sektion anlegen
2. **Agent baut Slices** → Slice-Liste abhaken, Commit-Hashes eintragen
3. **Agent hat alle DoD-Punkte erfüllt** → Status auf `🟡 READY-TO-VERIFY`
4. **User durchläuft Klick-Pfad** → wenn alles ok: User trägt `🟢 SIGNED-OFF` + Datum ein. Wenn nicht ok: User trägt Notes ein, Status zurück auf `🔵 IN-ARBEIT`
5. **Agent geht zum nächsten Milestone** — nicht vorher
## Konsistenz-Check
- DoD-Score in der Master-Tabelle muss zur Detail-Sektion passen
- `🟡 READY-TO-VERIFY` ohne 14/14 DoD = inkonsistent → Agent muss korrigieren
- `🟢 SIGNED-OFF` ohne Datum + User = inkonsistent → User muss ergänzen
## Pflege
Diese Datei ist Live-Dokument. Pflicht-Update bei jedem Status-Wechsel. Bei mehr als 5 Mile­stones IN-ARBEIT gleichzeitig: STOP, an User melden — wir wollen Fokus, nicht Chaos.
+31
View File
@@ -0,0 +1,31 @@
# docs/ — Wegweiser (Technik- & Boilerplate-Referenz)
`docs/` ist überwiegend **technische Plattform-Referenz** (geerbt aus der @toolctrl-Boilerplate) plus einige HiveGrid-spezifische Entscheidungs-/Bau-Dokumente.
> **Du willst PLANEN/steuern?** → nicht hier, sondern **`.planning/`** (Live-Planung: PROJECT, ROADMAP, STATE, REQUIREMENTS, ENTITY-MODEL).
> **Du willst BAUEN?** → `docs/milestones/` (die PLAN.md je Milestone) + `docs/ARCHITECTURE-DECISIONS.md` + `docs/INTEGRATION-CONTRACTS.md`.
**Stand:** 2026-06-15 · Infra-Realität: **Hetzner + Coolify + Cloudflare** (Scaleway/Fly sind raus — historische Erwähnungen in alten Sets sind nur Historie).
## HiveGrid-spezifisch (wichtig, gepflegt)
| Datei/Ordner | Inhalt |
|---|---|
| **ARCHITECTURE-DECISIONS.md** | verbindliche ADRs AD-1…AD-20 (inkl. Projekt-zentrisches Modell) |
| **INTEGRATION-CONTRACTS.md** | Modul-Integrationsvertrag (Sidebar/Routes/Search/Notifications/Entitlement-Gate) |
| **milestones/** | 176 Milestone-PLANs — **aktiv** von `.planning/PHASE-SOURCES.md` referenziert |
| **boilerplate-mapping/** + **boilerplate-inventory/** | Reuse-Abgleich gegen die Boilerplate (Quelle für Scope-Schnitte) |
| **feature-inventory.md** + **feature-decisions/** + **feature-manifests/** | Feature-Klassifikation. ⚠ `feature-manifests/*.json` werden **zur Laufzeit/von Tests geladen** — nicht anfassen |
| **TC-PLANNING-BRIEFING.md** · **MID-STREAM-CHANGES.md** · **PROGRESS-TRACKER.md** | Agent-Briefing · Rollback-Playbook · Live-Fortschritts-Tracker |
## Plattform-Referenz (Boilerplate — Nachschlagen, selten Startpunkt)
deployment*.md · worker.md · database.md / db-*.md · mail*.md · env-vars.md / env-access-inventory.md · plugins.md · api.md · observability.md · security.md + **security/** · **architecture/** · troubleshooting.md · upgrade(s).md · versioning.md · configuration.md · local-dev.md · quickstart.md · production-checklist.md / release-checklist.md · roles-matrix.md · data-retention.md
## Betriebs-/Spezial-Ordner
- **ops/** = operative Runbooks (Queues, PgBouncer, Backups, CDN, Stripe-Meter)
- **operations/** = Setup/Admin-Guides (Authentik-SSO, Plugin-Lifecycle, Capacitor) — *Namens-Doppel mit ops/, Konsolidierung wäre ein Aufräum-Kandidat*
- **observability/** = Monitoring (Sentry/Logging/Metrics)
- **deploy/** (Service-Runbooks, z.B. Meilisearch) · **deployments/** (Referenz-Deploy boiler-saas-ctrl) · **agent-playbooks/** · **contributing/** · **testing/** · **plugins/** · **ui-inventory/** · **compliance/** · **storage/** · **auth/** · **verification/**
## Historisch — NICHT Startpunkt
- **improvement-plan/** — **gehört der Boilerplate (upstream), nicht HiveGrid.** Es ist der Tech-Debt-/Reifungs-Plan der @toolctrl-Boilerplate (Phasen 01–11), kommt per Downstream-Merge mit und wird **upstream weiter gepflegt**. Die ~100 Code-Kommentare „siehe docs/improvement-plan/…" sind ebenfalls Boilerplate-Code. **Nicht verschieben/löschen** — das würde bei jedem `git merge upstream` kollidieren (AD-10). Status laut Boilerplate selbst (CURRENT-STATUS.md): überwiegend **historisch** (PROGRESS.md/VERIFICATION.md = reconciled), nur OPERATOR-CHECKLIST.md + BLOCKERS.md noch live. Für HiveGrid: **kein Planungs-Input** — unsere Quelle ist `.planning/` + `milestones/`.
- **archive/** · **releases/** · **reports/** — abgeschlossene Artefakte.
+286
View File
@@ -0,0 +1,286 @@
# ToolController-Briefing für HiveGrid 2.0
**Adressat:** ToolController + alle AI-Coding-Agents (Sub-Agents von TC), die an HiveGrid arbeiten.
**Stand:** 2026-05-10
**Verbindlichkeit:** dieses Dokument plus die hier referenzierten Pfade definieren das Vorgehen vollständig.
---
## 0. Was du baust
HiveGrid 2.0 ist eine Multi-Tenant-SaaS-Plattform für SHK-Handwerksbetriebe. Erstkunde: Geerkens. EU-Stack komplett (kein US-Anbieter). Repository-Wurzel: `/Users/raffaelsheikh/Documents/GitHub/HiveGrid-Cloud`.
176 Milestones in `docs/milestones/M001/` bis `docs/milestones/M176/`. Jedes Verzeichnis enthält genau eine Datei `PLAN.md`. Diese sind deine Arbeits-Aufträge.
## 0.1 Die Boilerplate IST deine Codebase — nicht „eine Vorlage"
Das Repo `HiveGrid-Cloud/` enthält die EU-sterilisierte ToolCTRL-Boilerplate als Fundament. Das ist **dein Code**, du editierst, erweiterst und löschst ihn. Du klonst nichts, du importierst nichts „von außen", du baust **nicht** parallel ein neues Projekt.
Konkret heißt das:
- `apps/web/`, `apps/api/`, `apps/worker/`, `packages/db/`, `packages/mail/`, `packages/queue/`, `packages/ui/`, `prisma/`, `package.json` mit pnpm-Workspace, Tailwind 4, shadcn/Radix, BullMQ, MailerSend → das ist deine Arbeitsbasis. Du nutzt sie.
- Du erweiterst, was fehlt: neue `packages/view-models/`, `packages/ui-hivegrid/`, `packages/ui-subframe/` (Phase 1 leer), neue Routes unter `app/routes/...`, neue Module unter `app/domain/...`, neue Prisma-Modelle in `prisma/schema/...`
- Du löschst, was nicht zu HiveGrid gehört (Affiliate/Marketing/CMS-Reste aus dem ToolCTRL-Boilerplate, falls noch vorhanden)
- Du baust **in** dieser Codebase, nicht **neben** ihr
## 0.2 Pre-Flight-Check beim Start (jedes Mal)
Bevor du das erste Milestone planst, verifiziere die Vorbedingungen:
1. `package.json` im Repo-Root existiert mit pnpm-Workspace-Konfig — wenn nein: STOP, melde „Boilerplate-Inhalt fehlt im Repo"
2. `apps/`, `packages/`, `prisma/` existieren — wenn nein: STOP
3. `pnpm install && pnpm typecheck && pnpm test` läuft grün — wenn nein: STOP, melde welche Tests rot sind
4. Kein US-Provider im Code: `grep -rE "@ai-sdk/openai|@ai-sdk/anthropic|remix-auth-google|from \"openai\"" --include="*.ts" --include="*.tsx" --include="*.json" .` ist leer — wenn nein: STOP, EU-Sterilisation nicht abgeschlossen
5. `docs/PROGRESS-TRACKER.md` existiert — wenn nein: STOP
Erst nach 5/5 grün beginnst du mit dem ersten Milestone.
---
## 1. Wo du alles findest
| Was | Wo |
|---|---|
| **Verbindliches Pflichtenheft v1.1** | `_bmad-output/pflichtenheft/` |
| → Vision + Anti-Liste | `_bmad-output/pflichtenheft/02-vision.md` |
| → 20 Architektur-Bauprinzipien | `_bmad-output/pflichtenheft/03-architektur.md` |
| → 7 Modul-Blöcke mit Detail-Pflichten | `_bmad-output/pflichtenheft/blocks/block1.md` … `block7.md` |
| → Anhang (Geparktes, Noah-Anmerkungen N1-N19) | `_bmad-output/pflichtenheft/99-anhang.md` |
| **Master-Milestone-Index** | `docs/milestones/README.md` |
| **Pflichtenheft → Milestone Coverage-Map** | `docs/milestones/PFLICHTENHEFT-COVERAGE.md` |
| **Per-Milestone-Plan** (176 Stück) | `docs/milestones/M???-<slug>/PLAN.md` |
| **Live-Progress-Tracker** (du updatest ihn) | `docs/PROGRESS-TRACKER.md` |
| **Agent-Regeln + Definition of Done** | `AGENTS.md` (Repo-Root) |
| **Integration-Contracts** (Sidebar, Routes, Wiring) | `docs/INTEGRATION-CONTRACTS.md` |
| **Mid-Stream-Korrektur-Playbook** | `docs/MID-STREAM-CHANGES.md` |
| **Change-Request-Workflow** | `docs/CHANGE-REQUESTS.md` (entsteht in M176) |
---
## 2. Wie du ein Milestone planst (Pflicht-Reihenfolge)
Pro Milestone arbeitest du diese Schritte ab — nicht überspringen:
1. **Lies `docs/milestones/M???-<slug>/PLAN.md` vollständig.** Sie enthält Ziel, Pflichtenheft-Sektionen, Abhängigkeiten, Out-of-Scope, Erfolgskriterium, Hinweise und am Ende die **Definition of Done**.
2. **Lies die referenzierten Pflichtenheft-Sektionen** (`_bmad-output/pflichtenheft/blocks/blockX.md` Sektion Y.Z). Klartext-Hinweise und Edge-Cases stehen dort, nicht in PLAN.md.
3. **Lies `AGENTS.md`.** Verbindliche Regeln für Layer-Trennung, drei Anti-Patterns, EU-Stack, Querschnitt-Wiring, Klartext-Pflicht.
4. **Lies `docs/INTEGRATION-CONTRACTS.md`.** Hier steht, wo dein Modul in Sidebar / Routes / Tab-Bar / Search-Index andockt.
5. **Prüfe Abhängigkeiten:** Sind alle in PLAN.md genannten Vorbedingungs-Milestones abgeschlossen? Wenn nein: STOP und an User melden.
6. **Plane Slices** (TC-Standard, 5–8 pro Milestone):
- Erste Slice ist meist Datenmodell + Migration
- Folge-Slices: Domain-Logik → Loader/Action → View-Model → UI-Component → Sidebar/Routing-Wiring → Tests
- Letzte Slice: Smoke-Test + Hot-Swap-Drill
7. **Plane Tasks pro Slice** (5 Tasks à 30 Min – 2 Std). Pro Task konkrete Files + Verify-Befehle.
8. **Vor Execute:** prüfe gegen die **Definition of Done** in PLAN.md. Sind alle Punkte über die geplanten Slices abgedeckt? Wenn nicht: ergänze Slices.
9. **Execute pro Slice atomar.** Pro Slice ein Commit. Forward-Only-DB-Migrations mit Inverse-SQL als Kommentar.
10. **Nach jeder Slice: Verify-Gate.** Erst nach grünem Verify zur nächsten.
---
## 3. UI-Bauen ohne Subframe (Phase-1-Bootstrap)
**Wichtig:** Der finale UI-Workflow läuft über Subframe (Designer baut → Subframe-Output → AI-Agent integriert via Adapter). **Aber jetzt** baust du UI mit den **vorhandenen Komponenten der Boilerplate** — sauber, austauschbar, testbar.
Ziel: Geerkens-tauglich aussehende UI, die später ohne Logik-Bruch durch Subframe-Output ersetzt werden kann.
### Was du verwenden darfst
| Layer | Quelle |
|---|---|
| Primitives | shadcn/ui + Radix-UI (alle `@radix-ui/*`-Pakete sind in der Boilerplate) |
| Icons | lucide-react |
| Tabellen | bestehende `components/ui/table.*` aus Boilerplate-`packages/ui` |
| Forms | react-hook-form + zod (bereits in Deps) |
| Charts | recharts oder @tremor/react |
| Tipp-Trigger / Suche | kbar |
| Editor | TipTap (für Notizen, Marketing-Editor) |
| Drag-&-Drop | react-beautiful-dnd / xyflow (für Automation-Editor) |
| PDF | @react-pdf/renderer |
| Touch-Signatur | signature_pad |
| Tailwind 4 | bereits konfiguriert |
### Was du NICHT machst
- ❌ KEIN Subframe-MCP-Aufruf für irgendwas in Phase 1.
- ❌ KEIN `packages/ui-subframe`-Inhalt füllen — das Verzeichnis bleibt leer (oder mit Dummy-README), bis der Designer kommt.
- ❌ KEINE Custom-CSS-Stylesheets pro Komponente — Tailwind-Utility-Klassen.
- ❌ KEINE eigenen Component-Libraries erfinden — nur shadcn/Radix-Composites.
### Wie du UI strukturierst
Genau gleich wie es später mit Subframe sein wird — **die Layer-Trennung ist jetzt schon Pflicht**, sonst funktioniert der spätere Hot-Swap nicht:
```
app/routes/<modul>/<route>.tsx → Loader baut View-Model, rendert <Page vm={...} actions={{...}} />
packages/view-models/src/<modul>/*.vm.ts → Pure Funktion (entity, ctx) → VM, mit Zod-Schema
packages/ui-hivegrid/src/pages/<modul>/... → React-Component, baut auf shadcn/Radix mit Tailwind, KEIN Server-Code
packages/ui-hivegrid/src/components/... → Wiederverwendbare Composites (Tabellen, Karten, Modale)
```
Components in `packages/ui-hivegrid/src/pages/<modul>/<Component>.tsx` empfangen IMMER:
- `vm: <ModulVM>` als Prop (das View-Model)
- `actions: { onX: (...) => ... }` als Prop (Callbacks)
KEINE Hooks für Daten-Fetching innerhalb dieser Components. KEINE direkten Server-Imports.
### Warum das den späteren Hot-Swap erlaubt
Wenn der Designer später Subframe-Output liefert, wird der Inhalt von `packages/ui-hivegrid/src/pages/<modul>/<Component>.tsx` durch den Subframe-konformen Adapter ersetzt — die **Schnittstelle** (Props: `vm` + `actions`) bleibt identisch. Routes, View-Models, Domain-Logik werden NICHT angefasst. Der Tausch ist atomar.
Genau deshalb gilt für die Phase-1-Bootstrap-UI:
- Kein direkter `useLoaderData` in Components (auch jetzt nicht!)
- Kein direkter Prisma-Import (auch jetzt nicht!)
- Alle Texte über i18n (M003)
- Alle Aktionen als Callback-Props
- View-Model hat Zod-Schema, das später dem Subframe-Adapter den Vertrag erzwingt
### Visuelles Anspruchsniveau
„Sauber genug zum Testen", nicht „pixel-perfect Designer-Output". Konkret:
- Konsistentes Spacing (Tailwind 4-Standard)
- Hell + Dunkel-Modus respektiert (nutze Tailwind `dark:`)
- DE-Long-Strings testen (47-Zeichen-Buttons dürfen nicht überlaufen)
- Empty-States, Loading, Error mit Klartext (siehe AGENTS.md Punkt E)
- Mobile-Viewport-Test (kein Desktop-Only-Layout)
---
## 4. Pro Milestone — Pflicht-Deliverables (Definition of Done)
Aus jeder PLAN.md am Ende. In Stichpunkten:
**Erreichbarkeit** (siehe INTEGRATION-CONTRACTS.md §1)
- Sidebar-Eintrag in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` mit Permission-Gate
- Route registriert
- Breadcrumb für Detail-Routes
- Mobile-Tab-Bar-Eintrag falls relevant
- Customer-Portal-Sichtbarkeit explizit konfiguriert
**Querschnitt-Wiring**
- Audit (auto via M001 oder manuell)
- Activity-Stream via `logActivity` (M045)
- Notifications via `notify()` (M011)
- Search-Index in `indexed-entities.ts`
- Permission-Filter via `withTenantScope()` + `withPermission()` (M004)
- Feature-Flag-fähig (M006)
**Code-Layer-Disziplin** (PR-Block bei Verstoß)
- Loader baut VM; UI empfängt nur VM + Actions
- Keine Prisma-/Server-Imports in `packages/ui-hivegrid/**`
- Keine Direktimporte aus `packages/ui-subframe` in `app/routes/**` (ist eh leer in Phase 1)
- Alle UI-Texte als i18n-Keys
**Tests**
- Unit-Test für VM-Builder mit Zod-Schema
- Smoke-Test in `e2e/smoke/<modul>.spec.ts`
- Hot-Swap-Drill grün (Smoke-Test gegen `packages/ui-mock` aus M017)
- Visual-Regression-Snapshot (M016)
**Klartext**
- Fehler-Texte aus M002-Catalog
- Empty/Loading/Error mit Klartext + nächster Aktion
- Permission-Denied → 404 statt 403
**Rollback-Pfad**
- Forward-Only-DB-Migration mit Inverse-SQL als Kommentar
- Atomic-Slice-Commits
- Feature-Flag vorhanden zum Abschalten ohne Deploy
**Klick-Pfad zum Prüfen** (steht in PLAN.md unten)
- 4-Schritt-Anleitung, wie der User die Funktion findet und testet
---
## 5. Drei harte Anti-Patterns (sofortiger PR-Block)
1. `@prisma/client` oder `~/db` in `packages/ui-hivegrid/**`
2. Direktimport aus `packages/ui-subframe` in `app/routes/**` (das Paket ist Phase-1 leer, aber der Import-Pfad bleibt verboten)
3. `useLoaderData<typeof loader>()` innerhalb von `packages/ui-hivegrid/**`
Plus: jede Erweiterung, die der Anti-Liste in `_bmad-output/pflichtenheft/02-vision.md` V.3 widerspricht. Vollständige Liste siehe dort.
---
## 6. EU-Stack — nicht verhandelbar
- KEIN `@ai-sdk/anthropic`, `@ai-sdk/openai`, `openai`
- KEIN `remix-auth-google`, KEIN Apple-/Google-/Microsoft-Login (Phase 1)
- JA Mistral primär, Scaleway sekundär, AlephAlpha tertiär
- JA Email+PW + Magic-Link + TOTP-2FA
- Hosting Scaleway (EU), DB in EU, Storage Scaleway-S3 (EU)
Wenn etwas im Boilerplate-Code noch US-Provider referenziert: STOP und an User melden — die Boilerplate sollte EU-sterilisiert sein.
---
## 7. Wenn du nicht weiterkommst
| Situation | Vorgehen |
|---|---|
| Pflichtenheft-Sektion ist mehrdeutig | STOP. Frage formulieren, an User. Kein Auto-Refactor. |
| Abhängigkeits-Milestone fehlt | STOP. An User: „M??? braucht M??? — wann fertig?" |
| Boilerplate hat Bug, der dich blockiert | STOP. Bug an User mit konkretem Pfad + Repro. |
| Anti-Listen-Punkt in PLAN.md gewünscht | STOP. Change-Request via `docs/CHANGE-REQUESTS.md` (M176). |
| Layer-Trennung lässt sich nicht einhalten | STOP. Architektur-Frage, nicht selber lösen. |
---
## 8. Erste konkrete Aufgabe
Beginne mit **M001 (Audit-Log default)**. Lies:
1. `docs/milestones/M001-audit-log-default/PLAN.md`
2. `_bmad-output/pflichtenheft/03-architektur.md` Sektion A.6
3. `AGENTS.md`
4. `docs/INTEGRATION-CONTRACTS.md` (§5 Audit-Wiring, §6 — der Audit-Eintrag selbst gehört zu diesem Milestone)
Plane M001 in Slices, präsentiere den Plan, warte auf User-Bestätigung, dann Execute.
Nach M001 → M002 → ... in der Reihenfolge der Master-Liste, ABER: prüfe in jeder PLAN.md die Abhängigkeiten — manche Milestones lassen sich parallelisieren (siehe `Wird benötigt von`-Felder).
---
## 9. Was du dem User nach jedem Milestone meldest
Kurzer Statusbericht in dieser Form:
```
Milestone: M??? — <Titel>
Status: READY-TO-VERIFY | BLOCKED | NEEDS-USER-INPUT
Slices: X von Y abgeschlossen
Tests: Smoke ✓ / Hot-Swap-Drill ✓ / Visual-Regression: <Anzahl Diffs zu prüfen>
DoD-Score: N/14 erfüllt
Sidebar-Eintrag: <Gruppe> > <Eintrag>
Klick-Pfad zum Prüfen: <wie aus PLAN.md>
PROGRESS-TRACKER aktualisiert: ✓
Offene Punkte (falls relevant): <Liste>
```
---
## 10. Progress-Tracker-Pflicht (`docs/PROGRESS-TRACKER.md`)
Diese Datei ist Live-Übersicht für den User. Du updatest sie **nach jedem Status-Wechsel**:
### Bei Milestone-Start
1. Status in der Master-Tabelle: `⚪ NICHT-GESTARTET` → `🔵 IN-ARBEIT`
2. Detail-Sektion gemäß Vorlage (Abschnitt „Vorlage" in der Tracker-Datei) anlegen — kopiere den Block, fülle Pflichtenheft-Bezug, leere Slice-Liste, leere DoD-Checkboxen ein
3. Klick-Pfad-Block aus PLAN.md übernehmen, mit konkreten User-Typ + Sidebar-Pfad ausfüllen
### Bei Slice-Abschluss
4. In der Detail-Sektion die Slice abhaken mit Commit-Hash
5. DoD-Checkboxen abhaken, sobald die jeweilige Pflicht erfüllt ist
6. DoD-Score in der Master-Tabelle aktualisieren (z.B. `8/14`)
### Bei Milestone-Abschluss (alle DoD-Punkte erfüllt + Tests grün)
7. Status in Master-Tabelle: `🔵 IN-ARBEIT` → `🟡 READY-TO-VERIFY`
8. „Notes vom Agent"-Block in Detail-Sektion ausfüllen (was war nicht offensichtlich, was muss User wissen)
9. STOP. Warte auf User-Sign-Off. Starte KEIN nächstes Milestone.
### Nach User-Sign-Off
10. User trägt selbst `🟢 SIGNED-OFF` + Datum in die Detail-Sektion ein
11. Du übernimmst diesen Status in die Master-Tabelle und beginnst das nächste Milestone
### Konsistenz-Pflicht
- DoD-Score in Master-Tabelle muss zur Detail-Sektion passen
- `🟡 READY-TO-VERIFY` ohne 14/14 DoD = Inkonsistenz → korrigiere sofort
- Mehr als 5 Milestones in `🔵 IN-ARBEIT` parallel → STOP, Fokus-Verlust → an User melden
+45
View File
@@ -0,0 +1,45 @@
# Agent Entrypoints
Use this file as the first stop when a coding agent plans or implements feature work in this boilerplate.
## Start Here
- Interpreting old improvement-plan status, handoffs, or verification notes:
read `docs/improvement-plan/CURRENT-STATUS.md` first, then follow the
canonical docs it links.
- New or migrated feature: read `docs/agent-playbooks/feature-integration.md` and update the matching manifest before implementation.
- Existing MVP import: read `docs/agent-playbooks/mvp-intake.md` before proposing code changes.
- Admin-managed tenant feature: read `docs/agent-playbooks/admin-managed-features.md`.
- Foundation, billing usage, notifications, flags, permissions, audit, or idempotency changes: read `docs/agent-playbooks/foundation-boundaries.md`.
- Worker-backed or browser-backed feature: read `docs/agent-playbooks/worker-backed-features.md`, `docs/ops/worker-queues.md`, and `packages/queue/src/payloads.ts`.
- Plugin, WASM, lifecycle, or hook work: read `docs/features/plugins.md`, `docs/operations/plugin-lifecycle.md`, and `docs/security/plugin-runtime-signoff.md`.
- UI surface or component changes: start with `docs/ui-inventory/surface-map.md`, then read `docs/agent-playbooks/ui-changes.md`, `docs/ui-inventory/`, and `apps/web/app/application/ui/uiRegistry.ts`.
- Navigation, sidebar, or route-helper changes: read `docs/navigation-architecture.md`.
- UI adapter or hotswap work: read `docs/agent-playbooks/ui-adapter-migration.md` and `packages/ui/README.md`.
- Feature ownership, dependencies, configuration, provider, worker and UAT contracts: read `docs/feature-manifests/`.
- New manifest scaffold: run `pnpm run manifest:new -- --id my-feature --title "My Feature" --classification plugin-candidate --ownership hybrid`, then replace placeholders before coding.
- Architecture Closure closure/status contract: start from the aligned sources `docs/archive/architecture-handoff-summary.md`, `docs/architecture-migration-status.md`, `docs/feature-inventory.md`, and `docs/feature-manifests/*.json`; read them together before planning implementation, extraction, or removal work.
## Agent Rule
Do not jump from a user request directly into implementation when the request imports an existing product, adds a proprietary feature, adds provider credentials, changes admin-managed behavior, or introduces workers.
For those requests, produce or update a feature manifest first. The manifest is the contract the implementation must satisfy.
## Expected Planning Output
For a substantial feature, the planning output should identify:
- Existing user flows and target boilerplate flows.
- Feature ownership: `tenant-owned`, `admin-managed`, `hybrid`, or `system-only`.
- Admin, tenant, public and API surfaces.
- Runtime route ownership and helper choice: admin host paths use `adminPath`, tenant app paths use `appPath` or `UrlUtils.getModulePath(...)`, and API host paths use `apiPath`/`/v1`.
- Tenant-scope and negative access checks for every loader/action/API mutation that touches tenant data.
- i18n surfaces, including new locale keys and `pnpm run verify:i18n` when UI text changes.
- Deploy/static gates affected by the change, especially `pnpm run verify:deploy-workflow`, Docker baseline/static checks, and production seed artifact checks for deploy work. Run live Docker smokes only when the task explicitly requires them.
- Database models and migrations.
- Provider credentials and secret ownership.
- Worker queues and jobs.
- Permissions and audit events.
- Billing or usage-metering impact.
- UAT checks the operator can run.
@@ -0,0 +1,43 @@
# Admin-Managed Feature Pattern
Use this when a tenant uses a feature whose policy, provider, key or global availability is controlled by the admin/platform.
## Definition
An admin-managed feature is configured by admin/platform operators and consumed by tenants. Tenants may see enabled state and allowed options, but they do not own sensitive provider configuration.
## Examples
- Tenant AI chatbot using an admin-owned AI provider key approved for the deployment region.
- Tenant integration using a platform-owned provider account.
- Tenant feature enabled only for selected plans or accounts.
- Browser automation where admin controls allowed domains and runtime policy.
## Required Manifest Entries
The feature manifest must declare:
- `integration.ownership = "admin-managed"` or `"hybrid"`.
- Admin routes that manage provider/policy.
- Tenant routes that consume the feature.
- Secrets and who owns them.
- Permissions for admin configuration and tenant usage.
- Audit events for configuration changes.
- Billing and usage impact.
- UAT checks for secret isolation and tenant enablement.
## Security Rules
- Provider keys are never tenant-owned unless explicitly designed as tenant-owned.
- Provider keys must not be sent to tenant loaders, API responses, browser bundles or logs.
- Admin policy must be checked server-side.
- Tenant enablement must be checked server-side.
- Admin configuration changes must emit audit events.
## Implementation Shape
- Admin settings store provider config, model/default policy, enablement and limits.
- Tenant feature state references admin-approved config.
- Tenant UI shows only allowed choices.
- API routes resolve effective configuration server-side.
- Worker jobs receive scoped IDs and load secrets server-side.
@@ -0,0 +1,68 @@
# Feature Integration Playbook
Use this playbook when adding a feature to the boilerplate or adapting a proprietary feature from another product.
## Goal
Make the feature understandable before implementation. A human operator and a coding agent should be able to answer where the feature lives, who owns its configuration, which flows it exposes, and how to verify it.
## Required Steps
1. Read `docs/agent-entrypoints.md`.
2. Find or create a manifest in `docs/feature-manifests/`.
3. Classify ownership:
- `tenant-owned`: tenant configures and uses it.
- `admin-managed`: admin/platform configures it; tenant only uses approved surfaces.
- `hybrid`: admin defines policy/defaults; tenant can configure local behavior.
- `system-only`: internal platform feature with no tenant-owned controls.
4. Map surfaces:
- Admin route source files and canonical admin-host runtime paths.
- Tenant route source files and canonical tenant-host runtime paths.
- Public route source files and root/web-host runtime paths.
- API route source files and canonical API-host `/v1/...` runtime paths.
- Link/form/action helpers: `adminPath`, `appPath`, `apiPath`, cross-host helpers, or `UrlUtils.getModulePath(...)` for tenant modules.
5. Map persistence:
- Prisma models.
- Existing tables reused.
- Migrations required.
- Tenant scoping fields.
6. Map configuration:
- Admin settings.
- Tenant settings.
- Environment variables.
- Secrets and their owner.
7. Map runtime:
- Request/response only.
- Queue-backed.
- Worker-backed.
- Browser-backed.
8. Map controls:
- Permissions.
- Audit events.
- Billing or usage limits.
9. Define UAT:
- Operator-visible checks.
- Negative checks for forbidden access.
- Secret exposure checks if providers are involved.
## Implementation Rules
- Do not expose provider secrets through tenant loaders, API responses, client bundles or logs.
- Do not add sidebar/nav entries without stable UI IDs.
- Do not add new root UI primitives without updating `docs/ui-inventory/components.json`.
- Do not add worker behavior without declaring queues and jobs in the manifest.
- Do not add admin-managed tenant behavior without admin permissions and audit events.
- Keep proprietary feature logic behind explicit module boundaries.
- Do not hard-code legacy runtime paths such as `/admin/...`, `/app/...`, or `/api/v1/...` outside tests that explicitly cover legacy redirects.
- Do not ship visible UI text without locale keys and `pnpm run verify:i18n`.
## Done Criteria
- Manifest updated and valid.
- Routes and surfaces match the manifest.
- Permissions are enforced on admin/API/tenant surfaces.
- Tenant-scope negative tests cover forbidden reads and mutations.
- Route helpers and runtime host ownership are tested for new navigation.
- i18n and deploy/static verifiers relevant to the change have run.
- Required worker queues/jobs exist or are explicitly deferred.
- UAT checks pass or are marked as deferred with a reason.
@@ -0,0 +1,24 @@
# Foundation Boundary Playbook
Use this when moving feature logic into the reusable boilerplate layer or when touching audit, idempotency, flags, permissions, billing usage, or notifications.
## Ownership Rules
- Runtime apps may orchestrate requests, headers, sessions, and responses.
- Shared business rules belong in `packages/*`, preferably `@toolctrl/foundation` for pure policy and `@toolctrl/db` for schema-backed server boundaries.
- UI and route modules must not reach around package boundaries for tenant-scoped persistence.
- Foundation exports must stay generic. Do not add product-specific roles, flags, templates, adapters, or catalog entries.
## Required Checks
- Audit writes use `@toolctrl/db/audit-events.server`.
- Idempotency keys use `@toolctrl/db/idempotency-keys.server` or the API idempotency store bridge.
- API usage limits use `@toolctrl/db/usage.server`; API routes should not recompute subscription feature math locally.
- Notification deliveries use `sourceKey` whenever a producer can derive a stable event key, so DB upsert prevents duplicate delivery records.
- Feature flags resolve in this order: explicit override, user scope, tenant scope, global scope, default.
## Done Criteria
- Add or update focused tests in the owning package.
- Update feature manifests when a feature changes ownership, billing, provider, worker, or admin-managed behavior.
- Run `pnpm run test:foundation`, the affected package tests, and the app-level route tests for changed runtimes.
@@ -0,0 +1,62 @@
# MVP Intake Playbook
Use this before adapting an existing MVP onto this boilerplate.
## Purpose
Prevent agents from copying product code blindly. The MVP must first be decomposed into flows, roles, data, providers, ownership and runtime behavior.
## Intake Checklist
Capture the following before implementation:
- Product summary: what the MVP does in one paragraph.
- Primary user roles.
- Admin/operator roles.
- Tenant roles.
- Critical user flows.
- Data objects and ownership.
- External providers.
- Secrets and credentials.
- Background jobs.
- Browser automation or long-running work.
- Billing or usage limits.
- Existing UI surfaces.
- Must-keep behavior.
- Behavior that should become plugin-only.
- Behavior that should be removed.
## Flow Mapping
For every flow, record:
- Entry point.
- Actor.
- Preconditions.
- Data read.
- Data written.
- Provider calls.
- Sync versus async behavior.
- Success state.
- Failure state.
- UAT check.
## Boilerplate Mapping
Map each flow into:
- `apps/web/app/routes/admin*` for admin/platform management files; runtime links must use `adminPath` or admin-host helpers.
- `apps/web/app/routes/app.$tenant*` for tenant-facing usage files; runtime links must use `appPath`, `UrlUtils.getModulePath(...)`, or an equivalent host-aware wrapper.
- `apps/api/src/app.ts` under `/v1` for canonical public/service APIs and webhooks. If a `/v1` route temporarily proxies a web `internal/api` handler, record `apps/api` as the external route owner and the web handler as the temporary implementation owner until extraction lands.
- `apps/web/app/modules/<feature>` for feature-owned application logic.
- `apps/worker` and `packages/queue` for async jobs.
- `docs/feature-manifests/<feature>.json` for the integration contract.
## Output Required From Agent
Before coding, the agent should produce:
- A short MVP decomposition.
- A feature manifest draft or update.
- A list of unknowns that block safe implementation.
- An implementation plan tied to the manifest.
@@ -0,0 +1,74 @@
# UI Adapter Migration Playbook
Use this when moving UI primitives toward `packages/ui` hotswap-ready adapters.
## Goal
App code should eventually depend on stable adapter contracts such as `@toolctrl/ui/buttons`, not directly on a specific library implementation. Migration happens category by category.
## Migration Order
1. Navigation and shell
2. Buttons
3. Dialogs and modals
4. Tabs and tables
5. Inputs
6. Feedback
## Adapter Levels
- `placeholder`: category exists but exports no real adapter contract.
- `available`: package exposes stable metadata/contract for the current implementation.
- `component-backed`: package exports real reusable components or wrappers.
- `migrated`: app imports use the package adapter instead of app-local primitives.
Current status:
- `buttons`: `available` contract for `Button` and `buttonVariants`.
- `layout`: `available` contract for `ShadcnSidebarLayout` and `UiRegistry`.
- `navigation`: `available` contract for `AppSidebar`, `AdminSidebar`, `DocsSidebar`, `NavMain`, `NavSecondary`, `NavQuickLinks`, `NavActions`, and `SidebarPrimitives`.
- all other categories: placeholder registry.
## Rules
- Start with metadata/contract exports before moving components.
- Keep adapter exports source-only and runtime-safe.
- Keep runtime components in `apps/web/app/` until a migration phase explicitly moves imports.
- Do not import app routes, data loaders, tenant context, admin context, or feature modules into `packages/ui`.
- Do not hide feature-specific behavior inside `packages/ui`.
- Keep shadcn/Radix as the current core default until a category-specific migration says otherwise.
- Treat Headless UI as legacy unless explicitly retained by a category decision.
## Required Checks
For each category migration:
- Update `packages/ui/README.md`.
- Update the relevant `packages/ui/src/<category>.ts`.
- Update `docs/ui-inventory/components.json`.
- Add or update adapter contract tests.
- Keep app imports unchanged unless the phase explicitly migrates them.
## Button Example
Current button adapter contract:
- `@toolctrl/ui/buttons`
- `buttonAdapterNames`
- `buttonAdapters`
- source: `apps/web/app/components/ui/button.tsx`
This records stable intent without moving the current shadcn implementation yet.
## Shell/Navigation Example
Current shell/navigation adapter contracts:
- `@toolctrl/ui/layout`
- `layoutAdapterNames`
- `layoutAdapters`
- `@toolctrl/ui/navigation`
- `navigationAdapterNames`
- `navigationAdapters`
These records point agents to the current runtime sources in `apps/web/app/` and are guarded by `apps/web/app/application/ui/uiAdapters.test.ts` plus the inventory tests.
@@ -0,0 +1,45 @@
# UI Change Playbook
Use this when an agent changes layout, navigation, shell, buttons, dialogs, inputs, tables, feedback UI, or any `apps/web/app/components/ui` primitive.
## First Reads
1. `docs/ui-inventory/surface-map.md`
2. `docs/ui-inventory/surfaces.json`
3. `docs/ui-inventory/components.json`
4. `apps/web/app/application/ui/uiRegistry.ts`
5. `docs/agent-playbooks/ui-adapter-migration.md` if the change touches `packages/ui`
## Agent Workflow
1. Identify the target by `data-ui-id`, not by visual guesswork.
2. Find the source file in `docs/ui-inventory/surfaces.json` or `components.json`.
3. Check whether the change is a surface change, component primitive change, or feature-specific UI change.
4. Preserve or add stable `data-ui-id` attributes for user-visible surfaces and actions.
5. Update `docs/ui-inventory/components.json` when adding root UI primitives.
6. Update `docs/ui-inventory/surfaces.json` when adding or renaming surfaces/actions/navigation IDs.
7. Add or update DOM tests for important `data-ui-id` behavior.
## Rules
- Do not add sidebar/nav items without stable IDs.
- Do not add dynamic UI items without `uiIdPattern` and `uiIdParams`.
- Do not move app components into `packages/ui` unless the adapter migration playbook says the category is ready.
- Do not change `packages/ui` exports in a way that imports app runtime or React components unless that migration phase explicitly owns it.
- Do not rely on visible text alone to locate UI in agent instructions; prefer `data-ui-id`.
## Common Requests
- "Change the app sidebar": start with `ui.surface.app.sidebar`.
- "Change admin sidebar settings": start with `ui.nav.admin.settings`.
- "Change header search": start with `ui.action.shell.search`.
- "Change header right actions": start with `ui.surface.shell.navActions`.
- "Change page content shell": start with `ui.surface.shell.main`.
## Done Criteria
- Target UI ID still exists in rendered DOM.
- New UI IDs are listed in `surfaces.json` when stable.
- Root primitives are listed in `components.json`.
- Tests cover changed registry, inventory, or DOM behavior.
@@ -0,0 +1,45 @@
# Worker-Backed Feature Pattern
Use this when a feature performs slow, retryable, external, scheduled, browser-backed or resource-heavy work.
## Worker Required When
- Work may exceed normal request time.
- Work needs retries.
- Work calls unreliable external providers.
- Work performs browser automation.
- Work processes files, imports, exports or media.
- Work should continue after a user navigates away.
- Usage metering or cleanup must be reliable.
## Required Manifest Entries
The feature manifest must declare:
- `integration.workers.required`.
- Queue names from `packages/queue/src/names.ts`.
- Job names from `packages/queue/src/payloads.ts`.
- Notes about retries, idempotency and cleanup.
- API route or UI flow that enqueues the job.
- UAT checks for worker success and failure states.
## Implementation Rules
- API routes enqueue jobs and return job/status IDs for long-running work.
- Workers load secrets server-side from admin/platform configuration.
- Jobs must be scoped by tenant/user where applicable.
- Jobs should be idempotent or have explicit duplicate protection.
- Browser jobs must enforce admin-owned navigation policy.
- Failures should be visible to the initiating user or admin.
- AI/browser-backed features use `toolctrl.ai-chatbot` and `toolctrl.browser-automation` unless a manifest explicitly justifies a new queue.
## Browser-Backed Work
Browser-backed features require extra constraints:
- Domain allowlist/denylist.
- Timeout policy.
- Screenshot/artifact storage policy.
- Credential handling rules.
- Cleanup jobs for sessions and artifacts.
- Audit events for start, completion and failure.
+123
View File
@@ -0,0 +1,123 @@
# Production API Runbook
This runbook is the production entrypoint for the dedicated Hono API application served from `apps/api`. It documents the currently supported `/v1` surface, authentication behavior, request correlation, CORS boundaries, safe diagnostics, and local static verification commands for operators and integrators.
## API base surface
The dedicated API is designed to run independently from the React Router web runtime at an API origin such as `https://api.example-origin`. The versioned API surface is rooted at `/v1`; examples below intentionally use generic origins and token names rather than environment-specific values.
The API server listens on `API_PORT` when set, falls back to `PORT`, and otherwise defaults to `3001`. Keep `apps/api/src/app.ts` as the importable Hono app boundary so tests, adapters, and deployment targets can exercise the app without starting a listener.
## `/v1` endpoint inventory
| Method | Path | Auth | Purpose |
| --- | --- | --- | --- |
| `GET` | `/healthz`, `/readyz`, `/v1/health`, `/v1/openapi.json`, `/v1/docs` | None | Health/readiness and public API contract discovery. |
| `POST` | `/v1/auth/login`, `/v1/auth/refresh-token`, `/v1/auth/logout` | Credentials or API session JWT | API session lifecycle. |
| `GET` | `/v1/session/current-user`, `/v1/accounts`, `/v1/accounts/:id` | Service token | Service principal and account metadata. |
| `GET` | `/v1/usage` | Tenant API key | Usage metadata for the credential-scoped tenant. |
| `POST` | `/v1/api-keys/validate` | Optional presented API key or service token | Redacted API-key/service-token validation helper. |
| `POST` | `/v1/ai/upload` | Service token | AI upload through configured service storage. |
| `POST` | `/v1/maintenance/export-retention/enqueue` | Service token | Operator maintenance enqueue action. |
| `POST` | `/v1/support/inbound-email` | Webhook signature | Signed inbound support email ingress that creates tenant support tickets. |
| `GET` | `/v1/analytics/public`, `/v1/analytics/admin` | Analytics token or service token | Analytics read surfaces. |
| `POST` | `/v1/analytics/events`, `/v1/analytics/page-views` | Analytics token | Public analytics ingress. |
| `GET` | `/v1/notifications/stream` | Service token | Notification diagnostics stream. |
| `GET` | `/v1/entities/:entity` | Service token | Entity metadata lookup. |
| `GET/POST/PATCH/DELETE` | `/v1/entities/:entity/rows*` | Service token or tenant API key | Tenant-scoped entity row operations. |
| `GET/POST/DELETE` | `/v1/relationships/*` | Service token or tenant API key | Tenant-scoped relationship operations. |
| `GET` | `/v1/exports/*` | Service token or tenant API key | Tenant-scoped export download operations. |
| `GET/POST` | `/v1/workspaces/*` | Service token | Workspace auth/invite boundaries. |
| `POST` | `/v1/events/webhooks/attempts/:id`, `/v1/support/inbound-email`, `/v1/billing/stripe/webhooks` | Webhook signature | Signed webhook ingress. |
Treat `/v1/openapi.json` and `apps/api/src/route-access-inventory.ts` as the detailed source of truth. If `apps/api/README.md` changes, update this runbook in the same change set so the documentation does not drift from the implementation contract.
Tenant-scoped row, relationship, and export requests resolve tenant scope from the stored tenant API key when one is used. Service-token callers must provide an explicit documented tenant scope, such as `tenantId` query parameters or supported tenant-scope headers where the OpenAPI operation lists them.
## Authentication and token behavior
Set `API_ACCESS_TOKEN` in the API runtime environment to enable the service-token endpoints. Authenticated calls use `Authorization: Bearer <service-token>`; key validation may also read the candidate key from the `x-api-key` header or from the JSON `apiKey` request body when using `POST /v1/api-keys/validate`.
Authentication failures return JSON error envelopes without echoing token values, API keys, bearer fragments, headers, request bodies, tenant identifiers, customer identifiers, or environment values. Support notes should record only the generic endpoint, response status, stable error code, and request ID. Never paste real authorization headers, raw API key inputs, or token-shaped values into tickets, chat, logs, screenshots, or documentation examples.
## OpenAPI contract
The API contract is available at:
```text
GET /v1/openapi.json
```
Use the OpenAPI document as the integration contract for supported paths, methods, request shapes, response shapes, and error schema expectations. Static documentation and examples should refer to `/v1/openapi.json` rather than copying large schema fragments into support notes, because copied schema snippets become stale quickly.
## Health and readiness expectations
`GET /v1/health` is the lightweight health endpoint for the dedicated API application. It should be usable by uptime checks and deployment health probes that need to confirm the API process can serve a request without requiring credentials.
Readiness triage should pair the health response with process-level deployment status and logs that are already redacted. Do not convert health checks into authenticated user, tenant, customer, queue, database, or browser-flow checks. If deeper readiness is needed for an incident, run the focused local checks listed below and correlate production observations by request ID instead of collecting raw request payloads.
## Request ID correlation
The API request-id middleware emits `x-request-id` on responses. Clients may supply `x-request-id`; the API echoes the supplied value in the response header and includes it in JSON error bodies. If the client does not provide a request ID, the API generates one.
Use `x-request-id` as the primary correlation key for support, incident response, and production diagnostics. Support records may include the request ID, endpoint path, method, response status, and stable error code. They must not include raw request bodies, authorization headers, API keys, bearer tokens, cookies, tenant names, customer names, full environment dumps, or customer-specific domains.
## CORS origin rules
CORS is the source of truth for browser-origin access to the API. Allowed browser origins come from canonical routing-derived app and admin origins, plus explicit `WEB_ORIGIN` and `ADMIN_ORIGIN` values when a deployment needs them. Treat `WEB_ORIGIN` and `ADMIN_ORIGIN` as complete origin strings including scheme and host, not host-only aliases.
Allowed CORS responses include `access-control-allow-origin` matching the allowed browser origin and still include `x-request-id` and the API security headers. Denied or malformed origins must not emit `access-control-allow-origin`, but should still emit `x-request-id` so incidents can be correlated safely. Positive and negative CORS checks are both required during triage because positive-only checks can miss over-broad allowlist regressions.
## JSON error envelope
API errors use a stable JSON envelope:
```json
{
"error": {
"code": "example_error_code",
"message": "Safe operator-readable message",
"requestId": "request-id-for-correlation"
}
}
```
`error.code` is the stable programmatic classification, `error.message` is safe to show to an operator, and `error.requestId` links the response to server-side diagnostics. Error envelopes must not echo tokens, API keys, authorization headers, request bodies, raw environment values, tenant identifiers, customer identifiers, or secret-shaped substrings.
## Redaction constraints
Apply these redaction rules to all API diagnostics, examples, incidents, and support notes:
- Redact bearer tokens, API keys, cookies, service-token values, and token-shaped substrings in full.
- Redact or generalize tenant identifiers, customer identifiers, customer domains, account names, and user-specific values.
- Do not paste raw request bodies, raw response bodies containing customer data, full headers, authorization headers, or environment dumps.
- Record generic labels such as `example-api-origin`, `example-web-origin`, `service-token`, and `request-id-for-correlation` instead of real deployment values.
- Prefer stable `error.code`, response status, endpoint path, method, and `x-request-id` for debugging context.
## Safe local and static verification
Run these deterministic checks from the repository root when editing this runbook or the API contract. They do not launch browsers, E2E flows, Docker services, registries, or production traffic.
```bash
test -s docs/api.md
python - <<'PY'
from pathlib import Path
text = Path('docs/api.md').read_text()
for marker in ('TO' + 'DO', 'T' + 'BD', 'FIX' + 'ME'):
assert marker not in text
PY
pnpm exec eslint apps/api/src/app.ts apps/api/src/cors.ts --max-warnings=0
pnpm exec vitest run apps/api/src/app.test.ts apps/api/src/cors.test.ts
pnpm run verify:production-docs
```
Browser automation, Playwright, Cypress, live Docker, live registry, and E2E execution are manual-only unless a human explicitly approves the exact focused run in the current session.
## Troubleshooting checklist
1. Confirm the path is under the supported `/v1` surface and compare it with `/v1/openapi.json`.
2. For auth failures, check whether `API_ACCESS_TOKEN` is configured and whether the request used the expected bearer or API-key input channel without copying the secret value.
3. For CORS failures, compare the request `Origin` with the full allowed origin string and test a denied origin to detect over-broad allowlists.
4. For missing diagnostics, confirm the response includes `x-request-id` and capture that request ID in the incident record.
5. For JSON errors, record only `error.code`, safe `error.message`, `error.requestId`, status, method, and endpoint.
6. If production behavior differs from this runbook, update the implementation and documentation together or record the verified contract change in the same review.
@@ -0,0 +1,131 @@
# Architecture Migration Status
This document is the canonical current-state snapshot for the repository architecture migration. It describes what is present today, which boundaries are already runtime-wired, and which surfaces remain skeleton-only so later pruning and extraction work can use one repo-state contract.
## Workspace and package manager state
- The repository is a private pnpm-managed project (`packageManager: pnpm@10.33.0`) with root package name `toolctrl-boilerplate` and root scripts that delegate to the `apps/web` React Router runtime.
- The root `package.json` keeps the top-level command surface: `dev`, `build`, `start`, and `typecheck` delegate into the workspace-owned web runtime and shared package boundaries.
- Workspace dependencies are already declared for the target package boundaries through `workspace:*` imports such as `@toolctrl/config`, `@toolctrl/db`, `@toolctrl/mail`, `@toolctrl/queue`, and `@toolctrl/ui`.
- The migration is therefore no longer root-hybrid for web source ownership: `apps/web/app` is authoritative for the current web application, while `apps/*` and `packages/*` provide conservative target boundaries for later route-by-route or category-by-category extraction.
## Boundary status
| Boundary | Status | Current state |
| ----------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps/web` | runtime source home | Physical home for the React Router web app under `apps/web/app` and `apps/web/public`; root-level commands still own the top-level developer entrypoints. |
| `apps/api` | runtime-wired API app | Dedicated Hono API app owns the canonical `/v1` route surface, route-access inventory, OpenAPI docs, health/readiness routes, and package scripts for dev/build/start/typecheck. Some `/v1` routes still proxy web-owned internal handlers until their service code is extracted, so document both the canonical API route and any temporary web implementation owner. |
| `apps/worker` | runtime-wired foundation, disabled by default | BullMQ worker entrypoint exists and can be run directly, but exits unless `WORKER_ENABLED=true`. It owns concrete handler paths for mail, webhooks, notifications, maintenance, storage scans, exports, imports, guarded AI/browser descriptors, `worker-general` outbox dispatch, `outbox.workflow`, `outbox.plugin-hook` fanout, and isolated `worker-plugin` invocation. Live Coolify deployment proof still belongs to the deployment/workers runbooks, not this architecture snapshot. |
| `packages/config` | partially runtime-wired catalog | Typed environment catalog and validation helpers document the current environment surface and are wired into web/API/worker runtime loaders plus env drift checks. |
| `packages/db` | partially runtime-wired boundary | Owns the server-side Prisma client boundary through `@toolctrl/db/client.server`, the Prisma 7 generated TypeScript client boundary at `packages/db/generated/prisma/client`, and the package command surface for generate, migrate deploy and seed. The schema and migrations still physically live under `prisma/` and are reached through the root Prisma config. |
| `packages/mail` | runtime-wired source boundary | Provides the Mailjet provider adapter and `MailProvider` contract for transactional mail. It is source-only but represents the active provider boundary that later API/worker runtime code should instantiate. |
| `packages/queue` | runtime-wired queue contract | Defines BullMQ queue names, typed payloads, producer helpers and factory helpers consumed by web/API enqueue paths and `apps/worker` processors. |
| `packages/shared` | skeleton-only | Intentionally empty cross-cutting package reserved for dependency-free shared types, utilities and constants once ownership is clear. |
| `packages/ui` | skeleton-only adapter contracts | Source-only UI adapter boundary. It exposes stable adapter metadata for selected categories but does not render components, import React or replace current app imports yet. |
## Runtime-wired versus skeleton-only summary
Runtime-wired or partially runtime-wired surfaces:
- `apps/web`: physical React Router web app source and public asset home; root scripts delegate into this runtime.
- `apps/api`: independent Hono API app and server scripts are present; current `/v1` routes include health/docs, auth/session, API-key validation, accounts/usage, analytics, notifications, workspaces, entities, relationships, exports, billing webhook ingress, and maintenance enqueue behavior. Widget and workflow `/v1` surfaces are plugin-disabled historical code paths, not active canonical API routes. Temporary proxy routes must be documented as API-owned entrypoints with web-owned implementation handlers until extracted.
- `apps/worker`: executable worker boundary exists but is intentionally opt-in through `WORKER_ENABLED=true`; processors are wired for the current queue payload contracts, including the five configured worker groups (`general`, `mail`, `import`, `webhook`, `plugin`) and the worker-general outbox/Workflow/Plugin-Hook fanout paths. This is repository/runtime contract evidence; environment-specific Coolify health still comes from `docs/deployment.md`, `docs/worker.md`, and `docs/ops/worker-queues.md`.
- `packages/db`: server-side Prisma client boundary, Prisma 7 generated TypeScript client output, and database command entrypoints exist in the package boundary; the schema and migrations still live in `prisma/` and are referenced by root Prisma config.
- `packages/mail`: Mailjet provider contract and adapter exist as the transactional mail boundary.
Skeleton-only or documentation/contract surfaces:
- `packages/shared`: intentionally empty until clear shared ownership emerges.
- `packages/ui`: adapter metadata and migration contracts only; current UI runtime imports remain in `apps/web/app/`.
## Architecture Closure final closure status
Architecture Closure closes the architecture snapshot as a handoff contract, not as a claim that every feature has already been deleted or extracted at runtime. API, worker and package boundaries are documented above: `apps/api`, `apps/worker`, `packages/config`, `packages/db`, `packages/mail` and `packages/queue` have concrete or partial runtime wiring, while `apps/web` owns the web source tree and `packages/shared` and `packages/ui` remain skeleton-only or contract surfaces until sequential atomic migration tasks move ownership. This preserves R001 handoff coverage and R002 sequencing by keeping the current React Router runtime authoritative until each downstream extraction replaces it directly.
The feature-pruning contract is now anchored in `docs/feature-manifests/` and validated by `apps/web/app/application/featureManifests.test.ts`. `docs/feature-inventory.md` remains the human-readable Core/Plugin/Removal inventory, while the manifests are the machine-readable source for `core`, `plugin-candidate`, `remove-candidate` and related pruning actions. Under R003, these classifications are removal and replacement instructions for future work; they are not a permanent legacy-support promise and should not be interpreted as runtime plugin extraction already completed.
S04 bundle/performance evidence is represented by the static build-boundary helpers `apps/web/app/utils/build/bundleScan.ts` and `apps/web/app/utils/build/manualChunks.ts`, which make heavy feature/dependency surfaces visible before pruning work removes or packages them. The optional manual follow-up remains `pnpm build`: use it as a human verification step for production bundle behavior when needed, but do not treat its absence here as contradicting the documented Architecture Closure closure.
### Portals/Sites DB Ownership
D006 records the DB ownership exception for Portals/Sites: do not move these persistence calls into `packages/db` as part of the current DB/Prisma ownership work. Until the feature/plugin decision determines whether Portals/Sites are extracted as optional plugins or removed from the default boilerplate, `apps/web/app/modules/portals/db/*` and `apps/web/app/modules/sites/db/sites.db.server.ts` remain the accepted temporary module-owned persistence boundaries. This is a documented deferral, not full R006 validation for Portals/Sites, and it is separate from the URL-boundary guidance below for internal tenant navigation versus public portal/site URLs.
## R003: clean replacement and removal rule
R003 is implemented as a pruning and boundary rule, not as a compatibility promise. Replaced or removed features should be either cleanly implemented in their target boundary or deleted from the active runtime; they should not be preserved through permanent legacy adapters, shadow call sites, or indefinite compatibility layers.
Temporary coexistence is acceptable only while a later migration task explicitly owns extraction and verification. Once a feature is marked as replaced or removed in the inventory/manifests, future implementation work should prefer direct deletion, direct replacement, or a short-lived migration step with clear removal criteria.
## UrlUtils migration inventory
This inventory tracks feature-specific URL construction that should move toward explicit `UrlUtils` helpers instead of legacy string concatenation or broad adapters. Each item records the current state, the first clean migration unit, and the scoped verification expected for that unit.
### Rows/Groups
Status: mixed `UrlUtils` adoption with concrete hard-path follow-ups.
- `apps/web/app/modules/rows/routes/Rows_New.server.ts` and `apps/web/app/modules/rows/routes/Rows_Edit.server.ts` already use `UrlUtils.currentTenantUrl(params, "404")` for tenant-scoped 404 redirects when autogenerated/system entity checks fail.
- Row create/edit flows delegate row destinations through `EntitiesApi.getNoCodeRoutes(...)`, `EntityHelper.getRoutes(...)`, route-relative links such as `entity.slug + "/new"`, and existing `currentEntityUrl`-covered route contracts rather than building every row URL inline.
- `apps/web/app/routes/app.$tenant/g.$group/index.tsx` builds group dashboard stat paths with `appPath(...)` and `adminPath(...)`; the old direct `/app/${tenantId}/...` and `/admin/...` string-concatenation wording is stale.
- `apps/web/app/routes/app.$tenant/g.$group/all.tsx` uses route-relative creation links and `EntitiesApi.getNoCodeRoutes(...)` for row list routing, so it is a lower-risk migration candidate than the group stat path builder.
- Rows create/edit actions accept `redirect` form/query parameters and pass them to `redirect(...)` when present. That trust boundary is intentionally only inventoried here; a later security-focused task should normalize same-origin/app-local redirects before changing behavior.
First atomic migration plan: keep `apps/web/app/routes/app.$tenant/g.$group/index.tsx` on `appPath(...)`/`adminPath(...)`, keep route-relative links in `all.tsx` unchanged, and extend the focused URL tests when new group dashboard destinations are introduced.
### Workflow Engine
Status: already aligned with direct tenant module URL rules.
- `apps/web/app/modules/workflowEngine/routes/workflow-engine.view.tsx` builds sidebar destinations through `UrlUtils.getModulePath(params, "workflow-engine...")`, matching the current direct tenant module rule for Workflow Engine sections such as workflows, variables, credentials, executions, templates, and danger.
- The shell route files for Workflow Engine remain thin delegates into `apps/web/app/modules/workflowEngine/routes/**`; the module route file is the authoritative URL construction surface for the sidebar.
- No legacy adapter is needed for Workflow Engine under R003 because the current call sites already target the canonical `getModulePath` behavior instead of preserving parallel hard-coded `/app/${tenant}` module strings.
First atomic migration plan: keep `workflow-engine.view.tsx` on `UrlUtils.getModulePath(...)` and add regression coverage only when new Workflow Engine shell routes or sidebar entries are introduced. Candidate paths are `apps/web/app/modules/workflowEngine/routes/workflow-engine.view.tsx` and the shell route delegates that import from `apps/web/app/modules/workflowEngine/routes/**`; verification is the focused UrlUtils contract asserting `workflow-engine` and nested `workflow-engine/*` paths for tenant params.
### Forms/FormBuilder
Status: active as Form Widget runtime; legacy form/survey routes remain removed.
- Tenant form administration lives under the direct tenant routes `/:tenant/forms` and `/:tenant/forms/:id` via `apps/web/app/routes/app.$tenant/forms.tsx` and `apps/web/app/routes/app.$tenant/forms.$id.tsx`.
- The embeddable public runtime lives under `/embed/forms/:formKey` and the submit API uses `/api/embed/forms/:formKey/submit`; these are public/embed boundaries, not tenant module navigation paths.
- Removed legacy surfaces stay removed: public `/f`, `/surveys`, form-based Help Desk survey routes, and `settings/forms` routing must not be reintroduced by default.
### Portals
Status: mostly aligned with `UrlUtils.getModulePath(...)` for internal tenant navigation, with a separate public portal URL boundary.
- The authenticated tenant shell under `apps/web/app/routes/app.$tenant/__portals/**` is the internal navigation surface for Portals. The list route uses `UrlUtils.getModulePath(params, "portals/new")` for new-portal navigation, and the portal detail shell builds paths from the canonical portal subdomain (`portal.subdomain || params.portal`) for overview, pages, users, analytics, pricing, domains, settings, and danger sections.
- `apps/web/app/routes/app.$tenant/__portals/portals.tsx` also contains route-relative internal links such as `to="new"` and `to={item.subdomain}`. These remain React Router tenant-shell links today; they are inventory candidates for explicit contract coverage rather than immediate migration because the surrounding route already establishes the tenant module context.
- External portal URLs are built outside the tenant-app route contract. The list loader obtains public URLs through `PortalServer.getPortalUrl(item)`, and `apps/web/app/modules/portals/utils/PortalUtils.ts` exposes `getUrl(item, path)` as a public portal URL helper based on `item.url`.
- Public portal URLs and portal server/domain links are external navigation boundaries. They must not be normalized through `UrlUtils.getModulePath(...)`, which is reserved for internal tenant React Router destinations.
- Under R003, Portals do not need a legacy URL adapter: current internal shell navigation already uses canonical module-path helpers, while public portal URLs should keep a separate public URL contract.
First atomic migration plan: add Portals contract assertions to `apps/web/app/utils/app/UrlUtils.test.ts` for tenant module paths such as `portals`, `portals/new`, `portals/:portal/pages`, and `portals/:portal/settings`; keep `PortalServer.getPortalUrl(...)` and `PortalUtils.getUrl(...)` outside `UrlUtils.getModulePath(...)`; then consider replacing route-relative list/detail links only if a later task introduces a concrete helper for portal subdomain detail paths.
### Sites
Status: helper-built internal tenant paths with a separate external site preview/deployment URL surface.
- `apps/web/app/routes/app.$tenant/sites/$type.tsx` builds primary new-site, detail, and settings links through `appPath(...)` via a local `tenantSitePath(...)` wrapper.
- The same route also renders external site URLs from `buildSiteUrlFromFields(site.serverHost, site.serverResolution, site.siteId)`. Those `href={siteUrl}` links open deployed/public sites and must stay outside `UrlUtils.getModulePath(...)` because they are not internal React Router tenant destinations.
- `apps/web/app/routes/app.$tenant/sites/new.tsx` redirects newly created sites and renders the back link through `appPath(...)`.
- `apps/web/app/modules/sites/components/site-form.tsx` is a form and external preview surface: it slugifies `siteId`, posts create/update form state, and previews deployment URLs through `buildSiteUrlFromServer(...)`. This slice should not change its internal behavior or route paths; later URL work should treat tenant/site IDs as normalized inputs before they flow into internal UrlUtils helpers, while keeping deployment preview URLs on the Sites public URL helpers.
First atomic migration plan: keep `apps/web/app/routes/app.$tenant/sites/$type.tsx` and `apps/web/app/routes/app.$tenant/sites/new.tsx` on helper-built internal destinations; keep `site-form.tsx` preview/deployment URL construction unchanged and explicitly outside internal tenant-app URL helpers.
### First UrlUtils migration backlog
1. Rows/Groups contract candidate: keep `apps/web/app/routes/app.$tenant/g.$group/index.tsx` on `appPath(...)`/`adminPath(...)` and extend tests when new group dashboard destinations are introduced.
2. Forms contract candidate: keep tenant Forms redirects and overview links on helper-built module paths while leaving public form URLs and external redirects on a separate public-form/security contract.
3. Sites contract candidate: keep internal Sites links/redirects on `appPath(...)`; keep public and deployed site preview URLs outside internal tenant navigation helpers.
4. Workflow Engine contract candidate: keep `apps/web/app/modules/workflowEngine/routes/workflow-engine.view.tsx` on existing `UrlUtils.getModulePath(params, "workflow-engine...")` usage and extend only contract tests when new Workflow Engine sections are introduced.
5. Portals contract candidate: keep internal Portals shell navigation on `UrlUtils.getModulePath(...)`, keep `PortalServer.getPortalUrl(...)` and `PortalUtils.getUrl(...)` as public portal URL boundaries, and add/maintain contract assertions for portal module paths before replacing route-relative links.
## Next implementation inputs
Use these files as the concrete inputs for follow-up implementation and pruning work:
- `docs/feature-inventory.md` — human-readable feature inventory and keep/plugin/remove/status classification.
- `docs/feature-manifests/*.json` — machine-readable feature manifests for boundary ownership, pruning decisions and downstream task planning.
This status snapshot should be updated whenever a boundary changes from skeleton-only to runtime-wired, when root runtime ownership moves into an `apps/*` or `packages/*` workspace, when feature-specific UrlUtils migration status changes, or when R003-driven pruning changes the active feature surface.
@@ -0,0 +1,112 @@
# Bundle Budgets
**Status:** active
**Owner:** web platform
**Related:** [`scripts/bundle-budgets.json`](../../scripts/bundle-budgets.json), [`scripts/verify-bundle-size.mjs`](../../scripts/verify-bundle-size.mjs), `docs/improvement-plan/10-compliance-polish/bundle-budget.md`
## Why
Web bundle growth is silent unless we measure it. The boilerplate tracks two complementary signals:
1. **Group budgets** (`BUDGETS` constant in `verify-bundle-size.mjs`): coarse caps for `root` / `initial_payload_js` / `vendor` / `route` / `other` / `css` / `total_js` / `manifest_js`. Hard CI gate — failure exits 1.
2. **Per-route + per-vendor-chunk budgets** (`scripts/bundle-budgets.json`): fine-grained gzip limits per route and per vendor chunk. **Non-blocking** — surfaces as a PR-comment with a Δ-from-main column. Reviewers decide whether growth is justified.
The per-route layer was added in task #71 (Phase 10) to make day-to-day creep visible without blocking legitimate feature work.
`initial_payload_js` is a dedicated hard signal for the boot asset aggregate the current parser can identify without reading Rollup/Vite dependency graphs: `entry.client-*`, `root-*`, and `manifest-*`. It intentionally excludes route, vendor, and component chunks so lazy feature payloads cannot hide a shell regression in `total_js`.
## Config schema
`scripts/bundle-budgets.json` validated by `scripts/bundle-budgets.schema.json`:
```jsonc
{
"default": { "limitKb": 200, "warningKb": 180 },
"routes": {
"app/$tenant/dashboard": { "limitKb": 250, "warningKb": 225 },
"auth/magic-link": { "limitKb": 80, "warningKb": 75 }
},
"vendorChunks": {
"vendor-react": { "limitKb": 150, "warningKb": 135 }
}
}
```
- All sizes are **gzip kilobytes** (the transport-relevant number).
- `limitKb` is the hard cap; assets above this report `[EXCEED]` in CI output and the PR comment.
- `warningKb` is the soft threshold; between `warningKb` and `limitKb` assets report `[WARN]`.
- Routes use React Router v7 route ids in either dot- or slash-form (`auth/magic-link` and `auth.magic-link` both match).
- Unmatched routes fall back to `default`.
- Unmatched vendor chunks (anything starting with `vendor-` or `chunk-`) are reported under the chunk basename and also fall back to `default`.
## Local workflow
```bash
# Build apps/web normally and print the per-route table:
pnpm --filter @toolctrl/web build
node scripts/verify-bundle-size.mjs
# Only the per-route table (skips group checks):
node scripts/verify-bundle-size.mjs --per-route-only
# Machine-readable JSON (used by the PR-comment workflow):
node scripts/verify-bundle-size.mjs --json > pr-bundle-report.json
```
`--json` emits only the non-blocking per-route/per-vendor report and exits with
code 0 when the report was produced. Keep the hard group-budget gate on the
plain `node scripts/verify-bundle-size.mjs` path so redirected JSON never mixes
machine-readable output with human diagnostics.
### Visualizer (treemap)
To inspect why a chunk is big:
```bash
pnpm --filter @toolctrl/web run bundle:visualize
open apps/web/build/stats.html
```
The visualizer is gated on `ANALYZE_BUNDLE=true` so the regular production build keeps its current footprint. It uses `rollup-plugin-visualizer` (treemap, gzip + brotli sizes per chunk).
**Security note:** `stats.html` contains source-map references and should never be deployed publicly. Treat it as a developer-only artifact.
## PR-comment workflow
`.github/workflows/bundle-budget.yml` (and `.gitea/workflows/bundle-budget.yml`) run on every PR that touches `apps/web/**`, `packages/**`, or the budget config / verifier. They:
1. Build `@toolctrl/web`.
2. Run `verify-bundle-size.mjs --json`.
3. Download the latest `bundle-baseline-main` artifact (captured by `bundle-budget-baseline.yml` on `main` pushes).
4. Render a sticky markdown comment via `scripts/render-bundle-budget-comment.mjs`.
5. Upsert the comment using its `<!-- bundle-budget-report -->` marker (GitHub: `marocchino/sticky-pull-request-comment`; Gitea: `scripts/post-bundle-budget-gitea-comment.mjs`).
The comment renders rows in severity order (`[EXCEED]` → `[WARN]` → unbudgeted → ok), with columns: status / kind / route or chunk key / current gzip size / configured limit / Δ from main. Δ is `—` if no baseline is available.
### When the baseline is missing
The first PR after a fresh checkout (or after a baseline-artifact expiry) sees `Δ from main = —` for every row. This is expected — the next push to `main` re-captures the baseline.
## When to raise a limit
1. Confirm the growth is intentional (a new feature shipped, a dependency landed) — not a regression from accidental imports.
2. Open the PR; the bundle-budget comment shows the Δ. Reviewers and the PR author should agree the new floor is acceptable.
3. Edit `scripts/bundle-budgets.json` in the **same PR** that ships the growth. Bump `warningKb` to ≈ (current gzip + 5 KB) and `limitKb` to ≈ (current gzip + 20 KB).
4. Call the change out in the PR description so the new floor is reviewable.
## Anti-patterns
- **Don't disable a budget by raising it to 9999 KB.** The point is to surface drift. If a route is legitimately huge, raise the limit to a tight new floor, not infinity.
- **Don't lazy-load critical-path code.** Bundle splits help non-critical routes, but the initial payload (`entry.client-*` / `root-*` / `manifest-*`) still has its own `initial_payload_js` hard group budget that must stay tight.
- **Don't bundle-split shared deps.** Splitting `react` into many chunks just shifts cost into chunk boot-up. Keep `vendor-react`, `vendor-core-utils` etc. as single chunks; the manualChunks mapping in `apps/web/app/utils/build/manualChunks.ts` is the source of truth.
## Troubleshooting
- **PR comment never shows up:** check the `Bundle Budget` workflow run — the `Build web` step may have failed (e.g. server-only import in a client export). The comment job only runs if all upstream steps succeed.
- **`vendor-monaco-...` reports as `vendor-monaco-...-` with default budget:** rollup occasionally emits chunk hashes ending in `-`; the baseline normalizer skips that suffix and the asset falls back to `default`. Either rename via `manualChunks.ts` or add an explicit budget entry.
- **CI run is much slower than `pnpm typecheck`:** the workflow does a full `pnpm --filter @toolctrl/web build`. That's the expected cost. If it ever exceeds ~30 minutes, profile with `ANALYZE_BUNDLE=true` locally first.
## Rollback
- If the per-route budget surfaces too many false alarms, lower the warning thresholds in `scripts/bundle-budgets.json` and ship a follow-up.
- If the entire PR-comment flow is broken, set `if: false` on the `measure` job in `bundle-budget.yml` — no other CI stage depends on it.
+121
View File
@@ -0,0 +1,121 @@
# CSRF — Single-Read Helper Contract
This document covers the runtime API contract of `validateCSRFToken` in
`apps/web/app/utils/session.server.ts`. The complementary route-level
inventory of which surfaces are protected lives in
`docs/security/csrf-inventory.md` — read that file when adding a new
mutating route.
## Background — the body-already-consumed bug
Before the May 2026 fix, `validateCSRFToken(request)` always tried to
locate the request token by either reading the `x-csrf-token` header or,
falling back, by cloning the request and re-parsing the body:
```ts
// historical implementation (broken when caller already drained the body)
const formData = await request.clone().formData();
```
Cloning a request whose body was already consumed throws
`TypeError: unusable` in undici. UI form-submits where the action handler
read `await request.formData()` first and validated afterwards crashed
in production with a 500. The bug was first surfaced during the B-009
lighthouse-bot session-cookie capture, where the workaround was to send
the token via the `x-csrf-token` header to bypass the body path.
## API contract
```ts
export type ValidateCSRFTokenOptions = {
/** Already-parsed form data. When set, the helper NEVER reads the body. */
formData?: FormData;
/** Skip body fallback entirely. Header must carry the token. */
headerOnly?: boolean;
};
export async function validateCSRFToken(
request: Request,
options?: ValidateCSRFTokenOptions,
): Promise<void>;
```
### Resolution order
1. `x-csrf-token` / `csrf-token` request header.
2. If `headerOnly` is `true`, stop here.
3. `options.formData?.get("csrf")` — used when the caller has already
parsed the body.
4. Body fallback: clone-and-parse the request as `FormData`,
`application/json`, or `URLSearchParams`. Reaching this branch on a
request whose body has already been consumed surfaces a CSRF-specific
error (`CSRF body already consumed; pass { formData } to validateCSRFToken.`)
instead of the opaque `TypeError: unusable` from undici.
### Calling patterns
```ts
// Preferred: validate before reading the body. Header-first short-circuit
// keeps this path zero-cost on header-bearing requests.
export async function action({ request }: ActionFunctionArgs) {
await validateCSRFToken(request);
const form = await request.formData();
// ...
}
```
```ts
// Required when the caller parses the body first (e.g. routing based on a
// form action discriminator). Inject the parsed FormData — never let the
// helper attempt to re-read the consumed body.
export async function action({ request }: ActionFunctionArgs) {
const form = await request.clone().formData();
await validateCSRFToken(request, { formData: form });
// ...
}
```
```ts
// Helpers / cross-file flows that receive a request plus pre-parsed
// FormData from the route action must forward the FormData.
async function loginFromRequest(request: Request, form: FormData) {
await validateCSRFToken(request, { formData: form });
// ...
}
```
```ts
// Bot / lighthouse / capture contexts where the body is reserved for a
// different purpose. The token must be sent in the x-csrf-token header.
await validateCSRFToken(request, { headerOnly: true });
```
## Migrated callsites (May 2026 fix)
- `apps/web/app/root.tsx` — the locale/theme action pre-parses form data
for routing and now hands the FormData to `validateCSRFToken`.
- `apps/web/app/modules/users/services/AuthService.ts` —
`loginFromRequest` receives `FormData` from `routes/login.tsx` and
`root.tsx`, and forwards it.
Any new helper that accepts `(request, formData)` from a caller MUST
forward the FormData to `validateCSRFToken`. The convention check is
covered by `apps/web/app/utils/session.csrf-regression.test.ts`.
## Verification
```bash
pnpm exec vitest run \
apps/web/app/utils/session.server.test.ts \
apps/web/app/utils/session.csrf-regression.test.ts \
apps/web/app/utils/session-csrf-sweep.test.ts
```
The regression file specifically asserts:
- Calling the helper after `await request.formData()` succeeds when the
parsed body is injected.
- Calling the helper after the body was consumed without injection raises
a CSRF-specific error — never the undici `TypeError: unusable`.
- The header-token fast path still skips the body even when the body has
already been read.
@@ -0,0 +1,141 @@
# Email Templates (React-Email)
**Package:** `packages/mail/src/templates-react/`
**Status:** active (Phase 10 task #75)
**Replaces:** `packages/mail/src/templates/transactional.ts` (deprecated)
## Overview
Transactional emails are React components. Every template is rendered
deterministically to `{ html, text }` via `react-dom/server` and
delivered through the standard Mailjet/queue provider chain.
We chose React components over the legacy string-template approach
for three reasons:
1. **XSS-safety by default** — React escapes interpolated values, so
we cannot accidentally forget an `escapeHtml` call when a new
prop is introduced. The old monolithic file had a recurring
audit finding around manual escaping.
2. **Component reuse** — header, footer, button, info-box, and
eyebrow are shared across templates. Brand-theming threads
through a single `MailBrand` prop instead of token-replacement
on raw HTML strings.
3. **Live preview** — `pnpm --filter @toolctrl/mail preview` boots
a local HTTP server that renders every template with realistic
sample data so designers can iterate without triggering live
email sends.
## Adding a new template
1. **Create the component** in `packages/mail/src/templates-react/transactional/<TemplateName>.tsx`.
Accept a `brand: MailBrand` prop plus the data the template needs.
Compose with `TransactionalScaffold` for the standard layout.
Avoid `dangerouslySetInnerHTML` — it is banned by code review.
2. **Validate URLs** — every CTA must go through `<Button>`. The
component throws `InvalidActionUrlError` for anything other than
`https://` or `mailto:` URLs.
3. **Register the template** in
`packages/mail/src/templates-react/render.ts`:
- Add the name to `ReactTransactionalTemplateName`
- Add the prop type to `ReactTemplatePropsMap`
- Add the component to the `COMPONENTS` map
4. **Add a snapshot test** at
`packages/mail/src/templates-react/transactional/<TemplateName>.test.tsx`
covering: default render (HTML + text snapshots), tenant brand
override, XSS-safety on at least one user-supplied prop, and
URL-validation error if the template takes an `actionUrl`.
5. **Add a preview entry** in `packages/mail/scripts/previews.ts`
so the new template renders in `pnpm preview`.
## Brand theming
Templates accept a `brand: MailBrand` prop with the following fields:
| Field | Source | Notes |
| --- | --- | --- |
| `productName` | `@toolctrl/brand` `BrandTokens.productName` | Defaults to "ToolCTRL". |
| `logoUrl` | `BrandTokens.assets.logoUrl` | Optional. Falls back to wordmark. |
| `primaryColor` | `BrandTokens.mail.colors.primary` | CTA background. |
| `primaryTextColor` | `BrandTokens.mail.colors.primaryText` | CTA foreground. |
| `tenantName` | derived from tenant `displayName` | Used in greeting/footer copy. |
| `supportEmail` / `supportFromName` | `BrandTokens.support.*` | Footer + sign-off. |
| `privacyUrl` / `termsUrl` / `imprintUrl` | `BrandTokens.legal.*` | DSGVO/CAN-SPAM compliance. |
`withBrandDefaults(override?)` merges a partial override into the
platform defaults — call it once per render so missing tenant fields
do not blank out the design.
## Plain-text fallback
`renderTransactionalEmail(template, props)` returns both `html` and a
plain-text `text` variant. The text is produced from the rendered
HTML by `htmlToPlainText` in `render-helpers.ts` — anchor URLs are
preserved as `label (url)` so text-mode clients keep functional
CTAs.
Mailjet forwards both parts in a MIME-multipart envelope; clients
choose which to display.
## Testing
- **Snapshot tests** are mandatory. Each template owns a
`__snapshots__/<TemplateName>.test.tsx.snap` capturing both the
HTML and the text output. Reviewers compare snapshot diffs against
the design intent.
- **XSS-safety tests** verify that React's default escaping turns
`<script>` into `&lt;script&gt;` for at least one user-supplied
prop per template.
- **URL-validation tests** verify that `javascript:`, `http://`,
`data:`, and malformed URLs are rejected when passed to `<Button>`.
- Run `pnpm --filter @toolctrl/mail test` to execute the suite.
## Live preview workflow
```
pnpm --filter @toolctrl/mail preview
```
Then open http://localhost:3001 — the index page lists every
template and links to the rendered HTML plus a `?format=text` link
for the plain-text fallback.
Sample props live in `packages/mail/scripts/previews.ts`. Update
them when adding a template; never commit real PII.
## Render pipeline
`renderTransactionalEmail(template, props)` from `@toolctrl/mail` is
the canonical entry point. The pre-Stripe-19 string-template renderer
(`renderTransactionalMailTemplate`) was retired in the legacy-cleanup
pass; the host application's wrapper at
`apps/web/app/modules/emails/utils/EmailTemplates.ts` adapts the
legacy template-name surface onto the React-Email pipeline.
| Legacy name | React template | Notes |
| --- | --- | --- |
| `welcome` | `welcome` | direct migration |
| `password-reset` | `password-reset` | direct migration |
| `user-invitation` | `invitation` | renamed for consistency |
| `email-verification` | `magic-link` | merged into magic-link flow (30-min expiry variant) |
| `account-setup` | `welcome` | unified onboarding (overridable subject/eyebrow/title/intro/actionLabel) |
| — | `magic-link` | new |
| — | `subscription-success` / `subscription-failed` | new (Stripe webhooks) |
| — | `invoice-sent` | new (Stripe invoices) |
| — | `tenant-notification` | new (in-app digest) |
## Security notes
- React's default escaping is the primary defense — manual
`escapeHtml` calls are no longer required and any new
template that performs string concatenation in HTML attributes
will be rejected in code review.
- `<Button>` enforces an allow-list of URL schemes
(`https:`, `mailto:`) to prevent magic-link phishing via
`javascript:` or `data:` URLs.
- `dangerouslySetInnerHTML` is forbidden in
`packages/mail/src/templates-react/`. If you need rich text,
build a structured prop shape and render it as JSX.
- Brand logo URLs come from `packages/brand` which enforces an
https-only allow-list; the React templates never load
arbitrary tenant-supplied URLs.
@@ -0,0 +1,121 @@
# Job Priority
Plan-based BullMQ job priority. Lets enterprise tenants jump the queue
over free-tier batches and pins internal/system jobs to the top.
## Why
Before this feature every BullMQ job ran at the default priority. A
free-tier tenant scheduling 10 000 mail jobs would block an enterprise
tenant's password-reset behind them. Cron and retention jobs competed
with user workload. There was no way to express "this auth mail must
run now" semantically.
## Priority levels
| Constant | Value | Use case |
| ------------------------ | ----- | --------------------------------------------------------- |
| `JOB_PRIORITY_CRITICAL` | `1` | Internal jobs, auth flows (password-reset, magic-link) |
| `JOB_PRIORITY_HIGH` | `3` | System-triggered workflows that aren't auth |
| `JOB_PRIORITY_NORMAL` | `5` | Default for tenant-scoped jobs without a plan lookup |
| `JOB_PRIORITY_LOW` | `10` | Marketing / bulk batches; free-tier when a lookup is set |
BullMQ treats `priority: 1` as highest. We intentionally use only 4
distinct levels — Grafana dashboards stay legible and the priority
sorted-set in Redis stays small.
## Resolution order (in `enqueueJob`)
```
1. options.priority (explicit numeric) → wins everything
2. options.priorityOverride ('critical'…) → mapped via priorityFromOverride
3. options.tenantId === null → CRITICAL (internal job)
4. options.tenantId set, lookup registered → resolveJobPriorityForTenant
5. options.tenantId set, no lookup → NORMAL
6. options absent → no priority field set
```
## Plan → priority mapping
| Plan slug | Priority |
| ------------ | ------------------ |
| `enterprise` | CRITICAL (1) |
| `pro` | NORMAL (5) |
| `free` | LOW (10) |
| unknown/null | NORMAL (5) |
The mapping lives in `packages/queue/src/priority.ts` in
`priorityFromPlan(plan)`.
## Producer guidelines
1. **Always pass `tenantId`** for tenant-scoped jobs. The descriptor
already carries the tenantId in most cases; pass the same value to
the enqueue options:
```ts
await enqueueJob(queue, descriptor, { tenantId });
```
2. **Pass `tenantId: null` for internal jobs** — retention, cleanup,
audit-compaction, backup triggers. They get CRITICAL automatically.
3. **Use `priorityOverride: 'critical'` for auth flows** regardless of
plan: password-reset, email-verification, magic-link, user
invitation. A free-tier user must still receive their reset mail
instantly.
4. **Use `priorityOverride: 'low'` for marketing batches.** Don't let
newsletter batches starve transactional mail.
5. **Never set `priorityOverride: 'critical'` from untrusted input.**
The override hook is server-only.
## Registering a tenant-plan lookup
The queue package keeps its plan-lookup pluggable so workers don't
have to import apps/web. Web boots the lookup during cache bootstrap:
```ts
// apps/web/app/utils/services/.server/tenantPlanLookup.server.ts
setSubscriptionToPlanTier((subscription) => {
// your classifier here — e.g. map stripe product ids to plan slugs
return subscription?.products[0]?.subscriptionProductId === "prod_enterprise"
? "enterprise"
: subscription?.products.length
? "pro"
: "free";
});
wireTenantPlanLookup();
```
Until a classifier is wired, the resolver returns NORMAL for every
tenant (CRITICAL still applies to internal jobs).
## Feature-flag
`JOB_PRIORITY_ENABLED=false` neutralises plan-based priority — every
tenant-scoped enqueue receives NORMAL (5). Internal jobs still get
CRITICAL because `tenantId: null` short-circuits before the flag is
checked. Use this for emergency rollback without a code change.
## Monitoring
The Counter `queue_jobs_enqueued_total{queue, priority}` increments
on every enqueue. The `priority` label is one of
`critical|high|normal|low|other` (low-cardinality).
Useful Grafana queries:
```promql
# Enqueue rate by priority for a queue
sum by (priority) (rate(queue_jobs_enqueued_total{queue="toolctrl.email"}[5m]))
# Share of critical vs total
sum(rate(queue_jobs_enqueued_total{priority="critical"}[5m]))
/
sum(rate(queue_jobs_enqueued_total[5m]))
```
## Rollback
1. Set `JOB_PRIORITY_ENABLED=false` in env → next enqueue runs with
NORMAL.
2. To remove the feature entirely: revert the producer-call-site
commits; the resolver helper stays but goes unused.
3. In-flight jobs keep the priority assigned at enqueue time.
@@ -0,0 +1,190 @@
# Lighthouse-CI audit and Web-Vitals reporting
This document describes how the Lighthouse-CI audit is wired into the
Toolctrl Boilerplate, how to run it locally when debugging a regression,
and how the Web-Vitals → Sentry bridge surfaces real-user metrics.
Spec: `docs/improvement-plan/10-compliance-polish/lighthouse.md`
(Task #70). Companion artefacts: `.lighthouserc.staging.json`,
`.lighthouserc.json`, `scripts/lighthouse-puppeteer-auth.cjs`,
`scripts/rotate-lighthouse-session-cookie.mjs`,
`.github/workflows/lighthouse.yml`, `.gitea/workflows/lighthouse.yml`,
`apps/web/app/lib/observability/web-vitals.ts`,
`apps/web/app/lib/pwa/registerServiceWorker.ts`,
`apps/web/public/manifest.webmanifest`, `apps/web/public/sw.js`.
## Where the audit runs
A meaningful Lighthouse audit needs a live server, a database and seeded
content. Reproducing that whole stack inside a CI job is brittle, so the
CI workflow audits the **deployed staging site**
(`https://boiler.saas-ctrl.com`) rather than a CI-local server.
That makes it a **post-deploy / monitoring audit, not a per-commit merge
gate** — staging always lags the commit under review. The workflow
(`.gitea/workflows/lighthouse.yml`, mirrored in `.github/`) runs:
- **nightly** at 03:00 UTC (`schedule`), and
- **on demand** (`workflow_dispatch`).
A red run means the deployed staging site fell below a threshold — treat
it as a production-quality alarm, not a blocked PR.
The Gitea runner image (`node:20-bookworm`) ships no browser, so the
workflow installs `chromium` via `apt-get` and points LHCI at it with
`CHROME_PATH=/usr/bin/chromium`. The GitHub runner (`ubuntu-latest`)
already bundles Chrome.
## Audit targets
`.lighthouserc.staging.json` audits the public staging routes — the
surface that renders without authentication:
| URL |
| -------------------------------------------- |
| `https://boiler.saas-ctrl.com/login` |
| `https://boiler.saas-ctrl.com/privacy-policy` |
| `https://boiler.saas-ctrl.com/terms-and-conditions` |
Only **Performance** is a hard gate (`error`). The product is a B2B
SaaS, not a search-discoverable marketing site, so the SEO category is
not asserted at all. Accessibility and Best-Practices stay visible as
`warn` — accessibility is already hard-gated separately by `a11y.yml`
(axe-core plus the `jsx-a11y` ESLint baseline, task #73), so a second
blocking gate here would be redundant.
| Category | Threshold | Severity |
| --------------- | --------- | ------------------------- |
| Performance | 0.95 | error |
| Accessibility | 0.95 | warn |
| Best Practices | 0.95 | warn |
| PWA | 0.90 | warn |
| SEO | — | not asserted (irrelevant) |
Numeric audits (FCP / LCP / TBT / CLS) stay at `warn` severity so trend
regressions remain visible without failing the run.
## Session cookie rotation
The workflow mints a fresh staging session before every LHCI run via
`scripts/rotate-lighthouse-session-cookie.mjs`. The script logs in as the
dedicated `lighthouse-bot` account, extracts the CSRF token from
`/login`, posts the login form with the CSRF header, then appends only
the resulting session cookie to the Actions environment. It never prints
the cookie or password.
Required workflow secrets:
| Secret | Purpose |
| ------ | ------- |
| `LIGHTHOUSE_LOGIN_EMAIL` | Email of the staging Lighthouse bot user. |
| `LIGHTHOUSE_LOGIN_PASSWORD` | Password of the staging Lighthouse bot user. |
The session cookie name is `RJ_session`; workflows also pass
`LIGHTHOUSE_SESSION_COOKIE_NAME=RJ_session` and
`LIGHTHOUSE_COOKIE_DOMAIN=boiler.saas-ctrl.com`.
During rollout the workflow still forwards the older
`LIGHTHOUSE_SESSION_COOKIE` secret as a fallback. If the login email or
password secret is absent, the script reuses that pre-provisioned cookie
without printing it. Permanent operation should use the login secrets so
each run mints a fresh cookie.
## Authenticated routes
Auditing `/app/$tenant/*` and `/admin/*` needs that freshly minted
`RJ_session` cookie. `scripts/lighthouse-puppeteer-auth.cjs` injects the
cookie for protected URLs and is already wired into
`.lighthouserc.staging.json`. The current staging target list still
audits only public routes; add protected URLs once the exact tenant/admin
coverage list is chosen. Without a protected URL in the config, the
cookie step is a readiness/rotation guard rather than an auth coverage
expansion.
## Running Lighthouse locally
`.lighthouserc.json` is the local-iteration config (URLs on
`http://localhost:3000`); `pnpm verify:lighthouse` runs it via
`scripts/run-lighthouse.mjs`. Bring the app up first:
```sh
# Build apps/web (writes hashed assets the runtime SW expects to cache)
pnpm --filter @toolctrl/web build
# Serve the build at http://localhost:3000.
pnpm --filter @toolctrl/web start &
# Run the full LHCI flow against the local server
pnpm verify:lighthouse
```
To reproduce a CI staging finding locally, point LHCI at the staging
config instead — no local server needed, just a Chrome install:
```sh
pnpm dlx @lhci/cli@0.13.x autorun --config=./.lighthouserc.staging.json
```
The run prints the temporary-public-storage report URLs in its output;
share those when debugging a regression.
## PWA manifest + service-worker
`apps/web/public/manifest.webmanifest` describes the installable app.
`apps/web/public/sw.js` is a minimal cache-first / network-first service
worker registered from `root.tsx` via `registerServiceWorker`.
Cache strategy:
- `/assets/...` (hashed by React-Router build) → **cache-first**
- `/api/...`, `/app/...`, `/admin/...` → **network-first** with cache fallback
- everything else (marketing routes, brand assets) → **network-first**
The `CACHE_VERSION` constant inside `sw.js` is the invalidation lever —
bump it on any deploy that changes the asset graph in a backward-
incompatible way. Old caches are dropped automatically in the activate
handler.
To debug a stuck SW, open DevTools → Application → Service Workers and
click "Unregister". The next page load picks up the new `sw.js`.
## Web-Vitals → Sentry
`apps/web/app/lib/observability/web-vitals.ts` registers callbacks for
CLS, LCP, INP, TTFB, and FCP via the `web-vitals` library. Each metric is
captured as a Sentry message named `web-vitals.<metric>` with tags:
```text
web.vital.name = CLS | LCP | INP | TTFB | FCP
web.vital.rating = good | needs-improvement | poor
```
In the Sentry dashboard:
1. Filter to `message:"web-vitals.LCP"` to view a metric in isolation.
2. Group by `tags.web.vital.rating` to see the share of "poor" loads.
3. Group by `transaction` (Sentry React-Router integration) to compare
metrics across routes.
The helper is SSR-safe (bails on `typeof window === "undefined"`) and
silently no-ops when Sentry isn't initialised, so the same code runs in
dev, preview, staging, and production without extra gating.
## Troubleshooting CI runs
| Symptom | Likely cause / fix |
| -------------------------------------------------- | ----------------------------------------------------------------------------- |
| `Chrome installation not found` / `Healthcheck failed!` | The runner has no browser. The Gitea workflow installs `chromium`; confirm the install step ran and `CHROME_PATH` is set. |
| Cookie rotation step fails with missing env vars | Add `LIGHTHOUSE_LOGIN_EMAIL` and `LIGHTHOUSE_LOGIN_PASSWORD` as repo Actions secrets. |
| Audit reports the `/login` page for a protected URL | Cookie injection failed or the bot no longer belongs to the tenant/admin surface being audited. |
| Performance score < 0.95 only on first run | Cold cache; LHCI reruns automatically (`numberOfRuns=3`). |
| Audit times out | Lower `numberOfRuns` to 1 temporarily while iterating; restore afterwards. |
| All URLs fail to load | Staging is down or redeploying — check the `web` container on staging. |
## Updating thresholds
Permanent relaxation requires a `.lighthouserc.staging.json` commit plus
a paragraph in this document explaining the trade-off. Prefer switching
a category from `error` to `warn` over dropping it, so regressions stay
visible — SEO is the one deliberate exception (not relevant to a B2B
SaaS, so not asserted at all).
@@ -0,0 +1,270 @@
# Transactional Outbox Pattern
The outbox pattern keeps side-effects (mail sends, webhooks, audit logs,
workflow triggers) **transactionally consistent** with the database writes
that triggered them. Without it, a crash between `DB COMMIT` and "call
mailer" leaves the DB updated but the side-effect lost.
This document describes how to publish and consume outbox events in this
codebase. See `docs/improvement-plan/03-scaling-infrastructure/outbox-pattern.md`
for the broader design rationale.
## When to publish
Publish **inside** a Prisma `$transaction` block whenever a successful DB
write should fan out an asynchronous side-effect.
```ts
import { publishOutboxEvent } from "@toolctrl/db/outbox";
await prisma.$transaction(async (tx) => {
const subscription = await tx.subscription.update({
where: { id },
data: { status: "active" },
});
await publishOutboxEvent(tx, {
tenantId: subscription.tenantId,
type: "billing.subscription.updated",
payload: { subscriptionId: subscription.id, status: "active" },
dispatchKey: `stripe:${stripeEvent.id}`, // optional idempotency key
});
});
```
`publishOutboxEvent` is type-constrained to require a
`Prisma.TransactionClient` — calling it with a top-level `PrismaClient`
fails to type-check.
## Event-type naming convention
Events follow `<domain>.<entity>.<verb>`, lowercase, dot-separated. The
dispatcher routes by the first segment by default:
| Prefix | Target BullMQ queue |
| ------------------------ | ------------------------------ |
| `tenant.subscription.` | `outbox.notification` |
| `mail.` | `outbox.mail` |
| `webhook.` | `outbox.webhook` |
| `notification.` | `outbox.notification` |
| `audit.` | `outbox.audit` |
| `workflow.` | `outbox.workflow` |
| `search.index.` | `outbox.search-index` |
| `rag.index.` | `outbox.rag-index` |
| `ai.cost.` | `outbox.ai-cost` |
| `marketing.lead.` | `outbox.marketing` |
| `form.submission.` | `outbox.form-submission-route` |
| `billing.` | `outbox.notification` |
| `plugin.hook.` | `outbox.plugin-hook` |
| `plugin.capability-use.` | `outbox.plugin-capability-use` |
Add a routing rule before introducing a new prefix — events with no
matching rule go straight to FAILED with a Sentry alert. See
`apps/worker/src/outbox/event-router.ts`.
Workflow routing has an active receiver/subscriber path when the worker is
configured for the `general` queue group with the workflow execution settings.
`apps/worker/src/jobs/workflow-trigger.ts` parses `workflow.*` outbox jobs,
applies receiver-side idempotency, fans out to matching live event-trigger
workflows, starts queued manual `workflow.execution.requested` executions, and
resumes queued `workflow.execution.continue.requested` continuations through the
selected executor. HTTP mode uses the signed internal bridge; hybrid/native mode
can resume worker-native safe-core continuations when the only native preflight
gap is the current `waitForInput` boundary. Candidates that fail preflight are
skipped or recorded as rejected.
`apps/worker/src/jobs/workflow-trigger-store.ts` provides the worker-native
Prisma lookup/preflight port. Application events publish
`workflow.trigger.event` through `EventsService`. The route-backed row mutation
producers for `row.created`, `row.updated`, and `row.deleted` record the row
write, application event, and workflow outbox event in the same transaction
before post-commit notifications/webhook callouts run. Internal mutations
without a request context remain intentionally eventless unless the caller opts
into an event context. Workflow row blocks pass a `workflow` event context so
requestless `rowCreate`, `rowUpdate`, and `rowDelete` blocks still publish row
events without enabling external webhook callouts. A source-inventory test guards the current execution
boundary: manual and tenant API runs must publish
`workflow.execution.requested` instead of executing inline, non-stream
continuations must publish `workflow.execution.continue.requested`, only the
internal workflow bridge may execute
`type: "trigger"` directly, and the legacy `WorkflowEngine.trigger(...)`
facade must stay absent.
The worker-native block contract is intentionally explicit. `email` is the
first-class mail block, `httpRequest` is the outbound/webhook-style block, and
conditions are represented by `if` plus `switch`. There is no separate
`mail`, `webhook`, `notification`, `audit`, `delay`, or `schedule` block id in
the registry today; adding any of those requires updating
`packages/foundation/src/workflows.ts`, its capability matrix tests, workflow
UI i18n, and worker-native execution/preflight behavior together.
Operationally, the outbox dispatcher and workflow subscriber are separate from
mail delivery workers. Production-like Coolify stacks need a persistent
`worker-general` (`WORKER_QUEUE_GROUP=general`) for the dispatcher and
`outbox.workflow`, plus a persistent `worker-mail`
(`WORKER_QUEUE_GROUP=mail`) for `outbox.mail`, `outbox.webhook`,
`outbox.marketing`, and `toolctrl.email` work. A green outbox row does not prove
that both services exist or that the downstream BullMQ job finished.
### Non-durable exception: stream runs
Workflow stream starts remain an explicit exception to the worker-durable
execution boundary. The stream route starts through
`WorkflowsExecutionsService.stream(...)`, creates a running execution row, and
then calls `execute(...)` in the web process so the existing SSE/emitter
progress contract can deliver local run updates to the browser. Web restarts can
interrupt an in-flight stream run before the worker can resume it. Stream
continuations are already queued through `workflow.execution.continue.requested`;
moving stream starts to the worker requires a distributed progress channel or
SSE broker rather than a partial outbox migration.
The worker bridge has an opt-in real-Postgres regression test at
`apps/worker/src/jobs/workflow-trigger-e2e.test.ts`. It skips without a real
database URL and runs with `WORKFLOW_TRIGGER_E2E_DATABASE_URL`, falling back to
`OUTBOX_E2E_DATABASE_URL` or any non-stub `DATABASE_URL`. The test publishes a
workflow trigger outbox event, claims and routes it through the production
dispatcher seam, then executes a live safe-core workflow through the native
worker runner and verifies idempotent re-delivery. It also verifies the
`workflow.executed` plugin hook path by routing `plugin.hook.workflow.executed`
through `outbox.plugin-hook` fanout into `toolctrl.plugin-invoke`. A paired
failure-path fixture forces an unsupported native block, verifies terminal
`status: "error"` persistence, then routes `plugin.hook.workflow.failed`
through the same fanout boundary into `toolctrl.plugin-invoke`.
The canonical non-browser readiness gate for this path is
`pnpm run verify:workflow-outbox-native`. It composes the existing focused
worker, foundation, and source-inventory tests for `outbox.workflow` routing,
workflow-trigger subscriber bootstrap, native/hybrid executor selection,
credential-free native execution boundaries, worker health fail-loud settings,
and queued manual/API/continuation producers. It is intentionally not a live
drain probe; use the opt-in real-Postgres regression or the operator-gated E2E
drain readiness check when an environment-level worker/Redis/Postgres proof is
needed.
## Dispatcher
`apps/worker/src/outbox/dispatcher.ts` polls every 1 second (configurable
via `OUTBOX_DISPATCH_INTERVAL_MS`). Each tick:
1. **Atomic claim**: `UPDATE … SELECT … FOR UPDATE SKIP LOCKED RETURNING *`
moves up to `OUTBOX_DISPATCH_BATCH_SIZE` (default 100) PENDING rows to
PROCESSING with `lockedUntil = now() + 5 min`. Two dispatchers can run
concurrently without ever claiming the same event.
2. **Crash recovery**: rows stuck in PROCESSING past `lockedUntil` are
re-claimed automatically.
3. **Routing**: each event's `eventType` is mapped to a BullMQ queue via
the event router.
4. **Enqueue + status update**: claimed rows are processed with bounded
concurrency (`OUTBOX_DISPATCH_CONCURRENCY`, default 10) so a large
batch does not serialize every Redis enqueue and status update.
The default BullMQ enqueue path adds receiver-level retries with exponential
backoff before marking the outbox row `DONE`; transient subscriber failures
are retried by the target queue instead of being lost after dispatch.
It also uses a deterministic BullMQ `jobId` of `outbox:<eventId>` so a
dispatcher crash after enqueue but before the Postgres `DONE` update does
not create a second receiver job when the same outbox row is re-claimed.
- success → `status = DONE`, `processedAt = now()`
- failure → increment `retries`, `scheduledAt = now() + backoff(retries)`
with backoff `[1, 2, 4, 8, 16, 32]` minutes.
- retries > `OUTBOX_MAX_RETRIES` (default 6) → `status = FAILED` (dead-
letter).
`DONE` means the dispatcher successfully handed the event to the target BullMQ
queue and recorded that handoff in Postgres. It does not mean the subscriber
processed the job, the provider call succeeded, or a domain state machine
reached a terminal success state. For workflow events, operators must also
watch the `outbox.workflow` BullMQ job state and the associated
`WorkflowExecution` status. For mail/webhook events, also watch the
`worker-mail` subscriber queues and the `toolctrl.email` job state.
The dispatcher is enabled by `OUTBOX_DISPATCHER_ENABLED=true`. Defaults to
**off** in dev so a missing Redis URL does not crash the worker.
## Retention
`worker-general` registers a daily `cleanup.run` maintenance job named
`outbox-done-retention`. It deletes only rows with `status = DONE` and a
non-null `processedAt` older than `OUTBOX_DONE_RETENTION_HOURS` (default 720
hours, 30 days). The deletion is bounded by `OUTBOX_DONE_RETENTION_BATCH_SIZE`
(default 1000, clamped to 1..10000) and uses `FOR UPDATE SKIP LOCKED`, so large
first runs drain gradually without blocking the dispatcher.
FAILED and CANCELLED rows are intentionally retained for audit and incident
review. Deleting DONE rows releases their `dispatchKey` uniqueness after the
retention window; producers that require longer idempotency windows must use a
domain-level idempotency key as well.
## Receiver-side idempotency
BullMQ delivery is at-least-once: a receiver crash between
"job processed" and "ack" causes a redelivery. Wrap your handler in
`withOutboxIdempotency` so duplicate deliveries are a no-op.
```ts
import { withOutboxIdempotency } from "@toolctrl/db/outbox-receiver";
import { createIdempotencyStore } from "@toolctrl/shared-store";
const idempotencyStore = createIdempotencyStore({
store: sharedStore,
prefix: "outbox",
ttlSeconds: 60 * 60 * 24, // 24h — longer than the longest retry window
});
await withOutboxIdempotency(idempotencyStore, job.data.outboxEventId, async () => {
await sendMail(job.data.payload);
});
```
`claim()` does an atomic `SET NX EX`. If the claim fails (another worker
already processed the event), the handler is skipped and the cached
result returned.
## Operational
### Pending count alert
`SELECT count(*) FROM "OutboxEvent" WHERE status = 'PENDING' AND scheduledAt < now() - interval '60 seconds'`
Alert when > 1000 for > 5 minutes — that means the dispatcher is behind.
### Downstream queue alert
Outbox `DONE` counts are not sufficient for worker health. Alert separately on
target BullMQ queue depth, failed jobs, and stalled jobs for `outbox.workflow`,
`outbox.mail`, `outbox.webhook`, `outbox.marketing`, and `toolctrl.email`.
Workflow incidents must include the `WorkflowExecution` terminal status because
`workflow.execution.requested` can be handed to BullMQ successfully while the
execution later fails, stalls, or remains queued behind a missing bridge.
### Re-queueing failed events
Use `toolctrl-cli outbox requeue` after fixing the underlying receiver.
The CLI supports single-row and filtered bulk requeues, `--dry-run`, JSON
output, and guardrails that avoid replaying `PENDING` / `PROCESSING`
rows. Prefer it over raw SQL so `processedAt`, `lastError`, retry state,
and replay warnings stay consistent.
Raw SQL is an emergency-only fallback for a DBA-led incident when the CLI
cannot run; record the exact statement and reason in the incident log.
### Environment
| Variable | Default | Purpose |
| ----------------------------- | ------- | ------------------------------------------------- |
| `OUTBOX_DISPATCHER_ENABLED` | `false` | Master switch for the dispatcher worker |
| `OUTBOX_DISPATCH_INTERVAL_MS` | `1000` | Polling cadence (ms) |
| `OUTBOX_DISPATCH_BATCH_SIZE` | `100` | Max events claimed per tick |
| `OUTBOX_DISPATCH_CONCURRENCY` | `10` | Max claimed events processed in parallel per tick |
| `OUTBOX_MAX_RETRIES` | `6` | Retries before dead-letter |
| `OUTBOX_DONE_RETENTION_HOURS` | `720` | DONE-row replay/debug retention window |
| `OUTBOX_DONE_RETENTION_BATCH_SIZE` | `1000` | Max DONE rows purged per maintenance run |
## Trade-offs
- **Latency**: ~1s polling tick. A LISTEN/NOTIFY variant could cut that
to <10ms; deferred until needed.
- **At-least-once delivery**: receivers MUST be idempotent
(`withOutboxIdempotency` is the cheap path).
- **Storage**: each successful event leaves a DONE row until the
`outbox-done-retention` job purges DONE rows older than
`OUTBOX_DONE_RETENTION_HOURS`. FAILED rows are kept for operator triage and
compliance review.
@@ -0,0 +1,196 @@
# Plugin Hooks & Lifecycle
**Phase 06 Task #22 foundation.** Builds on the WASM sandbox runtime from #21.
## What ships
This task delivers the _foundation_ for plugin host integration. Marketplace
browsing, hot-reload, per-instruction fuel, the full 15-20 hook-point catalogue
and broader marketplace workflows are deferred to follow-up work (`#23`
audit-docs + backlog/marketplace). Tenant plugin audit events are now exposed
through `apps/web/app/routes/app.$tenant/settings/plugins.$id.audit.tsx`.
### 1. Hook registry — `packages/plugin-runtime/src/hooks/registry.ts`
A central, immutable map of hook-name → zod payload schema. Foundation
hooks are pre-registered:
| Hook name | Outbox event type | When it fires |
| ------------------- | ------------------------------- | -------------------------------------------------------- |
| `tenant.created` | `plugin.hook.tenant.created` | `createTenant` row insert succeeded |
| `user.created` | `plugin.hook.user.created` | `register` user row insert succeeded |
| `entity.created` | `plugin.hook.entity.created` | `RowsApi.create` after row + log write |
| `entity.updated` | `plugin.hook.entity.updated` | `RowsApi.update` after row + log write |
| `entity.deleted` | `plugin.hook.entity.deleted` | `RowsApi.del` after hardDelete |
| `workflow.created` | `plugin.hook.workflow.created` | Tenant-scoped workflow definition is created through reviewed service/import paths |
| `workflow.updated` | `plugin.hook.workflow.updated` | Tenant-scoped workflow definition is updated through reviewed service paths |
| `workflow.deleted` | `plugin.hook.workflow.deleted` | Tenant-scoped workflow definition is deleted through reviewed service paths |
| `workflow.executed` | `plugin.hook.workflow.executed` | Tenant-scoped workflow execution reaches terminal status |
| `workflow.failed` | `plugin.hook.workflow.failed` | Tenant-scoped workflow execution reaches terminal error status |
Adding a new hook is a single `registerHook({ name, outboxEventType,
payloadSchema })` call at module load time. The registry validates that the
event type matches `plugin.hook.<entity>.<verb>` so the outbox dispatcher's
existing routing rules cover it without changes.
### 2. Hook emission — `packages/plugin-runtime/src/hooks/emit.ts`
`emitPluginHook(tx, def, payload, publish)` validates the payload and forwards
to the outbox publisher. The host-side wrapper
`apps/web/app/modules/plugins/services/.server/pluginHookPublisher.ts` owns a
one-statement Prisma transaction so callers stay a single line:
```ts
await emitPluginHookFireAndForget(PLUGIN_HOOK_TENANT_CREATED, {...}, {
tenantId,
dispatchKey: `tenant:${id}:created`,
});
```
The wrapper is intentionally **non-fatal to the surrounding business write**:
hook delivery failures are logged but never propagate into tenant signup, row
CRUD, or workflow service calls. It still returns a promise, and reviewed
workflow definition producers await it so the one-shot outbox transaction is
attempted before the service returns. A malformed payload triggers
`PluginHookValidationError`, which is also swallowed and logged.
### 3. Plugin lifecycle service — `apps/web/app/modules/plugins/services/.server/PluginLifecycleService.ts`
`installPlugin`, `enablePlugin`, `disablePlugin`, `listPlugins`. Each mutation
writes a hash-chained audit-event row through the existing
`createAuditEvent` helper. The service is permission-guarded at the route
layer: read-only list/settings views require `app.plugins.use`; capability-use
audit trails require `app.settings.auditTrails.view`; install, capability grant,
enable, and disable require `app.plugins.manage`.
### 4. Tenant admin UI — `apps/web/app/routes/app.$tenant/settings/plugins.tsx`
A minimal settings page that lists installed plugins with their capabilities,
manifest preview, and an enable/disable toggle. CSRF-validated form
submissions go through the lifecycle service.
### 5. Plugin migration applier — `packages/plugin-runtime/src/migrations.ts`
`applyPluginMigration({ pluginId, sql }, deps)` parses the SQL, enforces
`plugin_<sanitized-pluginId>_` table prefix, requires a `tenantId` column on
every CREATE TABLE, and rejects anything outside the `CREATE TABLE / CREATE
INDEX` allowlist. Forbidden tokens (GRANT, DROP DATABASE, COPY, pg\_\*, etc.)
are caught at validation time so the host never runs them.
### 6. Crash tracker — `packages/plugin-runtime/src/crash-tracker.ts`
SharedStore-backed sliding window per `(pluginId, tenantId)`. Failures
increment the counter (TTL = configurable window, default 10 minutes).
Successes decrement (floored at 0). Once the threshold (default 10) is
crossed the tracker calls the injected `disablePlugin` seam, calls the
optional `notify` seam, and resets the counter so a re-enable starts
fresh. Wired into `apps/worker/src/jobs/plugin-invoke.ts` via an optional
`recordOutcomeForCrashTracking` seam; `plugin_not_found` / `plugin_disabled`
outcomes are deliberately skipped so configuration glitches don't trigger
auto-disable.
### 7. Workflow definition hooks
`apps/web/app/modules/workflowEngine/services/WorkflowsService.ts` emits
metadata-only `workflow.created`, `workflow.updated`, and `workflow.deleted`
hooks for tenant-scoped service-path definition writes and reviewed template
imports. The reviewed create/update/delete/import paths await the publisher
attempt after the workflow write succeeds; publisher failures are recorded by
the hook publisher logger and remain non-fatal to the workflow mutation.
Platform-scoped definitions and writes that bypass those reviewed paths remain
outside current producer coverage.
### 8. Workflow execution hook — `workflow.executed`
`apps/web/app/modules/workflowEngine/services/WorkflowsExecutionsService.ts`
emits `workflow.executed` after `updateWorkflowExecution` returns a terminal
tenant-scoped `success` or `error` status. The payload intentionally carries
only execution metadata (`tenantId`, `workflowId`, `workflowExecutionId`,
`type`, `status`, `durationMs`, `errorCode`, `appliesToAllTenants`,
`occurredAt`). `errorCode` is null for `success` and required for `error`. Raw
error strings, workflow input, and workflow output bodies are not included
because they can contain provider responses, URLs, or secret-like values.
Failed executions also emit `workflow.failed` with the same metadata-only shape
but a fixed `status: "error"` and required `errorCode`. The separate failure
hook lets plugins subscribe only to failure automation while existing
`workflow.executed` subscribers continue to receive the full terminal-status
stream.
Current producer coverage for workflow lifecycle hooks:
| Lifecycle event | Hook(s) | Producer coverage | Payload boundary |
| --------------- | ------- | ----------------- | ---------------- |
| Tenant-scoped definition create | `workflow.created` | `WorkflowsService.create` and reviewed tenant template imports | Metadata only; no blocks, credentials, or raw input |
| Tenant-scoped definition update | `workflow.updated` | `WorkflowsService.update` | Metadata plus changed field names only |
| Tenant-scoped definition delete | `workflow.deleted` | `WorkflowsService.del` | Metadata only |
| Tenant-scoped web/API/manual execution success | `workflow.executed` | `WorkflowsExecutionsService` | Terminal metadata, `errorCode: null` |
| Tenant-scoped web/API/manual execution error | `workflow.executed`, `workflow.failed` | `WorkflowsExecutionsService` | Terminal metadata, stable `errorCode`; no raw error |
| Worker-native trigger execution | `workflow.executed`, `workflow.failed` | `apps/worker/src/jobs/workflow-trigger-native-executor.ts` | Terminal metadata only; failures use the stable `workflow_execution_failed` code |
### 9. Worker-side hook fanout — `apps/worker/src/jobs/plugin-hook-fanout.ts`
The outbox dispatcher routes `plugin.hook.*` events to
`outbox.plugin-hook` on the `general` worker group. The fanout consumer
loads enabled plugins for the tenant, filters manifests that declare the
hook, and enqueues one idempotent `plugin.invoke` job per subscriber on
the isolated `toolctrl.plugin-invoke` queue. Plugin execution itself stays
on `WORKER_QUEUE_GROUP=plugin`.
### 10. Per-user enablement — `prisma/schema/plugins.prisma`
Adds `PluginUserConfig { id, pluginId, userId, enabled, configJson }` with
unique `(pluginId, userId)` and FK cascade on Plugin delete. User-scoped hook
fanout loads `PluginUserConfig.enabled` when the validated hook payload includes
`userId`; `enabled === false` skips that user's plugin invocation while
tenant-default subscribers remain eligible.
### 11. Operator CLI — `packages/cli/src/commands/plugins/*`
```
toolctrl-cli plugins list [--tenantId X] [--enabled true|false]
toolctrl-cli plugins enable --tenantId X --pluginId com.acme.slack
toolctrl-cli plugins disable --tenantId X --pluginId com.acme.slack
toolctrl-cli plugins uninstall --tenantId X --pluginId com.acme.slack
```
## Remaining Slices
Current status and operator-facing caveats live in `docs/features/plugins.md`
and `docs/operations/plugin-lifecycle.md`. The historical implementation plan
in `docs/improvement-plan/06-plugin-system/hooks-lifecycle.md` remains useful
context, but it is not the current source of truth.
- The remaining hook points (`onWebhookOutbound`, `onEmailSend`,
`afterAuthLogin`, billing, integration-connected,
api-key-created, chatbot-message, export-run, etc.).
- Synchronous output hooks (`emitHookSync` with a 500ms wall-clock budget
for `transform` / `cancel` capabilities).
- Dedicated marketplace/install dialog UX. The operator-side install path runs
through manifest verification, rejects unknown hook names, and tenant
settings already approve missing manifest-declared grants via checkbox plus a
server-verified digest.
- Plugin secret storage and encrypted host bindings remain pending. The
reusable settings iframe shell already enforces opaque-origin sandboxing,
CSP, and a versioned postMessage protocol; manifest `ui.settings.entry`
metadata is routed through the server-side bundle loader, and non-secret
user config persists through `PluginUserConfig.configJson`.
- Marketplace browsing + plugin signing-key rotation UI.
- Hot-reload + per-instruction fuel limit.
- Global (cross-tenant) crash counter for operator-level auto-disable.
## Operational notes
- `PLUGIN_AUTO_DISABLE_THRESHOLD` (default 10) and
`PLUGIN_AUTO_DISABLE_WINDOW_MS` (default 600_000) are read by the
worker bootstrap when constructing the crash tracker.
- The outbox dispatcher routes `plugin.hook.*` events to
`outbox.plugin-hook`; that consumer fans out to `toolctrl.plugin-invoke`.
Keep the general worker and the separate plugin worker running in
production-like topology so health checks, queue wiring, and isolation are
exercised. Before external runtime signoff, the production `worker-plugin`
should remain idle/gated unless `PLUGIN_RUNTIME_PRODUCTION_ENABLED=true` is
intentionally enabled.
- Audit events use action codes `plugin.installed`, `plugin.enabled`,
`plugin.disabled` and `plugin.crash.auto_disabled` (the last fired by
the crash-tracker notifier).
@@ -0,0 +1,129 @@
# Plugin system (Phase 06 #21 foundation)
> **Runtime gate:** This is staging/foundation runtime documentation, not
> production authorization. Production third-party plugin execution remains
> blocked until the external WASM sandbox review is signed off in
> [Plugin Runtime Signoff](../security/plugin-runtime-signoff.md).
This document describes the WASM-sandbox foundation shipped under Phase 06 Task #21. It captures the runtime choice, manifest schema, signature trust model, capability gating, rate-limit, audit handling, and the work that is intentionally deferred to follow-up tasks.
## Scope
What the foundation ships:
- **`@toolctrl/plugin-runtime`** — host-side runtime:
- Manifest schema (Zod) with closed-enum capabilities + reverse-DNS plugin IDs + SemVer versions + hex-encoded Ed25519 signature fields.
- Ed25519 signature verifier against the operator-controlled trusted-keys allowlist (`PLUGIN_TRUSTED_PUBLIC_KEYS` CSV).
- WebAssembly sandbox wrapper (`createPluginSandbox`) with deterministic memory cap + host-fn fuel budget, invoked through a killable worker-thread boundary with a wall-clock budget.
- Capability gateway (`createCapabilityGateway`) that wraps host-fn maps and rejects undeclared capability calls.
- **`@toolctrl/plugin-sdk`** — TS types plugin authors build against (`HookHandler`, `HostApi`, `PluginContext`, `defineHook`) and a `hello-world` example.
- **`Plugin` Prisma model + migration** — one row per installed plugin per tenant.
- **`plugin` worker queue group** with the `toolctrl.plugin-invoke` BullMQ queue.
- **`handlePluginInvokeJob`** worker handler (`apps/worker/src/jobs/plugin-invoke.ts`) that wires Plugin lookup → binary load → signature verify → rate-limit → sandbox invoke → audit.
What is **deferred** (tracked in `docs/improvement-plan/06-plugin-system/wasm-sandbox.md` under `## Deviations from Plan` once that file is updated by the executing agent):
- Hot-reload — operators restart the `worker-plugin` pod to pick up new binaries.
- Marketplace integration — backlog (#24).
- CLI build/sign/publish is shipped; see [Plugin CLI Reference](../plugins/cli-reference.md).
- Full guided hook-lifecycle UI (#22) — install/guided consent dialogs remain
deferred; tenant settings now supports list-level approval for missing
manifest-declared capability grants with explicit review confirmation and
stale-manifest digest checks.
- Audit + docs deep-dive (#23) — author docs and the capability matrix have
landed; remaining gaps are external audit sign-off, richer toolchain notes,
and high-risk host bindings beyond the current fail-closed surface.
- E2E testcontainer harness — staging deployment + manual smoke tests in the meantime.
- Worker-thread pool with epoch interruption — the current worker bootstrap uses one killable worker thread per invocation; a pooled runtime and true per-instruction interruption remain deferred.
Operational lifecycle status, worker topology, and current production gaps are maintained in [Plugin Lifecycle Operations](../operations/plugin-lifecycle.md). Product-facing status and user/admin behavior are maintained in [Plugins](../features/plugins.md).
## Runtime choice
**Default: Node 22's built-in `WebAssembly` (V8) with host-fn-driven fuel charging.**
Rationale: native Wasmtime bindings for Node (`@bytecodealliance/jco`, `@bytecodealliance/preview2-shim`) are still pre-1.0 and add a ~40 MB build asset. For the foundation we ship a portable wrapper around Node's built-in `WebAssembly` runtime — sufficient for hard memory limits via `WebAssembly.Memory { maximum }`, a host-fn fuel budget on capability calls, and a worker-thread wall-clock kill for non-yielding code.
**Upgrade path:** `PluginSandbox` is an abstract surface — `invoke`, `close`, `fuelRemaining`. A Wasmer or Wasmtime-jco backend can be dropped in behind the same contract once either offers a stable Component-Model story on Node. This is the documented escape hatch when richer per-instruction fuel becomes a requirement.
## Manifest schema
A plugin ships a `plugin.toml` (or `plugin.json`) alongside the `.wasm` binary. The runtime parses it via `parsePluginManifest`, which returns a discriminated `Result`:
```ts
{
schemaVersion: 1,
id: "de.toolctrl.hello", // reverse-DNS, lowercase
version: "1.0.0", // SemVer
name: "Hello",
entry: "plugin.wasm",
wasmSha256: "<64 hex chars>", // SHA-256 of the .wasm at signing
signature: "<128 hex chars>", // Ed25519 sig over the .wasm bytes
signedBy: "<64 hex chars>", // Ed25519 public key (32 bytes)
allowedHosts: ["api.example.com"], // outbound HTTP allowlist
capabilities: [
{ name: "log" },
{ name: "http.outbound", scope: { allowedHosts: ["api.example.com"] } },
],
hooks: [{ name: "onHello", export: "handleHello" }],
}
```
The schema is `strict()` — unknown top-level fields are rejected. Unknown capability names are rejected against the closed enum in `packages/plugin-runtime/src/capabilities.ts`.
## Signature trust model
- Ed25519 chosen for deterministic signatures, audited Node `libcrypto` implementation, and short 64-byte signatures.
- Operators seed `PLUGIN_TRUSTED_PUBLIC_KEYS` (CSV of hex-encoded 32-byte public keys). Plugins signed by any key outside this list are rejected with `public_key_not_trusted`.
- `verifyPluginSignature`:
1. SHA-256-hashes the `.wasm` bytes and compares against `manifest.wasmSha256` — fail-closed on mismatch.
2. Confirms `manifest.signedBy` is in the trusted-keys allowlist — fail-closed otherwise.
3. Verifies `manifest.signature` over the `.wasm` bytes using `signedBy` — fail-closed otherwise.
- All three checks must pass; the function returns a discriminated reason on failure and never throws.
## Capability gating
The capability gateway wraps a host-fn record so that calls to capabilities the plugin did not declare in its manifest, or that the tenant did not approve for this invocation, throw `CapabilityDeniedError`. The default fuel cost is 100 units per host-fn call; configurable per gateway. Deny paths (`not_declared_in_manifest`, `not_granted_for_tenant`, `no_host_implementation`) fire the `onDenied` callback so the worker can audit them. The plugin worker currently exposes safe synchronous `env.toolctrl_log(i32)` and `env.toolctrl_audit_record(i32)` imports through `invokeSync(...)`; higher-risk host functions remain absent and fail closed.
## Rate limit
The worker handler consumes a per-tenant + per-plugin sliding-window rate limit before invoking the sandbox. The limit's parameters (window size, max calls) are owned by the worker bootstrap and read from env. The handler's `consumeRateLimit` seam returns `{ allowed, remaining }`; the handler short-circuits to `rate_limit_exceeded` on a deny.
## Audit
`handlePluginInvokeJob` always writes an audit row, even on failure. Invocation audit persistence is fail-closed: if the audit write fails, the handler logs `plugin.invoke.audit_failed` and throws so BullMQ can retry/dead-letter instead of acknowledging a plugin invocation with no forensic trail. The audit row carries the tuple `(tenantId, pluginId, version, hookName, outcome)`.
Capability-use telemetry is batched through `PluginCapabilityUseRecorder`. The production plugin worker enables the recorder's fail-closed mode; if the outbox sink is unavailable, the sandbox invocation fails and the plugin invocation is recorded as failed. Local/dev callers can keep the default soft mode when they need best-effort telemetry only.
## Deferred env vars
The full env-var matrix in `docs/improvement-plan/06-plugin-system/wasm-sandbox.md` is broader than the current worker bootstrap. The foundation currently wires:
| Env var | Default | Used by |
|---|---|---|
| `PLUGIN_TRUSTED_PUBLIC_KEYS` | (empty → fail-closed) | `parseTrustedPublicKeys` |
| `PLUGIN_MAX_FUEL` | 1000000 | sandbox fuel limit |
| `PLUGIN_MAX_MEMORY_MB` | 64 | sandbox memory limit |
| `PLUGIN_MAX_WALL_CLOCK_MS` | 5000 | worker-thread sandbox wall-clock kill |
| `PLUGIN_OUTPUT_SANITIZATION_STRICTNESS` | moderate | plugin output redaction strictness |
| `PLUGIN_BINARY_BASE_PATH` | `/app/var/plugins` | plugin binary lookup root for the worker bootstrap |
| `PLUGIN_RATE_LIMIT_PER_MINUTE` | 100 | worker sliding-window rate limit |
| `PLUGIN_RATE_LIMIT_WINDOW_SECONDS` | 60 | worker sliding-window rate limit |
| `PLUGIN_AUTO_DISABLE_THRESHOLD` | 10 | per-tenant/plugin crash auto-disable threshold |
| `PLUGIN_AUTO_DISABLE_WINDOW_MS` | 600000 | auto-disable sliding window |
| `PLUGIN_AUDIT_RETENTION_DAYS` | 90 | `PluginCapabilityUse` retention window |
| `PLUGIN_AUDIT_RETENTION_BATCH_SIZE` | 1000 | audit-retention cleanup batch size |
| `PLUGIN_AUDIT_RETENTION_MAX_BATCHES` | 100 | audit-retention cleanup max batches per run |
| `PLUGIN_AUDIT_RETENTION_CRON` | `0 3 * * *` | audit-retention scheduler pattern |
| `PLUGIN_INSTALL_RATE_LIMIT_PER_MIN` | 5 | tenant plugin-install route rate limit |
| `PLUGIN_SETTINGS_WRITE_RATE_LIMIT_PER_MIN` | 60 | per-user plugin settings write route rate limit |
| `PLUGIN_SETTINGS_FRAME_VIOLATION_RATE_LIMIT_PER_MIN` | 120 | client-reported plugin settings iframe violation route rate limit |
| `PLUGIN_MAX_INSTALLS_PER_TENANT` | 50 | tenant plugin-install cap |
| `WORKER_CONCURRENCY_PLUGIN` | 5 | `group-runtime.ts` |
| `WORKER_QUEUE_GROUP=plugin` | — | worker pod entry-point |
Richer host-binding controls remain runtime follow-up work. Do not treat undocumented knobs as production-enforced until the worker bootstrap reads them; the operator-facing status is tracked in [Plugin Lifecycle Operations](../operations/plugin-lifecycle.md).
## Why this is "foundation"
The full spec describes a 4–5 week effort covering 17 implementation steps and dual-track SDKs (AssemblyScript + Rust), per-instruction fuel via Wasmtime, multi-worker-thread pools, hot-reload, marketplace, signing-key rotation. The foundation shipped here is the **structural skeleton plus minimum-viable WASM execution** — every later step is the same `PluginSandbox` / `CapabilityGateway` / `handlePluginInvokeJob` contract with a richer implementation behind it.
@@ -0,0 +1,201 @@
# Read / Write Split (Prisma `$extends`)
> Phase 03 task #41. Status: code-complete; replica provisioning is operator-owned.
## TL;DR
```ts
import { db, dbRead } from "~/utils/db.server";
// Default — primary (write target). Use for all writes, auth, read-after-write.
await db.user.update({ where: { id }, data: { lastSeenAt: new Date() } });
// Explicit primary — same client, communicates intent.
import { getWriteDbClient } from "@toolctrl/db/client.server";
// Replica — eventually consistent. Use for listings, dashboards, public reads.
const users = await dbRead.user.findMany({ where: { tenantId } });
```
`dbRead` is wired to `DATABASE_READ_URL` when set. When unset (single-DB dev,
self-hosted setups) it returns the same client as `db`, so the code path is
identical and lag = 0.
## Convention: when to use which
| Use `dbRead` (replica) | Use `db` / `getWriteDbClient()` (primary) |
| ---------------------- | ----------------------------------------- |
| List endpoints / paginated grids | All mutations (`create`, `update`, `delete`, `upsert`) |
| Public marketing pages (sites, portals) | Auth-resolution paths (session → user → tenant) |
| Knowledge-base public reads + search | Stripe webhook handlers reading state they just wrote |
| Analytics dashboards & aggregations | Outbox dispatcher reading `PENDING` events (see #42) |
| Admin tenant / role enumerations (post-auth) | Anything inside a `db.$transaction(...)` callback |
| Dashboard counts (`count()` / `groupBy()`) | Idempotency-key lookups (request-scoped consistency) |
| Search-result rendering | Read-after-write inside the same request |
When in doubt: leave on `db` (= primary). The replica is an optimisation,
not a correctness requirement. **Auto-routing is intentionally not
implemented** — the spec rejects it because it makes read-after-write
hazards invisible.
## Read-after-write hazard
Streaming replication is **eventually consistent**:
- Typical lag: < 1 s
- Burst writes (bulk imports, k6 load tests): up to 5–10 s
- A query issued on the replica milliseconds after the write hits the
primary will read the **pre-write** state.
**Mitigation:**
1. Always use `db` inside transactions and for writes.
2. Never re-read on `dbRead` what you just wrote on `db` in the same
request. If you must, route the read through `db` too.
3. Auth-critical paths (`getUserAndTenants`, session resolution) stay on
`db` even though they're "reads" — they gate the rest of the
request, and a stale read can mis-route a tenant boundary check.
## Fallback semantics
`getReadDbClient()`:
```ts
export function getReadDbClient(): DbClient {
const readUrl = process.env.DATABASE_READ_URL;
if (!readUrl || readUrl.length === 0) {
return getDbClient(); // single instance, no double connection
}
// memoised replica singleton
if (!global.__dbRead) {
global.__dbRead = createDbClient({ connectionString: readUrl });
}
return global.__dbRead;
}
```
| State of `DATABASE_READ_URL` | `dbRead` returns |
| ---------------------------- | --------------------------------- |
| unset / empty | the write client (`db === dbRead`) |
| valid `postgres://...` | a distinct read-only client |
This means CI, local dev, and any deploy without a replica continue to
work without code changes. All migrations are zero-risk to roll out.
## Telemetry
`@toolctrl/db/telemetry.server` exposes `attachPrismaTelemetry()` — a
`$extends({ query })` wrapper that records every operation with
`{ client: 'read' | 'write', model, operation, durationMs, ok }`. Wire it
in the app bootstrap by adapting the recorder to your metrics sink
(prom-client `Histogram` / `Counter` or an OpenTelemetry `Meter`). The
canonical counter shape is `prisma.queries.total{client,model,operation}`.
## Coolify / staging setup (operator-owned)
Replica provisioning is **out of scope** for the application repo — these
steps live with the platform operator.
1. **Primary Postgres** (Coolify service `postgres`). `postgresql.conf`:
```
wal_level = replica
max_wal_senders = 3
wal_keep_size = 256MB
```
Then:
```sql
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD '...';
CREATE USER read_user WITH ENCRYPTED PASSWORD '...';
GRANT CONNECT ON DATABASE toolctrl TO read_user;
GRANT USAGE ON SCHEMA public TO read_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO read_user;
```
2. **Replica Postgres** (Coolify service `postgres-replica`,
`postgres:16-alpine`):
```bash
# Init from primary
until pg_basebackup -h postgres -D /var/lib/postgresql/data -U replicator -P -R; do
sleep 5
done
```
`postgresql.replica.conf`:
```
hot_standby = on
primary_conninfo = 'host=postgres port=5432 user=replicator password=...'
```
3. **App env**:
```sh
DATABASE_URL=postgres://app_user:...@pgbouncer:6432/toolctrl?pgbouncer=true&connection_limit=10
DATABASE_READ_URL=postgres://read_user:...@postgres-replica:5432/toolctrl?connection_limit=10
```
Optionally front the replica with its own PgBouncer for connection pooling.
4. **Verification:**
```sql
-- On primary
SELECT * FROM pg_stat_replication;
-- On replica
SELECT pg_last_xact_replay_timestamp(), now() - pg_last_xact_replay_timestamp() AS lag;
```
Grafana panel `prisma.queries.total{client="read"}` should rise on
dashboard / KB / portal traffic.
## Rollback
- **Per-call-site:** `git revert <migration-commit>` flips that module
back to `db` (primary). No data change.
- **Global:** unset `DATABASE_READ_URL` and restart the app. `dbRead === db`,
all traffic hits the primary.
- **Replica down:** Prisma's read client will throw connection errors.
Either unset `DATABASE_READ_URL` (degraded performance, no errors) or
add a circuit-breaker (future work, out of scope here).
## Migration recipe (for future modules)
1. **Find candidate read-only functions** — pure `findMany`, `findUnique`,
`findFirst`, `count`, `groupBy`, `aggregate` with no surrounding write.
2. **Check the caller graph** — is this function called immediately
after a write that updates the same rows? If yes, leave on `db`.
3. **Replace `db.<model>.<op>` with `dbRead.<model>.<op>`** and add a
one-line comment explaining the choice.
4. **Add `dbRead` to the file's imports** alongside `db` (keep `db` for
the mutations still in the file).
5. **Run** `pnpm lint --max-warnings=0` and `pnpm typecheck`.
6. **Tests** stay green because of the fallback semantics — single-DB
CI doesn't see a difference.
7. **Commit per-module** (`03-scaling/rw-split: migrate <module> ...`)
so a single revert can roll back one module.
## What was migrated in #41
- `apps/web/app/modules/crm/services/CrmService.ts` — user enumeration
for sync display.
- `apps/web/app/modules/knowledgeBase/db/*.db.server.ts` — public KB
reads, listings, search, featured-article queries, dashboard counts.
- `apps/web/app/modules/analytics/db/analyticsUniqueVisitors.db.server.ts`
— `groupBy` aggregations.
- `apps/web/app/utils/db/tenants.db.server.ts` — admin tenant enumerations
(post-auth).
- `apps/web/app/modules/sites/db/sites.db.server.ts` — read-only site
loaders (auth-gating `getSiteByTypeAndSiteId` left on primary).
- `apps/web/app/modules/portals/db/portals.db.server.ts` and
`portalPages.db.server.ts` — listings, page lookups, dashboard counts.
`appData.ts` was **not** migrated wholesale; only the secondary tenant
listings it consumes were moved. The auth-resolution path stays on
primary by design.
@@ -0,0 +1,181 @@
# Storybook
A single Storybook instance under `apps/storybook/` aggregates component
stories from the boilerplate UI packages.
## Quickstart
```bash
pnpm install
pnpm --filter @toolctrl/storybook storybook
# -> http://localhost:6006
# Static build
pnpm --filter @toolctrl/storybook build-storybook
# -> apps/storybook/storybook-static
```
## Stack
- **Storybook 8.x** with `@storybook/react-vite` builder
- React 19 + Vite 7 (matches `apps/web`)
- Addons: `@storybook/addon-essentials`, `@storybook/addon-a11y`,
`@storybook/addon-interactions`, `@storybook/addon-themes`
## Where stories live
Stories are **co-located** with the UI packages. The `main.ts` glob picks
them up automatically:
```
packages/ui/src/stories/*.stories.tsx
packages/ui-app/src/stories/*.stories.tsx
packages/ui-subframe/src/stories/*.stories.tsx
packages/ui-mock/src/stories/*.stories.tsx
```
> **Why `packages/ui/` only today?** The i18n hardcoded-strings guard
> whitelists `packages/(ai|cli|config|entitlements|mail|plugins|queue|realtime|ui)/`,
> so stories under `packages/ui/src/stories/` author freely without
> tripping the gate. Stories in `ui-app`, `ui-subframe`, or `ui-mock`
> work too — but currently need to avoid uppercase user-facing prop
> literals (8+ char run starting `[A-ZÄÖÜ]`) or include their own
> reviewed-line patterns. A follow-up will widen the guard.
## Authoring a story
Use **Component Story Format 3 (CSF-3)**:
```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { MyComponent } from "./MyComponent";
const meta: Meta<typeof MyComponent> = {
title: "UI / Category / MyComponent",
component: MyComponent,
tags: ["autodocs"],
argTypes: {
variant: { control: { type: "select" }, options: ["a", "b"] },
},
args: { variant: "a" },
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
export const Default: Story = {};
export const VariantB: Story = { args: { variant: "b" } };
```
### Conventions
- **Title path**: `UI / <Category> / <Component>` (lowercase variants work
too if the case trips a guard).
- **Tags**: include `"autodocs"` to get an auto-generated docs page.
- **Render functions** when you need state or composition. Prefer plain
`args` for simple variants.
- **A11y opt-out**: `parameters: { a11y: { disable: true } }` plus a
comment explaining why. Default policy is "fail on a11y violations".
## Theming
`apps/storybook/.storybook/preview.tsx` wraps every story in a theme
decorator from `@storybook/addon-themes`. The toolbar exposes a
`light` / `dark` toggle that swaps the `dark` class on the preview root.
Stories should style themselves via the CSS variables declared in
`apps/storybook/.storybook/preview.css` (`--color-bg`, `--color-fg`,
`--color-primary`, …) so they flip automatically. See the **Theme
Guide** docs page inside Storybook for the full token table.
## A11y addon
`@storybook/addon-a11y` runs axe-core in the story panel. The
`preview.tsx` defaults enable the WCAG-style `color-contrast` rule.
Violations show up in the **Accessibility** panel.
## Test-runner & a11y gate
`@storybook/test-runner` walks every story registered in `main.ts` and
runs the `postVisit` hook in `.storybook/test-runner.ts` against each
render. The hook injects `axe-playwright` and re-runs the same ruleset
the in-canvas panel uses (`#storybook-root` as the context, WCAG-2.2-AA
tags) — but in a real headless Chromium where `color-contrast`
actually resolves CSS, so failures are real WCAG regressions.
Run locally:
```bash
# Terminal 1
pnpm --filter @toolctrl/storybook storybook
# Terminal 2
pnpm --filter @toolctrl/storybook test:storybook
```
The runner respects per-story opt-outs via `parameters.a11y.disable =
true` (must come with a justifying code comment).
## CI
`.github/workflows/storybook.yml` and `.gitea/workflows/storybook.yml`
run three jobs on PRs that touch `apps/storybook/` or any
`packages/ui*/src/stories/` path:
1. **build-storybook** — `pnpm --filter @toolctrl/storybook
build-storybook`; uploads the static site as a 14-day artifact.
2. **test-storybook** — downloads the artifact, serves it via
`http-server`, waits for readiness, installs Playwright Chromium,
and runs `pnpm test:storybook` against `http://127.0.0.1:6006`.
Any axe violation fails the job, blocking the PR until fixed or
explicitly opted-out.
3. **visual-regression** — Lost-Pixel diff (see below). Self-skips
until a baseline is committed.
The main CI Quality-Gates job is unchanged.
## Visual-regression (Lost-Pixel)
`lost-pixel` is wired via `apps/storybook/lostpixel.config.ts`. It
walks the built static Storybook, screenshots every story, and diffs
each shot against a committed baseline under
`apps/storybook/.lostpixel/baseline/`. A pixel diff above the 0.1 %
threshold fails CI and uploads the diff PNGs as a `lost-pixel-diff`
artifact.
The `visual-regression` CI job is **baseline-gated**: its first step
checks for `apps/storybook/.lostpixel/baseline/*.png` and, if none are
committed yet, skips the rest of the job with a notice instead of
failing. This keeps the job green until an operator opts in.
**Bootstrap (one-time, operator):**
```bash
pnpm --filter @toolctrl/storybook build-storybook
pnpm --filter @toolctrl/storybook visual-regression:update
git add apps/storybook/.lostpixel/baseline
git commit -m "test(storybook): commit Lost-Pixel baseline screenshots"
```
From the next PR on, the job blocks merges on unintended visual drift.
`.lostpixel/current/` and `.lostpixel/difference/` are per-run output
and git-ignored.
Chromatic (hosted, paid above a free tier) remains an alternative —
it would need `CHROMATIC_PROJECT_TOKEN` in CI secrets and is
deliberately **not** wired.
## Troubleshooting
- **Story missing?** Confirm the path matches the glob in
`apps/storybook/.storybook/main.ts`.
- **Story render error?** Run `pnpm --filter @toolctrl/storybook storybook`
locally and check the iframe error overlay.
- **A11y violation that should be allowed?** Use
`parameters.a11y.config.rules` to disable a specific axe rule, or
`parameters.a11y.disable = true` if a story is intentionally
decorative.
- **i18n guard flags story content?** Either keep user-facing prop
literals under 9 characters / starting lowercase, or route them
through translation keys (`@toolctrl/i18n`).
@@ -0,0 +1,160 @@
# Test Coverage Policy
**Status:** completed; no-regression gate in maintenance
**Spec:** `docs/improvement-plan/10-compliance-polish/test-coverage.md`
**Owner:** Engineering quality / release maintenance
This document explains how test coverage is measured, gated, and reported in the
ToolCTRL boilerplate. Per-path thresholds are encoded as data in
`scripts/coverage-targets.json` and enforced by `scripts/verify-test-coverage.mjs`.
CI workflows run the gate on GitHub PRs and `push` to `main`, while the
Gitea coverage workflow currently runs on pull requests. Keep
`.github/workflows/coverage.yml`, `.gitea/workflows/coverage.yml`, and
this policy aligned when trigger coverage changes.
## Why
- **Per-path thresholds** reflect risk: auth, billing, tenant-isolation,
shared-store, and outbox modules carry materially higher correctness and
security risk than view components or marketing pages. A flat
repository-wide gate either over-protects unimportant code or under-protects
the parts that matter.
- **No-regression baseline** is active. Critical paths carry a
`sprintTarget` field that records the release floor. The active gate has
been raised to the latest passing coverage run for the covered critical
paths, so contributors are blocked on regressions rather than asked to chase
already-closed backlog rows.
## Tooling
- **Provider:** V8 native coverage via `@vitest/coverage-v8` and
`vitest@3.2.4`.
- **Reporters:** `text-summary`, `json-summary`, `html`, `lcov`. Configured in
`vitest.config.ts` so `scripts/verify-test-coverage.mjs` can use a stable
`vitest run --coverage` invocation.
- **Output:** `coverage/` (git-ignored). `coverage-summary.json` feeds the
threshold gate and the README badge; `html/` is uploaded by CI as a build
artifact.
## Local commands
```bash
# Run vitest with coverage and enforce per-path thresholds.
pnpm run verify:test-coverage
# Print the shields.io badge URL for the current coverage-summary.json.
pnpm coverage:badge
# Render the per-path table as a markdown PR comment.
node scripts/coverage-summary.mjs --pr-comment
```
## Critical paths
The following modules carry tighter thresholds because regressions there have
disproportionate impact. Each entry in `scripts/coverage-targets.json` with
`"criticalPath": true` and a `sprintTarget` belongs to this list.
| Module | Active gate (stmt/br) | Sprint target |
| ------------------------------------------ | --------------------- | ------------- |
| `apps/api/src/auth/**` | 99 % / 92 % | 90 % / 85 % |
| `apps/api/src/billing/**` | 99 % / 97 % | 99 % / 95 % |
| `apps/api/src/isolation/**` | 99 % / 99 % | 95 % / 90 % |
| `apps/web/app/services/auth/**` | 97 % / 88 % | 90 % / 85 % |
| `apps/web/app/utils/auth/**` | 97 % / 92 % | 90 % / 85 % |
| `apps/web/app/utils/session.server.ts` | 99 % / 98 % | 95 % / 90 % |
| `apps/web/app/utils/stripe.server.ts` | 95 % / 90 % | 85 % / 75 % |
| `apps/web/app/utils/db/permissions/**` | 99 % / 98 % | 90 % / 80 % |
| `apps/web/app/utils/db/tenants.db.server.ts` | 98 % / 95 % | 95 % / 90 % |
| `packages/entitlements/src/**` | 96 % / 89 % | 90 % / 80 % |
| `packages/db/src/outbox*` | 92 % / 88 % | 90 % / 80 % |
| `packages/shared-store/src/**` | 91 % / 90 % | 90 % / 80 % |
The active gate is the **no-regression baseline** — it is the floor below
which CI will fail. The sprint target records the release floor that the
latest ratchet already meets or exceeds for every critical-path row (see
[Maintenance and future ratchets](#maintenance-and-future-ratchets)).
## How to add a coverage gate for a new module
1. Open `scripts/coverage-targets.json`.
2. Append an entry with `name`, `pathPrefixes`, `includeGlobs`, `statements`,
`branches`. For critical modules, also set `criticalPath: true` and a
`sprintTarget` object.
3. Run `pnpm run verify:test-coverage` locally. The gate prints the actual
coverage; set the active gate at or slightly below that number so the
commit lands green.
4. Open a PR. The Coverage workflow will post a sticky comment showing the
new row in the per-path table.
## How to bump or lower a threshold
Bumps **up** (tighter gate) — most common after writing new tests:
1. Run `pnpm run verify:test-coverage` and read the actual coverage value.
2. Edit the relevant entry in `scripts/coverage-targets.json`.
3. Reference the commit that added the new tests in the PR description.
Bumps **down** (looser gate) — should be rare and reviewed:
1. Open the PR with a justification. Acceptable reasons include: code path
genuinely removed; module merged into a less-critical category; sprint
deferred for documented operational reasons.
2. Request review from a code owner.
3. **Never** lower a `criticalPath` gate without compensating mitigation
(e.g. a corresponding E2E test, a stricter integration suite). Coverage
floor for critical paths is a security control.
## What triggers a CI failure
`pnpm run verify:test-coverage` exits non-zero in any of these cases:
- vitest itself failed to run (test failure, config error, OOM)
- `coverage/coverage-summary.json` is missing (coverage reporter mis-configured)
- any target's actual `statements%` < `target.statements` **or**
actual `branches%` < `target.branches`
The GitHub workflow uploads `coverage/` as `coverage-report-<run_id>` even on
failure so contributors can drill into the HTML report.
## Type-only packages
Packages that intentionally emit no runtime JavaScript, such as
`@toolctrl/api-types`, must not add fake exports or `expect(true)` tests just
to satisfy statement coverage. Give these packages a 0/0 active gate and 0/0
sprint target, then cover their contract with `expectTypeOf` tests and
`tsc --noEmit` typecheck.
## Maintenance and future ratchets
Task #76 is completed. Keep the gate useful as a no-regression control:
1. Confirm `docs/testing/coverage-sprint-backlog.md` still has no
critical-path or standard-tier positive-gap rows before release closeouts.
2. Keep each active `statements`/`branches` gate at or above the relevant
`sprintTarget`.
3. Treat `sprintTarget` fields as explicit release-floor annotations unless a
future cleanup removes them consistently from docs and gate data.
4. Run `pnpm run verify:test-coverage` after threshold changes — it must pass
with the new gates.
5. Track additional Playwright/E2E coverage separately; it is not a blocker
for the completed per-module coverage gate.
## Out of scope (deferred to follow-up tasks)
- **Coverage trend dashboard** (`docs/testing/coverage-trend.md`) — nightly
cron-updated markdown table. Not implemented in this phase.
- **PR-comment diff vs main baseline** — current sticky comment shows totals
but not delta. A future PR can add baseline-fetch + delta rendering similar
to bundle-budget.
- **E2E coverage via Playwright** (5 golden-path flows) — tracked separately.
- **Additional critical-path test writing** — see
`docs/testing/coverage-sprint-backlog.md` for the ordered backlog.
## References
- Spec: `docs/improvement-plan/10-compliance-polish/test-coverage.md`
- Gate runner: `scripts/verify-test-coverage.mjs`
- Targets data: `scripts/coverage-targets.json`
- Summary/badge: `scripts/coverage-summary.mjs`
- CI: `.github/workflows/coverage.yml`, `.gitea/workflows/coverage.yml`
@@ -0,0 +1,93 @@
# Two-Tier Cache Pattern
**Status:** active
**Owner:** platform-scaling
**Related:** [`packages/shared-store/README.md`](../../packages/shared-store/README.md), `docs/improvement-plan/03-scaling-infrastructure/pubsub-cache-invalidation.md`
## Why
A SharedStore-only cache pays one Redis round-trip per read (~1-3 ms on a healthy network). Hot paths that hit the same key 10-30 times per request (entitlement checks, limit counters, UI feature gating) accumulate enough overhead that we need an in-process L1 — but an in-process L1 alone goes stale across replicas. The two-tier cache combines both:
| Tier | Backing | Latency | Consistency |
| --- | --- | --- | --- |
| L1 | `Map` / `LRUCache` in process | sub-microsecond | per-replica |
| L2 | `SharedStore` (Redis or memory) | 1-3 ms | cross-replica |
| Pub/Sub | Redis `PUBLISH` on `cache:<ns>:invalidate` | < 50 ms typical fan-out | best-effort, see safety net |
A mutating call invalidates its local L1+L2 and publishes the key on the namespace channel. Every replica's subscriber receives the message and drops the key from its L1 (and optionally the L2). Reads probe L1 first; on miss, fall back to L2; on L2 miss, refresh from the source-of-truth (typically Postgres) and populate both tiers.
## Decision rubric
Use a two-tier cache when **all three** hold:
1. The key is read more than ~5x per request, or by every request (subscription, tenant, entitlements, feature flag).
2. Source-of-truth mutations are rare relative to reads (Stripe webhook firing seconds-to-minutes apart, admin edits).
3. Stale reads have user-visible consequences (showing the wrong plan limit, denying access after an upgrade).
Otherwise, prefer:
- **In-process LRU only** when consistency doesn't matter (route-stable computed values, request-scoped caches).
- **SharedStore cache only** when the read-rate is low and consistency must be strict (idempotency keys, session lookups).
- **No cache** when the source is already fast (cached Prisma result, in-memory blueprint).
## Channel naming
Channels follow `shared-store:<store-namespace>:ch:cache:<cache-namespace>:invalidate`. Concretely, the web SharedStore (`namespace: "web"`) emits subscription invalidations on `shared-store:web:ch:cache:subscription:invalidate`. The API SharedStore (`namespace: "api"`) emits on `shared-store:api:ch:cache:subscription:invalidate`.
For cross-app fan-out (Stripe webhook in API → web replicas), both apps must use the same SharedStore namespace so they share a channel. The current implementation uses per-app namespaces (`api`, `web`) but both connect to the same Redis logical DB index (default `1`). The actual channel string is namespaced under the writing app's SharedStore prefix. **If you need cross-app fan-out**, either:
1. Use a dedicated shared namespace (e.g. `namespace: "platform"`) for both apps' two-tier caches, or
2. Have the publishing app PUBLISH the message via a SharedStore configured with the receiving app's namespace explicitly.
The Stripe webhook → subscription pattern in this codebase publishes on `shared-store:api:ch:cache:subscription:invalidate`. For a production multi-replica web deploy to actually receive that broadcast, configure both `apps/api/src/cache/subscription-cache-publisher.ts` and `apps/web/app/utils/services/.server/subscriptionCache.server.ts` to construct their SharedStores with the same namespace (recommended: introduce `getCacheSharedStore({ namespace: "cache" })` for both apps). The current implementation broadcasts within the API process and applies the same `clearTenantSubscriptionCache` semantics on the web side via local mutation-paths that already publish; the cross-process broadcast is the work item documented under Deviations in the detail-md.
## Reconnect semantics
ioredis auto-reconnects on connection loss with exponential backoff. While disconnected, the subscriber is offline and any PUBLISH issued by another replica is lost forever (pub/sub has no persistence). Mitigations layered in order:
1. **L1 TTL is the floor.** Default `CACHE_L1_TTL_SECONDS=300` (5 min). Any missed invalidation self-heals within five minutes because the L1 entry expires. Lower the TTL for hotter paths if stale-window tolerance shrinks; raise it for marketing-content-grade caches.
2. **Reconnect flush.** Each two-tier cache exposes `flushLocal()` so a higher-level boot hook can `cache.flushLocal()` on a Redis reconnect event. The minimal implementation is "TTL is enough"; a more aggressive deployment can wire `subscriber.on('ready', () => cache.flushLocal())` in app boot.
3. **Outbox fallback.** For strictly-consistent flows (billing, entitlement enforcement), the source of truth writes an outbox event (see `docs/architecture/outbox-pattern.md`); the outbox dispatcher consumes the event and re-issues the invalidation. This guarantees at-least-once delivery and survives Redis outages — at the cost of one DB round-trip and ~1 second dispatch latency.
## Channel inventory
| Channel | Publisher | Subscriber | Payload | Defined in |
| --- | --- | --- | --- | --- |
| `cache:subscription:invalidate` | `apps/api/src/billing/stripe-webhook-route.ts` (after `updateTenantSubscriptionProductFromStripe`); `apps/web/.../subscriptionService.ts` (`clearTenantSubscriptionCache`, `clearAllSubscriptionsCache`) | `apps/web/app/utils/services/.server/subscriptionCache.server.ts` → bridges into `clearCacheKey('tenantSubscription:<tenantId>')` | tenantId (string) or `"*"` for wildcard | `subscriptionCache.server.ts` + `subscription-cache-publisher.ts` |
| `cache:tenant:invalidate` | `apps/web/.../tenant-cache.server.ts` (`publishTenantInvalidation`) | `apps/web/.../tenant-cache.server.ts` → clears `tenant:`, `tenantSimple:`, `tenantIdOrSlug:` cachified keys | tenantId (string) or `"*"` | `tenant-cache.server.ts` |
| `cache:entitlements:invalidate` | _piggy-backs on subscription_ — entitlements derive from the subscription and recompute on the next read after the subscription L1 is flushed | _piggy-backs on subscription_ | n/a — chained | n/a (chain) |
| `cache:feature-flags:invalidate` | `packages/feature-flags` publishers, including CLI feature-flag enable/disable/rule updates | web feature-flag cache subscribers through the shared two-tier cache bootstrap | `{ flagKey?, tenantId?, all? }` | `packages/feature-flags/src/publisher.ts` |
## Implementing a new two-tier cache
1. Create a `<namespace>-cache.server.ts` module per app (api/web) that wraps `createTwoTierCache({ store: getSharedStore(), namespace, ttlSeconds, pubsubEnabled, logger })`.
2. Expose two functions: `publish<Namespace>Invalidation(key)` (for mutators) and `subscribe<Namespace>Cache()` (idempotent boot hook).
3. The subscriber callback bridges into the existing local cache representation (`clearCacheKey()` for cachified-LRU, `Map.delete()` for ad-hoc maps).
4. Add the boot hook to `apps/web/app/cache-bootstrap.server.ts` (or equivalent API bootstrap) so the subscriber starts before request traffic.
5. Add publisher call-sites at every mutation that owns the SoT (database write, admin action, scheduled job).
6. Write a co-located test that drives the two-tier helper directly against a `MemoryAdapter` and asserts the cachified-bridge fires.
## Feature-Flag Cache Notes
Feature-flag invalidation is now implemented through the shared two-tier cache
publisher path. CLI feature-flag mutations publish to
`cache:feature-flags:invalidate` after the audit write, and web subscribers
flush L1 entries immediately instead of waiting for the TTL window. Keep new
feature-flag mutators on the same publisher path.
## Kill switch
`CACHE_INVALIDATION_ENABLED=false` (env var) flips both publishers and subscribers into local-only mode. The cache still works — read-through still populates L1+L2 — but invalidate becomes a local-only delete + no PUBLISH, and subscribers do not start. The system degrades to "every replica has its own L1 that expires after `CACHE_L1_TTL_SECONDS`". Use this switch:
- During a Redis incident to stop publisher errors from spamming Sentry
- During a deployment when subscribers can't keep up with traffic patterns
- During investigation of a stale-cache bug to confirm whether the publisher or subscriber path is at fault
Always re-enable after the incident; the L1-only mode has no replica-consistency floor below the L1 TTL.
## Tests
- Unit tests for `createTwoTierCache` cover round-trip, fallback, cross-replica invalidation, `localOnly`, wildcard flush, pubsub kill-switch — `packages/shared-store/src/helpers/two-tier-cache.test.ts`
- Subscription cache integration: `apps/web/app/utils/services/.server/subscriptionCache.server.test.ts`
- Tenant cache integration: `apps/web/app/utils/db/tenant-cache.server.test.ts`
- Stripe webhook publish path: `apps/api/src/billing/stripe-webhook-route.test.ts` (asserts `publishSubscriptionInvalidation(tenantId)` is called after the DB update)
@@ -0,0 +1,139 @@
# Vector Store
`@toolctrl/vector-store` is a thin adapter layer over two interchangeable
nearest-neighbour stores. The interface is small enough that swapping one for
the other does not require code changes outside `createVectorStore`; the
identical conformance suite (`packages/vector-store/src/adapters/conformance.test.ts`)
guarantees behavioural parity.
## Why an adapter layer at all
The 80% case for tenants in this stack — knowledge bases with a few thousand
chunks, RAG-backed help-desk replies — sits comfortably inside pgvector. It
inherits the same operational story as the rest of Postgres: backups, replicas,
RLS, monitoring. No extra container, no extra credentials, no extra firewall
rule.
The 20% case (large RAG corpora, dedicated tenants, latency-sensitive workloads)
benefits from Qdrant's purpose-built HNSW implementation, richer filter
language and snapshot tooling. We keep that path open without forcing every
deployment to run a second data store.
## Adapter comparison
| Concern | pgvector | Qdrant |
| --- | --- | --- |
| Operational cost | Same Postgres you already run | Separate container + storage volume |
| Backups | Free — covered by `backup:create` | Snapshot API; cron script ships separately |
| Recall@10 on cosine | 0.95+ once HNSW is built | 0.97+ with default config |
| P95 search latency (10k items) | <30 ms | <15 ms |
| Filter expressiveness | `payload ->> 'key' = …` JSON ops | Native payload filter DSL |
| Multi-vector / sparse | Not yet | Native (not exposed in adapter) |
| Failure mode on missing collection | Throws Postgres `42P01`; adapter converts to empty result | Throws 404; adapter converts to empty result |
## Selection logic
```ts
import { createVectorStore } from "@toolctrl/vector-store";
const store = createVectorStore({
namespace: "rag-prod", // becomes a collection prefix
adapter: undefined, // optional override
qdrantUrl: process.env.QDRANT_URL, // forward env at the boundary
qdrantApiKey: process.env.QDRANT_API_KEY,
qdrantClientFactory: ({ url, apiKey }) =>
new QdrantClient({ url, apiKey }),
prismaWrite: getWriteDbClient(),
prismaRead: getReadDbClient(),
});
```
The factory picks an adapter in this order:
1. `adapter` parameter when supplied (test seam, dual-running migrations).
2. `qdrantUrl` truthy → Qdrant. Caller must also supply
`qdrantClientFactory`; the package does not pin a specific Qdrant SDK
version.
3. Otherwise pgvector with `prismaWrite` / `prismaRead`.
The package never reads `process.env` directly — it would break the
`env-access-enforcement` boundary test. Callers thread env values in.
## Tenant scoping
`createVectorStore` always returns a `TenantScopedVectorStore`. The bare
`searchVectors` method throws on this wrapper: searches must go through
`searchForTenant({ tenantId, ... })`. The wrapper injects the tenantId as the
first `must` filter clause and discards any caller-supplied `tenantId` clause
before forwarding the call, so a buggy or hostile caller cannot escape the
boundary.
Writes are guarded at the type level: `VectorItem.payload` extends
`{ tenantId: string }`, so omitting the tenant id at upsert time is a TS
compile error.
## pgvector tuning
HNSW parameters live on the adapter constructor and default to pgvector 0.7's
recommended starting point:
```ts
new PgvectorAdapter({
write, read,
hnsw: { m: 16, efConstruction: 64 },
});
```
- `m` controls graph degree. Bump to 24 for collections >500k vectors when
recall lags.
- `efConstruction` is the candidate-list size during index build. Bump to 128
for higher-recall corpora; build time roughly doubles.
The adapter creates one table per collection (`vec_<namespace>_<name>`) with
three indexes:
- HNSW on the vector column with the metric-specific operator class.
- GIN on the `payload` JSONB column for filter selectivity.
- B-tree on the generated `tenant_id` column for the (almost always)
tenant-scoped read path.
## Qdrant deployment notes
- Pin to a 1.x release. Adapter is tested against `1.12`.
- API key is mandatory whenever the URL is not localhost. The factory's caller
should reject `QDRANT_URL` without `QDRANT_API_KEY` at the boundary.
- Use a dedicated PVC; Qdrant cannot recover from a wiped storage directory.
- The adapter auto-creates collections on first upsert. Dimension mismatches
surface as `VectorDimensionMismatchError` so the embedding pipeline can
classify them for retry vs. dead-letter.
## Backups
- pgvector: piggy-backs on the Postgres backup pipeline (`backup:create`,
`backup:restore`). No additional work.
- Qdrant: use the Snapshot API. A dedicated cron job + S3 upload script will
ship with task #50; until then run manual snapshots before each schema
rollout.
## Migration paths
We do not provide a direct pgvector → Qdrant data transfer. Source-of-truth is
always the document/chunk table in Postgres, so the migration is a re-embed:
1. Deploy with `VECTOR_STORE_DEFAULT_ADAPTER=qdrant` and `QDRANT_URL` set.
2. Run `rag:reindex` (ships with #50). Embeddings are recomputed via the
embedding provider and written into Qdrant.
3. Smoke-search to confirm parity, then drop the pgvector tables via
`vector-store:gc` once the warm-window is clear.
Same procedure in reverse, with the obvious caveat that pgvector imposes a
practical ceiling around 1M vectors per collection before HNSW build cost
becomes painful.
## Namespace conventions
The factory's `namespace` argument is prefixed onto every collection name —
`namespace="rag-prod", collection="docs"` becomes the pgvector table
`vec_rag_prod_docs` and the Qdrant collection `rag-prod_docs`. Pick a
namespace per workspace or per environment so multiple deployments can share
the same store without colliding.
@@ -0,0 +1,69 @@
# Architecture Closure Handoff Summary
> **Archived status surface:** this document is preserved as the official
> Architecture Closure record. It remains referenced by static closure tests,
> but current runtime truth still comes from the canonical sources listed
> below.
## Closure scope
Architecture Closure closes as a documentation, manifest, and static-verification handoff contract for the architecture migration state. It does not claim that every runtime feature has already been extracted, removed, or rewritten; the `@toolctrl/web` React Router runtime in `apps/web/app` remains authoritative, with root scripts delegating to it, until later sequential tasks replace specific surfaces directly. The closure package ties together current boundary status, feature pruning decisions, manifest contracts, bundle evidence, and executor entrypoints so future agents can start from one consistent contract.
R001 is supported by complete Architecture Closure handoff coverage across the closure documents, feature manifests, static tests, and this summary. R002 is preserved by requiring sequential follow-up order and by avoiding any assumption that downstream feature slices can run in parallel or rely on parallel runtime changes.
## Canonical sources
Future agents should treat these files as the canonical Architecture Closure coverage set:
- `docs/architecture-migration-status.md` — current API, worker, package, pruning, URL-boundary, and bundle/performance handoff status.
- `docs/feature-inventory.md` — human-readable Core, Plugin, and Removal inventory for boilerplate pruning.
- `docs/feature-manifests/README.md` — executor-facing manifest contract and update rules.
- `docs/feature-manifests/*.json` — machine-readable ownership, dependency, configuration, provider, worker, UAT, and pruning contracts.
- `docs/agent-entrypoints.md` — first-stop routing for future feature, MVP import, admin-managed, worker-backed, UI, and Architecture Closure closure work.
- `apps/web/app/application/architectureClosureDocs.test.ts` — static closure assertions for Architecture Closure source alignment, closure status, pruning actions, and S04 evidence.
- `apps/web/app/application/featureManifests.test.ts` — manifest schema, dependency, secret ownership, worker declaration, config, and queue validation.
## Requirement coverage
- R001: covered by the aligned handoff sources above, the explicit Architecture Closure final closure status in `docs/architecture-migration-status.md`, the feature inventory, manifest README, JSON manifests, agent entrypoints, and static tests that guard those references.
- R002: covered by the sequential follow-up order below. Do not assume parallel feature-slice execution, parallel runtime ownership, or hidden migration completion outside the documented source set.
- R003 and pruning continuity: use `docs/feature-inventory.md` plus `docs/feature-manifests/*.json` before implementing, extracting, or removing features. Manifest classifications are action contracts (`core`, `plugin-candidate`, `remove-candidate`, or `mixed`), not permanent legacy-support promises.
- S04 bundle/performance continuity: consult the bundle evidence referenced from `docs/architecture-migration-status.md`; optional production-build verification remains manual unless explicitly approved outside Auto-Mode.
## Full non-E2E gate commands
Run these focused non-E2E gates when closing or revalidating the Architecture Closure handoff contract:
```sh
pnpm exec vitest run apps/web/app/application/architectureClosureDocs.test.ts apps/web/app/application/featureManifests.test.ts
pnpm exec eslint --ext .tsx,.ts docs apps/web/app/application --max-warnings=0
pnpm typecheck
```
No E2E, browser, Cypress, or Playwright execution is part of this Auto-Mode closure gate.
## Sequential follow-up order
Future work should proceed in this order:
1. Read `docs/agent-entrypoints.md` and route the work to the relevant playbook before implementation.
2. Read `docs/architecture-migration-status.md` for current boundary status and explicit skeleton-only versus runtime-wired ownership.
3. Read `docs/feature-inventory.md` for the human-readable Core, Plugin, and Removal decision context.
4. Read `docs/feature-manifests/README.md` and the matching `docs/feature-manifests/*.json` files.
5. Update or add the relevant manifest contract before changing feature code when ownership, configuration, provider, worker, UAT, or pruning behavior changes.
6. Implement one atomic migration, extraction, removal, or replacement step at a time, keeping the root runtime authoritative until that step directly replaces it.
7. Re-run the relevant static tests and scoped lint/test commands for the changed surface before continuing to the next step.
This order intentionally preserves R002 by preventing downstream agents from treating separate migration slices as parallel prerequisites.
## Manual build note
`pnpm build` remains an optional manual follow-up for production bundle behavior and deployment confidence. It is not an Auto-Mode completion gate for this handoff task, and lack of an Auto-Mode build run does not weaken the static Architecture Closure closure contract.
## Future-agent start points
- For any new or migrated feature, start at `docs/agent-entrypoints.md`, then follow `docs/agent-playbooks/feature-integration.md` and the relevant manifest.
- For plugin extraction or pruning work, start with `docs/feature-inventory.md`, `docs/feature-manifests/README.md`, and the matching `docs/feature-manifests/*.json` contract.
- For architecture boundary questions, start with `docs/architecture-migration-status.md` and keep runtime-wired versus skeleton-only distinctions intact.
- For closure verification, start with `apps/web/app/application/architectureClosureDocs.test.ts` and `apps/web/app/application/featureManifests.test.ts`.
- For optional production bundle confidence, schedule manual `pnpm build` after the static non-E2E gate instead of treating it as an Auto-Mode requirement.
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 29.5.0, SVG Export Plug-In . SVG Version: 9.03 Build 0) -->
<svg version="1.0" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1900 500" style="enable-background:new 0 0 1900 500;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000085213544704903965630000016245828043251886486_);}
.st1{fill:#F9F9F9;}
.st2{clip-path:url(#SVGID_00000038397202409254422500000000715208381988780936_);}
.st3{fill:#F1B729;}
</style>
<g id="Ebene_1_00000147200394007143738050000007847843949640695430_">
<g transform="matrix(1, 0, 0, 1, 0, 32)">
<g>
<g>
<defs>
<rect id="SVGID_1_" y="10.7" width="941.3" height="421.3"/>
</defs>
<clipPath id="SVGID_00000173870619096606741830000001135887134242725528_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000173870619096606741830000001135887134242725528_);">
<g>
<g transform="translate(-1.971007, 242.87892)">
<g>
<path class="st1" d="M28.6-164.7h58.6v102.5H219v-102.5h58.6V91.6H219V-11H87.2V91.6H28.6V-164.7z"/>
</g>
</g>
</g>
<g>
<g transform="translate(221.853252, 242.87892)">
<g>
<path class="st1" d="M103.2-164.7h58.6V91.6h-58.6C103.2,91.6,103.2-164.7,103.2-164.7z"/>
</g>
</g>
</g>
<g>
<g transform="translate(302.86895, 242.87892)">
<g>
<path class="st1" d="M110.1-135.4v-29.3H165l76.9,199.6h3.7l76.9-199.6h54.9v29.3l-95.2,227h-76.9L110.1-135.4z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g>
<g>
<defs>
<rect id="SVGID_00000050649768837086059390000000432696666009736893_" x="1186.7" y="42.7" width="704" height="421.3"/>
</defs>
<clipPath id="SVGID_00000060012912674953308700000002027295538902951813_">
<use xlink:href="#SVGID_00000050649768837086059390000000432696666009736893_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000060012912674953308700000002027295538902951813_);">
<g>
<g transform="translate(223.06958, 242.87892)">
<g>
<path class="st1" d="M1290.3-132.9h58.6v256.3h-58.6V-132.9z"/>
</g>
</g>
</g>
<g>
<g transform="translate(304.085278, 242.87892)">
<g>
<path class="st1" d="M1317.3-132.9h188.6c18.3,0,32.9,5.5,43.9,16.5c11,11,16.5,25.6,16.5,43.9V63c0,18.3-5.5,33-16.5,43.9
s-25.6,16.5-43.9,16.5h-188.6V-132.9z M1483.9,72.2c15.9,0,23.8-7.9,23.8-23.8V-57.8c0-15.9-7.9-23.8-23.8-23.8h-108V72.2
H1483.9z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g id="Ebene_5">
<path class="st1" d="M1418.2,271.1c9-1.7,17.2-4.8,24.5-9.1c6.3-3.9,12.1-9.6,17.2-17c5.1-7.5,7.7-17.6,7.7-30.6v-43.9
c0-18.3-5.5-32.9-16.5-43.9c-11-11-25.6-16.5-43.9-16.5h-184.9v13.5l66.1,114.6l-66.1,114.2v14h58.6V264l59.3-0.1l79.9,102.4h53.1
V337L1418.2,271.1z M1409,199.7c0,15.9-8.8,13-24.6,13h-103.5v-51.5h104.3c15.9,0,23.8,7.9,23.8,23.8V199.7z"/>
</g>
<g id="Ebene_4">
<polygon class="st1" points="907.1,315.2 766.4,315.2 766.4,263.9 877.5,263.9 862.3,237.6 876.7,212.7 766.4,212.7 766.4,161.4
906.3,161.4 927.5,124.9 927.5,110.2 707.8,110.2 707.8,366.5 927.5,366.5 927.5,350.6 "/>
</g>
<g id="Ebene_2_00000103265633317154261300000008405163556884147082_">
<g>
<g>
<polygon class="st3" points="1266.6,237.2 1218.5,153.9 1074.3,237.2 1170.4,237.2 1122.4,320.4 1026.3,320.4 978.2,237.2
1026.3,153.9 1218.5,153.9 1180.1,87.5 1170.4,70.7 978.2,70.7 882.1,237.2 978.2,403.6 1170.4,403.6 "/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 29.5.0, SVG Export Plug-In . SVG Version: 9.03 Build 0) -->
<svg version="1.0" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1900 500" style="enable-background:new 0 0 1900 500;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000092424140864995002100000002551967465525493131_);}
.st1{fill:#231F20;}
.st2{clip-path:url(#SVGID_00000050631655892142403640000003269334890553695391_);}
.st3{fill:#F1B729;}
</style>
<g id="Ebene_1_00000147200394007143738050000007847843949640695430_">
<g transform="matrix(1, 0, 0, 1, 0, 32)">
<g>
<g>
<defs>
<rect id="SVGID_1_" y="10.7" width="941.3" height="421.3"/>
</defs>
<clipPath id="SVGID_00000021828690431161971110000008666306399099804576_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000021828690431161971110000008666306399099804576_);">
<g>
<g transform="translate(-1.971007, 242.87892)">
<g>
<path class="st1" d="M28.6-164.7h58.6v102.5H219v-102.5h58.6V91.6H219V-11H87.2V91.6H28.6V-164.7z"/>
</g>
</g>
</g>
<g>
<g transform="translate(221.853252, 242.87892)">
<g>
<path class="st1" d="M103.2-164.7h58.6V91.6h-58.6C103.2,91.6,103.2-164.7,103.2-164.7z"/>
</g>
</g>
</g>
<g>
<g transform="translate(302.86895, 242.87892)">
<g>
<path class="st1" d="M110.1-135.4v-29.3H165l76.9,199.6h3.7l76.9-199.6h54.9v29.3l-95.2,227h-76.9L110.1-135.4z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g>
<g>
<defs>
<rect id="SVGID_00000029025352032733430810000007867561513642331544_" x="1186.7" y="42.7" width="704" height="421.3"/>
</defs>
<clipPath id="SVGID_00000132086783084342479510000013717535083693061769_">
<use xlink:href="#SVGID_00000029025352032733430810000007867561513642331544_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000132086783084342479510000013717535083693061769_);">
<g>
<g transform="translate(223.06958, 242.87892)">
<g>
<path class="st1" d="M1290.3-132.9h58.6v256.3h-58.6V-132.9z"/>
</g>
</g>
</g>
<g>
<g transform="translate(304.085278, 242.87892)">
<g>
<path class="st1" d="M1317.3-132.9h188.6c18.3,0,32.9,5.5,43.9,16.5c11,11,16.5,25.6,16.5,43.9V63c0,18.3-5.5,33-16.5,43.9
s-25.6,16.5-43.9,16.5h-188.6V-132.9z M1483.9,72.2c15.9,0,23.8-7.9,23.8-23.8V-57.8c0-15.9-7.9-23.8-23.8-23.8h-108V72.2
H1483.9z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g id="Ebene_5">
<path class="st1" d="M1418.2,271.1c9-1.7,17.2-4.8,24.5-9.1c6.3-3.9,12.1-9.6,17.2-17c5.1-7.5,7.7-17.6,7.7-30.6v-43.9
c0-18.3-5.5-32.9-16.5-43.9c-11-11-25.6-16.5-43.9-16.5h-184.9v13.5l66.1,114.6l-66.1,114.2v14h58.6V264l59.3-0.1l79.9,102.4h53.1
V337L1418.2,271.1z M1409,199.7c0,15.9-8.8,13-24.6,13h-103.5v-51.5h104.3c15.9,0,23.8,7.9,23.8,23.8V199.7z"/>
</g>
<g id="Ebene_4">
<polygon class="st1" points="907.1,315.2 766.4,315.2 766.4,263.9 877.5,263.9 862.3,237.6 876.7,212.7 766.4,212.7 766.4,161.4
906.3,161.4 927.5,124.9 927.5,110.2 707.8,110.2 707.8,366.5 927.5,366.5 927.5,350.6 "/>
</g>
<g id="Ebene_2_00000103265633317154261300000008405163556884147082_">
<g>
<g>
<polygon class="st3" points="1266.6,237.2 1218.5,153.9 1074.3,237.2 1170.4,237.2 1122.4,320.4 1026.3,320.4 978.2,237.2
1026.3,153.9 1218.5,153.9 1180.1,87.5 1170.4,70.7 978.2,70.7 882.1,237.2 978.2,403.6 1170.4,403.6 "/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+104
View File
@@ -0,0 +1,104 @@
# Password Policy
Server-side password validation that every password-write endpoint goes
through. Configurable via env without a code deploy. Implementation lives in
`apps/web/app/services/auth/password-policy.server.ts`.
## Defaults (OWASP 2024 aligned)
| Env var | Default | Effect |
|---|---|---|
| `AUTH_PASSWORD_MIN_LENGTH` | `12` | Minimum character count. |
| `AUTH_PASSWORD_REQUIRE_UPPER` | `true` | Requires at least one A–Z character. |
| `AUTH_PASSWORD_REQUIRE_LOWER` | `true` | Requires at least one a–z character. |
| `AUTH_PASSWORD_REQUIRE_DIGIT` | `true` | Requires at least one 0–9 character. |
| `AUTH_PASSWORD_REQUIRE_SPECIAL` | `false` | Requires at least one non-alphanumeric character. Off by default — adds friction without much entropy beyond the length floor. |
| `AUTH_PASSWORD_HIBP_CHECK` | `true` | Consults `api.pwnedpasswords.com` via SHA-1 k-anonymity. The plaintext password never leaves the server; only the first 5 characters of its SHA-1 are sent. |
| `AUTH_PASSWORD_HIBP_MAX_OCCURRENCES` | `0` | Accept passwords seen at most this many times in HIBP. `0` = reject any leak. |
Also enforced regardless of env:
- An absolute max length of 1024 characters, so the validate-then-hash chain
can't be amplified into a DoS via 100 MB password strings.
- If the caller supplies `ctx.email`, the password must not contain the
email's local-part (case-insensitive), provided the local-part is ≥3
characters. Short local-parts (`x@example.com`) are skipped to avoid
false positives.
## Enforcement points
Every endpoint that writes a password runs the policy:
| Route | Caller |
|---|---|
| `apps/web/app/utils/services/auth.service.server.ts` (`AuthService.validateRegistration`) | `validatePassword` before `users.db.server.ts` hashes/stores the password |
| `apps/web/app/routes/reset.tsx` | UserValidationUtils.validatePasswords |
| `apps/web/app/routes/invitation.$id.tsx` | UserValidationUtils.validatePasswords |
| `apps/web/app/routes/admin/settings/profile.tsx` (change password) | validatePassword directly |
| `apps/web/app/routes/app.$tenant/settings/profile.tsx` (change password) | validatePassword directly |
| `apps/web/app/routes/admin/accounts/users.tsx` (admin reset) | validatePassword directly |
| `apps/web/app/routes/app.$tenant/__portals/portals.$portal/users.tsx` (portal user creation / reset) | validatePassword directly |
Login (`apps/web/app/modules/users/services/AuthService.ts`) deliberately
does NOT run the strength policy. Enforcing 12-char + class requirements
there would lock out every user with a pre-policy password instead of
prompting them to rotate. The plain "is the field a non-empty 4+ char
string" presence-check in `UserValidationUtils.validatePassword` is what
guards the login form. The dashboard-banner soft-check from the detail-MD
is a follow-up.
## HIBP fail-open
If the HIBP API times out (>3 s), returns a 5xx, or refuses the connection,
the validation result carries `hibpBypassed: true` and the password is
accepted (the other policy rules still apply). Caller code can log the
bypass so operators have a signal when api.pwnedpasswords.com is having a
bad day. We chose fail-open over fail-closed because the alternative
("can't register new users while HIBP is down") is worse for availability
than the alternative ("a newly-leaked password might slip through during
an HIBP outage").
To force fail-closed during a known-bad incident: set
`AUTH_PASSWORD_HIBP_CHECK=false` to skip HIBP entirely until the API
recovers. (Yes, that's the same as the bypass — but the env switch makes
it explicit and avoids the per-request 3 s timeout cost.)
## Error model
Each failure produces a bounded i18n key plus optional non-secret args:
```ts
{
valid: false,
errors: [
{ code: "auth.password.errors.tooShort", args: { minLength: 12 } },
{ code: "auth.password.errors.missingDigit" }
]
}
```
Codes:
- `auth.password.errors.empty` — input was empty / wrong type
- `auth.password.errors.tooShort` — below `AUTH_PASSWORD_MIN_LENGTH`
- `auth.password.errors.tooLong` — above the absolute max (1024)
- `auth.password.errors.missingUpper`
- `auth.password.errors.missingLower`
- `auth.password.errors.missingDigit`
- `auth.password.errors.missingSpecial`
- `auth.password.errors.containsEmail`
- `auth.password.errors.pwned`
Routes are expected to surface the first error to the user; the i18n
catalog should map every code above.
## Operator playbook
- **Relax the policy** (e.g. temporary lower bound during a migration):
set `AUTH_PASSWORD_MIN_LENGTH=10` + restart. The change takes effect on
next request; no code deploy.
- **Tighten the policy**: set `AUTH_PASSWORD_REQUIRE_SPECIAL=true` and
bump `AUTH_PASSWORD_MIN_LENGTH=16`. Existing users keep their old
passwords until they next rotate; the new policy applies only to writes.
- **HIBP-Outage handling**: monitor `hibpBypassed` in the audit log; if
the rate sustains, set `AUTH_PASSWORD_HIBP_CHECK=false` to drop the 3 s
per-write timeout. Re-enable when api.pwnedpasswords.com recovers.
+38
View File
@@ -0,0 +1,38 @@
# Backlog
Date: 2026-05-28
This file captures the narrow optional product-module extraction backlog from the core-pruning slice. It is not the live global backlog. For current operator/deploy backlog truth, use `docs/improvement-plan/OPERATOR-CHECKLIST.md` and `docs/improvement-plan/PROGRESS.md`; for CDN #51/A7/B2 specifically, use `docs/ops/asset-cdn.md`.
## Optional Product Module Extraction
Status: backlogged.
Decision: keep optional Product Core surfaces in the repository for now. Forms and Surveys are excluded because they were selected for removal, not extraction.
Scope parked here:
- Workflow Engine
- Knowledge Base and Widgets
- Portals and Sites
- Email Marketing
- Analytics and Metrics
- Notifications UI and flows
- AI, Prompt Builder, and MCP surfaces
Why this is parked: these modules are useful boilerplate surfaces, but they increase route inventory, bundle manifest size, schema surface, worker ownership, i18n surface, and agent onboarding complexity.
Revisit when one of these triggers happens:
- route manifest size becomes the main deploy or runtime cost;
- a plugin/module registration system is ready;
- the product decides which optional modules belong in the default starter;
- a customer-facing template needs a smaller core distribution.
Acceptance criteria for future extraction:
- each extracted module has a feature manifest and ownership doc;
- routes register through a module/plugin boundary instead of unconditional route discovery;
- DB schema, queue jobs, workers, i18n keys, permissions, and seeds have clear module ownership;
- disabled modules do not ship route or bundle surface by default;
- verification gates cover enabled and disabled module states.
@@ -0,0 +1,252 @@
# Boilerplate-Inventur: Apps & Infra
Faktische Bestandsaufnahme der Boilerplate unter `/Users/raffaelsheikh/Documents/GitHub/boilerplate`.
Bereich: deploybare Apps (API, Worker, Mobile, Hocuspocus) + Root-Config, CI-Gates (`.gitea`), `verify:*`-Scripts, Dockerfile, `infra/`, `react-router.config.ts`.
Stand: nur dokumentiert, was tatsaechlich existiert. Pfade sind absolut.
Nicht-Apps-Pakete (`apps/web`, `apps/storybook`) werden hier nur erwaehnt, wo Apps&Infra darauf verweisen — sie gehoeren zu anderen Inventur-Bereichen.
Konventionen, die uebergreifend gelten:
- Alle Apps sind pnpm-Workspace-Pakete unter `@toolctrl/*`, `"type": "module"`, ESM mit `.js`-Suffix in relativen Imports (TS-NodeNext).
- Geteilte Logik kommt aus `@toolctrl/*`-Workspace-Paketen (`config`, `db`, `logger`, `metrics`, `queue`, `storage`, `shared-store`, `observability`, `foundation`, `plugin-runtime`, `mail`, `ai`, `search`, `vector-store`, `exports`, `imports`, `entitlements`, `email-sequences`, `email-templates`, `marketing-outbound`, `form-widget`, `realtime`, `error-reporter`).
- Keine App besitzt ein eigenes Prisma-Schema. Alle DB-Zugriffe laufen ueber `@toolctrl/db` (`@toolctrl/db/client.server`, `@toolctrl/db/*.server`). Modelle liegen in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`.
- Jede App haelt `src/otel.ts` (OpenTelemetry, self-noop ohne `OTEL_EXPORTER_OTLP_ENDPOINT`) und `src/sentry.ts` (self-disable ohne DSN).
- Build via `node scripts/build.mjs` pro App; OTel-Datei steht in `package.json` `sideEffects`.
---
## 1. `@toolctrl/api` — Public-API-Runtime (Hono)
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/api`
**Zweck:** Dedizierte Hono-API-App fuer `api.<domain>/v1`. Kanonischer API-Host fuer externe Integrationen; betreibt API-eigene Usage- und Billing-Webhook-Handler im eigenen Runtime statt ueber Web-Proxy. Listen-Port: `API_PORT` -> `PORT` -> `3001`.
**Stack:** `hono` 4, `@hono/node-server`, `zod`, `bcryptjs`, `jsonwebtoken`, `stripe` 19, `@sentry/node`, OpenTelemetry.
**Scripts (`package.json`):** `dev` (`tsx watch src/server.ts`), `start` (`node dist/server.js`), `typecheck`, `build` (`node scripts/build.mjs`). Package-Export: `"./openapi": "./src/openapi.ts"`.
### Zentrale Dateien
| Datei | Pfad | Inhalt |
|---|---|---|
| App-Komposition | `apps/api/src/app.ts` | `export const app = new Hono<ApiEnv>()`; Middleware-Kette + Route-Registrierung. Bewusst listener-frei (Tests/Adapter importieren die App). |
| Server-Entry | `apps/api/src/server.ts` | Startet den HTTP-Listener. |
| Dependency-Injection | `apps/api/src/app-dependencies.ts` | `getApiAppDependencies()`, `configureApiAppDependenciesForTests()`, Typ `ApiAppDependencies` (DB-Readiness, Usage, Billing). |
| Env-Typen | `apps/api/src/api-types.ts` | `ApiEnv` (Hono-Context-Typ). |
| OpenAPI | `apps/api/src/openapi.ts` | OpenAPI-Contract-Bausteine (Error-Envelope-Schema mit `code`/`message`/`requestId`, Rate-Limit-Responses); via `verify:openapi-security` geprueft. |
| Route-Inventar | `apps/api/src/route-access-inventory.ts` | `routeAccessInventory` + `routeAccessClasses` (11 Klassen: `public-health`, `public-docs`, `service-token`, `service-or-tenant-api-key`, `tenant-api-key`, `webhook-signature`, `jwt-auth`, `auth-credential-exchange`, `public-analytics-token`, `optional-api-key-validation`, `canonical-redirect`). Quelle der Wahrheit fuer Auth-Klasse je Route. |
| Auth-Primitive | `apps/api/src/auth.ts` | `authenticateServiceRequest`, `extractBearerToken`, `extractApiKeyFromRequest`, `isValidServiceToken`, `createServicePrincipal`, Typ `ServicePrincipal` (Service-Token via `API_ACCESS_TOKEN`). |
### Route-Module (`apps/api/src/routes/`)
`accounts-routes.ts`, `analytics-routes.ts`, `auth-routes.ts`, `devices-routes.ts`, `entity-row-routes.ts`, `maintenance-routes.ts`, `metrics.ts`, `notifications-routes.ts`, `relationship-routes.ts`, `system-routes.ts`, `workspace-routes.ts`, `shared.ts`, `status.ts`. Registriert in `app.ts` via `register*Routes(app)`.
Dokumentierte Endpunkte (README + app.ts): `GET /v1/health`, `GET /healthz`, `GET /readyz`, `GET /status`, `GET /metrics` (token), `GET /v1/openapi.json`, `GET /v1/docs`, `GET /v1/session/current-user`, `POST /v1/api-keys/validate`, `GET /v1/accounts[/:id]`, `GET /v1/usage`, `/v1/entities/*`, `/v1/relationships/*`, `/v1/exports/*`, `/v1/workspaces/*`, `POST /v1/billing/stripe/webhooks`, `POST /v1/events/webhooks/attempts/:id`.
### Querschnitt-Module (`apps/api/src/`)
- Auth-Tiefe: `auth/cookie/session-cookie.ts`, `auth/session/session-store.ts`, `auth/login-rate-limit.ts`, `auth/permission.ts`, `auth/scope.ts`.
- Middleware (`middleware/`): `sentryTracing.ts` (muss erstes Middleware sein), `request-id.ts`, `audit-context.ts`, `body-limit.ts`, `metrics.ts`, `error-envelope.ts` (`createErrorHandler`/`createNotFoundHandler`). Security-Header + CORS direkt in `app.ts`.
- Billing: `billing/stripe-webhook-route.ts` (`postStripeBillingWebhookRequest`).
- Idempotenz: `idempotency/store.ts`, `idempotency/shared-store-client.ts`.
- Weitere: `usage/usage-route.ts`, `pagination/rows.ts`, `cache/subscription-cache-publisher.ts`, `export-progress.ts`, `exportRetentionQueue.ts`, `webhookDeliveryQueue.ts`, `webhooks/mailersend-signature.ts`, `email-sequences/provider-webhook-route.ts`, `platform/maintenance/maintenance.ts` (Maintenance-Middleware), `plugin-hook-publisher.ts`, `validation/request-schemas.ts`, `config.ts` (Env via `@toolctrl/config`).
**Prisma-Modelle:** keine eigenen — DB-Zugriff via `@toolctrl/db`.
**HiveGrid-Relevanz:** Fundament fuer die **Public-API / externe API** (Service-Token + Tenant-API-Key, OpenAPI-Contract, Route-Access-Inventar als Auth-Matrix). Direkt nutzbar fuer HiveGrid-Module, die externen API-Zugriff brauchen (Integrations, Webhooks, Usage/Billing-Ingress, Entity-/Relationship-CRUD ueber API). Error-Envelope + Request-ID-Korrelation sind ein wiederverwendbares API-Querschnittsmuster.
---
## 2. `@toolctrl/worker` — BullMQ-Worker-Runtime (Cron + Queue-Consumer)
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/worker`
**Zweck:** Opt-in BullMQ-Worker. Standardmaessig deaktiviert (`WORKER_ENABLED!="true"` -> sofortiger Exit). Bei Aktivierung benoetigt `DATABASE_URL`; Redis aus `BULLMQ_REDIS_URL` (Fallback `REDIS_URL`). Verarbeitet Queue-Jobs + registriert single-instance Cron-Scheduler.
**Stack:** `bullmq` 5, `ioredis`, `pino`, `prom-client`, `@aws-sdk/client-s3` + `lib-storage`, `@qdrant/js-client-rest`, `meilisearch`, `clamscan`, `gpt-tokenizer`, `semver`, `stripe`, `@sentry/node`, OTel. Breitester `@toolctrl/*`-Abhaengigkeitsgraph aller Apps (u.a. `ai`, `search`, `vector-store`, `exports`, `imports`, `mail`, `marketing-outbound`, `entitlements`, `email-sequences`, `form-widget`).
**Scripts:** `dev` (`tsx src/index.ts`), `start`, `health` (`node dist/health.js`), `build`, `typecheck`.
### Zentrale Dateien
| Datei | Pfad | Inhalt |
|---|---|---|
| Entry | `apps/worker/src/index.ts` | Boot: laedt `otel.js` zuerst, validiert `WORKER_QUEUE_GROUP`, startet Runtime + Admin/Health-Server + alle Cron-Scheduler. ~1678 Zeilen. |
| Runtime | `apps/worker/src/runtime.ts` | `startWorkerRuntime` / `shutdownWorkerRuntime`, Typ `WorkerRuntimeStatus`. |
| Handler-Registry | `apps/worker/src/handlers.ts` | `buildHandlerRegistry(deps)`, `createRegisteredProcessor`, Typ `HandlerDependencies` (DI-Seams fuer mail/scan/export/import/notification/plugin/dsgvo/ai-cost). Pro `QueueName` ein Handler. |
| Queue-Auswahl | `apps/worker/src/queues.ts` | `getWorkerQueueNames(env)`, `defaultWorkerQueueNames` (`email`, `notifications`); Feature-Flags `IMPORT_QUEUE_ENABLED`, `EXPORT_RETENTION_QUEUE_ENABLED`, `SCAN_QUEUE_ENABLED`. |
| Queue-Gruppen | `apps/worker/src/group-runtime.ts` | `bootstrapWorkerGroup`, `resolveWorkerQueueGroup`, `resolveGroupConcurrency`, `InvalidWorkerQueueGroupError`, `DEFAULT_GROUP_CONCURRENCY`. Gruppen: `general`(10), `mail`(5), `import`(1), `webhook`(20), `plugin`(5); Override `WORKER_CONCURRENCY_<GROUP>`. |
| Connection | `apps/worker/src/connection.ts` | `createWorkerConnection(group)` — ioredis-Verbindung getaggt `worker-<group>-<pid>`. |
| Admin-Server | `apps/worker/src/admin-server.ts` | `startWorkerAdminServer` — `/status` (JSON) + `/metrics` (token, nur bei `METRICS_ENABLED`), Typ `WorkerStatusReport`. |
| Health-Server | `apps/worker/src/health-server.ts` | `startWorkerHealthServer` — Liveness/Readiness inkl. Redis-/DB-Probes, getrennter Port. |
| Outbox | `apps/worker/src/outbox/{bootstrap,dispatcher,event-router}.ts` | Transactional-Outbox-Dispatcher (FOR UPDATE SKIP LOCKED). `event-router.ts`: `resolveOutboxQueueName` + `DEFAULT_OUTBOX_ROUTING` (Prefix->Queue, longest-prefix-wins: `mail.`, `webhook.`, `notification.`, `audit.`, `workflow.`, `search.index.`, `rag.index.`, `ai.cost.`, `marketing.lead.`, `form.submission.`, `billing.`, `plugin.hook.`, `plugin.capability-use.`, `tenant.subscription.`). |
| Scan | `apps/worker/src/scanService.ts` | ClamAV-Datei-Scan (Storage-Objekte). |
| Metrics-Scheduler | `apps/worker/src/metrics-scheduler.ts` | periodische Metrik-Erhebung. |
### Konkrete Queues (README + handlers.ts)
`toolctrl.email`, `toolctrl.notifications`, `toolctrl.maintenance`, `toolctrl.ai-chatbot`, `toolctrl.browser-automation`, `toolctrl.file-scan`, `toolctrl.exports`, `toolctrl.imports`, `toolctrl.plugin-invoke`. (`aiChatbot`/`browserAutomation` werfen `UnsupportedWorkerJobError` — bewusst nicht verdrahtet.)
### Jobs / Cron-Scheduler (`apps/worker/src/jobs/`, Auswahl, alle single-instance auf `general` sofern nicht anders)
- AI-Cost: `ai-cost-persist*`, `ai-cost-aggregate*` (daily), `ai-cost-reconciliation*` (monthly, nur mit `STRIPE_SK`), `ai-cost-stripe-report*` (24h), `ai-cost-alert.ts`.
- DSGVO/GDPR: `dsgvo-grace-cron.ts` (24h), `dsgvo-deletion.ts`, `dsgvo-export.ts`, `dsgvo-verify.ts`.
- Audit: `audit-event-persist*`, `audit-retention.ts`, `plugin-audit-retention.ts` (naechtlich 03:00 UTC, `PLUGIN_AUDIT_RETENTION_CRON`).
- Workflow-Engine: `workflow-trigger*` (Outbox-Subscriber + Executor http/native/hybrid), `workflow-execution-stalled-reconciler*` (5m).
- Mail/Marketing: `transactional-mail-outbox*` (Gruppe `mail`), `email-sequence-*` (Gruppe `mail`, `MARKETING_SEQUENCES_ENABLED`), `marketing-outbound.ts`, `lead-stale-trigger.ts`, `trial-expiring-trigger.ts`.
- Forms: `form-submission-route*`, `form-submission-webhook-delivery*`, `form-submission-followups.ts`.
- Suche/RAG: `search-indexer*`, `rag-indexer*` (Qdrant via `@toolctrl/vector-store`).
- Plugins: `plugin-invoke*` (Gruppe `plugin`, WASM-Sandbox, prod-gated `PLUGIN_RUNTIME_PRODUCTION_ENABLED`), `plugin-hook-fanout*`, `plugin-capability-use*`, `plugin-sandbox-isolated.ts`, `plugin-sandbox-worker.ts`.
- Sonstiges: `export.ts`, `push-notification.ts`, `legacy-event-receiver*`, `outbox-done-retention-cron.ts` (24h).
**Prisma-Modelle:** keine eigenen — via `@toolctrl/db/*.server` (z.B. `export-jobs.server`, `import-jobs.server`, `notifications.server`, `outbox`, `outbox-retention`, `event-webhook-attempts.server`, `entities.server`, `rows.server`).
**HiveGrid-Relevanz:** Direktes Fundament fuer alle **asynchronen / geplanten** HiveGrid-Querschnitte: Cron/Scheduling, Queue-Consumer, Transactional-Outbox (Event-Routing), DSGVO/Compliance-Jobs, Mail-/Notification-Versand, Import/Export-Pipelines, Such-/RAG-Indexierung, AI-Cost-Tracking, Workflow-Engine-Trigger und Plugin-Invoke-Sandbox. Das Gruppen-/Concurrency-Modell und die DI-`HandlerDependencies` sind das Muster fuer jedes neue HiveGrid-Hintergrund-Modul.
---
## 3. `@toolctrl/mobile` — Capacitor-7-Native-Wrapper
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/mobile`
**Zweck:** iOS+Android-Shell, die das React-Router-v7-Web-Build (`apps/web/build/client`) als WebView laedt. **Keine eigene UI** — exakt ein Produkt-UI (Web). Bundelt nur native Konzerne: Bundle-ID, Plugins, Session-Persistenz, Sentry, Push-Registration. App-Status: Scaffold + Push-Backend (Task #65, Phase 08); echte Builds/Store-Submission sind Operator-Tasks.
**Stack:** Capacitor 7 (`@capacitor/core` 7.4.3, CLI/ios/android), Plugins `app`, `browser`, `local-notifications`, `preferences`, `push-notifications`, `splash-screen`, `status-bar`; `@sentry/capacitor`; `@toolctrl/api-client-ts` (TS-SDK). Tests via `vitest`.
**Scripts:** `cap`, `sync`, `open:ios`, `open:android`, `build:web` (`pnpm --filter @toolctrl/web build`), `typecheck`, `test` (`vitest run src`).
### Zentrale Dateien
| Datei | Pfad | Inhalt |
|---|---|---|
| Capacitor-Config | `apps/mobile/capacitor.config.ts` | `appId: "de.toolctrl.app"`, `appName: "ToolCtrl"`, `webDir: "../web/build/client"`. `readLiveReloadUrl()` injiziert `server.url` aus `MOBILE_LIVE_RELOAD_URL` (nur Dev). Plugin-Config Splash/StatusBar/Push/Preferences (Suite `de.toolctrl.app.secure`). |
| Boot | `apps/mobile/src/index.ts` | `bootstrapMobileShell(options, deps)` — idempotenter Boot: Sentry-Init, Auth-Rehydrate, opt-in Push-Registration. Side-effect-frei in Tests. |
| Auth | `apps/mobile/src/auth.ts` | Session-Persistenz (Capacitor Preferences, **unverschluesselt**) um `@toolctrl/api-client-ts`. Typ `MobileSession`, `rehydrateAuth`. |
| Push | `apps/mobile/src/pushRegistration.ts` | `registerPushNotifications({apiBaseUrl})` — APNs/FCM-Token gegen Backend registrieren. |
| Sentry | `apps/mobile/src/sentry.ts` | `initMobileSentry` (Sentry-Capacitor), spiegelt `apps/web/app/sentry.client.ts`. |
| Native | `apps/mobile/ios/`, `apps/mobile/android/` | Minimal-Skelette (Info.plist, project.pbxproj, AppDelegate, build.gradle, AndroidManifest, MainActivity). Operator regeneriert via `npx cap add ios/android`. |
**Prisma-Modelle:** keine.
**HiveGrid-Relevanz:** Fertiges Muster fuer eine **mobile App-Huelle**, falls HiveGrid native iOS/Android-Distribution braucht: WebView-Wrapper um die bestehende Web-UI, Push-Notification-Anbindung, mobile Session-Persistenz, SDK-Konsum (`api-client-ts`). Hinweis: Capacitor Preferences ist nicht verschluesselt (Token kurzlebig halten).
---
## 4. `@toolctrl/hocuspocus` — Realtime-Collab-Server (Yjs)
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/hocuspocus`
**Zweck:** Standalone-Node-Service (eigener Container) fuer Echtzeit-Kollaboration ueber Yjs/Hocuspocus. Persistiert CRDT-Dokumente in Postgres, authentifiziert via kurzlebiges JWT, ist tenant-isoliert.
**Stack:** `@hocuspocus/server` 4 + `@hocuspocus/extension-database` + `@hocuspocus/extension-logger`, `yjs` 13.6, `y-protocols`, `jsonwebtoken`, `@toolctrl/db`, `@toolctrl/logger`. Port: `HOCUSPOCUS_PORT` -> `PORT` -> `1234`.
**Scripts:** `dev` (`tsx watch src/index.ts`), `start`, `build`, `typecheck`, `test` (`vitest run src`). Eigenes `apps/hocuspocus/Dockerfile`.
### Zentrale Dateien
| Datei | Pfad | Inhalt |
|---|---|---|
| Entry | `apps/hocuspocus/src/index.ts` | `Server`-Bootstrap; refuses boot ohne `HOCUSPOCUS_AUTH_SECRET`. Debounce 2s / maxDebounce 10s / timeout 30s. Verdrahtet Logger + Auth + Database-Extension. |
| Config | `apps/hocuspocus/src/config.ts` | `getHocuspocusRuntimeConfig(env)` -> `{ port, host, authSecret, maxPayloadBytes }` (default 10 MiB). Env: `HOCUSPOCUS_PORT/HOST/AUTH_SECRET/MAX_PAYLOAD_BYTES` (+ Aliase). |
| Auth-Extension | `apps/hocuspocus/src/extensions/auth.ts` | `createAuthExtension({secret})`, `verifyCollabToken`, `parseDocumentName`, Typ `HocuspocusConnectionContext`. JWT HS256, prueft `tenantId:resourceType:resourceId`-Parity gegen DocumentName (Cross-Tenant-Schutz). Leeres Secret = jede Connection abgelehnt. |
| Database-Extension | `apps/hocuspocus/src/extensions/database.ts` | `createDatabaseExtension({db, maxPayloadBytes})`, `fetchCollabDoc`/`storeCollabDoc`. Upsert auf `collabDoc` (Composite-Key `tenantId_resourceType_resourceId`), bumpt `version`, refresht `lastUpdatedAt`, Bytes <-> Uint8Array. |
**Prisma-Modell (konsumiert, nicht besessen):** `CollabDoc` in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/collabDocs.prisma` (Feld `ydocState: Bytes`, Composite-Unique `(tenantId, resourceType, resourceId)`, `version`, `lastUpdatedAt`). Zugriff via `@toolctrl/db/client.server` (`getWriteDbClient`).
**HiveGrid-Relevanz:** Direktes Fundament fuer **Realtime-Collaboration / kollaborative Dokumente** in HiveGrid (z.B. geteilte Notizen, Live-Editing, gemeinsame Boards). Liefert tenant-isolierte CRDT-Persistenz + JWT-Auth-Bridge out-of-the-box. Web-Seite muss ein Collab-Token-Endpoint bereitstellen (verifiziert in `auth.ts`).
---
## 5. Root-Config & Build/Deploy
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/`
| Datei/Bereich | Pfad | Inhalt / Relevanz |
|---|---|---|
| React-Router-Config | `react-router.config.ts` | RR7: `ssr: true`, `future.unstable_optimizeDeps`. Steuert das Web-Build, das auch Mobile (`webDir`) konsumiert. |
| Dockerfile (multistage) | `Dockerfile` | Stages: `base` (node:20-bookworm-slim, amd64), `deps`, `production-deps`, `build`, `web-build`/`api-build`/`worker-build`, `*-runtime-artifacts`, finale Targets **`web` / `api` / `worker`** (`--target`). Non-root erzwungen (`verify:docker-non-root`). |
| Hocuspocus-Dockerfile | `apps/hocuspocus/Dockerfile` | Eigenes Image fuer den Collab-Service. |
| Compose (dev) | `docker-compose.yml` | Worker-Topologie: 5 Gruppen-Services `worker-general/-mail/-import/-webhook/-plugin` + `redis`; Processors per Default disabled. |
| Compose (staging) | `docker-compose.staging.yml` | Staging-Topologie. |
| ~~Fly~~ | ~~`fly.toml`~~ | _Entfernt 2026-06-15 — Infra = Hetzner/Coolify (ADR-009/010), nicht mehr Fly.io._ |
| Prisma-Config | `prisma.config.ts`, `prisma/schema/` | Multi-File-Prisma-Schema (Owner: `@toolctrl/db`); Apps konsumieren generierten Client. |
| Env | `.env.example`, `.env.fly.example`, `.env.production.example`, `.env.e2e-readiness`, `.env.test`, `env.d.ts` | Env-Katalog; Drift via `verify:env-drift`. |
| Lint/Format | `.eslintrc`, `.eslintrc.a11y-baseline.json`, `.prettierrc`, `tsconfig.json`, `vitest.config.ts`, `vitest.setup.ts` | Repo-weite Qualitaets-Config. |
| Lighthouse | `.lighthouserc.json`, `.lighthouserc.staging.json` | Lighthouse-CI-Budgets. |
| Hooks | `lefthook.yml` | Git-Hooks. |
| Playwright | `playwright.config.ts` | E2E-Config (a11y/mutating readiness). |
**HiveGrid-Relevanz:** Das Multistage-Dockerfile (gemeinsame `base`/`build`, getrennte Runtime-Targets `web`/`api`/`worker`) ist das wiederverwendbare Deploy-Fundament; die Compose-Worker-Gruppen geben die Produktions-Skalierungstopologie vor.
---
## 6. CI-Gates (`.gitea/workflows/`)
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/.gitea/`
| Workflow | Pfad | Zweck |
|---|---|---|
| CI | `.gitea/workflows/ci.yml` | Haupt-PR-Gate. Jobs: `quality` (Quality Gates), `api`, `worker`, `database`, `docker-smoke` (needs quality/api/worker/database), `resolve-sentry-environment`, `sentry-release-{api,worker,web}`. Runner `toolctrl-ci`, pnpm 10.33.0. |
| Deploy | `.gitea/workflows/deploy.yml` | Auf `push main` + `workflow_dispatch` (`stage`/`promote`). Preflight-Gates, Docker stage/promote in Gitea-Registry, cosign-Signatur, Coolify-Staging-Deploy + Health-Wait, DB-Migrate-Command. |
| a11y | `.gitea/workflows/a11y.yml` | Accessibility-CI. |
| Bundle-Budget | `.gitea/workflows/bundle-budget.yml` | Bundle-Size-Budget. |
| Coverage | `.gitea/workflows/coverage.yml` | Test-Coverage-Gate. |
| E2E-Readiness | `.gitea/workflows/e2e-readiness.yml` | E2E-Vorbedingungen. |
| Lighthouse | `.gitea/workflows/lighthouse.yml` | Performance/Lighthouse. |
| Promptfoo | `.gitea/workflows/promptfoo.yml` | LLM-Eval-Gate. |
| SDK-Codegen | `.gitea/workflows/sdk-codegen.yml` | TS-SDK aus OpenAPI generieren. |
| Storybook | `.gitea/workflows/storybook.yml` | Storybook-Build. |
| Sentry-Release (reusable) | `.gitea/workflows/_sentry-release.yml` + `.gitea/actions/sentry-release/action.yml` | Sentry-Release/Sourcemap-Upload. |
| Renovate | `.gitea/renovate.json` | Dependency-Updates. |
### CI-`quality`-Job — Gate-Reihenfolge (ci.yml, Auswahl)
verify:github-ci-workflow -> verify:gitea-ci-workflow -> verify:deploy-workflow -> verify:deploy-readiness -> verify:docker-non-root -> verify:ci-baseline -> verify:source-health -> verify:eslint-baseline -> verify:env-drift -> verify:health-baseline -> verify:ai-provider-deps -> verify:promptfoo-baseline -> verify:api-inventory -> verify:tenant-isolation-report -> verify:sidebar-routes -> **verify:i18n** -> verify:tenant-auth-baseline + verify:onboarding-flow -> **verify:phase-c-boundaries** -> verify:core-build-without-plugins-smoke + verify:removed-modules -> **verify:ui-visual** -> **test:hot-swap** -> verify:a11y-coverage -> test:a11y -> Docs-Gates -> typecheck -> lint -> test:foundation -> test:packages -> **test:mobile** -> test (unit) -> build:ci -> verify:bundle-size -> Sourcemap-Check -> verify:production-seed-artifact.
**`api`-Job:** verify:openapi-security -> typecheck -> `vitest run apps/api/src` -> build.
**`worker`-Job:** typecheck -> `vitest run` -> verify:workflow-outbox-native -> build -> `health` (mit `WORKER_ENABLED=false`).
**`database`-Job:** db:generate -> verify:db-migrate-status:staging (nicht-PR) -> `@toolctrl/db test`.
**`docker-smoke`-Job:** baut `--target web/api/worker` parallel, validiert Artefakte + Runtime-Smoke (`scripts/docker-{web,api,worker}-runtime-smoke.sh`) + `docker compose config`.
---
## 7. `verify:*`-Scripts (`scripts/verify-*`)
Alle `verify:*` sind npm-Scripts in `package.json`, die `scripts/verify-*.mjs` / `.ts` / `.sh` ausfuehren. Jeder Verifier hat i.d.R. eine `*.test.mjs/.ts`-Datei daneben (selbst getestet). Die vom Task explizit genannten Gates:
| Script (via `pnpm run`) | Datei | Was es prueft |
|---|---|---|
| `verify:phase-c-boundaries` | `scripts/verify-phase-c-boundaries.mjs` | Architektur-Boundaries der Phase-C-Pakete: `packages/foundation`, `view-models`, `ui-app`, `ui-subframe`, `ui-mock` duerfen **kein** `@toolctrl/db` (foundation/view-models zusaetzlich kein `react`/`react-dom`); prueft Paketnamen, verbotene deps, verbotene Imports (`~/`, `app/`), `packages/*` im Workspace. |
| `verify:i18n` | `scripts/i18n-check.ts` + `scripts/verify-hardcoded-strings.mjs` + ESLint auf `ui-*` | i18n-Key-Vollstaendigkeit + keine hardcodierten UI-Strings in den UI-Paketen. |
| `verify:ui-visual` | `scripts/verify-ui-mock-visual.ts` | Statischer Visual-Baseline-Check fuer das UI-Mock-Paket. |
| `verify:ui-hot-swap` / `test:hot-swap` | `scripts/verify-ui-hot-swap.ts` (+ `.test.ts`) | UI-Mock-Hot-Swap (Austauschbarkeit der UI-Implementierung). |
| `test` (unit) | inline (`scripts/vitest-cli-compat.mjs`) | Core-Tests (web app, scripts, api-inventory, db-boundary, create-toolctrl). |
**Weitere relevante `verify:*` (Auswahl, Apps&Infra-naeher Querschnitt):**
- API/Auth: `verify:openapi-security`, `verify:api-inventory`, `verify:api-auth-routing-baseline`, `verify:api-auth-enforced-baseline`, `verify:api-auth-hardening-baseline`, `verify:auth-api-body-baseline`, `verify:route-inventory`, `verify:tenant-auth-baseline`, `verify:mfa-scope-baseline`, `verify:impersonation-scope-baseline`, `verify:permission-matrix`, `verify:role-permission-baseline`, `verify:permissions-audit-baseline`.
- Worker/Domain-Boundaries: `verify:workflow-outbox-native`, `verify:db-boundary-baseline`, `verify:export-boundary-baseline`, `verify:import-boundary-baseline`, `verify:notification-boundary-baseline`, `verify:realtime-boundary-baseline`, `verify:storage-boundary-baseline`, `verify:billing-boundary-baseline`, `verify:transactional-mail-provider-baseline`, `verify:compliance-retention-baseline`, `verify:audit-cascade-guard`.
- Build/Deploy/Docker/CI: `verify:docker-baseline`, `verify:docker-non-root`, `verify:docker-runtime-smoke`, `verify:docker-api-runtime-smoke`, `verify:docker-worker-runtime-smoke`, `verify:ci-baseline`, `verify:github-ci-workflow`, `verify:gitea-ci-workflow`, `verify:deploy-workflow`, `verify:deploy-readiness`, `verify:db-migrate-status[:staging]`, `verify:bundle-size`, `verify:health-baseline`, `verify:health-ci-quality-baseline`, `verify:env-drift`, `verify:secrets`, `verify:licenses`, `verify:production-seed-artifact`.
- Qualitaet/Docs/a11y: `verify:source-health`, `verify:eslint-baseline`, `verify:test-density`, `verify:test-coverage`, `verify:a11y-baseline`, `verify:a11y-coverage`, `verify:color-contrast`, `verify:hardcoded-strings`, `verify:i18n-hardcoded-baseline`, `verify:theme-error-state-baseline`, plus diverse `verify:*-docs(-baseline)` und `verify:*-decision-report`.
- Plugins/Features: `verify:plugin-runtime-live`, `verify:core-build-without-plugins-smoke`, `verify:removed-modules`, `verify:feature-manifest-baseline`, `verify:ai-provider-deps`, `verify:promptfoo-baseline`.
**HiveGrid-Relevanz der Gates:** Dieses `verify:*`-Set ist die wiederverwendbare **Guard-Rail-Bibliothek** — Architektur-Boundaries (phase-c), i18n-Disziplin, UI-Hot-Swap/Visual, Docker-Non-Root, Deploy-Readiness, API-Auth-Baselines, Tenant-Isolation. HiveGrid-Module sollten dieselben Gates erben/erweitern; jeder neue Boundary (z.B. neues Querschnittspaket) bekommt einen eigenen `verify:*-baseline`.
---
## 8. `infra/` — Self-hosted Observability & Auth
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/infra`
| Verzeichnis | Pfad | Inhalt |
|---|---|---|
| Grafana | `infra/grafana/` | `docker-compose.yml`, Provisioning (Datasource Prometheus, Dashboards). Dashboards: `overview.json`, `api-latency.json`, `queue-health.json`, `business-kpi.json`, `ai-cost-per-tenant.json`. |
| Prometheus | `infra/prometheus/` | `prometheus.yml`, `docker-compose.yml`, `secrets/` (scrapt API+Worker `/metrics`, token-protected). |
| Sentry | `infra/sentry/` | Self-hosted Sentry: `install.sh`, `docker-compose.override.yml`, `relay/config.yml`, `sentry/sentry.conf.py`, Backup-Setup, Traefik-Labels. |
| Authentik | `infra/authentik/` | Self-hosted IdP: `docker-compose.yml`, Bootstrap-Blueprints (`01-groups.yaml`, `02-policies.yaml`, `03-application-toolctrl-admin.yaml`), Backup, Smoke, Traefik-Labels. |
| PgBouncer | `infra/pgbouncer/` | `pgbouncer.ini`, `userlist.txt.template` (Connection-Pooling vor Postgres). |
**HiveGrid-Relevanz:** Komplettes **self-hosted Observability-/Auth-Stack-Fundament**: Prometheus-Scrape von API+Worker-Metriken, vorgefertigte Grafana-Dashboards (inkl. Queue-Health + AI-Cost-per-Tenant + Business-KPI), Sentry fuer Error-Reporting (DSN je App), Authentik als IdP, PgBouncer fuer DB-Pooling. Direkt uebernehmbar fuer HiveGrid-Betrieb.
---
## Hinweise / Grenzen dieser Inventur
- Keine der vier Apps besitzt ein eigenes Prisma-Schema; alle Modelle (inkl. das von Hocuspocus konsumierte `CollabDoc`) liegen unter `prisma/schema/` und gehoeren zum `@toolctrl/db`-Paket (anderer Inventur-Bereich).
- `apps/web` und `apps/storybook` sind hier bewusst **nicht** inventarisiert (gehoeren zu Web-/UI-Bereichen), werden aber referenziert: Mobile laedt `apps/web/build/client`, Hocuspocus braucht einen Collab-Token-Endpoint der Web-App.
- `worker/src/index.ts` (~1678 Z.) wurde teil-gelesen (Boot/Cron-Registrierung bis Z. ~1100); die hier gelistete Job-/Cron-Menge stammt aus dem Datei-Listing `apps/worker/src/jobs/` + den gelesenen Bootstrap-Bloecken und ist faktisch, aber nicht jede einzelne Job-Datei wurde voll geoeffnet.
- `verify:*`-Beschreibungen basieren auf Script-Namen + `package.json`-Kommandos; voll gelesen wurde `verify-phase-c-boundaries.mjs`.
@@ -0,0 +1,261 @@
# Boilerplate-Inventur: Daten-Layer (Prisma)
> FAKTISCHE Inventur des Datenbank-Bereichs der Boilerplate.
> Boilerplate-Wurzel: `/Users/raffaelsheikh/Documents/GitHub/boilerplate`
> Nur gelesen — nichts verändert. Diese Datei ist das einzige Schreib-Artefakt.
## Übersicht
| Aspekt | Fakt |
| --- | --- |
| Paket | `@toolctrl/db` (`packages/db/package.json`, `private`, `type: module`) |
| ORM | Prisma `7.8.0` + `@prisma/client` `7.8.0` + `@prisma/adapter-pg` `7.8.0` (Decision: bei Prisma 7 bleiben — `packages/db/ORM-DECISION.md`) |
| Datenbank | PostgreSQL |
| Schema-Ort | `prisma/schema/*.prisma` (27 `.prisma`-Dateien inkl. `schema.prisma` + `app.prisma`-Platzhalter) |
| Migrationen | `prisma/migrations/` (150 Migrations-Ordner; `migration_lock.toml` → provider `postgresql`) |
| Generierter Client | `packages/db/generated/prisma/client` (Output via `generator client` in `schema.prisma`) |
| Modelle gesamt | **190** Prisma-Modelle (Brief nannte ~184) + **6** Enums |
| Verbindungs-URLs | NICHT im Schema (Prisma 7) — liegen in `packages/db/prisma.config.ts`: Migrate nutzt `DIRECT_URL` (Port 5432), Runtime `DATABASE_URL` (Port 6432 / PgBouncer); optional `DATABASE_READ_URL` für Read-Replica |
### Modelle pro Schema-Datei (faktische Zählung)
| Datei | Modelle | Datei | Modelle |
| --- | ---: | --- | ---: |
| `accounts.prisma` | 13 | `knowledgeBase.prisma` | 9 |
| `ai.prisma` | 8 | `marketingLeads.prisma` | 4 |
| `analytics.prisma` | 4 | `notifications.prisma` | 3 |
| `app.prisma` | 0 (Platzhalter `// custom database models`) | `onboarding.prisma` | 7 |
| `appsEngine.prisma` | 5 | `outbox.prisma` | 1 |
| `collabDocs.prisma` | 1 | `permissions.prisma` | 6 |
| `core.prisma` | 16 | `plugins.prisma` | 5 |
| `email.prisma` | 10 | `portals.prisma` | 12 |
| `emailSequences.prisma` | 3 | `promptFlows.prisma` | 12 |
| `entityBuilder.prisma` | 36 | `queue.prisma` | 2 |
| `featureFlags.prisma` | 3 | `sites.prisma` | 1 |
| `formWidget.prisma` | 2 | `subscriptions.prisma` | 14 |
| `helpDesk.prisma` | 2 | `workflows.prisma` | 10 |
| `imports.prisma` | 1 | `schema.prisma` | 0 (generator + datasource) |
---
## Querschnitt-Konventionen (über alle Schemas)
| Konvention | Fakt | Beispiele |
| --- | --- | --- |
| Primärschlüssel | `id String @id @default(cuid())` durchgängig. Ausnahmen: `FileChunk.id Int @default(autoincrement())`, `PluginInvocation.id String @id` (extern gesetzt), Join-Tabellen mit `@@id([...])` (z.B. `PromptFlowGroupEntity`). | — |
| Audit-Timestamps | `createdAt DateTime @default(now())` fast überall; `updatedAt DateTime @updatedAt` auf mutierbaren Entitäten. | nahezu alle Modelle |
| Tenant-Scoping | `tenantId String` (pflicht) oder `tenantId String?` (nullable für system-/global-scoped). `tenant Tenant @relation(... onDelete: Cascade)`. Nullable = system-/operator-weit. | `App.tenantId` (pflicht), `Workflow.tenantId?` (nullable), `Lead` (KEIN tenantId — operator-scoped bis Conversion) |
| Soft-Delete | NUR `Row.deletedAt DateTime?` (`entityBuilder.prisma`) mit `@@index([deletedAt])`. `ExportJob` hat `deletedAt` + `hardDeletedAt` (Retention). Sonst Hard-Delete via `onDelete: Cascade`. | `Row`, `ExportJob` |
| FK-Delete-Strategie | `onDelete: Cascade` (Default), `onDelete: SetNull` (Audit-/Tombstone-Spalten), `onDelete: NoAction`. | `ImpersonationAudit` (SetNull), `Chat.user` (NoAction) |
| Tamper-Evidenz | Hash-Ketten: `AuditEvent` (`previousHash`/`eventHash`), `PluginCapabilityUse` (`prevHash`/`rowHash`), `FeatureFlagAudit` (append-only). | `core.prisma`, `plugins.prisma`, `featureFlags.prisma` |
| DSGVO/PII | IPs als SHA-256-Hash (`ipHash`, `lastIpHash`); Pseudonymisierungs-Flags auf `User` (`pseudonymized`, `scheduledDeletionAt`, `deletionRequestId`); zentrale FK-Karte `packages/db/src/dsgvo/user-fk-map.ts`. | `FormSubmission.ipHash`, `Lead.lastIpHash`, `User` |
| Idempotenz | `dispatchKey`/`requestId`/`triggerEventId` `@unique`; `IdempotencyKey`-Modell für HTTP-Response-Caching. | `OutboxEvent.dispatchKey`, `AICostEvent.requestId` |
| MFA/Secrets | Verschlüsselte Spalten mit `…Encrypted`-Suffix; JSON-Configs mit verschlüsselten Secrets (`configJson`). | `User.totpSecretEncrypted`, `MarketingIntegration.configJson` |
---
## Paket `@toolctrl/db`
**Zweck:** Eigentümer-Paket für die server-seitige Prisma-Client-Grenze, den generierten Client und die Repository-Datenbank-Befehlsfläche. Schema + Migrationen bleiben (Stand jetzt) unter `prisma/`.
### Zentrale Dateien (mit absolutem Pfad)
| Datei | Inhalt / API |
| --- | --- |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/index.ts` | Root-Barrel: re-exportiert auth-boundary, conversations/history, audit-events, export-jobs, idempotency-keys, import-jobs, notifications, outbox(+claim/retention/receiver), permissions-seed, seed, settings, transactions, workspace-invites; `databaseBoundary` / `getDatabaseBoundary()` Metadaten-Objekt |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/client.server.ts` | `getDbClient()` (Dev-Singleton), `getWriteDbClient()` (Primary), `getReadDbClient()` (Replica via `DATABASE_READ_URL`, Fallback Primary), `getExtendedDbClient()` (`$extends` mit `.read`/`.write`), Typen `DbClient`/`ExtendedDbClient`. Nutzt `PrismaPg`-Adapter + `@toolctrl/logger` |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/transactions.server.ts` | `runDbTransaction(callback, client?)`, `runDbTransactionBatch(operations, client?)`, Typen `DbTransactionClient`/`DbTransactionRunner` |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/prisma.ts` | Konstanten der Ownership-Metadaten: `PRISMA_SCHEMA_PATH="prisma/schema"`, `PRISMA_MIGRATIONS_PATH="prisma/migrations"`, `PRISMA_GENERATED_CLIENT_PATH`, `PRISMA_SEED_COMMAND="tsx prisma/seed.ts"` |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/audit-hash.ts` | Tamper-evidente Hash-Kette für `AuditEvent`: `computeAuditHash()`, `verifyAuditChain()`, `canonicalizeAuditPayload()`; HMAC-SHA-256 via `AUDIT_CHAIN_SECRET` (Fallback SHA-256) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/dsgvo/user-fk-map.ts` | `USER_FK_MAP` — enumeriert ALLE Modelle mit FK auf `User.id` + Cascade-Strategie (`hard-delete` / `pseudonymize` / `retain-tombstone`) + `includeInExport`. Test erzwingt Vollständigkeit gegen Schema (`user-fk-map.test.ts`) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/dsgvo/pseudonymize.ts` | DSGVO-Pseudonymisierungs-Helfer |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/outbox.ts` | Transaktionaler Outbox-Publisher: `publishOutboxEvent(tx, args)`, `OUTBOX_EVENT_TYPE_PATTERN` (`<domain>.<entity>.<verb>`), `OUTBOX_DISPATCH_KEY_PATTERN`, `OUTBOX_MAX_PAYLOAD_BYTES=64 KiB` |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/prisma.config.ts` | Prisma-7-Config: Schema-/Migrations-Pfad, Seed-Befehl, `datasource.url = env("DATABASE_URL")` |
### Server-Module (Repository-Befehlsfläche, je `*.server.ts` mit `*.test.ts`)
Alle unter `packages/db/src/` (Auswahl der Domänen-Repositories):
`accounts`, `analytics`, `api-keys`, `audit-events`, `auth-boundary`, `auth-providers`, `conversations/history`, `device-tokens`, `email-sequences`, `entities`, `entitlements-seed`, `events`, `event-webhook-attempts`, `export-jobs`, `import-jobs`, `idempotency-keys`, `impersonation-audit`, `kpi`, `logs`, `notifications`, `permissions-seed`, `queue-failed-jobs`, `relationships`, `rows`, `seed`, `settings`, `subscriptions`, `support-tickets`, `telemetry`, `transactions`, `usage`, `users`, `widgets`, `workspace-invites`.
Subpath-Exporte sind in `package.json` deklariert (z.B. `@toolctrl/db/rows.server`, `@toolctrl/db/audit-events.server`, `@toolctrl/db/outbox`).
### Scripts & CI / verify
| Script | Zweck |
| --- | --- |
| `prisma:generate` | `prisma generate` (Root-Config) |
| `prisma:migrate:check` | `scripts/db/check-direct-url.sh --skip-ping` (erzwingt DIRECT_URL) |
| `prisma:migrate:deploy` | check + `prisma migrate deploy` |
| `prisma:migrate:status` | Migrations-Status |
| `seed` / `permissions:sync` | `tsx prisma/seed.ts` bzw. permissions-seed |
| `build` | esbuild-Bundles: `permissions-seed`, `production-seed`, `e2e-readiness-seed` |
| Root `verify:db-migrate-status` | `scripts/verify-db-migrate-status.mjs` (CI-Gate, benötigt `DATABASE_URL`) |
| Root `verify:db-migrate-status:staging` | `scripts/verify-db-migrate-status-staging.sh` |
**HiveGrid-Relevanz:** Fundament für JEDES HiveGrid-Modul. Die Client-/Transaktions-/Outbox-/Audit-Helfer und das Tenant-Scoping-Muster sind direkt wiederverwendbar. `getReadDbClient()`/`getExtendedDbClient()` deckt Read-Heavy HiveGrid-Dashboards ab. `user-fk-map.ts` ist Pflicht-Referenz für jedes neue Modul mit User-Bezug (DSGVO).
---
## Schema-Domänen (Modelle + Kernfelder + HiveGrid-Relevanz)
### accounts.prisma (13) — `prisma/schema/accounts.prisma`
Modelle: `User`, `MagicLinkToken`, `AdminUser`, `Tenant`, `TenantChatbotAccess`, `WorkspaceAuthProvider`, `ImpersonationAudit`, `TenantUser`, `TenantUserInvitation`, `Registration`, `Blacklist`, `TenantIpAddress`, `TenantType`.
- `User`: email-unique, `passwordHash?`, OAuth (`githubId`/`googleId`), MFA (`totpSecretEncrypted`, `mfaRecoveryCodeHashes`), DSGVO-Flags (`pendingDeletion`, `scheduledDeletionAt`, `pseudonymized`, `deletionRequestId`), `defaultTenantId`. Index `user_pending_deletion_due_idx`.
- `Tenant`: `slug @unique`, `name`, `active`, `subscriptionId?`, Chatbot-Felder, AI-Cost-Override-Felder, Theme. Zentraler Tenant-Scope-Anker.
- `TenantUser` (Membership: `type`/`joined`/`status` als Int), `TenantUserInvitation` (`tokenHash`/`tokenPrefix`/`expiresAt`/`status`), `WorkspaceAuthProvider` (SSO/SAML/OIDC pro Tenant), `AdminUser` (Admin-Panel + Authentik-OIDC `ssoProvider`/`ssoSubjectId`), `ImpersonationAudit`.
**HiveGrid-Relevanz:** Identity / Auth / Mandanten-Kern für ALLE HiveGrid-Module (Multi-Tenant-Fundament, SSO, Invitations, Impersonation-Audit). MFA + DSGVO-Selfservice direkt nutzbar.
### core.prisma (16) — `prisma/schema/core.prisma`
Modelle: `AppConfiguration`, `Log`, `AuditEvent`, `IdempotencyKey`, `ApiKey`, `ApiKeyLog`, `Event`, `EventWebhookAttempt`, `MetricLog`, `FileUploadProgress`, `FileChunk`, `FileObject`, `IpAddress`, `IpAddressLog`, `Widget`, `Credential`.
- `AuditEvent`: tamper-evidente Hash-Kette (`previousHash`/`eventHash`), `actorType`/`actorId`/`actorEmail`, `dataSensitivity`, `regulatoryScope[]`, `outcome`. Reichlich Indizes auf `(tenantId, createdAt)` etc.
- `ApiKey`: `keyHash`/`keyPrefix`/`keyLastFour`/`keyVersion` (gehashte Keys), `@@unique([tenantId, alias])`. `ApiKeyLog` für Request-Logging.
- `IdempotencyKey` (`@@unique([scope, key])`), `FileObject`/`FileUploadProgress`/`FileChunk` (Storage-Metadaten), `Credential` (globaler Secret-Store, `value Json`).
**HiveGrid-Relevanz:** Querschnitt — Audit-Trail (Compliance), API-Keys, File-Storage, Idempotenz, Telemetrie. Für HiveGrid-Logging/Audit und externe API-Integrationen unmittelbar verwendbar.
### permissions.prisma (6) — `prisma/schema/permissions.prisma`
Modelle: `Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser`.
- `Role` (tenant-scoped optional, `type`, `isDefault`, `assignToNewUsers`), `Permission` (global `name @unique`, optional `entityId`), Join `RolePermission`. `UserRole` mit nullable `tenantId` (partielle Eindeutigkeit via Migration-SQL). `Group`/`GroupUser` für Gruppen-basierte Berechtigung.
**HiveGrid-Relevanz:** RBAC-Fundament. Jedes HiveGrid-Modul mit Rollen/Rechten baut hierauf auf. Entity-gebundene Permissions koppeln an den Entity-Builder.
### app.prisma (0) — `prisma/schema/app.prisma`
Reiner Platzhalter (`// custom database models`) für projekt-/HiveGrid-spezifische Modelle.
**HiveGrid-Relevanz:** Vorgesehener Ort für HiveGrid-eigene Modelle ohne die Boilerplate-Domänen zu verändern.
### appsEngine.prisma (5) — `prisma/schema/appsEngine.prisma`
Modelle: `App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem`.
- `App` (`type`, `config` JSON-String, `@@unique([tenantId, type])`), `DataConnection` (verschlüsselte Credentials), `AppSyncRun` (Status/Mode/Metriken `itemsProcessed`…), `AppSyncItem` (Source→Destination-Mapping + Rollback).
**HiveGrid-Relevanz:** Integrations-/Sync-Engine — relevant für HiveGrid-Module, die externe Datenquellen (REST/OAuth/Webhook) synchronisieren, inkl. Rollback-fähiger Sync-Runs.
### subscriptions.prisma (14) — `prisma/schema/subscriptions.prisma`
Modelle: `SubscriptionProduct`, `SubscriptionPrice`, `SubscriptionUsageBasedPrice`, `SubscriptionUsageBasedTier`, `SubscriptionFeature`, `TenantSubscription`, `TenantSubscriptionProduct`, `TenantSubscriptionProductPrice`, `TenantSubscriptionUsageRecord`, `TenantProductConfiguration`, `TenantFeatureOverride`, `CheckoutSessionStatus`, `Credit`.
- Stripe-gekoppelt (`stripeId`, `stripeCustomerId`, `stripeSubscriptionId`), Usage-Based-Billing (Tiers, UsageRecords), `TenantFeatureOverride` (`@@unique([tenantId, featureKey])`), `TenantProductConfiguration.blueprintId` (Default `crm-saas`), `Credit` (Guthaben pro Tenant/User).
**HiveGrid-Relevanz:** Billing / Entitlements / Feature-Gating-Fundament für HiveGrid-Pläne. Gekoppelt mit `@toolctrl/entitlements` (Workspace-Dependency).
### notifications.prisma (3) — `prisma/schema/notifications.prisma`
Enums: `NotificationDeliveryStatus`, `NotificationPreferenceChannel`. Modelle: `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken`.
- `NotificationDelivery` (Channel IN_APP/EMAIL, `status`, `attemptsMade`, `providerMessageId`, `@@unique([tenantId, userId, channel, sourceKey])`), `UserNotificationPreference` (pro Channel+Category), `DeviceToken` (APNs/FCM/WebPush, `token @unique`, `revokedAt`).
**HiveGrid-Relevanz:** Benachrichtigungs-Querschnitt (In-App + E-Mail + Push) für alle HiveGrid-Module mit User-Notifications. Mobile-Push (Capacitor) bereits modelliert.
### email.prisma (10) — `prisma/schema/email.prisma`
Modelle: `TenantInboundAddress`, `Email`, `EmailRead`, `EmailCc`, `EmailAttachment`, `EmailSender`, `Campaign`, `OutboundEmail`, `OutboundEmailOpen`, `OutboundEmailClick`.
- Inbound (`Email` inbound/outbound, `messageId @unique`) + Outbound-Kampagnen (`Campaign`, `OutboundEmail` mit `sentAt`/`bouncedAt`/`unsubscribedAt`, Open/Click-Tracking).
**HiveGrid-Relevanz:** E-Mail-Infrastruktur (transaktional + Kampagnen) für HiveGrid-Kommunikationsmodule und CRM-Outreach.
### emailSequences.prisma (3) — `prisma/schema/emailSequences.prisma`
Modelle: `EmailSequence`, `EmailSequenceRun`, `EmailSequenceStepDelivery`.
- Drip-Kampagnen via Outbox-Events (`triggerEvent`), `stepsJson` (linear), Run-State, Step-Delivery mit Open/Click/Bounce. `@@unique([sequenceId, triggerEventId])` (Idempotenz).
**HiveGrid-Relevanz:** Marketing-/Lifecycle-Automation. Relevant für HiveGrid-Onboarding-/Nurture-Flows; eng an Outbox + marketingLeads gekoppelt.
### marketingLeads.prisma (4) — `prisma/schema/marketingLeads.prisma`
Enum: `LeadStatus`. Modelle: `Lead`, `LeadEvent`, `TenantAcquisition`, `MarketingIntegration`.
- `Lead` (NICHT tenant-scoped — operator-/sales-scoped, `email @unique`, First-Touch-UTM-Attribution, `score`, `convertedToTenantId`, `lastIpHash`), `LeadEvent` (Timeline-Append-Log), `TenantAcquisition` (Attributions-Snapshot bei Conversion, `tenantId @unique`), `MarketingIntegration` (verschlüsselte `configJson`, `triggerEvents[]`).
**HiveGrid-Relevanz:** Lead-/Funnel-/Attribution-Modul. Relevant für HiveGrid-Akquise/Marketing; zeigt das Muster „operator-scoped statt tenant-scoped".
### entityBuilder.prisma (36) — `prisma/schema/entityBuilder.prisma`
Größte Domäne (No-Code-/Dynamic-Entity-Engine). Kernmodelle:
- `Entity` (`name`/`slug`/`prefix` unique, `type` app/admin/all, Feature-Flags `hasTags`/`hasComments`/`hasTasks`/`hasViews`, `defaultVisibility`), `Property` (typisierte Spalten, `type Int`/`subtype`, `formulaId?`, `@@unique([entityId, name, tenantId])`), `EntityView` (Layout table/board/calendar…, Grid-Konfig, Filter/Sort/GroupBy), `EntityViewProperty`/`EntityViewFilter`/`EntityViewSort`, `PropertyAttribute`/`PropertyOption`, `EntityTag`, `EntityRelationship`/`RowRelationship` (Parent/Child), `EntityWebhook`/`EntityWebhookLog`.
- **Row-Daten:** `Row` (`folio`, `deletedAt?` Soft-Delete, `order`, FK auf Entity/Tenant/User), `RowValue` (polymorphe Werte: text/number/date/boolean), `RowValueMultiple`, `RowValueRange`, `RowMedia`, `RowPermission` (row-level: role/group/user/public), `RowTag`, `RowComment`/`RowCommentReaction`, `RowTask`.
- `Formula`/`FormulaComponent`/`FormulaLog`/`FormulaComponentLog`, `EntityGroup`/`EntityGroupEntity`/`EntityGroupConfiguration(Row)`, `EntityTemplate`, `ApiKeyEntity` (per-Entity-CRUD-Rechte), `TenantSettingsRow`, `SampleCustomEntity`.
**HiveGrid-Relevanz:** Kern-Datenmodell-Engine. Wenn HiveGrid dynamische/benutzerdefinierte Entitäten, Tabellen-Views, Row-Level-Permissions, Tags/Tasks/Comments oder Formeln braucht, ist dies das Fundament. Größter Wiederverwendungs-Hebel und größte Komplexität.
### portals.prisma (12) — `prisma/schema/portals.prisma`
Modelle: `Portal`, `PortalUser`, `PortalPage`, `PortalUserRegistration`, `PortalSubscriptionProduct`, `PortalSubscriptionPrice`, `PortalSubscriptionFeature`, `PortalUserSubscription`, `PortalUserSubscriptionProduct`, `PortalUserSubscriptionProductPrice`, `PortalCheckoutSessionStatus`.
- `Portal` (`subdomain @unique`, optional `domain`, SEO/Branding/Auth-Flags, eigene Stripe-Account), `PortalUser` (separate End-User-Identität pro Portal, `@@unique([portalId, email])`), eigenes Subscription-Subsystem (gespiegelt vom Haupt-Subscriptions-Modell).
**HiveGrid-Relevanz:** Customer-Portal / White-Label-Public-Sites mit eigener User-Basis und eigenem Billing. Relevant falls HiveGrid kundenseitige Portale (B2B2C) anbietet.
### promptFlows.prisma (12) — `prisma/schema/promptFlows.prisma`
Modelle: `PromptFlowGroup`, `PromptFlowGroupTemplate`, `PromptFlowGroupEntity`, `PromptFlow`, `PromptFlowInputVariable`, `PromptTemplate`, `PromptFlowOutput`, `PromptFlowOutputMapping`, `PromptFlowExecution`, `PromptTemplateResult`.
- LLM-Prompt-Pipelines, an `Entity`/`Property` gekoppelt (Output-Mapping in Row-Properties), `executionType` sequential/parallel, Streaming-Flag, Execution + Result-Log.
**HiveGrid-Relevanz:** AI-Prompt-Orchestrierung an Entity-Daten. Relevant für HiveGrid-Module, die strukturierte LLM-Outputs in Datensätze schreiben.
### ai.prisma (8) — `prisma/schema/ai.prisma`
Modelle: `Chat`, `ChatMessage`, `ChatMessagePart`, `GeneratedObject`, `AICostEvent`, `TenantAICostSummary`.
- `Chat` (Typ tenant/admin/marketing, Scope, anonym via `sessionId`), `ChatMessage`/`ChatMessagePart` (Multi-Part), `GeneratedObject` (LLM-Generierungen + Token/Cost-Felder), `AICostEvent` (append-only Cost-Log, `requestId @unique`, KEINE Prompt-Bodies, Stripe-Overage-Felder), `TenantAICostSummary` (vor-aggregiert pro Periode).
**HiveGrid-Relevanz:** AI-Chatbot + AI-Kostentracking/Billing. Für HiveGrid-Assistenten und AI-Cost-Dashboards direkt nutzbar.
### workflows.prisma (10) — `prisma/schema/workflows.prisma`
Modelle: `Workflow`, `WorkflowBlock`, `WorkflowBlockConditionGroup`, `WorkflowBlockCondition`, `WorkflowBlockToBlock`, `WorkflowExecution`, `WorkflowInputExample`, `WorkflowBlockExecution`, `WorkflowVariable`, `WorkflowCredential`.
- Visueller Workflow-Builder: Blocks + Conditions + Block-zu-Block-Edges, Execution-Tracking pro Block, tenant-scoped + `appliesToAllTenants`. `WorkflowVariable`/`WorkflowCredential` (`@@unique([tenantId, name])`).
**HiveGrid-Relevanz:** Automatisierungs-/Workflow-Engine (node-basiert). Relevant für HiveGrid-Prozessautomatisierung, gekoppelt an Outbox-Events.
### knowledgeBase.prisma (9) — `prisma/schema/knowledgeBase.prisma`
Modelle: `KnowledgeBase`, `KnowledgeBaseCategory`, `KnowledgeBaseCategorySection`, `KnowledgeBaseArticle`, `KnowledgeBaseRelatedArticle`, `KnowledgeBaseViews`, `KnowledgeBaseArticleViews`, `KnowledgeBaseArticleUpvotes`, `KnowledgeBaseArticleDownvotes`.
- Mehrsprachige Wissensdatenbank, tenant-scoped optional (`ownerKey` default "global"), Artikel mit Draft/Published-Content, Voting + View-Tracking.
**HiveGrid-Relevanz:** Help-Center / Docs-Modul. Relevant für HiveGrid-Self-Service-Wissensbasis.
### onboarding.prisma (7) — `prisma/schema/onboarding.prisma`
Modelle: `Onboarding`, `OnboardingFilter`, `OnboardingStep`, `OnboardingSession`, `OnboardingSessionAction`, `OnboardingSessionFilterMatch`, `OnboardingSessionStep`.
- Onboarding-Flows (modal/page), Filter-Targeting, Session-Tracking pro User/Tenant (`@@unique([onboardingId, userId, tenantId])`).
**HiveGrid-Relevanz:** User-Onboarding / Produkt-Touren für HiveGrid-Adoption.
### analytics.prisma (4) — `prisma/schema/analytics.prisma`
Modelle: `AnalyticsSettings`, `AnalyticsUniqueVisitor`, `AnalyticsPageView`, `AnalyticsEvent`.
- First-Party-Web-Analytics (Cookie-basiert, UTM, Device/Browser), an `Portal`/`User`/`PortalUser`/`FeatureFlag` gekoppelt.
**HiveGrid-Relevanz:** Eingebettetes Web-Analytics für HiveGrid-Portale/Sites; FeatureFlag-Event-Korrelation.
### featureFlags.prisma (3) — `prisma/schema/featureFlags.prisma`
Modelle: `FeatureFlag`, `FeatureFlagFilter`, `FeatureFlagAudit`.
- `FeatureFlag` (scope global/tenant/user, v2-Targeting `rules`/`variants` JSON, `enabledFrom/Until`, Flag-Dependencies via self-relation), `FeatureFlagFilter` (Prozent/Sprache/Page…), `FeatureFlagAudit` (append-only).
**HiveGrid-Relevanz:** Feature-Toggles / Gradual-Rollout / A-B für alle HiveGrid-Module.
### plugins.prisma (5) — `prisma/schema/plugins.prisma`
Modelle: `Plugin`, `TenantPluginCapabilityGrant`, `PluginUserConfig`, `PluginInvocation`, `PluginCapabilityUse`.
- WASM-Plugin-Runtime: `Plugin` (`wasmSha256`, `signaturePublicKey` Ed25519, `manifest` JSON, `@@unique([tenantId, pluginId])`), explizite Capability-Grants pro Tenant, per-User-Config, Invocation-Audit + per-Capability-Use mit Hash-Kette (`prevHash`/`rowHash`).
**HiveGrid-Relevanz:** Erweiterbarkeit via signierte sandboxed Plugins. Relevant falls HiveGrid Third-Party-/Custom-Erweiterungen unterstützt.
### imports.prisma (1) — `prisma/schema/imports.prisma`
Enum: `ImportJobStatus`. Modell: `ImportJob` (tenant-scoped, `importType`/`format`, Source-Bucket/Key, `acceptedRows`/`rejectedRows`, Error-Report-Bucket, `requestId`).
**HiveGrid-Relevanz:** Daten-Import-Jobs (CSV/etc.) für HiveGrid-Bulk-Onboarding.
### queue.prisma (2) — `prisma/schema/queue.prisma`
Enum: `ExportJobStatus`. Modelle: `QueueFailedJob` (BullMQ-DLQ, `@@unique([queueName, jobName, jobId])`), `ExportJob` (Daten-Export mit Retention `expiresAt`/`deletedAt`/`hardDeletedAt`).
**HiveGrid-Relevanz:** Background-Job-Fehler-Observability + DSGVO-konformer Datenexport.
### outbox.prisma (1) — `prisma/schema/outbox.prisma`
Enum: `OutboxStatus`. Modell: `OutboxEvent` (`eventType`, `payload` JSON, `status`, `retries`, `scheduledAt`, `dispatchKey @unique` für Idempotenz, `priority`, `lockedUntil`). Reiche Indizes für Dispatcher-Polling.
**HiveGrid-Relevanz:** Transaktionales Outbox-Muster — atomare Side-Effects mit DB-Writes. Querschnitt für ALLE HiveGrid-Module mit zuverlässiger Event-Auslieferung (Publisher: `packages/db/src/outbox.ts`).
### sites.prisma (1) — `prisma/schema/sites.prisma`
Modell: `Site` (tenant-scoped, `type` community/booking, `siteId @@unique([type, siteId])`, Branding/Copy JSON, Server-Resolution domain/path/subdomain).
**HiveGrid-Relevanz:** Multi-Site / Public-Sites (z.B. Booking/Community). Relevant für HiveGrid-Endkunden-Frontends.
### collabDocs.prisma (1) — `prisma/schema/collabDocs.prisma`
Modell: `CollabDoc` (Y.js/Hocuspocus Realtime-Docs, `ydocState Bytes`, `version`, `@@unique([tenantId, resourceType, resourceId])`).
**HiveGrid-Relevanz:** Realtime-Kollaboration (Live-Editing) für HiveGrid-Dokumente/Notizen.
### helpDesk.prisma (2) — `prisma/schema/helpDesk.prisma`
Modelle: `Feedback` (tenant/user-scoped, `message`, `fromUrl`), `SupportTicket` (`source`/`status`/`priority`, `inboundMessageId @unique`).
**HiveGrid-Relevanz:** Support-/Feedback-Modul für HiveGrid-Kundenservice.
### formWidget.prisma (2) — `prisma/schema/formWidget.prisma`
Modelle: `Form` (tenant-scoped, embeddable, `key @unique`, `definition` JSON, `fieldMapping`, `allowedOrigins[]`, Anti-Spam-`config`), `FormSubmission` (`payload` JSON, `ipHash`, `status` received/processed/failed/spam).
**HiveGrid-Relevanz:** Eingebettete Formulare (Lead-Capture, Kontakt) für HiveGrid-Public-Surfaces; koppelt an CRM/Entity-Rows.
---
## Hinweise für Modul-Abgleich-Agenten
- **Tenant-Scope-Regel:** `tenantId` nullable ⇒ system-/operator-weit; `tenantId` pflicht ⇒ strikt mandanten-isoliert. `Lead` ist bewusst NICHT tenant-scoped.
- **Soft-Delete ist die Ausnahme:** nur `Row.deletedAt` + `ExportJob`. Default ist Hard-Delete via `onDelete: Cascade`.
- **Vor jedem neuen User-FK:** Eintrag in `packages/db/src/dsgvo/user-fk-map.ts` ergänzen — der Test bricht CI sonst.
- **Side-Effects:** über Outbox (`publishOutboxEvent` in `$transaction`), nicht direkt.
- **Verbindungen:** Migrate = `DIRECT_URL`, Runtime = `DATABASE_URL` (PgBouncer), Reads optional `DATABASE_READ_URL`.
- **HiveGrid-eigene Modelle:** `prisma/schema/app.prisma` ist der vorgesehene leere Platzhalter.
@@ -0,0 +1,366 @@
# Boilerplate-Inventur: Domaenen-Pakete (Spezial)
> Faktische Bestandsaufnahme der fertigen Domaenen-Bausteine in der Boilerplate
> (`/Users/raffaelsheikh/Documents/GitHub/boilerplate`), die HiveGrid-Module direkt
> nutzen koennten. Quelle: gelesene Pakete, `index.ts`, READMEs, Prisma-Schemas,
> Service-Layer in `apps/web/app/modules/*` und Worker-Jobs.
>
> Konvention im gesamten Repo: Pakete heissen `@toolctrl/<name>`, sind `private: true`,
> `type: module`, ESM-only, `exports` zeigen direkt auf `./src/*.ts` (kein Build-Output);
> Build = `pnpm run typecheck`. Tests via `vitest run src`. Prisma-Schema ist
> per-Domain aufgesplittet unter `prisma/schema/*.prisma`. DB-spezifische
> Service-Logik liegt in `*.db.server.ts` / `*.server.ts` Dateien im Web-App-Modul.
---
## Reifegrad-Legende
- **Reif (fertig)** — Prisma-Modell + Service-Layer + Worker/Route + Tests vorhanden.
- **Solide (Helper/Schema-Layer)** — eigenstaendiges Paket mit Tests, aber bewusst dependency-frei (kein DB-/UI-Layer im Paket selbst).
- **Daten-Modell + Service** — Prisma-Modell + DB-/Service-Layer im Web-App-Modul (kein eigenes npm-Paket).
---
## Uebersichts-Tabelle
| Baustein | Code-Ort | Reifegrad | HiveGrid-Modul (Kandidat) |
| --- | --- | --- | --- |
| Subscriptions/Billing (Stripe) | `prisma/schema/subscriptions.prisma` + `apps/web/app/utils/stripe.server.ts` + `packages/entitlements` | Reif | Billing / Plaene / Entitlements-Querschnitt |
| Onboarding-Wizard | `prisma/schema/onboarding.prisma` + `apps/web/app/modules/onboarding` | Reif | Onboarding / User-Aktivierung |
| Help-Desk / Support | `prisma/schema/helpDesk.prisma` + `apps/web/app/modules/helpDesk` | Reif | Support / Feedback |
| Customer-Portals | `prisma/schema/portals.prisma` + `apps/web/app/modules/portals` | Reif | Kunden-/Partner-Portale, White-Label-Subdomains |
| Form-Widget / Form-Builder | `packages/form-widget` + `prisma/schema/formWidget.prisma` + `apps/web/app/modules/formWidget` | Reif | Lead-Capture / Formulare / Embeds |
| Import (DSGVO/CSV/JSON/XLSX) | `packages/imports` + `prisma/schema/imports.prisma` | Solide + Job | Daten-Import / Migration |
| Export (CSV/JSON/XLSX/PDF, DSGVO) | `packages/exports` + Worker `export.ts`/`dsgvo-export.ts` | Solide + Job | Daten-Export / DSGVO-Auskunft / Audit-PDF |
| Branding / Theme | `packages/brand` | Solide | Theming-Querschnitt / Mail-Branding |
| Entity-Builder (Custom-Felder) | `prisma/schema/entityBuilder.prisma` + `apps/web/app/components/entities` | Reif | No-Code-Entitaeten / Custom-Felder |
| Knowledge-Base (Help-Center) | `prisma/schema/knowledgeBase.prisma` + `apps/web/app/modules/knowledgeBase` | Reif | Help-Center / Wissensdatenbank |
| Analytics (Visitor/Event) | `prisma/schema/analytics.prisma` + `apps/web/app/modules/portals/utils/analytics-redaction.ts` | Daten-Modell + Service | Web-Analytics (Portal-fokussiert) |
| Marketing-Leads / Funnel | `prisma/schema/marketingLeads.prisma` + `packages/marketing-outbound` + `apps/web/app/utils/leads` | Reif | Marketing / Lead-Funnel / Attribution |
| Shared-Store (Redis-KV) | `packages/shared-store` | Solide | Querschnitt: Rate-Limit, Cache, Sessions, Idempotenz |
| API-Client / API-Types (SDK) | `packages/api-client-ts`, `packages/api-types` | Solide | Externe Integration / SDK / Webhook-Verify |
---
## 1. `@toolctrl/form-widget` + Form-Builder
**Zweck:** Geteilte Typen & Helper fuer das einbettbare iframe-Formular-Widget. Bewusst
**ohne Runtime-Abhaengigkeiten** auf den Rest des Codebases (nur `zod`), damit Web/API/Worker/CLI
es importieren koennen, ohne Prisma/React mitzuziehen.
**Haupt-Exporte** (`packages/form-widget/src/index.ts`):
| Export | Datei | Funktion |
| --- | --- | --- |
| `FormDefinitionSchema`, `FormFieldSchema`, `FormThemeSchema`, `FormSubmissionPayloadSchema`, `FieldTypeSchema` | `src/schema.ts` | Zod-Definitionen fuer Form/Feld/Theme/Submission-JSON. Feldtypen: text, email, number, textarea, select, radio, checkbox, honeypot |
| `HONEYPOT_FIELD_KEY` (= `"hp_extra_data"`) | `src/schema.ts` | Stabiler Honeypot-Feldschluessel |
| `matchAllowedOrigin`, `normalizeOrigin` | `src/origin.ts` | Origin-Allowlist mit Wildcard-Subdomain (`*.example.com`); leere Allowlist = nicht submittbar |
| `isHoneypotTriggered` | `src/honeypot.ts` | Anti-Spam Honeypot-Erkennung (still 204 zurueck) |
| `resolveSubmissionTarget`, `applyFieldMapping` | `src/routing.ts` | Submission-Routing: `generic` \| `crm-contact` \| `entity-row` + Field-Mapping (worker-hot-path, sync) |
**Prisma-Modelle** (`prisma/schema/formWidget.prisma`):
- `Form` — tenant-authored Definition + Theme + Delivery-Config (versioniertes `definition` JSON, `theme`, `targetType`, `targetEntityId`, `fieldMapping`, `allowedOrigins[]`, `notifyEmails[]`, `webhookUrl`, `config`, `key` fuer `<script data-form-key>`).
- `FormSubmission` — Inbox fuer Payloads (`payload` JSON, `sourceOrigin`, `ipHash` = SHA-256, kein Raw-IP, `status`: received/processed/failed/spam, `result` JSON).
**Form-Builder UI** (`apps/web/app/modules/formWidget/builder/`): `FormBuilder.tsx`, `Canvas.tsx`,
`FieldPalette.tsx`, `FieldProperties.tsx`, `SettingsTab.tsx`, `PreviewTab.tsx`, `SortableField.tsx`, `palette.ts`, `types.ts`.
**Worker-Jobs** (`apps/worker/src/jobs/`): `form-submission-route.ts`, `form-submission-webhook-delivery.ts`,
`form-submission-followups.ts` (+ Bootstraps).
**HiveGrid-Relevanz:** Direkt nutzbar fuer Lead-Capture-Formulare, eingebettete Kontakt-/Anfrage-Formulare,
Formular-zu-Entity-Routing. Das schema-Paket ist als zod-Contract sofort wiederverwendbar; CRM-Contact-Routing
und Entity-Row-Routing passen zum Entity-Builder.
---
## 2. `@toolctrl/imports` (Daten-Import)
**Zweck:** Dependency-freies (kein DB/Netz) Parser-/Dry-Run-Framework fuer CSV/JSON/XLSX-Importe inkl.
Column-Mapping, Validierung, Diagnose-Redaction (Secrets/PII) und Dedupe.
**Haupt-Exporte** (`packages/imports/src/index.ts`):
- `IMPORT_FORMATS` = `["csv","json","xlsx"]`, `ImportFormat`.
- Format-Erkennung: `detectImportFormatFromExtension`, `detectImportFormatFromContentType`, `isSupportedImportFormat`.
- Dry-Run-Preview: `createImportDryRunPreview` (mappt Quellspalten -> Zielfelder, prueft Pflichtfelder).
- Diagnose-Redaction: `createRedactedParserDiagnostic`, `redactDiagnosticMessage` (entfernt api-key/token/password/secret/body via Regex).
- `summarizeRowResults`.
- Zentrale Typen: `ColumnMapping`, `NormalizedImportRow`, `ImportRowResult`, `ParserDiagnostic`, `ImportParser`, `ImportParserInput/Output`, `ImportDryRunSummary`.
- Dedupe (`src/dedupe.ts`): `DuplicateKeyField`, `DuplicateDetectionRow`, etc.
**Prisma-Modell** (`prisma/schema/imports.prisma`):
- `ImportJob` — Job-Tracking (`status` enum `ImportJobStatus`: PENDING/RUNNING/COMPLETED/FAILED, `importType`, `format`, `sourceBucket`/`sourceKey`, `rowCount`, `acceptedRows`, `rejectedRows`, `errorReportBucket`/`errorReportKey`, `requestId`, tenant-scoped).
**HiveGrid-Relevanz:** Direkt nutzbar fuer Daten-Migration / Bulk-Import in Custom-Entities. Die
PII-/Secret-Redaction in Diagnose-Meldungen ist DSGVO-relevant. Dry-Run-Preview passt zu einem Import-Wizard.
---
## 3. `@toolctrl/exports` (Daten-Export inkl. DSGVO & Audit-PDF)
**Zweck:** Streaming-Export-Framework fuer CSV/JSON/XLSX/PDF mit Plan-/Format-Row-Caps,
S3-Multipart-Sink, Spreadsheet-Injection-Safety und einem dedizierten Compliance-Audit-PDF-Renderer.
**Haupt-Exporte** (`packages/exports/src/index.ts` re-exportiert):
| Subpfad | Datei | Inhalt |
| --- | --- | --- |
| `./csv`, `./json`, `./xlsx`, `./pdf` | `src/{csv,json,xlsx,pdf}/index.ts` | Format-Serializer (jeweils mit `stream.ts`) |
| `./types` | `src/types.ts` | `ExportTable`, `normalizeExportTable`, `ExportColumn`, `ExportRecord`, `ExportTableValidationError` |
| `./limits` | `src/limits.ts` | `EXPORT_PLAN_ROW_CAPS` (free 10k / pro 100k / enterprise unlimited), `TenantPlanTier`, `ExportFormat`, `ExportRowCapExceededError` |
| `./s3-multipart` | `src/sinks/s3-multipart.ts` | `createS3MultipartUploadSink` (8 MB Parts, Concurrency 4, AbortMultipartUpload on failure) |
| `./audit-pdf` | `src/audit-pdf.ts` | `serializeAuditEventsToPdf` / `serializeAuditEventsToPdfStream`, `AuditPdfEvent`, SOC2/HIPAA/GDPR-Evidence-PDF |
Format-Caps (`src/format-caps.ts`): `PDF_STREAM_HARD_ROW_LIMIT = 5.000`, `XLSX_STREAM_HARD_ROW_LIMIT = 500.000`, `XLSX_STREAM_THRESHOLD = 50.000`.
Zusaetzlich `src/spreadsheet-safety.ts` (CSV/XLSX-Injection-Schutz).
**Dependencies:** `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, `@react-pdf/renderer`, `exceljs`.
**Worker-Jobs** (`apps/worker/src/jobs/`):
- `export.ts` — generischer Export-Job.
- `dsgvo-export.ts` — DSGVO Personal-Data-Export (Phase 10 #68): aggregiert alle Rows mit `userId` via `USER_FK_MAP` (`packages/db/src/dsgvo/user-fk-map.ts`), serialisiert als JSON-Bundle, streamt zu S3, mintet Signed-URL, schreibt `AuditEvent` action=EXPORT_PERSONAL_DATA (regulatoryScope gdpr, dataSensitivity pii).
**Prisma:** Export-Jobs-Migration `prisma/migrations/20260508000000_export_jobs/`.
**HiveGrid-Relevanz:** Direkt nutzbar fuer DSGVO-Auskunft (Recht auf Datenkopie), Daten-Export aus
Custom-Entities, Audit-/Compliance-Reports als PDF. Plan-basierte Row-Caps koppeln an Entitlements/Billing.
---
## 4. `@toolctrl/brand` (Branding / Theme-Tokens)
**Zweck:** Dependency-freie (zod-frei, nur Node-Types) Brand-Token-Aufloesung: Produktname, Site-URL,
Legal-Links (Impressum/Datenschutz/AGB), Support-Kontakt, Logo/Favicon, Social-Links, transaktionale Mail-Farben.
**Haupt-Exporte** (`packages/brand/src/index.ts`):
- Typen: `BrandTokens`/`ToolCTRLBrandTokens`, `BrandTokenOverrides`, `BrandLinkConfiguration`, `BrandLegalLinks`, `BrandSupportContact`, `BrandAssets`, `BrandSocialLinks`, `TransactionalMailColors`, `AppConfigurationLike`.
- Defaults: `defaultBrandTokens`, `defaultBrandLinks` (Default Produktname "ToolCTRL", `tool-ctrl.de`).
- `createBrandTokens(overrides)` — Merge mit Defaults.
- `resolveTransactionalBrandTokens(appConfigLike)` — leitet Tokens aus einer App-Konfiguration ab (mehrstufiger Fallback brand.* -> top-level Felder).
- `brandCssVariables(tokens)` — CSS-Custom-Properties (`--toolctrl-*`).
- `loadBrandFromEnv(env)` + `BrandEnvSource` (`src/env.ts`) — laedt Overrides aus `BRAND_*` Env-Variablen (PRODUCT_NAME, SITE_URL, SUPPORT_EMAIL, LEGAL_*, LOGO_URL, MAIL_*).
**HiveGrid-Relevanz:** Theming-Querschnitt. Token-Modell (Legal/Support/Assets/Mail-Farben) ist
white-label-/multi-tenant-tauglich und direkt fuer Mail-Branding + CSS-Variablen einsetzbar. Hinweis:
Defaults sind ToolCTRL-spezifisch und muessten fuer HiveGrid umgesetzt/overriden werden.
---
## 5. `@toolctrl/shared-store` (Redis-KV-Querschnitt)
**Zweck:** Cross-Replica-State-Primitive auf Redis (prod) bzw. In-Memory (dev/test). Fundament fuer
horizontale Skalierung stateful Subsysteme. Dependency: `ioredis`. Optional: `@toolctrl/logger`.
**Haupt-Exporte** (`packages/shared-store/src/index.ts`):
- Adapter: `SharedStoreAdapter` (Interface, `src/adapters/adapter.ts`), `MemoryAdapter`, `RedisAdapter`.
- Factory: `createSharedStore` (`src/factory.ts`) — namespace, ttlDefault, redisUrl, redisDbIndex.
- Helper (`src/helpers/`): `createFixedWindowRateLimiter`, `createSlidingWindowRateLimiter`, `createCacheStore` (mit Cross-Instance-Invalidation), `createSessionStore`, `createIdempotencyStore`, `createTwoTierCache`.
**Konventionen** (README): Keys = `shared-store:<namespace>:<prefix>:<suffix>`; Redis-DB-Index-Allokation
(0 = BullMQ/`@toolctrl/queue`, 1 = shared-store KV, 2 = Pub/Sub). Lua-Skripte sind Konstanten (kein User-Input).
**HiveGrid-Relevanz:** Querschnitts-Infrastruktur fuer Rate-Limiting (Login/MFA/API),
Subscription-/Feature-Flag-Cache, Web-Sessions, Webhook-/Payment-Idempotenz. Kein Domaenen-Baustein im
engeren Sinn, aber Voraussetzung fuer skalierbares Multi-Replica-Deployment der Domaenen-Module.
---
## 6. `@toolctrl/api-client-ts` + `@toolctrl/api-types` (SDK)
**Zweck:** Offizielles TypeScript-SDK fuer die Boilerplate-REST-API, generiert aus dem OpenAPI-Spec
(`@hey-api/openapi-ts`), mit Wrapper fuer Auth, Retry (exp. Backoff, Retry-After), Idempotency-Keys,
Pagination-Iterator und HMAC-Webhook-Signatur-Verify.
**`@toolctrl/api-client-ts` Haupt-Exporte** (`src/index.ts`):
- `createToolctrlClient`, `ToolctrlApiError`, `ToolctrlNetworkError`, Typen `ToolctrlClient/ClientConfig/AuthConfig/RetryConfig/IdempotencyConfig`, `PaginatedResponse`, `PaginateOptions` (`src/client.ts`).
- `verifyWebhookSignature`, `signWebhookPayload`, `VerifyWebhookOptions`, `SignWebhookOptions` (`src/webhooks.ts`).
- Subpfade: `./client`, `./webhooks`, `./types` (generierte `types.gen.ts`), `./generated`.
- OpenAPI-Spec: `packages/api-client-ts/openapi.json`; Spec-Generator-Scripts `scripts/generate-spec.ts`.
**`@toolctrl/api-types` Haupt-Exporte** (`src/index.ts`):
- Zero-Runtime Re-Export: Wrapper-Typen aus `@toolctrl/api-client-ts` + `export type * from "@toolctrl/api-client-ts/types"` (alle OpenAPI-Operation-Typen). Dependency-frei.
**HiveGrid-Relevanz:** Direkt nutzbar, falls HiveGrid eine REST-API exponiert und ein typisiertes
SDK/Webhook-Verify braucht. Webhook-HMAC-Verify ist auch fuer Inbound-Webhooks (z.B. Stripe-Style) relevant.
Hinweis: `types.gen.ts` ist aus dem Boilerplate-Spec generiert — fuer HiveGrid neu zu generieren.
---
## 7. Subscriptions / Billing (Stripe) — Schema + Service
**Prisma-Modelle** (`prisma/schema/subscriptions.prisma`):
| Modell | Zweck |
| --- | --- |
| `SubscriptionProduct` | Stripe-Produkt-Spiegel (order, title, model, public, badge, billingAddressCollection, hasQuantity, canBuyAgain) |
| `SubscriptionPrice` | Preis pro Produkt (stripeId, type, billingPeriod, price Decimal, currency, trialDays) |
| `SubscriptionUsageBasedPrice` + `SubscriptionUsageBasedTier` | Metered/Usage-Based Billing (unit, usageType, aggregateUsage, tiersMode, billingScheme, Tier from/to/perUnit/flatFee) |
| `SubscriptionFeature` | Feature-Liste pro Produkt (name, type, value, accumulate) |
| `TenantSubscription` -> `TenantSubscriptionProduct` -> `TenantSubscriptionProductPrice` -> `TenantSubscriptionUsageRecord` | Tenant-Abo-Status, Stripe-Customer/Subscription-IDs, Period-Start/End, Quantity, Usage-Records |
| `TenantProductConfiguration` | `blueprintId` (default `crm-saas`) pro Tenant |
| `TenantFeatureOverride` | Per-Tenant Feature-Override (featureKey/override/reason) |
| `CheckoutSessionStatus` | Stripe-Checkout-Session-Tracking (pending, email, fromUrl/User/Tenant) |
| `Credit` | Guthaben/Credits pro Tenant/User (amount, type, objectId) |
**Service-Layer** (`apps/web/app/`):
- `utils/stripe.server.ts` — Stripe-Client (API-Version `2025-10-29.clover`), `getStripeClient`, `isStripeConfigured`, `isStripeTestMode`, Webhook-Signing-Secret. Config via `@toolctrl/config`.
- `utils/services/.server/subscriptionService.ts`, `subscriptionCache.server.ts`, `pricingService.ts`, `tenantPlanLookup.server.ts`.
- `utils/db/subscriptions/tenantSubscriptionProducts.db.server.ts`, `utils/db/stripe/checkoutSessions.db.server.ts`.
- `utils/helpers/SubscriptionHelper.ts`, `application/pricing/plans.server.ts`.
- UI: `components/core/settings/subscription/*` (Plans, ChangeSubscription, MyPayments, MyInvoices, MyUpcomingInvoice, MyPaymentMethods, TenantProduct), `components/core/pricing/*`.
- DTOs/Enums: `application/dtos/subscriptions/*`, `application/enums/subscriptions/*`.
**Entitlements-Paket** (`packages/entitlements/src/index.ts`) — Plan-/Feature-Gate-Engine:
- `evaluateEntitlement`, `evaluateProductAccess`, `featureGateRegistry`, `featureKeys`, `getFeatureGate`.
- `standardEntitlementPlans`, `getStandardEntitlementPlan`, `getStripePricePlaceholder`/`ProductPlaceholder`.
- Blueprints (`src/blueprints.ts`): `productBlueprints` (aktuell nur `crm-saas`), `getProductBlueprint`.
- AI-Quotas/Overage: `resolveTenantAIQuota`, `getAIQuotaPlanDefaults`, `resolveTenantAIOverage`, `computeMarkedUpCostCents`.
**Worker:** `apps/worker/src/jobs/ai-cost-stripe-report.ts` (+ cron/submitter/bootstrap) — meldet AI-Kosten als Stripe-Usage.
**HiveGrid-Relevanz:** Vollstaendiger Stripe-Billing-Stack (Produkte, Preise, Usage-Based, Trials,
Checkout, Tenant-Subscriptions, Credits) + Entitlements/Feature-Gating. Direkt als Billing-Modul nutzbar.
Blueprint-System (`crm-saas`) ist erweiterbar fuer HiveGrid-spezifische Plaene.
---
## 8. Onboarding-Wizard — Schema + Service
**Prisma-Modelle** (`prisma/schema/onboarding.prisma`):
- `Onboarding` (type: modal/page, realtime, active, canBeDismissed) -> `OnboardingFilter` (Zielgruppen-Filter), `OnboardingStep` (order, block JSON).
- Session-Tracking: `OnboardingSession` (status active/completed/dismissed, started/completed/dismissedAt, unique [onboarding,user,tenant]) -> `OnboardingSessionStep` (seenAt/completedAt), `OnboardingSessionAction`, `OnboardingSessionFilterMatch`.
**Service-Layer** (`apps/web/app/modules/onboarding/`):
- `services/OnboardingService.ts`, `OnboardingSessionService.ts`, `OnboardingStepsService.ts`, `OnboardingFiltersService.ts`.
- `db/onboarding.db.server.ts`, `onboardingSessions.db.server.ts`, `onboardingSessionSteps.db.server.ts`.
- Blocks/Components/DTOs/Utils: `blocks/OnboardingBlock*.tsx`, `components/Onboarding*.tsx`, `dtos/Onboarding*.ts`, `utils/Onboarding*Utils.ts`.
**HiveGrid-Relevanz:** Fertiger Onboarding-/Aktivierungs-Flow mit Modal/Page-Varianten, Zielgruppen-Filtern,
Step-Tracking und Realtime-Trigger. Direkt fuer User-Onboarding nutzbar.
---
## 9. Help-Desk / Support — Schema + Service
**Prisma-Modelle** (`prisma/schema/helpDesk.prisma`):
- `Feedback` — schlankes In-App-Feedback (message, fromUrl, tenant/user optional).
- `SupportTicket` — Tickets (source, status default `open`, priority default `normal`, subject, message, fromEmail, fromUrl, `inboundMessageId` unique fuer E-Mail-Dedup). Indizes auf [tenant,status,createdAt] und [source,createdAt].
**Service-Layer** (`apps/web/app/modules/helpDesk/`):
- `services/Feedback.server.ts`, `SupportTickets.server.ts`, `FeedbackRedaction.server.ts` (+ `.ts`).
- `db/feedback.db.server.ts`, `supportTickets.db.server.ts`.
- `components/FeedbackTable.tsx`.
**HiveGrid-Relevanz:** Leichtgewichtiges Support-/Feedback-Modul (kein voller Helpdesk mit Threads, aber
Ticket-Aufnahme inkl. Inbound-E-Mail-Dedup und PII-Redaction). Direkt fuer Support-Inbox nutzbar.
---
## 10. Customer-Portals — Schema + Service
**Prisma-Modelle** (`prisma/schema/portals.prisma`): umfangreiches White-Label-Portal-System:
- `Portal` — Subdomain (unique) + optional Custom-`domain`, SEO-Felder, Branding (Logo/Icon/Favicon + DarkMode), Auth-Anforderungen (EmailVerification/Organization/Name), Analytics-Toggles (Simple/Plausible/Google), `stripeAccountId` (Connect), `metadata`.
- `PortalUser` (separate Portal-User mit passwordHash, github/google OAuth, locale) + `PortalUserRegistration` (Token-basierte Einladung).
- `PortalPage` (name + attributes JSON).
- Eigenes Portal-Subscription-Subsystem: `PortalSubscriptionProduct/Price/Feature`, `PortalUserSubscription(Product)(Price)`, `PortalCheckoutSessionStatus` (gespiegeltes Tenant-Subscription-Modell fuer Portal-Endkunden).
**Service-Layer** (`apps/web/app/modules/portals/`):
- `services/Portal.server.ts`, `PortalStripe.server.ts`, `PortalPricing.server.ts`, `PortalSubscription.server.ts`, `StripeConnect.server.ts`.
- `db/portals.db.server.ts`, `portalUsers.db.server.ts`, `portalSubscriptionProducts.db.server.ts`, `portalUserSubscriptions.db.server.ts`, `portalCheckoutSessions.db.server.ts`, `portalPages.db.server.ts`.
- `utils/StripeConnectUtils.ts`, `PortalUtils.ts`, `analytics-redaction.ts`; `helpers/PortalUser*ModelHelper.ts`; `dtos/PortalDto.ts`, `PortalUserDto.ts`.
**HiveGrid-Relevanz:** Vollstaendiges Customer-/Partner-Portal mit eigener User-Basis, White-Label-Branding,
eigenem Stripe-Connect-Billing und eigener Analytics. Direkt fuer kundeneigene/partner-Portale nutzbar
(z.B. End-Kunden eines HiveGrid-Tenants). Komplexester Domaenen-Baustein im Set.
---
## 11. Entity-Builder (Custom-Felder / No-Code-Entitaeten)
**Prisma-Modelle** (`prisma/schema/entityBuilder.prisma`) — sehr umfangreich, Kern des No-Code-Systems:
- `Entity` — Definition (name/slug/prefix unique, type app/admin/all, hasTags/Comments/Tasks/Activity/Views/BulkDelete, defaultVisibility, onCreated/onEdit, hasApi).
- `Property` (Feld-Definition, type Int + subtype, isRequired/Hidden/Unique/ReadOnly, formula) -> `PropertyAttribute` (pattern/min/max/...), `PropertyOption` (Select-Optionen).
- `EntityView` + `EntityViewProperty/Filter/Sort` — Views (table/board/calendar/list/gallery, Grid-Spalten, groupBy).
- `Row` -> `RowValue` (text/number/date/boolean) + `RowValueMultiple`/`RowValueRange`/`RowMedia` — Datensaetze.
- Relationen: `EntityRelationship` (one-to-many etc.) + `RowRelationship`.
- Cross-Cutting auf Rows: `RowComment` (+ `RowCommentReaction`), `RowTag` (+ `EntityTag`), `RowTask`, `RowPermission` (row-level ACL: tenant/role/group/user/public).
- `EntityWebhook` (+ `EntityWebhookLog`) — Per-Entity-Outbound-Webhooks.
- `EntityTemplate`, `EntityGroup`/`EntityGroupEntity`/`EntityGroupConfiguration(Row)` — Gruppierung & Templates.
- `Formula`/`FormulaComponent`/`FormulaLog`/`FormulaComponentLog` — berechnete Felder.
- `ApiKeyEntity` — Per-Entity-API-Key-CRUD-Rechte. `TenantSettingsRow`, `SampleCustomEntity`.
**Service-/UI-Layer** (`apps/web/app/`):
- `components/entities/{rows,properties,views,relationships,webhooks,templates,entityTemplates}/...`.
- `utils/db/entities/...`, `routes/admin/entities`, `routes/app.$tenant/$entity.__autogenerated/...` (auto-generierte CRUD-Routen).
- `application/dtos/entities/*`, `application/enums/entities/*`, `modules/formulas/...`.
**HiveGrid-Relevanz:** Kompletter No-Code-Entity-Builder mit Custom-Feldern, Views, Relationen, Formeln,
Row-Level-Permissions, Kommentaren/Tasks/Tags und Per-Entity-Webhooks. Zentraler Baustein fuer
benutzerdefinierte Datenobjekte / Custom-Felder in HiveGrid-Modulen. Form-Widget routet direkt auf Entity-Rows.
---
## 12. Knowledge-Base (Help-Center) — Schema + Service
**Prisma-Modelle** (`prisma/schema/knowledgeBase.prisma`):
- `KnowledgeBase` (tenant-optional + `ownerKey` default `global`, basePath, slug, multi-language `languages`, layout, color, enabled, SEO).
- `KnowledgeBaseCategory` -> `KnowledgeBaseCategorySection` — Hierarchie.
- `KnowledgeBaseArticle` — Draft + Published Content (`contentDraft`, `contentPublished`, `contentPublishedAsText` fuer Suche), contentType, language, featuredOrder, publishedAt, related Articles (`KnowledgeBaseRelatedArticle`).
- Analytics/Voting: `KnowledgeBaseViews`, `KnowledgeBaseArticleViews/Upvotes/Downvotes` (je unique pro userAnalyticsId).
**Service-Layer** (`apps/web/app/modules/knowledgeBase/`):
- `service/KnowledgeBaseService.server.ts`, `KnowledgeBaseTemplatesService.server.ts`.
- `services/kbArticleSearch.server.ts`, `kbArticleRag.server.ts` (RAG/AI-Suche).
- `db/knowledgeBase.db.server.ts`, `kbArticles.db.server.ts`, `kbCategories.db.server.ts`, `kbCategorySections.db.server.ts`, `kbAnalytics.db.server.ts`.
- Public Components: `components/Kb*.tsx` (Search/Header/FeaturedArticles/Skeletons); DTOs `dtos/Kb*Dto.ts`; `utils/DefaultKbsTemplate.ts`, `KnowledgeBaseUtils.ts`.
**HiveGrid-Relevanz:** Fertiges Help-Center mit Kategorien/Sektionen/Artikeln, Draft/Publish-Workflow,
Multi-Language, Voting/Views-Analytics und AI/RAG-Suche. Direkt als Wissensdatenbank/Help-Center nutzbar.
---
## 13. Analytics (Visitor / PageView / Event)
**Prisma-Modelle** (`prisma/schema/analytics.prisma`):
- `AnalyticsSettings` (public, ignorePages/onlyPages).
- `AnalyticsUniqueVisitor` (cookie unique, UTM source/medium/campaign/content/term, referrer, Browser/OS/Device, Geo country/city, optional user/portal/portalUser).
- `AnalyticsPageView`, `AnalyticsEvent` (action/category/label/value, optional featureFlag-Bezug).
**Service-Layer:** v.a. portal-zentriert — `apps/web/app/modules/portals/utils/analytics-redaction.ts`
(PII-Redaction). Modelle haengen ueberwiegend an `Portal`/`PortalUser`.
**HiveGrid-Relevanz:** Eingebaute First-Party-Web-Analytics (Visitor/PageView/Event + UTM + Geo + Device).
Stark an Portals gekoppelt; fuer allgemeines Tracking als Vorlage nutzbar. Reifegrad: Daten-Modell + Service
(kein eigenstaendiges Paket, kein dediziertes Analytics-Dashboard-Modul nachgewiesen).
---
## 14. Marketing-Leads / Funnel + Outbound
**Prisma-Modelle** (`prisma/schema/marketingLeads.prisma`):
- `Lead` — system-of-record fuer Marketing-Prospects (NICHT tenant-scoped, gehoert Operator). First-Touch-Attribution (utmSource/Medium/Campaign/Term/Content, gclid, fbclid, referrer, landingPage, nie ueberschrieben), `status` enum `LeadStatus` (new/contacted/qualified/trial/customer/churned/disqualified), `score`, `notes` JSON, `convertedToTenantId`, `lastIpHash` (SHA-256, kein Raw-IP).
- `LeadEvent` — Activity-Timeline (captured/re-captured/status-changed/assigned/note-added/score-changed/converted).
- `TenantAcquisition` — Attribution-Snapshot eingefroren bei Tenant-Erstellung (tenantId unique, leadId optional, UTM-Spalten bleiben stabil auch nach GDPR-Anonymisierung des Leads).
- `MarketingIntegration` — operator-konfigurierte Outbound-Integration (type slack/discord/mailchimp/hubspot/brevo/webhook, `configJson` mit AES-256-GCM-verschluesselten Secrets, `triggerEvents[]`, lastDeliveredAt/lastError).
**Paket `@toolctrl/marketing-outbound`** (`packages/marketing-outbound/src/index.ts`):
- Adapter-Registry: `slack`, `discord`, `mailchimp`, `hubspot`, `brevo`, `webhook` (custom). `resolveAdapter(type)`, `ADAPTER_TYPES`.
- Pro Adapter `*Config` + `build*Payload`/`build*Blocks/Embed`. Typen `MarketingOutboundAdapter`, `MarketingLeadPayload`, `MarketingLeadEventType` (`src/types.ts`).
**Service-/Worker-Layer:**
- `apps/web/app/utils/leads/...` (Capture-/Admin-Logik), public Endpoint `/api/leads/capture` + `marketing.js` SDK (laut Schema-Kommentar).
- Worker: `apps/worker/src/jobs/marketing-outbound.ts` (dispatcht Outbox-Events durch Adapter), `lead-stale-trigger.ts`.
**HiveGrid-Relevanz:** Vollstaendiger Lead-Funnel: Public Lead-Capture mit First-Touch-Attribution,
Lead-Scoring, Status-Pipeline, Konversion-zu-Tenant mit DSGVO-festen Attribution-Snapshots, und
Outbound-Integrationen (Slack/Discord/Mailchimp/HubSpot/Brevo/Webhook). Direkt fuer Marketing-/Sales-Funnel nutzbar.
---
## Quellen-Hinweise
- Pakete: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/{form-widget,imports,exports,brand,shared-store,api-client-ts,api-types,entitlements,marketing-outbound}`
- Prisma-Schemas: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/{subscriptions,onboarding,helpDesk,portals,knowledgeBase,analytics,marketingLeads,entityBuilder,formWidget,imports}.prisma`
- Service-Layer: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/modules/{onboarding,helpDesk,portals,knowledgeBase,formWidget}` und `apps/web/app/utils/{stripe.server.ts,db,services,leads}`
- Worker-Jobs: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/worker/src/jobs/{export,dsgvo-export,form-submission-*,marketing-outbound,ai-cost-stripe-*,lead-stale-trigger}.ts`
- Stripe-API-Version im Code: `2025-10-29.clover` (`apps/web/app/utils/stripe.server.ts`)
</content>
@@ -0,0 +1,303 @@
# Boilerplate-Inventur: Foundation & Querschnitt
> Faktische Inventur der Querschnitts-Pakete im Boilerplate-Monorepo
> (`/Users/raffaelsheikh/Documents/GitHub/boilerplate`). Nachschlagewerk für
> die Modul-Abgleich-Agenten (HiveGrid M001–M017). Alle Pfade absolut.
> Stand der Quellen: Boilerplate-Repo, gelesen ohne Änderungen.
## Überblick
| Paket | npm-Name | Pfad | Zweck (1 Satz) |
| --- | --- | --- | --- |
| foundation | `@toolctrl/foundation` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/foundation` | Reine, DB-/Framework-agnostische Querschnitt-Primitiven (Errors, Audit, Permissions, Flags, Idempotency, Adapters, Notifications, Workflows, Concurrency, sichere Outbound-HTTP). |
| view-models | `@toolctrl/view-models` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/view-models` | VM-State-Maschine, Action-Callbacks und Registry-Gate/Shell-Registry-Typen für UI-ViewModels. |
| entitlements | `@toolctrl/entitlements` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/entitlements` | Feature-Gate-Registry, Entitlement-Auswertung, Standard-Pläne, Produkt-Blueprints, AI-Quotas/Overage. |
| feature-flags | `@toolctrl/feature-flags` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/feature-flags` | Feature-Flag-Engine v2: Targeting-Regeln, Evaluation, Cache, Invalidierung, React-Client-Hooks. |
| i18n | `@toolctrl/i18n` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/i18n` | Locale-Config, Intl-Formatter (Zahl/Währung/Datum/Liste), ICU-MessageFormat, RTL, `useTranslate`-Factory. |
| logger | `@toolctrl/logger` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/logger` | Pino-Logger-Factory mit Redaction sensibler Felder. |
| observability | `@toolctrl/observability` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/observability` | PII-scrubbender OpenTelemetry-SpanProcessor (framework-agnostisch). |
| metrics | `@toolctrl/metrics` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/metrics` | prom-client Registry/Default-Metriken, Token-Auth für `/metrics`, AI-Cost-Counter mit Cardinality-Disziplin. |
| error-reporter | `@toolctrl/error-reporter` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/error-reporter` | Error-Reporter-Interface, Sanitizer, Sentry-Adapter, OTel-Tracing-Bootstrap. |
| url | `@toolctrl/url` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/url` | Kanonische Pfad-/URL-Builder (app/admin/api, host-aware). |
| config | `@toolctrl/config` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/config` | Env-Variablen-Katalog + Validierung, Web/API/Worker-Config-Loader, Domain-Routing, Session-Cookie-Policy, Secret-Validatoren. |
| shared | `@toolctrl/shared` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/shared` | **Aktuell leer** (`src/index.ts` enthält nur `export {}`). Platzhalter für künftige dependency-freie Cross-Cutting-Typen/Utils. |
Konventionen über alle Pakete:
- `"type": "module"`, `"sideEffects": false`, `"private": true`, `version` `0.0.0`.
- Exports zeigen auf `./src/*.ts` (kein Build-Step; `build` = `typecheck`).
- Interne Imports verwenden `.js`-Endungen (ESM/NodeNext), z. B. `export * from "./errors.js"`.
- Tests liegen als `*.test.ts(x)` neben der Quelle; `pnpm test` = `vitest run src`.
- Pakete sind bewusst Prisma-/Framework-frei: DB-Zugriff wird per strukturell typisiertem Client-Parameter (`*Client`) bzw. Loader-Callback injiziert.
---
## @toolctrl/foundation
**Zweck:** Zentrale, reine Querschnitt-Bausteine ohne harte DB-/HTTP-Framework-Abhängigkeit. Prisma-Tabellen werden über strukturell typisierte Client-Interfaces erwartet (Dependency Injection statt Import).
**Zentrale Dateien**
- Barrel: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/foundation/src/index.ts` (re-exportiert errors, audit, permissions, flags, idempotency, adapters, notifications, workflows, concurrency).
- `package.json` exportiert zusätzlich `./outbound-http`.
**Haupt-Exporte je Modul**
| Datei (absolut) | Exporte | HiveGrid-Relevanz |
| --- | --- | --- |
| `…/packages/foundation/src/errors.ts` | `CatalogError` (Klasse), `CatalogErrorDefinition`, `catalogRegistry` (leer-frozen), `foundationErrors` (`forbidden` 403, `notFound` 404, `idempotencyConflict` 409, `idempotencyInProgress` 409), `toPinoLogObject(error)`, Typen `JsonValue`/`JsonPrimitive` | Querschnitt „Klartext-Diagnostik/CatalogError" — strukturierter Fehler mit `code`/`status`/`i18nKey`/`details` für alle Module M001–M017. `toPinoLogObject` für Logger-Integration. |
| `…/packages/foundation/src/audit.ts` | `auditEvent(input, client)`, `withAuditMiddleware(handler, buildEvent, client)`, Typen `AuditEventInput`, `AuditEventClient` | Querschnitt „Audit-Middleware". `withAuditMiddleware` wrappt Service-Handler und schreibt nach Erfolg ein `AuditEvent`. Relevant für jede mutierende Aktion (M001–M017). |
| `…/packages/foundation/src/permissions.ts` | `requirePermission(ctx, permission)`, `withTenantScope(tenantId, value)`, `withPermission(permission, getContext, handler)`, `withOwnerCheck(ctx, value)`, Typ `PermissionContext` | Querschnitt „withTenantScope/withPermission/withOwnerCheck". Tenant-Scoping + Permission-/Owner-Gating für alle Tenant-Module. Bei Fehlschlag `CatalogError(notFound)` (404, nicht 403 — Existenz-Versteckung). |
| `…/packages/foundation/src/flags.ts` | `defineFlag(def)`, `featureFlag(input)`, `selectFlagRecord(input)`, `normalizeFlagScope(record)`, Typen `FlagDefinition`, `FlagRecord`, `FlagResolutionInput`, `NormalizedFlagScope` | Querschnitt „defineFlag/featureFlag". Leichtgewichtige Flag-Auflösung (user > tenant > global, override). Für einfache Gates ohne Targeting-Engine. |
| `…/packages/foundation/src/idempotency.ts` | `withIdempotency(input, client, operation)`, Typen `IdempotencyClient`, `IdempotencyResponse` | Querschnitt „IdempotencyClient". Replay-sichere Mutationen (scope+key), In-Process-Lock, stabiler Fingerprint-Hash, `409`-Konflikt bei Hash-Mismatch / In-Progress. Für POST-Endpunkte mit Idempotency-Key in jedem Modul. |
| `…/packages/foundation/src/adapters.ts` | `defineAdapter(def)`, `getAdapter(key)`, Typen `AdapterDefinition`, `AdapterStatus` (`not_configured`/`configured`/`healthy`/`degraded`/`unavailable`) | Querschnitt „defineAdapter/getAdapter". Globales Adapter-Register (Provider-Slots) mit Status. `getAdapter` liefert Fallback `not_configured`. Für Provider-/Integrations-Module. |
| `…/packages/foundation/src/notifications.ts` | `notify(input, client, prefs)`, `notifyMany(...)`, `bundleNotifications(inputs)`, `isNotificationEnabled(input, prefs)`, `assertI18nKey(key)`, Typen `NotificationInput`, `NotificationClient`, `NotificationPreference`, `NotificationBundle`, `NotificationPreferenceChannel` (`IN_APP`/`EMAIL`), `NotificationDeliveryStatus` (`PENDING`/`SENT`/`FAILED`/`SKIPPED`/`READ`) | Querschnitt „notify/bundleNotifications". Tenant-pflichtige Notifications mit Preference-Check, Upsert per `sourceKey`, Metadata-Sanitizing (Redaction sensibler Keys), Bundling pro Tenant/User/Channel/Category. `assertI18nKey` erzwingt i18n-Key-Form. |
| `…/packages/foundation/src/workflows.ts` | `workflowBlockContracts`, `getWorkflowBlockContract(value)`, `CORE_/PRO_WORKFLOW_BLOCK_TYPES`, `NATIVE_WORKFLOW_TRIGGER_SUPPORTED_BLOCK_TYPES`, `extractWorkflowReferences(wf)`, `getMissingWorkflowReferences(...)`, `validateWorkflowRequiredInputs(wf)`, `preflightWorkflow(input)`, `getNativeWorkflowTriggerCapabilityErrors(blocks)`, Typen `WorkflowBlockType`, `WorkflowBlockContract`, `WorkflowPreflightResult` u. a. | Workflow-/Automation-Domäne (Block-Verträge, Referenz-Extraktion, Preflight). Relevant für ein evtl. Workflow-/Automations-Modul; ansonsten Querschnitt für Template-Variablen-Validierung. |
| `…/packages/foundation/src/concurrency.ts` | `mapWithConcurrency(items, concurrency, worker)` | Querschnitt-Utility: begrenzte parallele Verarbeitung (Batch-Jobs, Imports). |
| `…/packages/foundation/src/outbound-http.ts` (Subpath `@toolctrl/foundation/outbound-http`) | `fetchOutboundHttpWithRedirects(input, init, options)`, `readLimitedResponseText(response, limit)`, `validateOutboundHttpUrl(endpoint)`, Klasse `OutboundHttpUrlValidationError`, Typen `OutboundHttpFetchOptions`, `OutboundHttpAddressResolver`, `LimitedResponseBody` | Querschnitt-Sicherheit: SSRF-Schutz (Block private/internal IPs v4+v6, DNS-Resolve-Check, Redirect-Limit, Timeout, Response-Size-Limit). Für jede ausgehende HTTP-Aktion (Webhooks, httpRequest-Block, Integrationen). |
**Abhängigkeit:** `@toolctrl/i18n` (workspace).
**Genutzte Prisma-Modelle (per Client-Injection erwartet, Definition siehe unten):**
- `AuditEvent` — `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/core.prisma:55`
- `IdempotencyKey` — `…/prisma/schema/core.prisma:84` (Unique `@@unique([scope, key], name: "idempotency_key_scope_key")`, passt exakt zur `IdempotencyClient`-Where-Form)
- `NotificationDelivery` / `UserNotificationPreference` — `…/prisma/schema/notifications.prisma:14` / `:41`
---
## @toolctrl/view-models
**Zweck:** Reine Typ-/Helper-Schicht für UI-ViewModels — VM-State-Maschine, Action-Callbacks, Registry-Gate und Shell-Registry-Item-Typen.
**Zentrale Datei:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/view-models/src/index.ts` (einziges Quellmodul).
| Export | Beschreibung | HiveGrid-Relevanz |
| --- | --- | --- |
| `VMState<T>` (Union), `VMStatus` (`idle`/`loading`/`ready`/`empty`/`error`) | Diskriminierte Status-Maschine; `error` trägt `{ messageKey; code? }` (i18n-Key, kein Klartext) | Querschnitt „VMState" — einheitlicher Loader-/Empty-/Error-State für alle Listen-/Detail-VMs in M001–M017. |
| `ActionCallbacks<T>` (`onCreate`/`onUpdate`/`onDelete`/`onRefresh`) | Standard-CRUD-Callback-Vertrag | Querschnitt „ActionCallbacks" — konsistente Action-Verdrahtung. |
| `RegistryGate` (`{ permission?; flag? }`), `I18nLabel` (`{ labelKey; descriptionKey? }`) | Gating-/Label-Bausteine | Querschnitt „RegistryGate" — Sidebar/Breadcrumb/Mobile/Portal-Einträge per Permission + Flag gaten. |
| `SidebarRegistryItem`, `BreadcrumbRegistryItem`, `MobileRegistryItem`, `PortalRegistryItem`, `ShellRegistries` | Shell-Navigations-Registry-Typen (alle mit `RegistryGate` + `I18nLabel`) | Navigations-Registries für die App-Shell; jedes Modul registriert seine Einträge gegated. |
| `ready<T>(data)`, `empty<T>()` | Konstruktoren für `VMState` | Helper. |
| `assertI18nLabel(label)` | Erzwingt i18n-Key-Form für `labelKey`/`descriptionKey` (wirft sonst) | i18n-Disziplin in Registries. |
Keine Runtime-Dependencies.
---
## @toolctrl/entitlements
**Zweck:** Single Source of Truth für Feature-Gates, Plan-Stubs, Produkt-Blueprints und AI-Quotas/Overage; reine Entitlement-Auswertung.
**Zentrale Datei (Barrel):** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/entitlements/src/index.ts`
| Datei (absolut) | Schlüssel-Exporte | HiveGrid-Relevanz |
| --- | --- | --- |
| `…/src/features.ts` | `featureKeys` (19 Keys: `crm-basic`, `support-tickets`, `knowledge-base-read/-tenant`, `users`, `email-marketing`, `imported-lists`, `crm-based-lists`, `onboarding`, `api`, `workflow-engine`, `mcp`, `advanced-builder`, `import-export`, `rate-limit-per-minute/-second`, `priority-support`, `ai-credits`, `private-generations`), `featureGateRegistry`, `getFeatureGate`, `isFeatureKey`, `assertKnownFeatureKey`; Typen `FeatureKey`, `FeatureCategory`, `FeatureLimitType` (`not-included`/`included`/`monthly`/`max`/`unlimited`) | Querschnitt Entitlements: kanonische Feature-Keys + Limit-Typen für Gating in allen Modulen. |
| `…/src/evaluate.ts` | `evaluateEntitlement(input)`, `entitlementLimitTypes` (numerisch 0–4), Typen `EntitlementDecision`, `EntitlementDecisionReason`, `EvaluateEntitlementInput` | Kern-Entscheidung „darf der Tenant?" inkl. Trial-/Subscription-/Usage-Logik (`max`/`monthly` mit `remaining`). |
| `…/src/plans.ts` | `entitlementPlanIds`, `standardEntitlementPlans`, `getStandardEntitlementPlan`, `listStandardEntitlementPlanIds`, `getStripeProductPlaceholder`/`getStripePricePlaceholder`; Typen `EntitlementPlanStub`, `EntitlementPlanFeatureAssignment` u. a. | Standard-Plan-Stubs + Stripe-Placeholder-Mapping (Billing-Querschnitt). |
| `…/src/blueprints.ts` | `productBlueprints`, `productBlueprintIds`, `getProductBlueprint`, `getDefaultProductBlueprint`; Typen `ProductBlueprintDefinition`, `ProductBlueprintFeatureDefinition` | Produkt-Blueprints (Feature-Bündel pro Produkttyp). |
| `…/src/product-access.ts` | `evaluateProductAccess(input)`; Typen `ProductAccessInput`, `ProductAccessDecision`, `TenantFeatureOverride` (`enabled`/`disabled`/`inherit`) | Produkt-Zugriffsentscheidung inkl. Tenant-Override. |
| `…/src/ai-quotas.ts` | `aiQuotaCapabilityKeys`, `standardAIQuotaDefaults`, `getAIQuotaPlanDefaults`, `resolveTenantAIQuota`, `parseTenantAIQuotaOverride`; Typen `AIQuotaPlanDefaults`, `ResolvedTenantAIQuota`, `TenantAIQuotaOverride` | AI-Quoten pro Plan/Tenant — relevant falls HiveGrid AI-Capabilities gated. |
| `…/src/ai-overage.ts` | `resolveTenantAIOverage`, `computeMarkedUpCostCents`, `roundUpToBlock`, `getAIOveragePlanDefaults`, Defaults-Konstanten (`AI_OVERAGE_DEFAULT_MULTIPLIER` 1.5, `…_BLOCK_CENTS` 10, Min-Werte) | AI-Overage-Billing (Markup/Block-Rundung). |
Keine Runtime-Dependencies (rein, testbar). Verknüpfte Prisma-Modelle: `SubscriptionFeature` (`…/prisma/schema/subscriptions.prisma:65`), `PortalSubscriptionFeature` (`…/prisma/schema/portals.prisma:149`).
---
## @toolctrl/feature-flags
**Zweck:** Feature-Flag-Engine v2 mit Targeting-Regelbaum, zeit-/abhängigkeitsbasierter Evaluation, Variant-Bucketing, Eval-Cache und Cross-Process-Invalidierung; optionaler React-Client.
**Subpath-Exports (package.json):** `.` (Barrel), `./targeting`, `./cache`, `./client`.
| Datei (absolut) | Schlüssel-Exporte | HiveGrid-Relevanz |
| --- | --- | --- |
| `…/packages/feature-flags/src/index.ts` | re-exportiert targeting + `evaluateFlag`, `getFeatureFlagVariant`, Cache- und Publisher-API | Haupteinstieg. |
| `…/src/evaluate.ts` | `evaluateFlag(name, ctx, options)`, `getFeatureFlagVariant(...)`, Typen `StoredFeatureFlag`, `FlagVariant`, `FlagLoader`, `EvaluateFlagResult` (`reason`: u. a. `flag-not-found`/`disabled`/`parent-disabled`/`time-window-future`/`matched`), `EvaluateFlagOptions` | Querschnitt Feature-Flags (mächtige Variante zu `foundation/flags`): Zeitfenster, Parent-Dependency (Zyklus-Schutz `maxDepth` 8), sticky Variant-Bucketing. DB-frei via `FlagLoader`-Callback. |
| `…/src/targeting/types.ts` | Regel-Union `TargetingRule` (`boolean`/`allowlist`/`percentage`/`plan-tier`/`role-based`/`time-based`/`composite`), `EvaluationContext` (`tenantId`/`userId`/`email`/`planTier`/`roles`/`now`) | Targeting-Modell — Rollouts pro Tenant/Plan/Rolle/Prozent für jedes Modul. |
| `…/src/targeting/evaluator.ts` | `evaluateRule(rule, ctx)` | Reine Regel-Auswertung (kein I/O). |
| `…/src/targeting/percentage.ts` | `computeBucket(key, salt)`, `isInRolloutBucket(...)` | Sticky Percentage-Rollout. |
| `…/src/cache.ts` | `createFlagEvalCache(...)`, `buildCacheKey`, `hashContext`, `FEATURE_FLAGS_INVALIDATION_CHANNEL` (`"feature-flags:invalidate"`), Typ `FlagEvalCache` | Eval-Cache + Invalidierungs-Channel-Konstante. |
| `…/src/publisher.ts` | `publishFeatureFlagInvalidationViaStore(...)`, `publishFeatureFlagInvalidationOnce(...)`, Typ `PublishFeatureFlagInvalidationOptions` | Cross-Process-Cache-Invalidierung (via shared-store). |
| `…/src/client/context.tsx` (Subpath `./client`) | `FeatureFlagProvider`, `useFeatureFlag`, `useFeatureFlagEnabled`, `useFeatureFlagVariant`, Typen `FeatureFlagProviderProps`, `FeatureFlagsHydration` | React-Hooks/SSR-Hydration für Flag-gegatete UI. |
**Dependencies:** `@toolctrl/shared-store` (Pub/Sub); optionale Peers `@toolctrl/logger`, `react`.
**Prisma-Modelle:** `FeatureFlag`, `FeatureFlagFilter`, `FeatureFlagAudit` (append-only Audit-Trail) — `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/featureFlags.prisma:1/32/44`. `StoredFeatureFlag` spiegelt eine Teilmenge der `FeatureFlag`-Row, bleibt aber Prisma-frei.
---
## @toolctrl/i18n
**Zweck:** Locale-Konfiguration, SSR-sichere Intl-Formatter, ICU-MessageFormat-Pluralisierung, RTL-Erkennung und `useTranslate`-Hook-Factory.
**Subpath-Exports:** `.` (Barrel), `./intl`, `./icu`, `./useTranslate`.
| Datei (absolut) | Schlüssel-Exporte | HiveGrid-Relevanz |
| --- | --- | --- |
| `…/packages/i18n/src/index.ts` | `defaultLocale` (`"de"`), `supportedLocales` (`["de","en"]`), `i18nNamespaces` (`translations`, `errors`, `mail`, `common`, `settings`, `billing`, `crm`, `auth`, `emailMarketing`), `i18nConfig`, `isSupportedLocale`, `getDirection`, `isRTL`; re-exportiert Intl + ICU + `useTranslate` | Querschnitt i18n: Namespaces/Locales als SSOT; `assertI18nKey`/`assertI18nLabel` (in foundation/view-models) erzwingen Key-Form passend zu diesen Namespaces. |
| `…/src/intl/index.ts` → `number.ts`/`currency.ts`/`date.ts`/`list.ts` | `formatNumber`, `formatPercent`, `formatCompactNumber`, `formatCurrency`, `formatDate`, `formatRelativeTime`, `formatList` (+ Options-Typen) | Einheitliche, locale-explizite Formatierung server- wie clientseitig (Listen, Detail-Views, Mails). |
| `…/src/icu/index.ts` → `plural.ts` | `formatIcu`, `createIcuFormatter`, Typen `IcuValues`, `IcuOptions`, `IcuFormatter` | ICU-Plurale/Select für korrekte mehrsprachige Texte. |
| `…/src/useTranslate.ts` | `createUseTranslate(deps, defaults)`, `isIcuMessage`, Typen `Translator`, `ReactI18nextLike`, `UseTranslateOptions` | React-`useTranslate`-Factory mit automatischer ICU-Erkennung; react-i18next wird injiziert (kein harter Dep). |
**Dependencies:** `i18next`, `intl-messageformat`.
**Konvention (Querschnitt „i18n-assert"):** i18n-Keys folgen dem Muster `^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$` — durchgesetzt von `assertI18nKey` (foundation/notifications.ts) und `assertI18nLabel` (view-models). Fehler-Objekte tragen statt Klartext einen `i18nKey` (siehe `CatalogError`, `VMState.error.messageKey`).
---
## @toolctrl/logger
**Zweck:** Pino-Logger-Factory mit eingebautem Redaction sensibler Felder.
**Zentrale Datei:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/logger/src/index.ts`
| Export | Beschreibung | HiveGrid-Relevanz |
| --- | --- | --- |
| `createLogger(options?)` | Erzeugt Pino-Logger; `redact.paths` deckt `authorization`, `cookie`, `password`, `token`, `secret`, `apiKey`, `redisUrl`, `connectionString` (inkl. `*.`-Wildcards) ab, Censor `[Redacted]` | Querschnitt Logging: sicheres, strukturiertes Logging für alle Module. Spielt mit `toPinoLogObject` (foundation/errors) zusammen. |
| `createChildLogger(logger, bindings)` | Child-Logger mit Bindings (z. B. `requestId`, `tenantId`) | Request-/Tenant-Scoped Logging. |
| `resolveLogLevel(env?)` | Liest `TOOLCTRL_LOG_LEVEL`/`LOG_LEVEL` (Default `info`) | Env-getriebenes Level. |
| `defaultLogger` | Vorinstanziierter Default | — |
| Typen `LogLevel`, `ToolctrlLogger`, `CreateLoggerOptions` | — | — |
**Dependency:** `pino`.
---
## @toolctrl/observability
**Zweck:** Framework-agnostischer PII-scrubbender OpenTelemetry-SpanProcessor (Decorator über bestehenden Processor; keine harte OTel-SDK-Dependency).
**Zentrale Datei:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/observability/src/pii-span-processor.ts` (Barrel: `…/src/index.ts`).
| Export | Beschreibung | HiveGrid-Relevanz |
| --- | --- | --- |
| `PIISpanProcessor` | Wrappt Delegate-Processor, redigiert Attribute in `onEnd()` | Querschnitt Observability: verhindert PII-Leak in Traces für alle Module. |
| `HEADER_ALLOWLIST` | Erlaubte Header-Attribute (Rest, u. a. `authorization`/`cookie`/`x-api-key`, wird gedroppt) | — |
| `redactSpanAttributes`, `scrubDbStatement`, `scrubUrlQuery`, `isHeaderAttribute` | Einzel-Scrubber (Query-String → `?[scrubbed]`, DB-Literale → `?`) | — |
| Typen `DelegateSpanProcessor`, `PiiScrubableSpan` | Strukturelle OTel-Shapes (keine SDK-Bindung) | — |
Keine Runtime-Dependencies.
---
## @toolctrl/metrics
**Zweck:** prom-client basierte Metrics-Registry, Default-HTTP-Metriken, Token-Auth für den `/metrics`-Endpoint und cardinality-disziplinierter AI-Cost-Counter.
**Zentrale Datei (Barrel):** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/metrics/src/index.ts`
| Datei (absolut) | Schlüssel-Exporte | HiveGrid-Relevanz |
| --- | --- | --- |
| `…/src/registry.ts` | `createMetricsRegistry(options)`, Typen `MetricsRegistry`, `CreateMetricsRegistryOptions` | Querschnitt Metrics: zentrale Registry je App. |
| `…/src/defaults.ts` | `createDefaultMetrics(registry)`, `HISTOGRAM_BUCKETS_SECONDS`, Typen `DefaultMetrics`, `LatencyHistogramOptions` | Standard-Latenz-/Request-Metriken. |
| `…/src/auth.ts` | `isMetricsEnabled(env)`, `validateMetricsTokenAuth(options)`, `redactMetricsToken(token)`, Typen `MetricsTokenAuthResult`, `MetricsTokenAuthOptions` | Schützt `/metrics`-Endpoint per Token. |
| `…/src/ai-cost.ts` | `recordAICostCents(input, registry)`, `resetAICostMetricsForTests()`, Konstanten `MAX_TENANT_BUCKETS` (100), `OTHER_BUCKET`, `ANONYMOUS_BUCKET`, Typ `AICostMetricInput` | AI-Kosten-Counter `ai_cost_cents_total{tenant_bucket,provider,model,capability}` mit Top-N+Other-Bucketing (Cardinality-Schutz). |
**Dependency:** `prom-client`.
---
## @toolctrl/error-reporter
**Zweck:** Provider-agnostisches Error-Reporting (Interface + No-op + Sentry-Adapter), Sanitizing sensibler Daten und optionaler OTel-Tracing-Bootstrap.
**Zentrale Datei (Barrel):** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/error-reporter/src/index.ts`
| Datei (absolut) | Schlüssel-Exporte | HiveGrid-Relevanz |
| --- | --- | --- |
| `…/src/index.ts` | `reportError(reporter, event)` (schluckt Reporter-Fehler), `createNoopErrorReporter()`, Typen `ErrorReporter`, `ErrorReportEvent`, `ErrorReportContext`, `ErrorReportSeverity` | Querschnitt Error-Reporting: einheitliches Melden inkl. `component`/`operation`/`requestId`/`tenant`-Kontext. Sanitisiert vor Versand. |
| `…/src/sanitize.ts` | `sanitizeErrorReportValue(value)`, `__internal` | Entfernt sensible Felder vor Reporting. |
| `…/src/sentry.ts` | `createSentryErrorReporter(options)`, `sanitizeSentryEvent(raw)`, `SENTRY_HEADER_ALLOWLIST`, `SENTRY_DROP_EXCEPTION_TYPES`, Typen `SentryErrorReporterOptions`, `SentryLikeModule`, `SentryInitOptions` | Optionaler Sentry-Adapter (Sentry-SDK strukturell injiziert). |
| `…/src/tracing.ts` | `initializeTracing(options)`, `shutdownTracing()`, Typen `TracingOptions`, `NodeSdkLikeModule`, `OtlpExporterLikeModule` | OTel-NodeSDK-Bootstrap mit OTLP-Exporter; OTel-Pakete via dynamic import optional. |
Keine festen Runtime-Dependencies (SDKs strukturell/optional).
---
## @toolctrl/url
**Zweck:** Kanonische, host-aware Pfad- und URL-Builder für App-/Admin-/API-Surfaces (multi-tenant Subdomains).
**Zentrale Datei:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/url/src/canonical.ts` (Barrel: `…/src/index.ts`).
| Export | Beschreibung | HiveGrid-Relevanz |
| --- | --- | --- |
| `appPath`/`appUrl` (mit `tenantSlug`), `adminPath`/`adminUrl`, `apiPath`/`apiUrl` (mit `version` `v1`), `default` | Bauen relative Pfade bzw. absolute URLs (`mode: "host-aware"`, Default-Protokoll `https`) | Querschnitt Routing/Links: konsistente Tenant-App-, Admin- und API-Links für alle Module (M001–M017). |
| Typen `AppUrlOptions`, `AdminUrlOptions`, `ApiUrlOptions`, `CanonicalUrlMode`, `ApiVersion`, `PathInput` u. a. | — | — |
Keine Runtime-Dependencies.
---
## @toolctrl/config
**Zweck:** Typisierter Env-Variablen-Katalog (SSOT für Doku/Drift), Validierungs-Helfer, Web/API/Worker-Config-Loader, Domain-Routing und Session-Cookie-Policy.
**Zentrale Datei (Barrel):** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/config/src/index.ts`
| Datei (absolut) | Schlüssel-Exporte | HiveGrid-Relevanz |
| --- | --- | --- |
| `…/src/index.ts` | `listEnvVariables(opts)`, `getEnvVariableDefinition(name)`, `groupEnvVariablesByClassification()`, `getRequiredEnvVariableNames(opts)`, `validateEnv(input, opts)`, `envVariableDefinitions` | Querschnitt Konfiguration: Env-Anforderungen pro Runtime-Target ableiten/validieren. |
| `…/src/catalog.ts` | `envVariableDefinitions` (große Liste; jede mit `classification`, `targets`, `kind`, `source`, `requiredInProduction`) | Single Source of Truth aller Env-Variablen. |
| `…/src/types.ts` | Typen `RuntimeTarget` (`shared`/`web`/`api`/`worker`), `EnvClassification` (6 Klassen), `EnvVariableDefinition`, Web/Api/Worker-Config-Shapes (`WebConfig`, `ApiConfig`, `WorkerConfig`, …) | Config-Vertrag. |
| `…/src/web.ts` / `api.ts` / `worker.ts` | `loadWebConfig`/`loadRequiredWebConfig` (+ `WebConfigError`); analog API/Worker | Runtime-Config-Loader mit Diagnostics. |
| `…/src/routing.ts` | `getDomainRoutingConfig`, `hostMatches`, `normalizeHost`, Typen `DomainRoutingConfig`, `DomainRoutingEnv` | Multi-Tenant Domain-Routing (passt zu `@toolctrl/url`). |
| `…/src/session-cookie.ts` | `resolveSessionCookiePolicy`, `resolveApiSessionCookiePolicy`, Typen `SessionCookiePolicy`, `SessionCookieSameSite` | Sichere Session-Cookie-Policy. |
| `…/src/secret-validator.ts` | `validateRequiredSecretString`, `validateOptionalSecretString`, `validateOptionalSecretUrl`, `validateBooleanFlag` | Secret-/Flag-Validierung mit Diagnostics. |
| `…/src/plugin-lifecycle.ts` | `resolvePluginLifecycleRuntimeEnv`, Typ `PluginLifecycleRuntimeEnv` | Plugin-Lifecycle-Env. |
Keine deklarierten Runtime-Dependencies. README dokumentiert die Klassifikation und das vollständige Inventar (`…/packages/config/README.md`).
---
## @toolctrl/shared
**Zweck:** Reservierter Platz für dependency-freie, app-übergreifende Typen/Konstanten/Utilities.
**Status:** **Leer.** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/shared/src/index.ts` enthält ausschließlich `export {};`. README (`…/packages/shared/README.md`) beschreibt nur geplante Kandidaten und die Regel „kein web/API/worker/provider/DB-spezifischer Code". Für M001–M017 derzeit ohne nutzbaren Inhalt.
---
## Relevante Prisma-Modelle (Querschnitt) — exakte Pfade
| Modell | Pfad | Genutzt von |
| --- | --- | --- |
| `AuditEvent` | `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/core.prisma:55` | foundation `audit.ts` (`AuditEventClient`). Felder u. a. `tenantId`, `actorType` (req.), `action`, `targetType/Id`, `requestId`, `outcome` (Default `success`), `dataSensitivity`, `regulatoryScope[]`, `metadata`, `previousHash`/`eventHash` (Hash-Chain). |
| `IdempotencyKey` | `…/prisma/schema/core.prisma:84` | foundation `idempotency.ts`. `@@unique([scope, key], name: "idempotency_key_scope_key")` deckt die `IdempotencyClient`-Where-Form, `expiresAt`-Index für Cleanup. |
| `NotificationDelivery` | `…/prisma/schema/notifications.prisma:14` | foundation `notifications.ts`. Enums `NotificationDeliveryStatus`/`NotificationPreferenceChannel`; `@@unique([tenantId, userId, channel, sourceKey])` für Upsert. |
| `UserNotificationPreference` | `…/prisma/schema/notifications.prisma:41` | foundation `notifications.ts` (`NotificationPreference`). |
| `FeatureFlag` / `FeatureFlagFilter` / `FeatureFlagAudit` | `…/prisma/schema/featureFlags.prisma:1` / `:32` / `:44` | feature-flags-Paket (`StoredFeatureFlag` spiegelt Teilmenge; v2-Felder `rules`/`variants`/`enabledFrom`/`enabledUntil`/`parentFlagId`). |
| `SubscriptionFeature` | `…/prisma/schema/subscriptions.prisma:65` | entitlements (Plan-Feature-Zuordnung). |
| `PortalSubscriptionFeature` | `…/prisma/schema/portals.prisma:149` | entitlements (Portal-Features). |
---
## CI-/Verify-Scripts (Querschnitt-relevant)
Pfade: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/scripts/…`; npm-Scripts in `…/boilerplate/package.json`.
| Script (npm) | Datei | Bezug |
| --- | --- | --- |
| `verify:i18n` | `scripts/i18n-check.ts` + `scripts/verify-hardcoded-strings.mjs` + ESLint über `packages/ui-*` | i18n-Disziplin (Querschnitt i18n / i18n-assert). |
| `verify:env-drift` | `scripts/check-env-drift.mjs` | Prüft `@toolctrl/config`-Katalog gegen `.env*`-Beispiele + Code. |
| (Baseline) | `scripts/i18n-audit.mjs` | Baseline-Scan hardcodierter DE/EN-Strings (zählt, erzwingt nicht). |
| `verify:audit-cascade-guard` (Datei vorhanden) | `scripts/verify-audit-cascade-guard.mjs` | Schützt Audit-Cascade-Verhalten (`prisma/schema/accounts.prisma`). |
| `verify:workflow-outbox-native` | u. a. `packages/foundation/src/workflows.test.ts` | Workflow-Block-/Preflight-Verträge (foundation `workflows.ts`). |
| `verify:tenant-isolation-report` | `scripts/tenant-isolation-report.ts` | Tenant-Scoping-Report (passt zu `withTenantScope`). |
Zusätzlich besitzt jedes Paket `pnpm test` (vitest über `src`) und `pnpm typecheck`. Test-Dateien liegen jeweils neben der Quelle (z. B. `audit.test.ts`, `idempotency`-Coverage in `index.test.ts`/`concurrency.test.ts`, `evaluate.test.ts`, `pii-span-processor.test.ts`, `secret-validator.test.ts`).
---
## Kurz-Mapping „angefragte Querschnitt-Patterns → Fundort"
| Angefragtes Pattern | Fundort (Export → Datei) |
| --- | --- |
| Audit-Middleware | `withAuditMiddleware`, `auditEvent` → `…/foundation/src/audit.ts` |
| Klartext-Diagnostik / CatalogError | `CatalogError`, `foundationErrors`, `catalogRegistry`, `toPinoLogObject` → `…/foundation/src/errors.ts` |
| i18n-assert | `assertI18nKey` → `…/foundation/src/notifications.ts`; `assertI18nLabel` → `…/view-models/src/index.ts`; Key-Regex; `verify:i18n` |
| withTenantScope / withPermission / withOwnerCheck | alle → `…/foundation/src/permissions.ts` (+ `requirePermission`) |
| defineFlag / featureFlag | → `…/foundation/src/flags.ts` (leichtgewichtig); mächtige Engine: `evaluateFlag` → `…/feature-flags/src/evaluate.ts` |
| IdempotencyClient | `withIdempotency`, `IdempotencyClient` → `…/foundation/src/idempotency.ts` |
| defineAdapter / getAdapter | → `…/foundation/src/adapters.ts` |
| notify / bundleNotifications | `notify`, `notifyMany`, `bundleNotifications`, `isNotificationEnabled` → `…/foundation/src/notifications.ts` |
| VMState / ActionCallbacks / RegistryGate | alle → `…/view-models/src/index.ts` |
@@ -0,0 +1,345 @@
# Boilerplate-Inventur: Integrationen, KI & Apps-Engine
> Faktische Inventur des Boilerplate-Bereichs "Integrationen, KI & Apps-Engine".
> Quelle (NUR LESEN): `/Users/raffaelsheikh/Documents/GitHub/boilerplate`
> Erstellt fuer den Modul-Abgleich gegen HiveGrid 2.0. Alle Pfade absolut.
> Nichts erfunden — jeder Eintrag verweist auf konkrete Datei(en).
## Inhaltsverzeichnis / abgedeckte Pakete
| Paket | npm-Name | Zweck (kurz) |
|---|---|---|
| `packages/ai` | `@toolctrl/ai` | KI-Provider-Layer (Mistral/Scaleway/Aleph Alpha), Multi-Provider-Fallback, Embeddings/RAG-Pipeline, Cost-Tracking, Prompt-/Tool-Registry, Guards, Streaming |
| `packages/vector-store` | `@toolctrl/vector-store` | Vektor-Store-Adapter (pgvector + Qdrant) hinter einem Interface, tenant-scoped Wrapper |
| `packages/search` | `@toolctrl/search` | Typisierter Meilisearch-Wrapper, tenant-scoped Such-Index, Index-Schemas, Outbox-Publisher |
| `packages/plugins` | `@toolctrl/plugins` | Feature-Manifest-Schema + Loader (Apps-/Feature-Inventur-Manifeste) |
| `packages/plugin-runtime` | `@toolctrl/plugin-runtime` | WASM-Sandbox, Capability-Gateway, Manifest, Signatur, Hooks, Audit-Chain, Crash-Tracker, Migrations |
| `packages/plugin-sdk` | `@toolctrl/plugin-sdk` | Typdefinitionen + Settings-Iframe-Client fuer Plugin-Autoren (`defineHook`, `HostApi`) |
| `packages/storage` | `@toolctrl/storage` | S3/Scaleway-Storage-Adapter + lokaler Adapter, Signed-URLs, Virus-Scan (ClamAV/magic-bytes), Quarantaene |
| `packages/realtime` | `@toolctrl/realtime` | SSE/Realtime-Event-Serialisierung + Client/Server-Helfer (Notifications) |
| `packages/mail` | `@toolctrl/mail` | MailerSend-Provider, Queued-Mail-Provider, React-Email Transaktionstemplates |
| `packages/email-templates` | `@toolctrl/email-templates` | React-basierte Sequenz-Email-Templates (welcome, trial-reminder, re-activation) |
| `packages/email-sequences` | `@toolctrl/email-sequences` | Reine Logik: Sequenz-Engine, Condition-DSL, Step-Runner, Unsubscribe-Token |
| `packages/marketing-outbound` | `@toolctrl/marketing-outbound` | Outbound-Webhook-Adapter (Slack, Discord, Mailchimp, HubSpot, Brevo, Custom) |
| `packages/queue` | `@toolctrl/queue` | BullMQ-Wrapper: Queue-Namen, typisierte Producer/Jobs, Idempotenz, Prioritaeten, Redis-Connection |
Konvention durchgaengig: jedes Paket exportiert `exports` mit `.` -> `./src/index.ts` (Source-Exports, kein Build-Schritt zwingend), Scripts meist `test` / `typecheck` (teils `build`, `preview`). Pakete sind bewusst **dependency-arm** und lesen **nie `process.env` direkt** — Boundary-Code (apps/api, apps/worker) reicht env-Snapshots herein (Env-Access-Enforcement-Tests).
---
## 1. `@toolctrl/ai` — KI-Provider-Layer, Fallback, RAG, Cost
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ai`
**Deps:** `@ai-sdk/mistral`, `@ai-sdk/openai-compatible`, `ai` (Vercel AI SDK), `gpt-tokenizer`, `@toolctrl/entitlements`
**Subpath-Exports:** `.`, `./cost`, `./embeddings`, `./embeddings/providers/{openai,scaleway,aleph-alpha,anthropic}`
### 1.1 Provider-Adapter & Multi-Provider-Fallback
| Asset | Pfad | Was |
|---|---|---|
| Provider-Adapter-Interface + Registry | `packages/ai/src/index.ts` | `AIProviderAdapter`, `AIProviderName = "mistral" \| "scaleway" \| "aleph-alpha"`, `getAIProviderAdapter()`, `getAIProviderAdapterForModel()`, `getPrimaryProvider()`, `getAIProviderDiagnostics()`, Modell-Katalog `aiModels` (mit Preisen pro Mio Tokens), `defaultAIModel = "mistral-large-latest"`, `AIProviderAdapterError` mit typisierten Codes |
| Mistral-Provider | `packages/ai/src/providers/mistral.ts` | `createMistralLanguageModel()` via `@ai-sdk/mistral` `createMistral`, `resolveMistralModel(env)`, `MISTRAL_API_KEY`/`MISTRAL_MODEL` |
| Scaleway-Provider (OpenAI-kompatibel) | `packages/ai/src/providers/scaleway.ts` | `createScalewayLanguageModel()`, Default-BaseURL `https://api.scaleway.ai/v1`, Default-Modell `mistral-small-3.2-24b-instruct-2506`, `SCALEWAY_API_KEY`/`SCALEWAY_BASE_URL`/`SCALEWAY_MODEL` |
| Aleph-Alpha-Provider (OpenAI-kompatibel) | `packages/ai/src/providers/aleph-alpha.ts` | `createAlephAlphaLanguageModel()`, Default-BaseURL `https://api.aleph-alpha.com/v1`, Default-Modell `pharia-1-llm-7b-control`, `ALEPH_ALPHA_API_KEY`/`...BASE_URL`/`...MODEL` |
| OpenAI-Compatible-Bruecke | `packages/ai/src/providers/compatible.ts` | `createCompatibleLanguageModel()` ueber `@ai-sdk/openai-compatible` — gemeinsame Basis fuer Scaleway + Aleph Alpha |
| Health-Tracker + Fallback | `packages/ai/src/fallback.ts` | `AIProviderHealthTracker` (Failure-Window 30 min), `getFallbackProviderSelection()`, `fallbackProviderOrder = ["mistral","scaleway","aleph-alpha"]` — durchlaeuft Provider in fester Reihenfolge, ueberspringt ungesunde/unkonfigurierte |
**HiveGrid-Relevanz:** Kern fuer jedes KI-Modul (Chatbot, Prompt-Flows, Generieren, RAG-Antworten). EU-Provider Scaleway/Aleph Alpha = DSGVO-Argument. Multi-Provider-Fallback deckt Querschnitt "KI-Verfuegbarkeit/Resilienz" ab.
### 1.2 Embeddings-Pipeline (RAG-Schreib-/Leseseite)
**Pfad:** `packages/ai/src/embeddings` — Subpath `@toolctrl/ai/embeddings`
| Asset | Pfad | Was |
|---|---|---|
| Provider-Interface + Typen | `packages/ai/src/embeddings/types.ts` | `EmbeddingProvider` (`embed`/`embedSingle`, `dimensions`, `maxBatchSize`), `EmbeddingProviderName = "openai" \| "scaleway" \| "aleph-alpha" \| "anthropic"`, `EmbeddingProviderError` mit typisierten Codes, `EmbeddingProviderEnv`-Snapshot |
| Factory + Registry | `packages/ai/src/embeddings/factory.ts` | `createEmbeddingProvider()`, `registerEmbeddingProviderAdapter()` (Adapter registrieren sich selbst), `resolveEmbeddingProviderName(env)` (Default openai->scaleway->aleph-alpha), `resolveEmbeddingDimensions(env)` |
| Embed-Adapter (opt-in subpaths) | `packages/ai/src/embeddings/providers/{openai,scaleway,aleph-alpha,anthropic}.ts` | Scaleway-Default `bge-multilingual-gemma2` (1024 dim), Batch 64, max 4096 Tokens; Anthropic ist bewusst Stub/Platzhalter |
| Schreibseite | `packages/ai/src/embeddings/embed-and-upsert.ts` | `embedAndUpsert()` — chunked, embeds, upsert in `VectorUpsertSink`; erzwingt `tenantId` im Payload; deterministische `buildChunkId()`; optionale Kostenverfolgung ueber `executeLLMCall` (capability `embedding`) |
| Leseseite | `packages/ai/src/embeddings/retrieve.ts` | `retrieveRelevantContext()` — Query embedden, Fan-out ueber Collections, Rank-Merge, min-Score 0.7, top-K 8; tenant-scoped via `RagRetrievalStore.searchForTenant` |
| Chunking | `packages/ai/src/embeddings/chunking.ts` | `splitForEmbedding()`, `countTokens()`, `DEFAULT_CHUNK_TOKENS`/`...OVERLAP_TOKENS` |
| Outbox-Publisher (RAG-Index) | `packages/ai/src/embeddings/outbox-publisher.ts` | `publishRagUpsertEvent`/`publishRagDeleteEvent`, `ragEventType()` -> `rag.index.<entity>.<verb>` (mapped auf BullMQ-Queue `outbox.rag-index`), Idempotenz via `dispatchKey` |
**HiveGrid-Relevanz:** Komplette RAG-Grundlage (Knowledge-Base-Suche, KI-Antworten mit Quellen, semantische Suche). Outbox-Pattern erlaubt async Reindex bei jedem Domain-Write. Direkt nutzbar fuer HiveGrid-Module "Wissensdatenbank/RAG" und "KI-Assistent".
### 1.3 Cost-Tracking, Quota & Guards
| Asset | Pfad | Was |
|---|---|---|
| Cost-Barrel | `packages/ai/src/cost/index.ts` | Re-Export: `countTokens`, `countCallCost`, `executeLLMCall`, `estimateLLMCost`, Pricing-Tabelle (`getPricingTable`, `LATEST_PRICING_VERSION`), Spend-Cache, Quota |
| LLM-Wrapper | `packages/ai/src/cost/executeLLMCall.ts` | `executeLLMCall()` — wrappt jeden Provider-Call, leitet Token/Kosten ab, emittiert Outbox-Event `ai.cost.event.persist`, Quota-Pre-Check + Soft-Degradation + Concurrency-Slot; `setLLMCostPublisher/MetricsHook/ErrorHandler`; Flag `AI_COST_TRACKING_ENABLED=false` deaktiviert; capabilities `chat`/`chat-stream`/`embedding`/`completion`/`rerank`/`tool-call` |
| Quota-Guard | `packages/ai/src/cost/assertCredit.ts` | `assertTenantAICreditAccess()`, Concurrency-Counter (Redis-aehnlich), Soft-Degradation-Threshold |
| Spend-Cache | `packages/ai/src/cost/spend-cache.ts` | `createSpendCache()`, Monatsspend-Caching (TTL) |
| Token-Zaehlung | `packages/ai/src/cost/tokens.ts` | `countTokens`/`countMessageTokens` (gpt-tokenizer + Heuristik-Fallback) |
| Guards | `packages/ai/src/guards/index.ts` | `assertAICostBudget()`, `createFixedWindowAIRateLimiter()` (pro userId), `createAIAuditEvent()`/`redactAIAuditPayload()` (PII-Redaction, prompt/tool_call/mutation), `createReviewRequiredResult()`, `AIGuardError` |
**HiveGrid-Relevanz:** Querschnitt "Billing/Usage-Metering" (KI-Kosten pro Tenant), "Rate-Limiting", "Audit/DSGVO" (Redaction). Persistiert in Prisma `AICostEvent` (siehe unten). Direkt fuer HiveGrid-Modul "KI-Kontingente/Abrechnung".
### 1.4 Prompt-/Tool-Registry, Streaming, Eval
| Asset | Pfad | Was |
|---|---|---|
| Prompt-Registry | `packages/ai/src/prompts/index.ts` | `createAIPromptRegistry()` mit Versionierung, `render(selection, variables)`, `{{var}}`-Templates, `AIPromptRegistryError` |
| Tool-Registry | `packages/ai/src/tools/index.ts` | `createAIToolRegistry()` (scoped Tools fuer Vercel AI SDK `ToolSet`), `toToolSet(selection)`, Secret-Redaction in Fehlern |
| Streaming | `packages/ai/src/streaming/index.ts` | `createAITextStream()` ueber `streamText` (Vercel AI SDK), Guard-Evaluator, Prompt-/Tool-Registry-Integration, `getAITextStreamDiagnostics()` mit Redaction |
| Promptfoo-Provider + Evals | `packages/ai/src/promptfoo-provider.ts`, `packages/ai/test/evals/`, Repo-Root `promptfoo.config.yaml` | Eval-Harness (promptfoo-Baseline-Test) |
**HiveGrid-Relevanz:** "Prompt-Flows / Prompt-Bibliothek" (Versionierung), "KI-Tools/Agenten" (scoped Tool-Registry), Streaming-Chat-UX.
---
## 2. `@toolctrl/vector-store` — Vektor-Store-Adapter (pgvector + Qdrant)
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/vector-store`
**Deps:** `@qdrant/js-client-rest`
**Subpath-Exports:** `.`, `./factory`, `./adapters/pgvector`, `./adapters/qdrant`, `./wrappers/tenant-scoped`
| Asset | Pfad | Was |
|---|---|---|
| Adapter-Interface + Typen | `packages/vector-store/src/types.ts` | `VectorStoreAdapter` (createCollection/upsert/search/delete/health), `VectorItem` mit Pflicht-`payload.tenantId`, `VectorFilter`/`FilterClause` (Qdrant-Filter-Sprache), `VectorMetric = "cosine"\|"euclidean"\|"dot"`, `VectorDimensionMismatchError` |
| Factory (Adapter-Auswahl) | `packages/vector-store/src/factory.ts` | `createVectorStore()` -> waehlt pgvector (Default) oder Qdrant (`adapter:` oder `qdrantUrl`); `resolveAdapter()`; Namespace-Pflicht; gibt tenant-scoped Store zurueck |
| pgvector-Adapter | `packages/vector-store/src/adapters/pgvector.ts` | `PgvectorAdapter` ueber `PrismaLike` (`$queryRawUnsafe`/`$executeRawUnsafe`), erstellt `vec_<namespace>_<collection>`-Tabellen, HNSW-Index (`m`, `efConstruction`), Payload-Filter via `payload ->> 'key'` |
| Qdrant-Adapter | `packages/vector-store/src/adapters/qdrant.ts` | `QdrantAdapter` ueber `QdrantClientLike`, Namespace-Praefix |
| Tenant-Scope-Wrapper | `packages/vector-store/src/wrappers/tenant-scoped.ts` | `withTenantScope()` -> `TenantScopedVectorStore`; `searchForTenant()` injiziert `tenantId`-must-Filter zwingend; bare `searchVectors()` wirft; `listVectorIds()` scoped nach tenant/sourceType/sourceId |
| Konformanztest | `packages/vector-store/src/adapters/conformance.test.ts` | Erzwingt byte-fuer-byte gleiches Verhalten beider Adapter |
**Prisma:** Kein Prisma-Modell — pgvector legt eigene `vec_*`-Tabellen via Raw-SQL an. Migration: `prisma/migrations/20260517085146_04_enable_pgvector/migration.sql` (aktiviert `pgvector`-Extension).
**HiveGrid-Relevanz:** Speicher-Backend fuer RAG/semantische Suche. pgvector = "kein Extra-Service" (laeuft auf Haupt-Postgres), Qdrant = Upgrade-Pfad. Tenant-Isolation built-in. Direkt fuer HiveGrid "RAG"/"semantische Suche".
---
## 3. `@toolctrl/search` — Meilisearch Such-Index
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/search`
**Deps:** `meilisearch`, `zod`
**Subpath-Exports:** `.`, `./schemas`, `./testing`
| Asset | Pfad | Was |
|---|---|---|
| Client-Adapter | `packages/search/src/client.ts` | `createSearchClient()` — kapselt Meilisearch-SDK (nie re-exportiert), `SearchClient`/`IndexHandle` (ensure/search/upsert/delete/deleteByFilter), `meiliFactory`-Injection fuer Tests |
| Query-Layer (tenant-scoped) | `packages/search/src/query.ts` | `searchInIndex`, `searchWithFallback`, `bulkIndex`, `deleteFromIndex`, `health`, `TenantScopeError` — jede Suche erzwingt `tenantId`-Filter |
| Index-Helper | `packages/search/src/index-helper.ts` | `createSearchIndex()`, `validateIndexSchema()`, `IndexSchemaValidationError` |
| Index-Schemas | `packages/search/src/schemas/` (`kb-articles.ts`, `crm-contacts.ts`, `rows.ts`, `entities.ts`, `users.ts`, `index.ts`) | `ALL_SEARCH_INDEXES`, `BULK_REINDEX_SUPPORTED_INDEXES`, `getSearchIndexSchema()` |
| Outbox-Publisher | `packages/search/src/outbox-publisher.ts` | `publishSearchUpsertEvent`/`publishSearchDeleteEvent`/`publishSearchBulkReindexEvent`, `searchEventType()` |
| Sanitize | `packages/search/src/sanitize.ts` | `sanitizeDocument()` |
**HiveGrid-Relevanz:** Volltextsuche fuer KB-Artikel, CRM-Kontakte, generische Entity-Rows, User. Outbox-getriebener Reindex. Querschnitt "Suche" parallel zur Vektorsuche (hybrid moeglich).
---
## 4. Apps-Engine / Plugin-System (Kern-Fokus)
Wichtige Unterscheidung im Boilerplate:
- **`@toolctrl/plugins`** = Feature-/App-**Manifest-Inventur** (Klassifizierung von Features als core/plugin-candidate, Integration-Surfaces) — kein Laufzeit-Sandbox.
- **`@toolctrl/plugin-runtime` + `@toolctrl/plugin-sdk`** = echtes **WASM-Plugin-Laufzeitsystem** mit Capability-Modell (das "Apps-Engine"-Adapter-/Sandbox-Pattern).
- **Prisma `appsEngine.prisma`** = separate **Data-Sync-"App"-Engine** (App/DataConnection/AppSyncRun) fuer externe Datenquellen-Integrationen.
### 4.1 `@toolctrl/plugins` — Feature-Manifest-Schema + Loader
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/plugins` (keine Deps)
| Asset | Pfad | Was |
|---|---|---|
| Manifest-Schema | `packages/plugins/src/manifest-schema.ts` | `FeatureManifest` (schemaVersion "1.0", classification core/plugin-candidate/remove-candidate/mixed, `integration.surfaces` admin/tenant/public/api-Routes, `integration.workers` queues/jobs, externalProviders, permissions, auditEvents, billingImpacts, uat, `boundaries`, `pruning`), `validateFeatureManifestShape()` (handgeschriebener Validator, kein zod) |
| Loader | `packages/plugins/src/manifest-loader.ts` | `loadFeatureManifests()` |
**HiveGrid-Relevanz:** Methodik-/Governance-Werkzeug — beschreibt, welche Features Plugin-Kandidaten sind und welche Surfaces/Worker/Provider sie brauchen. Nuetzlich als Vorlage fuer HiveGrid-Modul-/Plugin-Klassifizierung, weniger als Laufzeit.
### 4.2 `@toolctrl/plugin-runtime` — WASM-Sandbox + Capability-Gateway
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/plugin-runtime`
**Deps:** `@toolctrl/logger`, `@toolctrl/shared-store`, `semver`, `zod`
**Subpath-Exports:** `.`, `./capabilities`, `./hooks/registry`, `./manifest`
**README:** `packages/plugin-runtime/README.md`
| Asset | Pfad | Was |
|---|---|---|
| WASM-Sandbox | `packages/plugin-runtime/src/runtime.ts` | `createPluginSandbox()` — Node-22 `WebAssembly`, Memory-Cap (Default 64 MB), Fuel-Budget (Default 1.000.000), JSON-Payload-ABI (`toolctrl_payload_len`/`...byte`), `invoke(hookName, payload)`, typisierte Failures (compile/instantiate/fuel_exhausted/memory_limit/trap). Upgrade-Pfad: Wasmer/Wasmtime hinter gleichem Interface |
| Capability-Enum | `packages/plugin-runtime/src/capabilities.ts` | `PLUGIN_CAPABILITY_NAMES` (http.outbound, db.read/write, storage.read/write, log, audit, tenant.info, secret.read), Risk-Metadata, **`PLUGIN_AVAILABLE_HOST_CAPABILITY_NAMES = ["log","audit"]`** (nur diese in Prod aktiv), `productionAvailable`-Flags |
| Capability-Gateway | `packages/plugin-runtime/src/capability-gateway.ts` | `createCapabilityGateway()` — deny-by-default; prueft manifest-deklariert + tenant-granted + Scope; `CapabilityDeniedError` mit Gruenden (not_declared/not_granted/scope_violation/no_host_implementation); laedt Fuel pro Call; `onCall`-Hook fuer Audit |
| Manifest (Plugin) | `packages/plugin-runtime/src/manifest.ts` | `PluginManifestSchema` (zod): Reverse-DNS-id, SemVer, `entry` (.wasm), `wasmSha256`, Ed25519 `signature`/`signedBy`, `allowedHosts` (SSRF-Allowlist, kein localhost/IP/.internal), `capabilities` mit `scope` (allowedHosts/keyPattern/tablePrefix), `hooks` (name->export), optional `ui.settings` (HTML + SHA256/Integrity), `min/maxHostVersion`; `parsePluginManifest()` (safe), `manifestDeclaresCapability()` |
| Signatur | `packages/plugin-runtime/src/signature.ts` | `verifyPluginSignature()` (Ed25519), `parseTrustedPublicKeys()`, `canonicalPluginSignaturePayload()` |
| Capability-Grants/Scope | `packages/plugin-runtime/src/capability-grants.ts`, `.../capability-scope.ts` | `PluginCapabilityGrant`, `isCapabilityGrantScopeSatisfied()`, `isCapabilityCallWithinScope()` |
| Hooks-Registry | `packages/plugin-runtime/src/hooks/registry.ts` | zod-validierte Hook-Payloads: `tenant.created`, `user.created`, `entity.{created,updated,deleted}`, `workflow.{created,updated,deleted,executed,failed}`; `registerHook()`, `validatePluginHookPayload()`, Prod-Available-Liste |
| Hook-Emit | `packages/plugin-runtime/src/hooks/emit.ts` | `emitPluginHook(tx, def, payload, publish)` — validiert + publiziert `plugin.hook.<name>` Outbox-Event in der DB-Transaktion (Fan-out auf Worker-Seite), `PluginHookValidationError` |
| Audit (Capability-Use) | `packages/plugin-runtime/src/audit.ts` | `createPluginCapabilityUseRecorder()`, `hashCapabilityPayload()` (kein Roh-Payload), Batch-Sink |
| Audit-Chain (Tamper-Evidence) | `packages/plugin-runtime/src/audit-chain.ts` | Per-Tenant Hash-Chain (`prevHash`/`rowHash`), `stampPluginCapabilityUseChain()`, `verifyPluginCapabilityUseChain()` |
| Crash-Tracker | `packages/plugin-runtime/src/crash-tracker.ts` | `createCrashTracker()` — auto-disable Plugin nach N Crashes |
| Migrations | `packages/plugin-runtime/src/migrations.ts` | `validatePluginMigration()`/`applyPluginMigration()` — Plugin-eigene SQL mit Keyword-Blocklist + Statement-Limit + `tablePrefix`-Scoping |
| Version-Check | `packages/plugin-runtime/src/version-check.ts` | `isPluginCompatibleWithHost()` (semver-Range gegen Host) |
| Production-Gate | `packages/plugin-runtime/src/production-gate.ts` | `assertPluginRuntimeProductionEnabled()`, Env `PLUGIN_RUNTIME_PRODUCTION_GATE_ENV` |
### 4.3 `@toolctrl/plugin-sdk` — Autoren-Typen + Settings-Client
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/plugin-sdk` (keine Deps)
**README + Beispiele:** `packages/plugin-sdk/README.md`, `packages/plugin-sdk/examples/{hello-world,simple-hook-listener,capability-use,async-task}/` (jeweils `plugin.toml` + `handler.ts`)
| Asset | Pfad | Was |
|---|---|---|
| SDK-Typen + `defineHook` | `packages/plugin-sdk/src/index.ts` | `HookHandler`/`TypedHookHandler`, `defineHook()`, `PluginContext`, `HostApi` (log/audit/tenant/http/storage/secret), `PluginCapabilityName`/`PluginHookName` (gespiegelt aus runtime), Hook-Payload-Typen, ABI-Konstanten (`PLUGIN_PAYLOAD_ABI_VERSION`), Settings-Iframe-Protokoll: `createPluginSettingsClient()` (postMessage get/set, Timeout, Channel) |
**Prisma (Plugins):** `prisma/schema/plugins.prisma`
- `Plugin` (tenantId, pluginId, version, manifest Json, wasmSha256, signaturePublicKey, enabled)
- `TenantPluginCapabilityGrant` (per-tenant Consent pro Capability, scopeJson) — Install darf erst `enabled` werden, wenn alle deklarierten Capabilities Grants haben
- `PluginUserConfig` (per-user opt-in + configJson)
- `PluginInvocation` (Audit pro Invoke: outcome/durationMs/fuelUsed)
- `PluginCapabilityUse` (per-Capability-Call Audit mit `payloadHash`, Hash-Chain `prevHash`/`rowHash`)
**Queue:** Queue-Name `toolctrl.plugin-invoke`, Job `plugin.invoke` (siehe Queue-Abschnitt).
**HiveGrid-Relevanz (4.2/4.3):** Das ist das eigentliche **Apps-Engine/Plugin-Adapter-Pattern** fuer HiveGrid: sicheres, sandboxed Drittanbieter-/Erweiterungssystem mit Capability-Consent, Signatur-Verifikation, Hook-Lifecycle, Audit-Chain und Crash-Schutz. Direkt relevant fuer HiveGrid-Module "Apps/Marketplace", "Integrationen", "Erweiterbarkeit". Achtung: in Prod sind nur `log`/`audit`-Capabilities freigeschaltet — http/db/storage/secret sind Foundation-Stubs (`productionAvailable: false`).
### 4.4 Prisma Apps-Engine (Data-Sync) — `appsEngine.prisma`
**Pfad:** `prisma/schema/appsEngine.prisma`
| Modell | Was |
|---|---|
| `App` | Installierte App pro Tenant (`type`-Identifier z.B. "sample-data-sync", `config` JSON, isActive) |
| `DataConnection` | Verbindung zu externer Datenquelle (`type` "rest-api"/"oauth2"/"webhook", verschluesselte Credentials in `config`) |
| `AppSyncRun` | Sync-Lauf (status pending/running/completed/partial/failed/..., executionMode manual/scheduled/webhook, items processed/created/updated/failed) |
| `AppSyncRunLog` | Log-Eintraege pro Lauf |
| `AppSyncItem` | Einzel-Item-Mapping (sourceId/destinationId, action, rollback-Daten) |
**HiveGrid-Relevanz:** Separate "Apps-Engine" fuer **externe Datensynchronisation** (ETL-aehnlich, idempotent, mit Rollback + Audit). Relevant fuer HiveGrid-Module "Integrationen/Datenimport" und externe Connector-Apps — komplementaer zum WASM-Plugin-System (4.2).
---
## 5. `@toolctrl/storage` — S3/Scaleway-Storage, Signed-URLs, Virus-Scan
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage`
**Deps:** `@aws-sdk/client-s3`, `clamscan`, `file-type`
| Asset | Pfad | Was |
|---|---|---|
| S3-/Local-Adapter | `packages/storage/src/index.ts` | `createS3StorageAdapter()` (endpoint/region/bucket/credentials, forcePathStyle, publicBaseUrl) ueber `@aws-sdk/client-s3` — Scaleway-/jeder-S3-kompatibel; `createLocalStorageAdapter()` (Dev, Pfad-Traversal-Schutz); `StorageAdapter` (putObject/deleteObject), `StorageAdapterError` mit typisierten Codes + Secret-Redaction |
| Signed-URLs | `packages/storage/src/signed/index.ts` | `createSignedUploadUrl`/`createSignedDownloadUrl` (PUT/GET, expiry, contentType, checksumSha256) |
| Virus-Scan-Service | `packages/storage/src/scan/index.ts` | `createStorageScanService()` (scan -> bei infected quarantaene), `ScanVerdict` clean/infected/error |
| ClamAV-Scanner | `packages/storage/src/scan/clamav-scanner.ts` | `createClamAVStorageScanner()` ueber `clamscan`, `ClamAVUnreachableError` |
| Magic-Bytes-Adapter | `packages/storage/src/scan/magic-bytes-adapter.ts` | `createMagicBytesScanAdapter()` ueber `file-type` (Content-Type-Verifikation) |
| Allow-List | `packages/storage/src/scan/allow-list.ts` | `DEFAULT_ALLOW_LIST`, `resolveAllowList()` |
| S3-Quarantaene | `packages/storage/src/scan/s3-quarantine.ts` | `createS3StorageQuarantineAdapter()` |
| Access-Control | `packages/storage/src/access/index.ts` | Storage-Zugriffspruefung |
**HiveGrid-Relevanz:** Datei-Uploads/Anhaenge (KB, CRM, Apps), DSGVO-EU-Storage via Scaleway-S3. Virus-Scan + magic-bytes-Pruefung = Sicherheits-Querschnitt fuer jedes Upload-Modul. Signed-URLs fuer direkten Client-Upload/Download.
---
## 6. `@toolctrl/realtime` — SSE/Realtime-Events
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/realtime` (keine Deps)
**README:** `packages/realtime/README.md`
| Asset | Pfad | Was |
|---|---|---|
| Event-Typen | `packages/realtime/src/events.ts` | `REALTIME_EVENT_NAMES` (notification.snapshot/update, keepalive, error), `RealtimeEvent`-Union, `validateRealtimeEvent()`, Notification-Stream-Item-Typen |
| Server (SSE-Serialisierung) | `packages/realtime/src/server.ts` | `serializeRealtimeEvent()` (SSE `id/event/data`, retry), `serializeRealtimeComment()`, Newline-Sanitizing |
| Client | `packages/realtime/src/client.ts` | Client-seitige Event-Verarbeitung |
| Guards | `packages/realtime/src/guards.ts` | Type-Guards fuer Events |
**HiveGrid-Relevanz:** Live-Benachrichtigungen (In-App-Notifications) via SSE — kein WebSocket-Server, sondern SSE-Stream. Querschnitt "Realtime/Notifications". Hinweis: WebSocket im engeren Sinn ist hier **nicht** vorhanden; Realtime = SSE.
---
## 7. E-Mail-Stack
### 7.1 `@toolctrl/mail` — Provider + Templates
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/mail`
**Deps:** `@react-email/components`, `@react-email/render`, `@toolctrl/brand`, `@toolctrl/queue`, `react`, `react-dom`
**README:** `packages/mail/README.md`; Preview-Server: `packages/mail/scripts/preview-server.ts`
| Asset | Pfad | Was |
|---|---|---|
| MailerSend-Provider | `packages/mail/src/mailersend.ts` | `createMailerSendProvider()`/`...FromEnv()`, API `https://api.mailersend.com`, Timeout, `MailMetricsHook`, Templates/Personalization/Attachments/Tracking |
| Queued-Mail-Provider | `packages/mail/src/queue.ts` | `createQueuedMailProvider()` — Versand via `@toolctrl/queue` (async) statt direkt |
| Mail-Typen | `packages/mail/src/types.ts` | `MailProvider`, `SendMailInput`, `MailAddress`, `MailAttachment`, `MailProviderError` |
| React-Email Render | `packages/mail/src/templates-react/render.ts` | `renderTransactionalEmail()`, `reactTransactionalTemplateNames`, Brand-Helfer |
| Transaktionstemplates | `packages/mail/src/templates-react/transactional/` | Welcome, MagicLink, PasswordReset, Invitation, InvoiceSent, InvoiceTenant, SubscriptionSuccess/Failed, TenantNotification |
| Layout-Primitives | `packages/mail/src/templates-react/_layout/` | BaseLayout, Header, Footer, Button (`assertSafeUrl`/`InvalidActionUrlError`), InfoBox, Eyebrow, theme |
### 7.2 `@toolctrl/email-templates` — Sequenz-Templates
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/email-templates`
**Subpath-Exports:** `./sequences/{welcome,trial-reminder,re-activation,generic-step}`
| Asset | Pfad | Was |
|---|---|---|
| Render + Theme | `packages/email-templates/src/render.ts`, `.../theme.ts` | React-Email Render-Layer |
| Sequenz-Templates | `packages/email-templates/src/sequences/` | welcome, trial-reminder, re-activation, generic-step |
### 7.3 `@toolctrl/email-sequences` — Sequenz-Engine (reine Logik)
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/email-sequences` (keine Deps)
**Subpath-Exports:** `.`, `./types`, `./engine`, `./step-runner`, `./conditions`, `./unsubscribe-token`
| Asset | Pfad | Was |
|---|---|---|
| Engine | `packages/email-sequences/src/engine.ts` | `startSequenceRun()`/`advanceSequenceRun()` — Store + Queue injiziert (kein Prisma/BullMQ direkt); `SequenceRunStatus`, idempotent ueber `(sequenceId, triggerEventId)` |
| Step-Runner | `packages/email-sequences/src/step-runner.ts` | `runStep()` — rendert + sendet einen Schritt |
| Condition-DSL | `packages/email-sequences/src/conditions.ts` | `evaluateCondition()`/`evaluateAllConditions()`, `resolvePath()` |
| Unsubscribe-Token | `packages/email-sequences/src/unsubscribe-token.ts` | `signUnsubscribeToken()`/`verifyUnsubscribeToken()`/`buildUnsubscribeUrl()`, Secret-Pflicht |
**Prisma:** `prisma/schema/emailSequences.prisma` — `EmailSequence`, `EmailSequenceRun`, `EmailSequenceStepDelivery`. `prisma/schema/email.prisma` (App-Email-Config).
**HiveGrid-Relevanz (7.x):** Transaktions-Mails (Auth, Billing, Notifications) + Marketing-/Lifecycle-Sequenzen (Onboarding, Trial-Reminder, Reaktivierung). EU-Provider MailerSend. Querschnitt "Kommunikation/E-Mail" fuer praktisch jedes HiveGrid-Modul; Sequenz-Engine direkt fuer "Marketing/Funnel/Lifecycle".
---
## 8. `@toolctrl/marketing-outbound` — Outbound-Webhook-Adapter
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/marketing-outbound` (keine Deps)
**Subpath-Exports:** `./adapters/{slack,discord,mailchimp,hubspot,brevo,custom-webhook}`
| Asset | Pfad | Was |
|---|---|---|
| Adapter-Interface + Registry | `packages/marketing-outbound/src/index.ts` | `adapterRegistry` (slack/discord/mailchimp/hubspot/brevo/webhook), `resolveAdapter(type)`, `ADAPTER_TYPES` |
| Shared-Typen + SSRF-Guard | `packages/marketing-outbound/src/types.ts` | `MarketingOutboundAdapter<TConfig>` (`parseConfig`/`send`), `MarketingLeadPayload`/`MarketingLeadEventType` (captured/qualified/converted...), `adapterFetch()` (Timeout + SSRF-Allowlist, nur HTTPS, kein RFC1918/metadata), `maskEmailIfRequested()` (PII), `MarketingAdapterConfigError` |
| Konkrete Adapter | `packages/marketing-outbound/src/adapters/*.ts` | je `buildXPayload()` + Adapter-Objekt (Slack-Blocks, Discord-Embed, Mailchimp/HubSpot/Brevo-Payloads, Custom-Webhook) |
**Prisma:** `prisma/schema/marketingLeads.prisma` — `MarketingIntegration` (type, configJson verschluesselt, `triggerEvents` String[], enabled, lastDeliveredAt/lastError), `Lead`, `LeadEvent`, `TenantAcquisition`.
**HiveGrid-Relevanz:** Lead-/Marketing-Integrationen mit Drittsystemen (CRM, Chat, ESP). Worker dispatcht Outbox-Events nach `MarketingIntegration.type`. SSRF-/PII-Schutz im Adapter. Direkt fuer HiveGrid-Module "Marketing/Funnel" und "Integrationen (Outbound-Webhooks)".
---
## 9. `@toolctrl/queue` — BullMQ + Idempotenz + Prioritaeten
**Pfad:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/queue`
**Deps:** `bullmq`
**Subpath-Exports:** `.`, `./runtime` (CJS `runtime.cjs`)
**README:** `packages/queue/README.md`
| Asset | Pfad | Was |
|---|---|---|
| Queue-Namen | `packages/queue/src/names.ts` | `queueNames`: `toolctrl.email`, `.notifications`, `.maintenance`, `.ai-chatbot`, `.browser-automation`, `.file-scan`, `.exports`, `.imports`, `.plugin-invoke`; `isQueueName()` |
| Job-Payloads (typisiert) | `packages/queue/src/payloads.ts` | `AiJob` (`ai.chat-completion`/`ai.embedding`/`ai.browser-task`), `PluginInvokeJob` (`plugin.invoke`), `TransactionalMailJob`, `WebhookDeliveryJob`, `NotificationJob`, `CleanupJob`, `FileScanJob`, `ExportJob`/`ImportJob`, Workflow-Outbox-Events; `QueueJobPayloads`-Map |
| Producer + Idempotenz | `packages/queue/src/producers.ts` | `enqueueJob()`, `build*Job()`-Builder, `buildIdempotentJobId()` (jobId/idempotencyKey/export-/importJobId/metadata.idempotencyKey), `buildRetryJobOptions()`, `buildIdempotentJobId`, `setEnqueueObserver()` |
| Prioritaeten | `packages/queue/src/priority.ts` | `JOB_PRIORITY_{CRITICAL,HIGH,NORMAL,LOW}`, `resolveJobPriorityForTenant()` (Plan-basiert), `priorityFromPlan/Override`, `setTenantPlanLookup()` |
| Redis-Connection | `packages/queue/src/connection.ts` | `parseRedisUrl()`, `resolveRedisConnectionOptions()`, `...FromProcessEnv()` |
| Queue-Gruppen | `packages/queue/src/groups.ts` | `WORKER_QUEUE_GROUPS`, `getGroupForQueue()`, `getQueueNamesForGroup()` |
| Runtime (BullMQ-Factory) | `packages/queue/src/runtime.ts` / `runtime.cjs` | `createQueue`/`createWorker`/`createQueueEvents` (typisiert) |
**Prisma:** `prisma/schema/queue.prisma` — `QueueFailedJob` (DLQ-Spiegel), `ExportJob` + `ExportJobStatus`-Enum. Outbox-Pattern: `prisma/schema/outbox.prisma`.
**HiveGrid-Relevanz:** Zentrale asynchrone Verarbeitung fuer KI-Jobs, Plugin-Invokes, Mail, Notifications, File-Scan, Exporte/Importe. **Idempotenz** ueber jobId/idempotencyKey + plan-basierte **Prioritaeten** = Querschnitt fuer alle HiveGrid-Worker-Module. AI/Plugin/Mail-Queues binden die obigen Pakete an den Worker an.
---
## Querschnitts-Konventionen (faktisch beobachtet)
- **Tenant-Isolation** ist ueberall am Seam verankert: `payload.tenantId`-Pflicht (vector-store), `searchForTenant`/`tenantId`-Filter (search, vector-store), `tenantId` Pflichtargument (embeddings), per-tenant Grants/Audit-Chain (plugin-runtime).
- **Outbox-Pattern** fuer asynchrone Seiteneffekte: RAG-Reindex (`rag.index.*`), Such-Reindex, Plugin-Hooks (`plugin.hook.*`), AI-Cost (`ai.cost.event.persist`), Marketing-Dispatch — alle in DB-Transaktion committet, Worker fan-out.
- **Kein direkter `process.env`-Zugriff** in den Paketen (Env-Snapshots vom Boundary) — Env-Access-Enforcement-Tests.
- **Secret-/PII-Redaction** durchgaengig in Fehlern/Audit (ai/guards, ai/tools, ai/streaming, storage, marketing-outbound).
- **Adapter-/Factory-Pattern** als roter Faden: `getAIProviderAdapter`, `createEmbeddingProvider`+Registry, `createVectorStore`+`resolveAdapter`, `createSearchClient`+`meiliFactory`, `adapterRegistry` (marketing), `createS3StorageAdapter`/`createLocalStorageAdapter`, Capability-`hostFns`-Map (plugin-runtime).
- **Tests:** jedes Paket hat `*.test.ts` neben den Quellen; Eval-Harness in `packages/ai/test/evals` + Repo-Root `promptfoo.config.yaml`; CI/verify ueber `pnpm` Workspace-Scripts (`test`/`typecheck`/`build`), Repo-Root `package.json` + `lefthook.yml`.
## Wichtige Abgrenzungen / Luecken (faktisch)
- **WebSocket:** Es gibt **kein** WebSocket-Paket; Realtime laeuft ueber **SSE** (`@toolctrl/realtime`).
- **Plugin-Capabilities in Prod:** Nur `log` + `audit` sind hostseitig verfuegbar (`PLUGIN_AVAILABLE_HOST_CAPABILITY_NAMES`); http/db/storage/secret/tenant.info sind deklarierbar, aber `productionAvailable: false` (Foundation).
- **WASM-Fuel:** Keine echte Per-Instruktions-Meterung (V8-Limit) — Fuel wird pro Capability-Call/Invoke geladen; Wall-Clock-Timeout liegt beim Aufrufer.
- **Anthropic-Embeddings:** Bewusster Stub/Platzhalter (`packages/ai/src/embeddings/providers/anthropic.ts`).
- **Zwei "Apps-Engine"-Bedeutungen:** WASM-Plugin-System (`plugin-runtime`/`plugin-sdk`) vs. Data-Sync-Apps (`prisma/schema/appsEngine.prisma`, `App`/`DataConnection`/`AppSyncRun`) — beim Modul-Abgleich nicht verwechseln.
@@ -0,0 +1,382 @@
# Boilerplate-Inventur: UI-Layer & App-Shell
Faktische Bestandsaufnahme des Bereichs "UI-Layer & App-Shell" der Boilerplate
(`/Users/raffaelsheikh/Documents/GitHub/boilerplate`). Stand: Read-only-Inventur,
keine Aenderungen vorgenommen.
Inventarisierte Pakete/Pfade:
`packages/ui`, `packages/ui-app`, `packages/ui-mock`, `packages/ui-subframe`,
`packages/view-models` (Vertrags-Fundament), `apps/web`, `apps/storybook`.
## TL;DR — Reale Layer-Trennung
Die Layer-Trennung ist **noch nicht voll aktiviert**. Sie ist als bewusst
strukturierte "Adapter-/Vertrags-Grenze" angelegt, waehrend die echten
Laufzeit-Komponenten weiterhin in `apps/web/app/` leben.
- `@toolctrl/view-models` — reine TypeScript-Typen + kleine Helfer: View-Model-
Statusmodell (`VMState`), Shell-Registry-Typen (Sidebar/Breadcrumb/Mobile/
Portal), i18n-Key-Validierung. Kein React.
- `@toolctrl/ui-app` — App-Shell-Registry-Factories + headless `AppShell`-Stub
(rendert nur `data-*`-Attribute, kein echtes Layout). Konsumiert view-models.
- `@toolctrl/ui-mock` — Fixture-/Mock-Generator fuer Hot-Swap- und Visual-Tests
(erzeugt statisches HTML + serialisierbares Modul aus einem `MockPage`).
- `@toolctrl/ui-subframe` — **leerer Platzhalter** (`createSubframePlaceholder()`
liefert `{ status: "empty" }`). Subframe-Adapter ist reserviert, nicht gebaut.
- `@toolctrl/ui` — **source-only Adapter-Metadaten** (keine React-Komponenten).
Beschreibt nur stabile Namen + Verweise auf die echten Implementierungen in
`apps/web/app/`.
- `apps/web/app/` — echte Laufzeit: App-Shell (shadcn/Radix), Sidebar-Konfig,
RR7-Routing, ~707 `.tsx`-Komponenten, Theme/Dark-Mode, i18n.
- `apps/storybook` — Storybook 8 (react-vite) ueber die `packages/ui`-Stories.
Verbindung von Vertrag → echter Code laeuft ueber **stabile `data-ui-id`-IDs**
(`packages/.../application/ui/uiRegistry.ts`) und die maschinenlesbare
Inventur in `docs/ui-inventory/` (von Tests gegen das Dateisystem geprueft).
---
## Paket: `@toolctrl/view-models`
**Zweck:** Vertrags-Fundament fuer View-Model → UI. Reine Typen + winzige
Validierungs-/Konstruktor-Helfer, ohne React-Abhaengigkeit. Bildet die
gemeinsame Sprache, gegen die `ui-app`, `ui-mock` und die echte App komponieren.
**Zentrale Datei:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/view-models/src/index.ts`
**package.json:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/view-models/package.json` (Name `@toolctrl/view-models`, Export `.` → `./src/index.ts`)
**Haupt-Exporte (Typen):**
| Export | Bedeutung |
| --- | --- |
| `VMStatus` | `"idle" \| "loading" \| "ready" \| "empty" \| "error"` |
| `VMState<T>` | Diskriminierte Union ueber `VMStatus`; `error` traegt `{ messageKey; code? }` (i18n-Key, nicht Klartext) |
| `ActionCallbacks<T>` | `onCreate/onUpdate/onDelete/onRefresh` — Standard-CRUD-Callback-Vertrag |
| `I18nLabel` | `{ labelKey; descriptionKey? }` — UI-Labels sind **immer i18n-Keys** |
| `RegistryGate` | `{ permission?; flag? }` — Sichtbarkeits-Gate (Permission/Feature-Flag) pro Registry-Item |
| `SidebarRegistryItem` | `id/href/icon?/children?` + `I18nLabel` + `RegistryGate` |
| `BreadcrumbRegistryItem` | `id/href?` + Label + Gate |
| `MobileRegistryItem` | `id/href/priority?` + Label + Gate |
| `PortalRegistryItem` | `id/slot/order?` + Label + Gate |
| `ShellRegistries` | `{ sidebar?; breadcrumbs?; mobile?; portals? }` — das Shell-Konfig-Aggregat |
**Helfer-Funktionen:** `ready<T>(data)`, `empty<T>()`, `assertI18nLabel(label)`
(erzwingt Regex `^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$` fuer Label- und Description-Keys).
**HiveGrid-Relevanz:** Hoch und querschnittlich. Das `VMState`-Statusmodell und
der i18n-Key-Zwang sind direkt uebernehmbar als Daten-/UI-Vertrag fuer alle
HiveGrid-Module (Listen/Detail/Editoren), und die `ShellRegistries`-Typen sind
die Grundlage fuer datengetriebene Navigation (App-Shell-Querschnitt).
---
## Paket: `@toolctrl/ui-app`
**Zweck:** App-Shell-Schicht. Liefert Registry-Factories (sortiert/normalisiert
Sidebar/Breadcrumb/Mobile/Portal-Items) und einen headless `AppShell`-Stub.
Der Stub rendert **kein** echtes Layout — nur ein `<div>` mit `data-ui-shell`
und Zaehler-Attributen. Echte Shell lebt in `apps/web`.
**Zentrale Datei:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui-app/src/index.ts`
**package.json:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui-app/package.json` (deps: `@toolctrl/view-models`, `react`; Test-Subpath-Exports fuer axe/a11y-Template)
**Haupt-Exporte:**
| Export | Art | Verhalten |
| --- | --- | --- |
| `AppShellProps` | Typ | `{ registries?: ShellRegistries; children? }` |
| `createSidebarRegistry(items)` | fn | Kopie der Items |
| `createBreadcrumbRegistry(items)` | fn | Kopie der Items |
| `createMobileRegistry(items)` | fn | sortiert nach `priority` (default 100) |
| `createPortalRegistry(items)` | fn | sortiert nach `order` (default 100) |
| `createShellRegistries(input)` | fn | aggregiert + normalisiert alle 4 Registries |
| `EmptyPage({ labelKey })` | Komponente | Empty-State-Stub (`data-page-state="empty"`) |
| `AppShell({ registries, children })` | Komponente | headless Shell-Stub mit `data-*`-Zaehlern |
**a11y-Test-Vertrag:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui-app/src/test-utils/a11y-template.ts`
exportiert `generateA11yTest(Component, defaultProps, options)` — kanonisches
axe-Test-Template (jede Komponente in `ui-app`/`ui-subframe` soll eine
co-lokierte `<Name>.a11y.test.tsx` mitliefern). axe-Setup unter `test-setup/axe.ts`.
**HiveGrid-Relevanz:** App-Shell-/Navigations-Querschnitt. Die Registry-
Factories sind die kanonische Art, HiveGrid-Sidebars/Breadcrumbs aus Daten zu
bauen (mit Priority/Order-Sortierung und Permission/Flag-Gating). `generateA11yTest`
ist als Compliance-/a11y-Querschnitt direkt nutzbar.
---
## Paket: `@toolctrl/ui-mock`
**Zweck:** Hot-Swap-/Visual-Test-Fundament. Erzeugt aus einem deklarativen
`MockPage`-Deskriptor deterministisch (a) ein serialisierbares JS-Modul und
(b) statisches HTML inkl. eigenem Inline-CSS — als "Drop-in"-Ersatz fuer eine
echte Seite. Liefert stabile Hashes/Metriken fuer Visual-Snapshots.
**Zentrale Datei:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui-mock/src/index.ts`
**package.json:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui-mock/package.json` (dep: `@toolctrl/view-models`)
**Visual-Baseline:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui-mock/visual-baseline/`
**Haupt-Exporte:**
| Export | Art | Verhalten |
| --- | --- | --- |
| `MockPage<T>` | Typ | `{ id; route; title?/titleKey?; description?/descriptionKey?; state: VMState<T>; registries? }` |
| `HotSwapDrillData` | Typ | `{ cards[]; activity[] }` — Demo-Datenform |
| `MockVisualSnapshot` | Typ | `htmlHash/moduleHash/stateHash` (FNV-1a) + Byte-/Item-Metriken |
| `MockArtifacts` | Typ | `{ module; html; snapshot }` |
| `createMockPage(page)` | fn | Identitaet (Typ-Anker) |
| `createMockRegistry(pages)` | fn | `Map<route, MockPage>` |
| `generateHotSwapMock(page)` | fn | erzeugt `export const mockPage = ...` Modul-String |
| `generateMockPageHtml(page)` | fn | volles statisches HTML (Sidebar/Breadcrumbs/Cards/Activity/State) |
| `createVisualSnapshot(page, artifacts?)` | fn | Snapshot mit stabilen Hashes |
| `generateMockArtifacts(page)` | fn | `{ module, html, snapshot }` in einem Schritt |
| `createHotSwapDrillPage()` | fn | fertige Referenz-`MockPage` (`/mock/phase-c/hot-swap`) |
Enthaelt eine eingebaute Mini-Uebersetzungstabelle (`defaultMockTranslations`)
fuer die Mock-Texte (i18n-Keys → Klartext, deterministisch).
**HiveGrid-Relevanz:** Test-/QA-Querschnitt. Das Mock-Drop-in-Muster (deklarativer
Deskriptor → deterministisches HTML + Hash) ist als Vorlage fuer HiveGrid-Visual-
Regression und fuer das Bauen von Hot-Swap-/Whitelabel-Vorschauen verwendbar.
---
## Paket: `@toolctrl/ui-subframe`
**Zweck:** **Reservierte, leere** Paket-Grenze fuer kuenftige Subframe-generierte
UI. Bewusst leer, bis ein spaeterer Slice Import-Regeln + Review-Gates definiert.
**Zentrale Datei:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui-subframe/src/index.ts`
- `uiSubframeRegistry = [] as const`
- `createSubframePlaceholder()` → `{ status: "empty" }`
**README:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui-subframe/README.md`
("Reserved package boundary for future Subframe-generated UI imports. Exports an empty registry.")
**HiveGrid-Relevanz:** Niedrig (Stub). Relevant nur als Marker dafuer, dass der
Subframe-Adapter-Layer der Boilerplate **nicht gebaut** ist — siehe `MEMORY.md`
("Subframe mandatory for all routes"). HiveGrid muesste diese Grenze selbst fuellen.
---
## Paket: `@toolctrl/ui`
**Zweck:** Source-only **Adapter-Metadaten** ("category migration boundary").
Enthaelt **keine** React-Komponenten und importiert React nicht. Beschreibt
stabile Namen pro Kategorie und zeigt per `source`-Feld auf die echten
Implementierungen in `apps/web/app/`. Erlaubt Agenten/Tools, stabile Namen
anzusprechen, bevor Laufzeit-Imports migriert sind.
**README:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui/README.md`
**package.json:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui/package.json`
**Migration-Doku:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/ui/docs/migration.md`
**Subpath-Exporte (Kategorien):** `.`, `./buttons`, `./inputs`, `./dialogs`,
`./tables`, `./layout`, `./navigation`, `./feedback`.
**Zentrale Dateien:**
| Datei | Inhalt |
| --- | --- |
| `src/index.ts` | Re-Exporte aller Kategorien |
| `src/types.ts` | `uiAdapterCategories`, `UiAdapterDefinition`, `UiAdapterLifecycle` (`placeholder/available/deprecated`), `createEmptyAdapterRegistry`, `isUiAdapterCategory` |
| `src/navigation.ts` | `navigationAdapters` (status `available`): `AppSidebar`, `AdminSidebar`, `DocsSidebar`, `NavMain`, `NavSecondary`, `NavQuickLinks`, `NavActions`, `SidebarPrimitives` — mit `source`-Pfaden in `apps/web/app/...` |
| `src/layout.ts` | `layoutAdapters`: `ShadcnSidebarLayout`, `UiRegistry` |
| `src/buttons.ts` | `buttonAdapters` (`Button`, `buttonVariants`) |
| `src/inputs.ts`, `dialogs.ts`, `tables.ts`, `feedback.ts` | derzeit leere Readonly-Registries (Placeholder bis Migrations-Phase) |
| `src/stories/*.stories.tsx` | ~21 Storybook-Stories (Button, Input, Select, Dialog, DataTable, Table, Tabs, Toast, Tooltip, Popover, Card, Badge, Avatar, Checkbox, Radio, Switch, Textarea, Form, EmptyState, LoadingState, Sidebar) ueber `_demo-primitives.tsx` |
Alle `source`-Pfade aus `navigation.ts`/`layout.ts` (absolut):
`/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/application/sidebar/AppSidebar.tsx`,
`.../AdminSidebar.tsx`, `.../DocsSidebar.ts`,
`.../components/layouts/sidebars/shadcn/nav-main.tsx`, `nav-secondary.tsx`,
`nav-quick-links.tsx`, `nav-actions.tsx`,
`.../components/ui/sidebar.tsx`,
`.../components/layouts/sidebars/shadcn/ShadcnSidebarLayout.tsx`,
`.../application/ui/uiRegistry.ts`.
**HiveGrid-Relevanz:** Konventions-Querschnitt. Liefert die Taxonomie
(buttons/inputs/dialogs/tables/layout/navigation/feedback) und das Lifecycle-
Modell, an dem sich eine HiveGrid-Komponentenbibliothek ausrichten kann. Faktisch
ist es jedoch nur Metadaten — die echten Komponenten liegen in `apps/web`.
---
## App: `apps/web` — echte App-Shell, Sidebar-Konfig, Routing
**package.json:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/package.json`
**Stack:** React Router 7 (RR7), Vite, Tailwind v4, shadcn/Radix. SaasRock-aehnliche Struktur.
### App-Shell / Layout
| Asset | Pfad | Was |
| --- | --- | --- |
| `AppLayout` | `apps/web/app/components/app/AppLayout.tsx` | Einstieg: `SkipToContent` + `CommandPalette` + `ShadcnSidebarLayout`; Prop `layout: "app" \| "admin" \| "docs"` |
| `ShadcnSidebarLayout` | `apps/web/app/components/layouts/sidebars/shadcn/ShadcnSidebarLayout.tsx` | Authentifizierte Shell: `SidebarProvider` → `ShadcnAppSidebar` + `SidebarInset` (Header mit Breadcrumb/`SidebarTrigger`/`NavActions`, `<main id="main-content">`). Liest Tenant-`themeConfig` + Dark-Mode → injiziert `--sidebar*` CSS-Variablen. |
| `ShadcnAppSidebar` | `apps/web/app/components/layouts/sidebars/shadcn/app-sidebar.tsx` | Waehlt je `layout` die Menue-Quelle: `AppSidebar`/`AdminSidebar`/`DocsSidebar`; wendet Permission-/Sichtbarkeits-Filter (`getVisibleItem`) + Active-State an; rendert `NavMain`/`NavSecondary` |
| `NavMain` | `.../shadcn/nav-main.tsx` | Rendert `SidebarGroupDto[]` → shadcn `SidebarMenu` mit collapsible Untermenues; setzt `data-ui-id`-Attribute via `getUiIdAttributes` |
| weitere Nav | `.../shadcn/nav-secondary.tsx`, `nav-quick-links.tsx`, `nav-actions.tsx`, `nav-user.tsx`, `team-switcher.tsx` | Sekundaer-Nav, Quick-Links, Header-Aktionen (Search/AI/Notifications/Feedback), User-/Team-Switcher |
### Sidebar-Konfiguration (datengetrieben)
| Asset | Pfad | Was |
| --- | --- | --- |
| `SideBarItem` (Interface) | `apps/web/app/application/sidebar/SidebarItem.ts` | Item-Vertrag: `id/title/path/icon/items/permission/adminOnly/tenantUserTypes/isCollapsible/isSecondary/isQuickLink/redirectTo/uiIdPattern/uiIdParams/...` |
| `SidebarGroupDto` | `apps/web/app/application/sidebar/SidebarGroupDto.ts` | `{ id?; items: SideBarItem[]; title?; type?: "main"\|"secondary"\|"quick-link" }` |
| `AppSidebar(props)` | `apps/web/app/application/sidebar/AppSidebar.tsx` | Baut Tenant-App-Menue dynamisch aus `entities`, `entityGroups`, `productAccess` (Feature-Gating: crm-basic, email-marketing, workflow-engine, api, ai-credits); host-aware Pfade via `appPath`/`tenantPath` |
| `AdminSidebar(props)` | `apps/web/app/application/sidebar/AdminSidebar.tsx` | Statisches Admin-Menue in Segmenten Manage/Build/Market/Settings; Permission-getaggt; Plugin-Routen via `enabledAdminRouteItem`/`disabledPluginRouteSlugs` ausblendbar |
| `DocsSidebar()` | `apps/web/app/application/sidebar/DocsSidebar.ts` | Statischer Docs-Baum; vergibt stabile `ui.nav.docs.*`-IDs (`withDocsSidebarIds`) |
### UI-Registry (stabile IDs / data-ui-id)
| Asset | Pfad | Was |
| --- | --- | --- |
| `uiRegistry.ts` | `apps/web/app/application/ui/uiRegistry.ts` | Single source of truth fuer stabile UI-IDs: `UI_SURFACE_IDS`, `UI_ACTION_IDS`, `UI_NAV_IDS`, `UI_ID_PATTERNS`; Helfer `getSidebarSurfaceId(layout)`, `uiIdSegment(value)`, `getUiIdAttributes(item)` (erzeugt `data-ui-id` / `-pattern` / `-param-*`) |
### RR7-Routing-Konvention (`app/routes`)
| Asset | Pfad | Was |
| --- | --- | --- |
| Route-Manifest | `apps/web/app/routes.ts` | Nutzt **Remix-v1-Flat-Folder-Convention** ueber `remixConfigRoutes` + `createRoutesFromFolders` (NICHT die RR7-Default-Convention). Definiert `defaultIgnoredRouteFilePatterns` (`*.test.*`, `*.server.*`, `*.css`, dotfiles) und `defaultDisabledPluginRouteFilePatterns` (deaktivierbare Plugin-Routen) |
| Convention-Impl | `apps/web/app/utils/compat/v1-route-convention.ts` | Kopie von `remix-run/v1-compat-utils`; Param-Prefix `$`, Escape `[ ]`, Optional `( )`, Module-Exts `.js/.jsx/.ts/.tsx/.md/.mdx` |
| `react-router.config.ts` | `apps/web/react-router.config.ts` | RR7-App-Config |
| Routen-Tests | `apps/web/app/routes.test.ts`, `apps/web/app/utils/compat/v1-route-convention.test.ts` | Verifizieren das Manifest |
**Routen-Topologie (Auszug, `apps/web/app/routes/`, ~102 Eintraege):**
- Tenant-Space: `app.$tenant.tsx` (Layout) + `app.$tenant/` (dashboard, crm, email-marketing, notifications, settings, forms, g.$group, sites, internal, __portals, __widgets, __workflows, $entity.__autogenerated).
- Admin-Space: `admin.tsx` + `admin/` (accounts, analytics, entities, workflow-engine, api, feature-flags, navigation, portals, prompts, email-marketing, marketing-leads, knowledge-base, help-desk, onboarding, metrics, settings, audit-trails, events).
- Docs: `docs/`. Auth: `auth.*`, `forgot-password.*`, `login`-Flows. Fehlerseiten: `401/403/404/500/$`.
- Hot-Swap-Demo (dev-only): `apps/web/app/routes/hot-swap-demo.tsx`.
Shell-Verdrahtung: `app.$tenant.tsx` wrappt Tenant-Routen ueber
`<AppLayout layout="app">`; `app-sidebar.tsx` zieht die Menue-Daten ueber
`useAppOrAdminData()` (RR7 Loader-Daten) und ruft die passende Sidebar-Factory.
### Theme / Dark-Mode
| Asset | Pfad | Was |
| --- | --- | --- |
| `root.tsx` | `apps/web/app/root.tsx` | Setzt `dark`-Klasse aus `userSession.lightOrDarkMode` (gated ueber `DARK_MODE_IN_APP`), `theme-${rootData.theme}` (+ `-scaled`); Actions `toggleLightOrDarkMode`, `setTheme`, `setLocale` (Persistenz in User-Session/DB) |
| `themes.css` | `apps/web/app/themes.css` (86 KB) | Vordefinierte Themes als CSS-Variablen |
| `globals.css` | `apps/web/app/globals.css` | Globale Tokens/Tailwind-Base |
| `Theme`-Enum | `apps/web/app/application/enums/shared/Theme.ts` | `LIGHT \| DARK` |
| Theme-Preview-Store | `apps/web/app/stores/themePreviewStore.ts` (via `useThemePreviewStore`) | Live-Preview-Override im Shell-Layout |
| `parseThemeConfig` / `ThemeConfig` | `apps/web/app/utils/theme/ThemeConfig.ts` | Parst Tenant-`themeConfig` (Farben inkl. `darkColors`, `sidebar*`-Farben) |
| `tailwind.config.ts` | `boilerplate/tailwind.config.ts` (Repo-Root) | Tailwind-Setup |
**Prisma-Bezug (Theme/Shell-State):**
- `prisma/schema/accounts.prisma:17` — `User.lightOrDarkMode String?` (Hell/Dunkel-Praeferenz pro User).
- `prisma/schema/accounts.prisma:188` — `Tenant.themeConfig String?` (Per-Tenant-Whitelabel-Theme als serialisierter JSON-String).
- `prisma/schema/portals.prisma` — `Portal`, `PortalPage`, ... (Portal-Feature; Bezug zu `PortalRegistryItem`/Admin-Portals-Nav).
### i18n in der UI
| Asset | Pfad | Was |
| --- | --- | --- |
| Web-i18n-Setup | `apps/web/app/locale/` (`i18n.ts`, `i18next.server.ts`, `i18next.global.server.ts`, `format.ts`, `date-fns.ts`) | i18next + RR7 SSR-Integration |
| Locale-Daten | `apps/web/public/locales/{de,en}/*.json` | Namespaces: `translations` (legacy Bulk), `common`, `errors`, `mail`, `settings`, `billing`, `crm`, `auth`, `emailMarketing` |
| Shared-Paket | `packages/i18n/src/index.ts` (`@toolctrl/i18n`) | `defaultLocale="de"`, `supportedLocales=["de","en"]`, `i18nNamespaces`, `i18nConfig`, `getDirection`/`isRTL` (RTL-Support vorbereitet), Re-Exporte `formatNumber/Currency/Date/RelativeTime/List`, `formatIcu`, `createUseTranslate` |
Sidebars/Registries verwenden durchgaengig i18n-Keys (`t(...)` bzw. `labelKey`),
nie Klartext — `assertI18nLabel` (view-models) erzwingt das auf Vertragsebene.
### Komponenten-/Page-Bibliothek (real, in `apps/web`)
Insgesamt ~707 `.tsx`-Komponenten (ohne `*.test.tsx`/`*.stories.tsx`) unter
`apps/web/app/components/`. Verzeichnis-Highlights:
| Bereich | Pfad | Inhalt (Auszug) |
| --- | --- | --- |
| Primitives (shadcn/Radix) | `apps/web/app/components/ui/` | `button.tsx`, `input.tsx`, `select.tsx`, `dialog.tsx`, `sheet.tsx`, `drawer.tsx`, `popover.tsx`, `table.tsx`, `tabs.tsx`, `form.tsx`, `sidebar.tsx`, `command.tsx`, `toast.tsx`/`sonner.tsx`, `badge.tsx`, `card.tsx`, `calendar.tsx`, `chart.tsx`, `multi-select.tsx`, `stepper.tsx`, `pagination.tsx`, ... |
| Tabellen | `apps/web/app/components/ui/tables/` | `TableSimple.tsx`, `TableNew.tsx`, `TablePagination.tsx`, `GridPagination.tsx`, `ColumnSelector.tsx`, `CollapsibleRow.tsx`, `RowsLoadMoreCard.tsx`, `PreviewTableSimple.tsx` |
| Formulare | `apps/web/app/components/ui/forms/`, `.../input`, `.../selectors`, `.../editors` | `FormGroup.tsx`, `InputGroup.tsx` + reichhaltige Input-/Selector-/Editor-Komponenten |
| Modale/Dialoge | `apps/web/app/components/ui/modals/`, `.../slideOvers` | `Modal.tsx`, `ModalOrSlide.tsx`, `ConfirmDialog.tsx`, `ConfirmModal.tsx`, `ActionResultModal.tsx`, `ErrorModal.tsx`, `SuccessModal.tsx`, `OpenModal.tsx`, ... |
| Empty/Loading | `apps/web/app/components/ui/emptyState`, `.../loaders` | Empty-/Loading-States |
| Command-Palette | `apps/web/app/components/ui/commandPalettes/CommandPalette` | Kommando-Palette (im Shell verdrahtet) |
| Entitaeten/Editoren | `apps/web/app/components/entities`, `.../editors`, `.../json`, `.../pdf` | Entity-Builder-UI, Rich-Editoren, JSON-/PDF-Viewer |
| Weitere | `aceternity`, `ai-elements`, `analytics`, `audit`, `brand`, `cookies`, `dashboard`, `images`, `motion-primitives`, `notifications`, `pages`, `public`, `recaptcha` | App-spezifische Komponenten-Cluster |
### Hot-Swap-Aktivierung (Drop-in, real verdrahtet)
| Asset | Pfad | Was |
| --- | --- | --- |
| Renderer | `apps/web/app/components/ui/hot-swap/HotSwapPageRenderer.tsx` | Bruecke: `MockPage`-Deskriptor (`@toolctrl/ui-mock`) → RR7-JSX, nutzt `createShellRegistries()` aus `@toolctrl/ui-app` |
| Demo-Route | `apps/web/app/routes/hot-swap-demo.tsx` | Rendert `createHotSwapDrillPage()`; gated auf `NODE_ENV !== "production"` |
| Renderer-Test | `apps/web/app/components/ui/hot-swap/HotSwapPageRenderer.test.tsx` | Prueft Deskriptor → DOM (Sidebar/Breadcrumbs/Header/Cards/Activity) |
**HiveGrid-Relevanz (apps/web gesamt):** Sehr hoch. Dies ist der reale,
sofort wiederverwendbare Fundus: vollstaendige authentifizierte App-Shell
(App/Admin/Docs), datengetriebene Sidebar mit Permission-/Feature-Gating,
RR7-Flat-Folder-Routing inkl. Tenant- (`app.$tenant`) und Admin-Trennung,
Theme/Dark-Mode mit Per-Tenant-Whitelabel (`Tenant.themeConfig`), i18n (de/en),
und eine breite shadcn/Radix-Komponentenbibliothek (Tabellen/Forms/Modale).
Querschnittlich relevant fuer praktisch jedes HiveGrid-Modul.
---
## App: `apps/storybook`
**Zweck:** Storybook-8-Workbench + Visual-Regression (lost-pixel) + a11y-Addon.
**package.json:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/storybook/package.json`
(`@storybook/react-vite` 8.6, Addons: essentials/a11y/interactions/themes; `lost-pixel` 3.22; dep `@toolctrl/i18n`)
**Zentrale Dateien:**
| Asset | Pfad | Was |
| --- | --- | --- |
| Storybook-Config | `apps/storybook/.storybook/main.ts` | Stories aus `apps/storybook/src/**` **und** `packages/{ui,ui-app,ui-subframe,ui-mock}/src/**/*.stories.*`; Tailwind-v4-Vite-Plugin; Alias `~` → `apps/web/app` |
| i18n-Bootstrap | `apps/storybook/.storybook/i18n.ts`, `preview.tsx`, `preview.css` | i18next + Theme-Decorator in Preview |
| Lost-Pixel | `apps/storybook/lostpixel.config.ts`, `.lostpixel/` | Visual-Regression-Config + Baselines |
| Test-Runner | `apps/storybook/.storybook/test-runner.ts` | `axe-playwright`-basierte a11y-Checks im Test-Runner |
| Docs (MDX) | `apps/storybook/src/Introduction.mdx`, `src/ThemeGuide.mdx` | Einfuehrung + Theme-Guide |
| Scripts | `package.json` | `storybook` (dev :6006), `build-storybook`, `test:storybook`, `visual-regression`(:update) |
**Status:** Stories sind aktuell (~21) groesstenteils inline-styled Demos ueber
`packages/ui/src/stories/_demo-primitives.tsx` (CSS-Variablen-Tokens aus
`preview.css`); der "echte Tailwind-v4-Pipeline"-Pfad ist im `main.ts` als
Migrations-Vorbereitung schon vorhanden (Tailwind-Plugin no-op fuer heutige Stories).
**HiveGrid-Relevanz:** QA-/Design-Querschnitt. Direkt nutzbar als Visual-
Regression- und a11y-Gate fuer eine HiveGrid-Komponentenbibliothek; der Alias
auf `apps/web/app` erlaubt, echte App-Komponenten in Stories zu ziehen.
---
## Maschinenlesbare UI-Inventur & Verifikations-Gates
Verbindet Vertrag ↔ echte Komponenten und wird von der Test-Suite geprueft.
| Asset | Pfad | Was |
| --- | --- | --- |
| Komponenten-Inventur | `docs/ui-inventory/components.json` | 71 Eintraege (`stableName`/`path`/`category`/`library`/`status`); Schema `docs/ui-inventory/schema.json` |
| Surface-Map | `docs/ui-inventory/surfaces.json` (+ `surfaces.schema.json`, `surface-map.md`) | Mapping stabiler `data-ui-id` → Quelldateien/Renderer |
| Inventur-Test | `apps/web/app/application/ui/uiInventory.test.ts` | Prueft, dass Inventur-Pfade existieren, alle `components/ui/*.tsx`-Primitives gelistet sind und Registry-IDs in der Surface-Map stehen |
| Adapter-Test | `apps/web/app/application/ui/uiAdapters.test.ts`, `uiRegistry.test.ts` | Prueft `@toolctrl/ui`-Adapter ↔ App-Registry-Konsistenz |
| Hot-Swap-Status | `docs/ui-inventory/hot-swap-status.md` | Dokumentiert, was verdrahtet ist (`ui-app`/`ui-mock` aktiv, `ui-subframe` Platzhalter) + Aktivierungs-Checkliste |
| Boundary-Gate | `scripts/verify-phase-c-boundaries.mjs` (npm `verify:phase-c-boundaries`) | Erzwingt, dass Aktivierung keine Laufzeit-Layer-Grenze verletzt |
| Hot-Swap-Verify | `scripts/verify-ui-hot-swap.ts` (npm `verify:ui-hot-swap`, `test:hot-swap`) | Mock-getriebener Hot-Swap-Drill |
| Visual-Verify | `scripts/verify-ui-mock-visual.ts` (npm `verify:ui-visual`) | Mock-Visual-Baseline |
**HiveGrid-Relevanz:** Governance-/Konventions-Querschnitt. Das Muster
"stabile ID-Registry + maschinenlesbare Inventur + dateisystem-geprueftes Gate"
ist direkt als HiveGrid-UI-Governance uebertragbar (Whitelabel/Hot-Swap-Faehigkeit
mit hartem CI-Schutz der Layer-Grenzen).
---
## Wichtige Vorbehalte (Anti-Erfindung)
- `@toolctrl/ui` enthaelt **keine** lauffaehigen Komponenten — nur Adapter-Metadaten,
die auf `apps/web/app/...` verweisen. Echte Primitives = `apps/web/app/components/ui/*`.
- `@toolctrl/ui-subframe` ist **leer** (Placeholder). Es gibt **keinen** gebauten
Subframe-Adapter-Layer in der Boilerplate.
- Der `AppShell` in `@toolctrl/ui-app` ist ein **headless Stub** (nur `data-*`),
nicht die echte sichtbare Shell. Sichtbare Shell = `ShadcnSidebarLayout`.
- Hot-Swap ist **nur** an der dev-only Demo-Route (`hot-swap-demo.tsx`) end-to-end
aktiv; Produktiv-Routen rendern weiterhin direkt aus `apps/web/app/components/`.
- Routing nutzt die **Remix-v1-Flat-Folder-Convention** (Compat-Layer), nicht die
RR7-Default-`routes.ts`-Convention.
- Es gibt **keine** Prisma-Modelle, die exklusiv zu den UI-Paketen gehoeren;
UI-relevant sind `User.lightOrDarkMode` und `Tenant.themeConfig` (accounts.prisma)
sowie `Portal*` (portals.prisma).
@@ -0,0 +1,123 @@
# Boilerplate-Abgleich M00 — Querschnitt-Hülle (Bauprinzipien + Hot-Swap)
> Abgleich der 17 Querschnitt-Milestones **M001–M017** gegen die Boilerplate-Foundation
> (`/Users/raffaelsheikh/Documents/GitHub/boilerplate`). Eigene Greps/Reads verifiziert,
> nicht nur Inventur übernommen. Alle Pfade absolut. Boilerplate nur gelesen, nichts geändert.
| Feld | Wert |
| --- | --- |
| **Modul-ID** | M00 (Sammel-Hülle für M001–M017) |
| **Name** | Querschnitt-Hülle (Bauprinzipien + Hot-Swap) |
| **Milestones** | M001 Audit-Log, M002 Klartext-Diagnostik, M003 i18n-Pflicht, M004 Tenant-Scope+Owner, M005 URL-Tampering+Mass-Action, M006 Feature-Flags, M007 Idempotenz, M008–M010 Apps-Engine-Adapter/State-Machine, M011 Notifications, M012 Workspace-Topologie, M013 View-Model-Konvention, M014 Action-Callback-Contract, M015 Subframe-Adapter, M016 Visual-Regression-Gate, M017 Hot-Swap-Drill |
| **Block** | Querschnitt-Hülle Teil 1 (Bauprinzipien, M001–M011) + Teil 2 (UI-Hot-Swap-Architektur, M012–M017) |
| **Pflichtenheft-Quelle** | `_bmad-output/pflichtenheft/03-architektur.md` (A.1, A.4, A.5, A.6, A.7, A.8) + AGENTS.md + INTEGRATION-CONTRACTS.md |
| **Reuse-Level** | **HIGH** — fast die gesamte Hülle existiert in der Boilerplate als lauffähige Patterns + CI-Gates. HiveGrid füllt Inhalte (Error-Texte, Flags, i18n-Keys, Adapter, Sidebar-Einträge). Echte Lücken: Activity-Stream-Service (`logActivity`), Search-`indexed-entities`-Registry, Subframe-Adapter-Pattern (Paket leer), plus durchgängiger Naming-Drift in der HiveGrid-Doku. |
---
## Übernehmbar aus Boilerplate
| Milestone | Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- | --- |
| M001 Audit | `…/packages/foundation/src/audit.ts` | `auditEvent(input, client)`, `withAuditMiddleware(handler, buildEvent, client)`, Typen `AuditEventInput`/`AuditEventClient` (Pattern, Prisma-frei per Client-Injection) | **1:1** als Service-Wrapper |
| M001 Audit (DB) | `…/packages/db/src/audit-events.server.ts` | `createAuditEvent` als **single write-path**, tamper-evidente Hash-Chain pro Tenant (`audit-hash.ts`), Outbox-Publish; Felder `actorIp`, `regulatoryScope`, `dataSensitivity`, `outcome`, `sourceModule` | **1:1** — HiveGrid liefert nur die Module-spezifischen `action`/`targetType`-Werte |
| M001 Audit (Schema) | `…/prisma/schema/core.prisma:55` (`AuditEvent`) | Vollständiges Audit-Modell inkl. `previousHash`/`eventHash` Hash-Chain | **1:1** |
| M001 Audit (CI) | `…/scripts/verify-audit-cascade-guard.mjs`, `verify-permissions-audit-baseline.mjs` | Schützt Audit-Cascade-Verhalten + Permission-Audit-Baseline | **1:1** als Gate |
| M002 Klartext | `…/packages/foundation/src/errors.ts` | `CatalogError`, `CatalogErrorDefinition`, `catalogRegistry` (**`Object.freeze({})` — leer**), `foundationErrors` (`forbidden` 403, `notFound` 404, `idempotencyConflict`/`idempotencyInProgress` 409), `toPinoLogObject` | **als-vorlage** — Engine 1:1, HiveGrid füllt `catalogRegistry` mit Klartext-Definitionen (`code`/`status`/`i18nKey`/`details`) |
| M003 i18n | `…/packages/i18n/src/index.ts` | `defaultLocale` `"de"`, `supportedLocales` `["de","en"]`, `i18nNamespaces` (`crm`, `auth`, `settings`, `errors`, `mail`, …), Intl-Formatter, ICU, `createUseTranslate` | **1:1** Engine; HiveGrid ergänzt Namespaces/Keys |
| M003 i18n (assert) | `…/packages/foundation/src/notifications.ts` (`assertI18nKey`), `…/packages/view-models/src/index.ts` (`assertI18nLabel`) | Erzwingt Key-Form `^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$` statt Klartext | **1:1** |
| M003 i18n (CI) | `…/scripts/i18n-check.ts` + `…/scripts/verify-hardcoded-strings.mjs` + ESLint über `packages/ui-app|ui-subframe|ui-mock` (`pnpm verify:i18n`) | Build-Lint gegen hartkodierte Strings; läuft im CI (`ci.yml:101`) | **1:1** Gate; ESLint-Scope auf HiveGrid-UI-Pakete erweitern |
| M004 Tenant/Owner | `…/packages/foundation/src/permissions.ts` | `requirePermission`, `withTenantScope`, `withPermission`, `withOwnerCheck`, `PermissionContext` — Fehlschlag wirft **`CatalogError(notFound)` = 404, nicht 403** (Existenz-Versteckung, A.5.3) | **1:1** |
| M004 Tenant (CI) | `…/scripts/tenant-isolation-report.ts` (`pnpm verify:tenant-isolation-report`), `verify-permission-matrix.mjs`, `verify-role-permission-baseline.mjs` | Tenant-Scoping- + Permission-Matrix-Report | **1:1** Gate |
| M005 URL-Tampering | (gleiches Paket) `…/packages/foundation/src/permissions.ts` + `…/packages/foundation/src/outbound-http.ts` | 404-statt-403-Verhalten (kein Owner-Leak) + SSRF-Schutz `validateOutboundHttpUrl`/`fetchOutboundHttpWithRedirects` (block private IPv4/6, DNS-Check, Redirect-/Size-/Timeout-Limit) | **1:1** für Hardening; Mass-Action-Limit (s. Lücken) ist HiveGrid-Logik |
| M006 Feature-Flags (leicht) | `…/packages/foundation/src/flags.ts` | `defineFlag`, `featureFlag` (user>tenant>global, override), `selectFlagRecord`, `normalizeFlagScope` | **1:1** für einfache Gates |
| M006 Feature-Flags (Engine v2) | `…/packages/feature-flags/src/*` (`evaluate.ts`, `targeting/*`, `cache.ts`, `publisher.ts`, `client/context.tsx`) | Targeting (boolean/allowlist/percentage/plan-tier/role/time/composite), Zeitfenster, Parent-Dependency, sticky Bucketing, Eval-Cache + Cross-Process-Invalidierung, React-Hooks | **1:1** mächtige Variante (M031–M033 nutzen sie) |
| M006 Feature-Flags (Schema) | `…/prisma/schema/featureFlags.prisma:1/32/44` (`FeatureFlag`/`FeatureFlagFilter`/`FeatureFlagAudit`) | Flag-Tabellen inkl. append-only Audit-Trail, `rules`/`variants`/`enabledFrom`/`parentFlagId` | **1:1** |
| M007 Idempotenz | `…/packages/foundation/src/idempotency.ts` | `withIdempotency(input, client, operation)`, `IdempotencyClient`, `IdempotencyResponse` — In-Process-Lock, stabiler Fingerprint-Hash, 409 bei Hash-Mismatch/In-Progress | **1:1** |
| M007 Idempotenz (DB) | `…/packages/db/src/idempotency-keys.server.ts`, Schema `…/prisma/schema/core.prisma:84` (`@@unique([scope, key])`) | Idempotency-Key-Persistenz passend zur Client-Where-Form | **1:1** |
| M008–M010 Apps-Engine (Adapter-Register) | `…/packages/foundation/src/adapters.ts` | `defineAdapter`, `getAdapter`, `AdapterStatus` (`not_configured`/`configured`/`healthy`/`degraded`/`unavailable`) — globales Provider-Slot-Register mit Fallback `not_configured` | **1:1** (verifiziert: Enum exakt wie behauptet) |
| M008–M010 Apps-Engine (Schema/State-Machine) | `…/prisma/schema/appsEngine.prisma` (`App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem`) | App-Installation pro Tenant, Sync-Run-State-Machine (`pending/running/completed/partial/failed/cancelled/reverted`), `executionMode` (`manual/scheduled/webhook`), Sync-Logs + Items | **1:1** State-Machine; HiveGrid baut KWP/Gmail/AirCall-Adapter darauf |
| M008–M010 Apps-Engine (Plugin-Loader) | `…/packages/plugins/src/*` (`manifest-loader.ts`, `manifest-schema.ts`) + `…/packages/plugin-runtime/src/*` (`capabilities`, `capability-gateway`, `capability-grants`, `signature`, `crash-tracker`, `production-gate`, `runtime`, `audit-chain`) + `…/packages/plugin-sdk` | Vollständiges Plugin-Capability-/Sandbox-/Signatur-/Crash-Tracking-System | **als-vorlage** für externe Adapter (über Inventur hinaus vorhanden) |
| M011 Notifications | `…/packages/foundation/src/notifications.ts` | `notify`, `notifyMany`, `bundleNotifications`, `isNotificationEnabled`, `assertI18nKey`, Typen `NotificationInput`/`NotificationClient`/`NotificationPreference`, Channels `IN_APP`/`EMAIL`, Status `PENDING/SENT/FAILED/SKIPPED/READ` | **1:1** — Preference-Check, Upsert per `sourceKey`, Metadata-Redaction, Bundling |
| M011 Notifications (DB/Schema) | `…/packages/db/src/notifications.server.ts`; Schema `…/prisma/schema/notifications.prisma:14/41` (`NotificationDelivery` `@@unique([tenantId,userId,channel,sourceKey])`, `UserNotificationPreference`) | Persistenz + Preferences | **1:1** |
| M011 Notifications (CI) | `…/scripts/verify-notification-boundary-baseline.mjs` | Erzwingt Notify-über-Service (kein direkter Email/Push) | **1:1** Gate |
| M012 Workspace-Topologie | `…/packages/view-models`, `…/packages/ui-app`, `…/packages/ui-subframe`, `…/packages/ui-mock` | Alle vier Pakete existieren und bauen (`pnpm verify:phase-c-boundaries`, Phase-0 SIGNED-OFF) | **1:1** |
| M012 Topologie (CI) | `…/scripts/verify-phase-c-boundaries.mjs` (`pnpm verify:phase-c-boundaries`) | Erzwingt Layer-Trennung (Anti-Pattern: Prisma in ui-*, ui-subframe-Direktimport, `useLoaderData` in ui-*) | **1:1** Gate |
| M013 View-Model-Konvention | `…/packages/view-models/src/index.ts` | `VMState<T>` (`idle/loading/ready/empty/error`, `error` trägt `messageKey`+`code`), `VMStatus`, `ready`, `empty` | **1:1** (verifiziert: Union exakt wie behauptet) |
| M014 Action-Callback-Contract | `…/packages/view-models/src/index.ts` | `ActionCallbacks<T>` (`onCreate`/`onUpdate`/`onDelete`/`onRefresh`) | **1:1** |
| M013/M014 Registry-Gating | `…/packages/view-models/src/index.ts` | `RegistryGate` (`permission?`/`flag?`), `I18nLabel`, `SidebarRegistryItem`/`BreadcrumbRegistryItem`/`MobileRegistryItem`/`PortalRegistryItem`/`ShellRegistries`, `assertI18nLabel` | **1:1** Typen für die Shell-Registries |
| M015 Subframe-Adapter | `…/packages/ui-subframe/` (README + leeres `src/index.ts`) | Reservierte Vendor-Import-Boundary; **exportiert bewusst leere Registry** | **als-vorlage** — Boundary steht, der Adapter-Code (M015-Kern) ist NOCH NICHT gebaut (s. Lücken) |
| M016 Visual-Regression | `…/scripts/verify-ui-mock-visual.ts` (`pnpm verify:ui-visual`), Baseline `…/packages/ui-mock/visual-baseline/`; CI `ci.yml:117` | Visual-Regression-Gate läuft im CI | **1:1** Gate; HiveGrid ergänzt Snapshots pro Hauptansicht |
| M017 Hot-Swap-Drill | `…/scripts/verify-ui-hot-swap.ts` (`pnpm verify:ui-hot-swap`, `pnpm test:hot-swap`); CI-Job „Test UI mock hot-swap" `ci.yml:119-120`; `…/packages/ui-mock/` als Drop-in | Tausch `ui-app`↔`ui-mock` in CI; bricht bei versteckter Kopplung | **1:1** Gate; HiveGrid hält Smoke-E2E hot-swap-fähig |
| Querschnitt (Logging) | `…/packages/logger/src/index.ts` | `createLogger` mit Redaction (`authorization`/`cookie`/`password`/`token`/`secret`/`apiKey`/…), `createChildLogger`, `resolveLogLevel` | **1:1** |
| Querschnitt (Observability/Metrics) | `…/packages/observability` (`PIISpanProcessor`), `…/packages/metrics` (`createMetricsRegistry`, `recordAICostCents`, Token-Auth) | PII-Scrubbing + Prometheus-Metriken | **1:1** |
| Querschnitt (URL/Config) | `…/packages/url/src/canonical.ts` (`appPath/appUrl` host-aware, `adminPath`, `apiPath`), `…/packages/config/src/*` (Env-Katalog, Session-Cookie-Policy, Domain-Routing, Secret-Validatoren) | Kanonische Tenant-/Admin-/API-Links + Env-Validierung | **1:1** |
| Querschnitt (Entitlements) | `…/packages/entitlements/src/*` (`featureKeys`, `evaluateEntitlement`, `standardEntitlementPlans`, `productBlueprints`) | Feature-Gate-/Plan-Auswertung (ergänzt Feature-Flags um Billing-Gating) | **anpassen** — HiveGrid-Plan-/Seat-Modell befüllen (M153–M156) |
---
## Vorhandene Daten-Modelle (Prisma)
Alle verifiziert in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`:
| Modell | Pfad | Querschnitt |
| --- | --- | --- |
| `AuditEvent` | `…/prisma/schema/core.prisma:55` | M001 — Hash-Chain (`previousHash`/`eventHash`), `actorType`, `outcome`, `dataSensitivity`, `regulatoryScope[]`, `metadata`, `requestId` |
| `IdempotencyKey` | `…/prisma/schema/core.prisma:84` | M007 — `@@unique([scope, key], name: "idempotency_key_scope_key")`, `expiresAt`-Index |
| `FeatureFlag` / `FeatureFlagFilter` / `FeatureFlagAudit` | `…/prisma/schema/featureFlags.prisma:1 / :32 / :44` | M006 — v2-Felder (`rules`/`variants`/`enabledFrom`/`enabledUntil`/`parentFlagId`) + append-only Audit |
| `NotificationDelivery` / `UserNotificationPreference` | `…/prisma/schema/notifications.prisma:14 / :41` | M011 — `@@unique([tenantId,userId,channel,sourceKey])` für Upsert |
| `App` / `DataConnection` / `AppSyncRun` / `AppSyncRunLog` / `AppSyncItem` | `…/prisma/schema/appsEngine.prisma` | M008–M010 — App-Install pro Tenant, Sync-State-Machine, Sync-Logs/Items |
| `SubscriptionFeature` / `PortalSubscriptionFeature` | `…/prisma/schema/subscriptions.prisma:65` / `…/prisma/schema/portals.prisma:149` | Entitlements (Plan-/Portal-Feature-Zuordnung) |
| (weitere relevante Schemas) | `permissions.prisma`, `accounts.prisma`, `plugins.prisma`, `outbox.prisma`, `queue.prisma` | Permissions, Tenant/User, Plugin-Manifest, Outbox (Audit/Search-Publish), BullMQ |
**Insgesamt 30 `.prisma`-Dateien** (nicht 184 Einzelmodelle in einer Datei — die Inventur-Angabe „184 Modelle" bezieht sich auf das Gesamt-Schema über alle Dateien).
---
## Lücken / komplett neu zu bauen
1. **Activity-Stream-Service `logActivity()` — FEHLT komplett.**
- Grep über `packages/` und `apps/web/app` nach `logActivity`/`ActivityStream`/`activity-stream` ergab **keinen Treffer**.
- DoD #14 (AGENTS.md) und INTEGRATION-CONTRACTS.md §„Activity-Stream" fordern `logActivity` auf Kontakt/Firma/Lead/Verkaufsfall/Auftrag. Das ist HiveGrid-spezifisch und **muss neu gebaut werden** (eigener Querschnitt-Service, distinkt vom Audit-Log). Fachlich angesiedelt in M045 (Aktivitäts-Stream), aber als Querschnitt-Vertrag hier zu definieren.
2. **Search-`indexed-entities`-Registry — FEHLT am behaupteten Ort.**
- AGENTS.md DoD #16 verweist auf `packages/db/src/search/indexed-entities.ts`. Dieser Pfad/Ordner **existiert nicht** (`packages/db/src/search/` fehlt).
- Die Such-Engine liegt stattdessen in `…/packages/search/src/*` (`index-helper.ts`, `client.ts`, `outbox-publisher.ts`, `query`, `sanitize`). Die **zentrale Entity-Registrierungs-Konvention** (`indexed-entities`) muss neu angelegt + im DoD-Pfad korrigiert werden.
3. **Subframe-Adapter-Pattern (M015-Kern) — NUR Boundary, kein Code.**
- `…/packages/ui-subframe/` ist bewusst leer (README: „exports an empty registry … import rules and review gates … in a later slice").
- Es existiert **kein `adapters/`-Verzeichnis** in `…/packages/ui-app/` (nur `src/`, `src/test-utils`, `test-setup`). Das in M015 geforderte Type-Bridge-Adapter-Pattern (Subframe-Props ↔ HiveGrid-VM) ist noch zu bauen.
4. **Mass-Action-Schutz (M005) — Engine fehlt.**
- SSRF-Hardening + 404-statt-403 sind vorhanden; ein dediziertes **Bulk-/Mass-Action-Limit + Bestätigungs-/Vorschau-Flow** (Pflichtenheft A.10.1 „würde 1.400 Personen betreffen") ist als generischer Querschnitt **nicht** vorhanden und neu zu konzipieren.
5. **Naming-Drift in der HiveGrid-Doku (kein Code-Gap, aber Blocker für jeden Folge-Agent).**
- AGENTS.md, alle `PLAN.md` und PROGRESS-TRACKER-Vorlagen referenzieren `packages/ui-hivegrid/...`, `packages/ui-hivegrid/src/app-shell/sidebar.config.ts`, `@hivegrid/foundation`.
- Boilerplate-Realität: Paket heißt **`packages/ui-app`** (npm `@toolctrl/ui-app`), Foundation ist **`@toolctrl/foundation`**, und die Sidebar liegt **nicht** in einem Paket-`sidebar.config.ts`, sondern in `…/apps/web/app/application/sidebar/` (`AppSidebar.tsx`, `AdminSidebar.tsx`, `SidebarItem.ts`, `SidebarGroupDto.ts`) — gegated via CI `pnpm verify:sidebar-routes`.
- Der PROGRESS-TRACKER Phase-0 listet diese Umbenennung explizit als **⚪ NICHT-GESTARTET** („Doku-Naming-Anpassung an Boilerplate-Realität"). Muss vor M018ff. erfolgen, sonst greifen DoD-Pfade ins Leere.
6. **`catalogRegistry` ist leer (erwartet, kein Bug).** `Object.freeze({})` in `errors.ts` — HiveGrid muss alle Klartext-Fehler-Definitionen (M002, Bauprinzip A.1) befüllen. Kein Default-Katalog vorhanden.
7. **Locale-Inhalte nur DE/EN-Gerüst.** `…/apps/web/public/locales` existiert; HiveGrid-Namespaces/Keys (`crm`, …) sind zu befüllen (Phase 1: nur Deutsch, A.4.2).
---
## Empfohlenes Vorgehen
**Phase 0 (Voraussetzung, vor jedem Folge-Modul):**
1. **Naming-Drift beheben** (Tracker Phase-0-Punkt): in AGENTS.md, allen `PLAN.md`, PROGRESS-TRACKER-Vorlagen `ui-hivegrid`→`ui-app`, `@hivegrid/foundation`→`@toolctrl/foundation` ersetzen; Sidebar-Pfad auf `apps/web/app/application/sidebar/` und Such-Registry-Pfad auf `packages/search/...` korrigieren. Ohne diesen Schritt sind alle DoD-Checklisten falsch verlinkt.
2. Pre-Flight (`pnpm typecheck && pnpm test`, EU-Sterilisations-Grep) gemäß AGENTS.md durchlaufen.
**Phase 1 — 1:1 übernehmen (kein Code, nur Wiring/Inhalt):** M001 (audit), M004/M005-Hardening (permissions/outbound-http), M007 (idempotency), M011 (notify), M012 (Topologie), M013/M014 (VMState/ActionCallbacks) sind als Engine fertig. Hier nur HiveGrid-Werte einfüllen und CI-Gates (`verify:phase-c-boundaries`, `verify:tenant-isolation-report`, `verify:notification-boundary-baseline`, `test:hot-swap`, `verify:ui-visual`) als Pflicht-Pipeline verdrahten.
**Phase 2 — Inhalte befüllen (Engine vorhanden, Daten fehlen):** M002 `catalogRegistry` mit Klartext-Fehlern; M003 i18n-Namespaces/Keys (DE) + ESLint-Scope auf HiveGrid-UI-Pakete erweitern; M006 Flags definieren (`defineFlag`/Engine v2 für M031–M033).
**Phase 3 — Apps-Engine aufsetzen:** M008–M010 auf vorhandenem `appsEngine.prisma` + `foundation/adapters.ts` + `packages/plugins`/`plugin-runtime` aufbauen; State-Machine ist da, KWP/Gmail/AirCall-Adapter sind HiveGrid-Arbeit.
**Phase 4 — echte Neubauten:**
- **`logActivity()`-Querschnitt-Service** (Lücke 1) — vor den ersten Stammdaten-Modulen (M042ff.), da DoD #14 pflicht.
- **`indexed-entities`-Such-Registry** (Lücke 2) auf Basis `packages/search/` anlegen, DoD-Pfad korrigieren.
- **Subframe-Adapter-Pattern M015** (Lücke 3) — `packages/ui-app/src/adapters/` + Beispiel-Adapter (DataTable→KontaktList), abgesichert durch M016/M017-Gates.
- **Mass-Action-Schutz M005** (Lücke 4) — generischer Bulk-Confirm/Preview-Flow.
**Reihenfolge-Logik:** Engine-Übernahme (Phase 1) ist risikolos und sofort. Die vier echten Neubauten (Phase 4) blockieren die fachlichen Module — `logActivity` und `indexed-entities` zuerst, da bereits M042 sie braucht; Subframe-Adapter + Mass-Action können parallel laufen.
@@ -0,0 +1,238 @@
# Boilerplate-Abgleich: M01 — CRM-Kern
> Faktischer Abgleich der HiveGrid-Anforderungen (Modul 1) gegen die Boilerplate
> (`/Users/raffaelsheikh/Documents/GitHub/boilerplate`). Read-only verifiziert mit
> eigenen Greps/Reads, nicht nur Inventur. Alle Pfade absolut.
## Kopf
| Feld | Wert |
| --- | --- |
| Modul-ID | M01 |
| Name | CRM-Kern (Kontakte + Firmen + Leads) |
| Milestones | M042 (Kontakt-Stammdaten), M043 (Firma-Stammdaten + Hierarchie), M044 (Tags + Custom-Felder + Datenherkunft), M045 (Aktivitäts-Stream), M046 (Listen + Segmente + DSGVO-Felder) |
| Pflichtenheft-Block | Block 2, Abschnitte 1.1–1.13 (`_bmad-output/pflichtenheft/blocks/block2.md`) |
| Detail-Spec | `_bmad-output/specs/M01-crm-kern.md` |
| **Reuse-Level** | **medium** |
## Reuse-Verdict in einem Satz
Das Daten-Modell des Moduls (Contact/Company/Lead als **dedizierte, first-class
Prisma-Modelle** mit Mehrfach-Email/-Telefon, M:N-Relation, Activity-Sourcing-Tabelle,
Custom-Field-Definitionen, Tags) existiert in der Boilerplate **nicht** — es gibt nur
(a) eine generische No-Code-Engine (`entityBuilder`), auf der die Boilerplate ihr eigenes,
architektonisch **unvereinbares** CRM (`company`/`contact`/`opportunity` als `Entity`+`Row`)
betreibt, und (b) einen operator-scoped Marketing-`Lead` (kein Tenant-CRM-Lead). Der gesamte
**Querschnitt** (Audit, Permissions/Tenant-Scope, Notifications, i18n, Feature-Flags,
View-Models, Search, Export/DSGVO, App-Shell, Form-Capture) ist hingegen **reif vorhanden
und direkt nutzbar**. Daher: solides Fundament + viele Vorlagen, aber die CRM-Kern-Tabellen
und die domänen-spezifische Logik sind **neu zu bauen**.
---
## Wichtige Architektur-Vorbemerkung (entscheidet das gesamte Mapping)
Die Boilerplate enthält **zwei** CRM-Konzepte, die beide NICHT zur HiveGrid-Spec passen:
1. **Boilerplate-„CRM" über den Entity-Builder** (`apps/web/app/modules/crm/`):
`company`, `contact`, `opportunity`, `submission` sind **keine eigenen Prisma-Modelle**,
sondern werden zur Laufzeit als `entityBuilder.Entity`-Definitionen geseedet
(`apps/web/app/modules/crm/services/ProductCoreCrmEntities.server.ts`,
`productCoreCrmEntityNames = ["company","contact","opportunity"]`). Ein Kontakt ist ein
`Row` mit `RowValue`-Einträgen (`firstName`, `lastName`, `email`, `jobTitle`,
`marketingSubscriber`). Verifiziert in `CrmService.ts` (`RowValueHelper.getText({..., name: "email"})`).
→ **Inkompatibel** mit der HiveGrid-Spec, die explizit dedizierte, typisierte Tabellen
mit Constraints, Indizes (`tenant_id, archived_at, last_activity_at`), `pg_trgm`-Fuzzy-Index,
`ContactCompanyRelation` (M:N mit Rolle/Primary), separate `ContactEmail`/`ContactPhone`-Tabellen
und eine zentrale `Activity`-Event-Sourcing-Tabelle fordert.
2. **Marketing-`Lead`** (`prisma/schema/marketingLeads.prisma:14`): bewusst **NICHT
tenant-scoped** (operator-/sales-of-record, `@@unique([email])`, First-Touch-UTM,
`convertedToTenantId`). Das ist der „Wer-wird-HiveGrid-Kunde"-Funnel, **nicht** der
tenant-interne CRM-Lead aus M01/M02 (der ist tenant-scoped, hat Status
`new|qualified|converted|lost|spam` und konvertiert zu einem `Contact`, nicht zu einem `Tenant`).
**Konsequenz:** Die HiveGrid-CRM-Modelle gehören neu nach `prisma/schema/app.prisma`
(der vorgesehene leere Platzhalter, `prisma/schema/app.prisma`). Die Entity-Builder-Variante
wird NICHT übernommen (allenfalls als konzeptionelle Vorlage für Custom-Felder-UI). Das
Boilerplate-CRM-Modul (`apps/web/app/modules/crm`, `routes/app.$tenant/crm`, `routes/admin/crm`)
sollte für HiveGrid entfernt/ersetzt werden, da es die andere Architektur transportiert.
> Hinweis: Das HiveGrid-Cloud-Repo selbst enthält aktuell **noch keinen** Code
> (`apps/`/`packages/` fehlen, nur `prisma/` + `docs/` + Specs vorhanden). Die Boilerplate
> ist die zu übernehmende Basis (gem. AGENTS.md „Die Boilerplate IST deine Codebase").
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `packages/foundation/src/permissions.ts` | `withTenantScope()`, `withPermission()`, `withOwnerCheck()`, `requirePermission()` — Tenant-Isolation + Permission-/Owner-Gate; 404-statt-403 (`CatalogError(notFound)`) | **1:1** — deckt Block I komplett (crm.read_own/read_all/write/delete/admin, Owner-Check, 404-Versteckung) |
| `packages/foundation/src/audit.ts` | `auditEvent()`, `withAuditMiddleware()` + `AuditEvent`-Hash-Kette (`packages/db/src/audit-hash.ts`, `prisma/schema/core.prisma:55`) | **1:1** — Audit-Default (AV #6) für jede Schreib-Aktion |
| `packages/foundation/src/notifications.ts` + `prisma/schema/notifications.prisma` | `notify()`, `bundleNotifications()`, Preference-Check, In-App/Email/Push | **1:1** — Notification-Wiring (Verantwortlichen-Wechsel, Lead-Zuweisung) |
| `packages/foundation/src/errors.ts` | `CatalogError`, `foundationErrors` (forbidden/notFound/conflict), `toPinoLogObject` | **als-vorlage** — HiveGrid-Error-Catalog (M002) auf diesem Muster; Klartext-Texte (Block C/D/F/G/H „Klartext-Hinweise") als i18n-Keys ergänzen |
| `packages/foundation/src/idempotency.ts` | `withIdempotency()` (Lead-Promotion-Transaktion, Bulk-Actions) | **1:1** |
| `packages/view-models/src/index.ts` | `VMState<T>`, `VMStatus`, `ActionCallbacks<T>`, `RegistryGate`, `I18nLabel`, `SidebarRegistryItem`, `assertI18nLabel` | **1:1** — VM-/Action-Vertrag für alle Listen-/Detail-VMs (AGENTS.md C.18, INTEGRATION-CONTRACTS §11) |
| `packages/feature-flags/src/*` + `prisma/schema/featureFlags.prisma` | Flag-Engine v2 (Targeting, Cache, React-Hooks) bzw. leichtgewichtig `foundation/src/flags.ts` | **1:1** — Feature-Flag-fähig (M006, DoD) |
| `packages/i18n/src/*` + `apps/web/public/locales/{de,en}/crm.json` | Locales, Intl-Formatter, ICU; **CRM-Namespace `crm` existiert bereits** (`i18nNamespaces` in `packages/i18n/src/index.ts`) | **anpassen** — Keys neu für HiveGrid-CRM füllen; Namespace + Formatter-Infra 1:1 |
| `packages/db/src/client.server.ts`, `transactions.server.ts`, `outbox.ts` | `getDbClient()/getReadDbClient()/getExtendedDbClient()`, `runDbTransaction()`, `publishOutboxEvent()` | **1:1** — DB-Grenze + Lead-Promotion-Transaktion + Side-Effects via Outbox |
| `packages/db/src/dsgvo/user-fk-map.ts`, `dsgvo/pseudonymize.ts` | Zentrale User-FK-Karte (CI-Test erzwingt Vollständigkeit) + Pseudonymisierung | **anpassen** — neue Contact/Lead-User-FKs (`assigned_to_user_id`, `created_by_user_id`) eintragen, sonst bricht CI |
| `packages/exports/*` + `apps/worker/src/jobs/dsgvo-export.ts` | Streaming-Export CSV/JSON/XLSX/PDF, S3-Sink, **DSGVO-Personal-Data-Export** (JSON-Bundle, Signed-URL, Audit-Event) | **anpassen** — Block D „DSGVO-Daten-Export (alle Daten als JSON)" + Block C „Export als CSV (gefilterte Auswahl)"; an HiveGrid-Contact-Schema anpassen |
| `packages/imports/*` + `prisma/schema/imports.prisma` | Dependency-freies Parser-/Dry-Run-/Dedupe-Framework (CSV/JSON/XLSX), PII-Redaction; `src/dedupe.ts` (`DuplicateKeyField`, `DuplicateDetectionRow`) | **anpassen** — `lead_source = import`, Dedupe-Logik als Basis für Block G (Duplikat-Erkennung); Dry-Run-Preview für Import-Wizard |
| `packages/search/src/*` (`@toolctrl/search`) + `apps/web/app/modules/crm/services/crmContactSearch.server.ts` | Meilisearch-Client + **SQL-Fallback** (`searchWithFallback`, `SEARCH_BACKEND=sql`); Beispiel-Such-Implementierung über Kontakt-Felder | **als-vorlage** — Block G (Listen-Suche + Auto-Complete + globale Suche M24/M056); HiveGrid-Implementierung muss auf neue Tabellen statt `RowValue` zeigen; Trigram/`pg_trgm` ist im Schema selbst zu ergänzen (nicht vorhanden) |
| `packages/form-widget/src/routing.ts` | `resolveSubmissionTarget`/`applyFieldMapping` mit Ziel `"crm-contact"` (verifiziert) + `prisma/schema/formWidget.prisma` (`Form`, `FormSubmission`) | **anpassen** — `lead_source = form` Eingang (Block A, M02); Routing-Ziel auf HiveGrid-Lead/Contact umbiegen |
| `apps/web/app/components/layouts/sidebars/shadcn/*` + `apps/web/app/application/sidebar/{AppSidebar.tsx,SidebarItem.ts,SidebarGroupDto.ts}` + `ShadcnSidebarLayout.tsx`, `AppLayout.tsx` | Vollständige authentifizierte App-Shell, datengetriebene Sidebar mit Permission-/Feature-Gating, Tenant-Theme, Breadcrumbs, Command-Palette | **als-vorlage** — HiveGrid fordert eigene `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` (existiert noch nicht); Shell-Mechanik + Permission-Filter übernehmen, in HiveGrid-6-Gruppen-Schema (`stammdaten`, `arbeiten`) überführen |
| `apps/web/app/components/ui/**` (~707 .tsx, shadcn/Radix) | Tabellen (`TableNew`, `TablePagination`, `RowsLoadMoreCard`, `ColumnSelector`, `GridPagination`), Forms/Inputs/Selectors/Editors, `multi-select.tsx`, Modals (`ConfirmDialog`), Empty-/Loading-States, `command.tsx`, `badge.tsx` | **als-vorlage** — deckt nahezu alle UI-Element-Anforderungen aus Block C/D/E (Listen-Tabelle, Spalten-Selektor, Cursor-„Mehr laden", Filter, Bulk-Checkboxes, Inline-Edit, Tag-Editor via multi-select, Markdown-Editor). MEMORY-Regel: HiveGrid-Routen müssen via Subframe + `packages/ui-hivegrid`-Adapter laufen → diese Komponenten als Adapter-Backing |
| `apps/web/app/components/ui/tables/ColumnSelector.tsx`, `GridPagination.tsx`, `RowsLoadMoreCard.tsx` | Spalten-Konfiguration + Cursor-Pagination („Mehr laden", 50/Page) | **als-vorlage** — Block C Performance-Anforderung (10k+, server-side) |
| `prisma/schema/marketingLeads.prisma` (`Lead`, `LeadEvent`, `LeadStatus`) | Lead-Timeline-Append-Log + Status-Enum + Conversion-FK-Pattern | **als-vorlage** — NUR konzeptionell für M02 Tenant-Lead (Status-Maschine, Event-Timeline, Promotion-FKs). NICHT 1:1 (operator- vs. tenant-scoped, andere Status-Werte) |
| `prisma/schema/queue.prisma` (`ExportJob` mit `expiresAt`/`hardDeletedAt`) | Retention/Grace-Pattern (Soft→Hard mit Frist) | **als-vorlage** — DSGVO-Auto-Lösch von Spam-Leads (30 Tage), `archived_at`-Grace |
| `apps/web/app/application/ui/uiRegistry.ts` + `docs/ui-inventory/*` + `scripts/verify-*boundaries*.mjs` | Stabile `data-ui-id`-Registry + maschinenlesbare Inventur + Layer-Grenz-Gates | **als-vorlage** — UI-Governance/Hot-Swap (M016/M017, DoD-Tests) |
| `packages/ui-mock/src/*` | Hot-Swap-Mock-Generator (deterministisches HTML + Hashes) | **1:1** — Hot-Swap-Drill (DoD: Smoke-Test gegen `ui-mock`) |
| `packages/entitlements/src/features.ts` (`crm-basic` Feature-Key) | Feature-Gate-Registry; `crm-basic` existiert bereits | **anpassen** — CRM-Feature-Gating; Permission-Keys (`crm.*`) gehören laut INTEGRATION-CONTRACTS §10 nach `packages/entitlements/src/permissions.ts` (noch zu erstellen) |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
**Direkt CRM-deckend: KEINE.** Verifiziert via
`grep "^model (Contact|Company|ContactCompany|ContactEmail|ContactPhone|Activity|CustomField|Tag)"`
über `prisma/schema/*.prisma` → 0 Treffer (einziger Tag-Treffer ist `EntityTag` der No-Code-Engine).
Relevante **angrenzende / als Fundament nutzbare** Modelle:
| Modell | Pfad | Relevanz für M01 |
| --- | --- | --- |
| `AuditEvent` | `prisma/schema/core.prisma:55` | Audit-Trail-Default (AV #6) — tamper-evident Hash-Kette, `dataSensitivity`/`regulatoryScope` für PII/GDPR |
| `IdempotencyKey` | `prisma/schema/core.prisma:84` | Idempotente Lead-Promotion / Bulk-Actions |
| `Log` | `prisma/schema/core.prisma:36` | Per-`Row`-Aktivitäts-Timeline der No-Code-Engine (`rowId`, `action`, `details`, `commentId`) — **konzeptionelle Vorlage** für die HiveGrid-`Activity`-Tabelle (Block E), NICHT übernehmbar (kein `subject_type`/`actor_kind`/`summary_text`, an `Row` gebunden) |
| `Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser` | `prisma/schema/permissions.prisma` | RBAC-Fundament — Block I Permission-Bundles (`crm.none`→`crm.admin`) als `Role`+`RolePermission` abbildbar |
| `User`, `Tenant`, `TenantUser` | `prisma/schema/accounts.prisma` | `assigned_to_user_id`/`created_by_user_id`-FKs, Tenant-Scope-Anker, DSGVO-Flags |
| `NotificationDelivery`, `UserNotificationPreference` | `prisma/schema/notifications.prisma` | Notification-Wiring |
| `FeatureFlag` (+ Filter/Audit) | `prisma/schema/featureFlags.prisma` | Feature-Flag-fähig (DoD) |
| `Lead`, `LeadEvent`, `LeadStatus` | `prisma/schema/marketingLeads.prisma:14` | **NUR Vorlage** für tenant-scoped CRM-Lead (M02); semantisch operator-scoped, NICHT 1:1 |
| `Form`, `FormSubmission` | `prisma/schema/formWidget.prisma` | Lead-Capture-Eingang (`lead_source=form`) |
| `ImportJob` | `prisma/schema/imports.prisma` | Bulk-Import (`lead_source=import`) |
| `ExportJob` | `prisma/schema/queue.prisma` | DSGVO-Export-Job + Retention-Pattern |
| `entityBuilder.*` (`Entity`, `Property`, `EntityView`, `Row`, `RowValue`, `RowComment`, `RowTag`, `EntityTag`, `RowTask`, `RowPermission`, `EntityRelationship`) | `prisma/schema/entityBuilder.prisma` | **Konkurrierendes** Daten-Modell für Custom-Felder/Tags/Activity — als **Vorlage** für Block F/H, aber Architektur (polymorphe `RowValue`) wird für M01 NICHT übernommen (Spec fordert typisierte Tabellen) |
**Property-Typen** (`apps/web/app/application/enums/entities/PropertyType.ts`): NUMBER, TEXT,
DATE, MEDIA, SELECT, BOOLEAN, MULTI_SELECT, MULTI_TEXT, RANGE_NUMBER, RANGE_DATE, FORMULA.
→ deckt die Spec-Custom-Field-Typen `text|number|date|select|multiselect|boolean` ab;
**fehlend** als eigene Typen: `url`, `email` (Spec Block F) — in der Boilerplate über TEXT +
`subtype`/`PropertyAttribute`-Validierung gelöst; HiveGrid muss `url`/`email` als
`field_type` neu vorsehen.
---
## Lücken / komplett neu zu bauen
**Daten-Layer (alles neu, nach `prisma/schema/app.prisma`):**
- `Contact` (alle Felder: salutation, title, first/last_name, address_*, birthdate, notes,
language, assigned_to_user_id, lead_source-Enum, is_endkunde, archived_at) — **fehlt**
- `Company` (legal_form, vat_id, tax_number, industry, website, address_*, …) + optionale
Mutter-Tochter-Hierarchie (M043) — **fehlt**
- `ContactCompanyRelation` (M:N, role, is_primary, since/until_date) — **fehlt**
- `ContactEmail` / `ContactPhone` (value, label-Enum, is_primary) — **fehlt**
- **Tenant-scoped `Lead`** (mit Status `new|qualified|converted|lost|spam`,
converted_to_contact_id, converted_to_deal_id) — **fehlt** (Marketing-`Lead` ist anders)
- `Activity` (zentrale Event-Sourcing-Tabelle: actor_kind, activity_type-Enum ~25 Werte,
subject_type/subject_id, related_entity_refs[], metadata, summary_text) — **fehlt**
(kein generisches subject-polymorphes Activity-Modell in der Boilerplate; `Log` ist row-gebunden)
- `CustomFieldDefinition` + `CustomFieldValue` (applicable_to contact/company/lead,
field_type inkl. url/email, options[], JSONB-Wert) — **fehlt** als dedizierte Tabellen
- `Tag` (8 Farben-Constraint, applicable_to[]) + `EntityTag`-Äquivalent für CRM-Entities — **fehlt**
(No-Code-`EntityTag`/`RowTag` ist row-gebunden, nicht übernehmbar)
- Spezifische Indizes: `(tenant_id, archived_at, last_activity_at)`,
`pg_trgm`-Trigram-Index auf Namen, FTS-Index — **alle neu** (Spec Block C/G)
- Branchen-Template-Seeding (SHK-Custom-Fields + Tag-Defaults) als Konfig-Datei (AV #3, A.3) — **neu**
**Domänen-Logik / Services (neu):**
- Lead-Promotions-Transaktion mit Auto-Email-Match (4 Optionen a–d) — **neu**
- Duplikat-Erkennung (Hard: Email/Phone; Soft: Name+Firma / Name+PLZ) + Inline-Live-Check +
Bulk-Merge-Tool mit Field-by-Field-Auswahl + Verknüpfungs-Umhängen — **neu**
(Dedupe-Primitiven aus `packages/imports/src/dedupe.ts` als Basis, aber CRM-Merge-Logik neu)
- `logActivity()`-Service (`@hivegrid/activity`, INTEGRATION-CONTRACTS §5) + Activity-Stream-VM
(Tag-Bucket-Gruppierung, Filter, Permission-aware, Firma=Aggregat der Kontakt-Activities) — **neu**
- Tag-Merge / Auto-Cleanup, Custom-Field-Verwaltung (Drag&Drop-Sortierung, Archive vs. Hard-Delete) — **neu**
**Querschnitt-Verdrahtung (HiveGrid-Konventionen — Ziel-Pfade existieren in Boilerplate noch nicht):**
- `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` — **fehlt** (kein `packages/ui-hivegrid`
in der Boilerplate); Sidebar-Gruppen STAMMDATEN (Kontakte/Firmen/Tags/Custom-Felder) + ARBEITEN (Lead-Inbox)
- `packages/ui-hivegrid/src/adapters/` (Subframe-Adapter) + `breadcrumbs/` + `mobile/tab-bar.config.ts`
+ `customer-portal/portal-shell.config.ts` — **alle neu** (DoD §13/§14)
- `packages/db/src/search/indexed-entities.ts` — **fehlt** (Verzeichnis `packages/db/src/search`
existiert nicht); Contact/Company/Lead als suchbare Entitäten registrieren (M056)
- `packages/entitlements/src/permissions.ts` (`crm.*`-Keys) — **fehlt** als Datei
- `@hivegrid/*`-Service-Fassaden (`activity`, `audit`, `notifications`) — Boilerplate nutzt
`@toolctrl/*`; Umbenennung/Fassade ist HiveGrid-Aufgabe (kein PR-blockierender Unterschied)
**UI (neu, aber stark gestützt):**
- Subframe-Designs für jede Route (MEMORY: Subframe pflicht) — **neu**, Boilerplate-`ui-subframe`
ist leerer Platzhalter
- Inline-Edit-Felder, Markdown-Notiz-Editor, Tag-Editor (ad-hoc anlegbar), Promotions-Modal,
Side-by-Side-Merge-UI, Custom-Field-Renderer pro Typ — als Komposition aus vorhandenen
shadcn-Primitiven, aber CRM-spezifisch **neu**
---
## Empfohlenes Vorgehen (Reihenfolge)
**Phase 0 — Boilerplate-Bereinigung (vor M042):**
1. Boilerplate-CRM (Entity-Builder-basiert) zur Seite legen/entfernen, damit die andere
Architektur nicht in HiveGrid einsickert: `apps/web/app/modules/crm/`,
`apps/web/app/routes/app.$tenant/crm/`, `apps/web/app/routes/admin/crm/` (und der
No-Code-`crm`-Seed in `ProductCoreCrmEntities.server.ts`). Den `crm`-i18n-Namespace und
die `crm-basic`-Feature-Gate-Infrastruktur behalten.
2. HiveGrid-Querschnitt-Pakete anlegen, die noch fehlen: `packages/ui-hivegrid/` (sidebar.config,
adapters, breadcrumbs, mobile, customer-portal), `packages/db/src/search/indexed-entities.ts`,
`packages/entitlements/src/permissions.ts`. (Diese sind modul-übergreifend — ggf. eigenes
Fundament-Milestone, aber M01 ist der erste Konsument.)
**Phase 1 — Daten-Fundament (M042 → M043):**
3. CRM-Modelle in `prisma/schema/app.prisma` neu: `Contact`, `ContactEmail`, `ContactPhone`,
dann `Company`, `ContactCompanyRelation` (M:N). Forward-Only-Migration mit Inverse-SQL-Kommentar
(DoD). `pg_trgm`-Extension + Trigram-/FTS-Indizes + `(tenant_id, archived_at, last_activity_at)`-Index.
4. **Sofort übernehmen:** `withTenantScope()`+`withPermission()`+`withOwnerCheck()`
(`packages/foundation/src/permissions.ts`) für jeden Loader/Action; `auditEvent`/Middleware
für Schreib-Aktionen; neue User-FKs in `packages/db/src/dsgvo/user-fk-map.ts` (sonst CI-Bruch);
VM-Builder gegen `VMState`/`ActionCallbacks` (`packages/view-models`).
5. UI per Subframe → `ui-hivegrid`-Adapter, gestützt auf shadcn-Primitiven (`apps/web/app/components/ui/**`).
Sidebar-Einträge STAMMDATEN registrieren (INTEGRATION-CONTRACTS §1/§2).
**Phase 2 — Querschnitt-Features (M044 → M045):**
6. Tags (`Tag`+CRM-Tag-Join, 8-Farben-Constraint) und Custom-Felder
(`CustomFieldDefinition`+`CustomFieldValue`) neu — `PropertyType`-Enum + Entity-Builder-Custom-Field-UI
(`apps/web/app/components/entities/properties/*`) als **Vorlage**, `url`/`email`-Typen ergänzen.
SHK-Defaults als Seed-Konfig (A.3).
7. `Activity`-Tabelle + `logActivity()` neu (zentrales Event-Sourcing, INTEGRATION-CONTRACTS §5);
`Log`-Modell + `hasActivity`-Konzept der Engine als **Vorlage**. Activity-Stream-VM mit
Tag-Buckets/Filtern/Permission-Gate; Firma=Aggregat.
**Phase 3 — Listen, Suche, DSGVO (M046):**
8. Listen-VM mit server-side Filter (Permission-aware-Default, AV #5), Cursor-Pagination
(`RowsLoadMoreCard`/`GridPagination` als Vorlage), Spalten-Selektor (`ColumnSelector`),
Bulk-Actions. Such-Anbindung via `@toolctrl/search` (`searchWithFallback`, SQL-Fallback) auf
die neuen Tabellen; Registrierung in `indexed-entities.ts`.
9. Duplikat-Erkennung: `packages/imports/src/dedupe.ts`-Primitiven als Basis; Inline-Live-Check +
Bulk-Merge-Tool (`/admin/duplicates/`) neu.
10. DSGVO: `packages/exports` + `apps/worker/src/jobs/dsgvo-export.ts` an Contact-Schema anpassen
(Per-Contact-JSON-Export, Audit-Event); CSV-Export der gefilterten Listen-Auswahl.
**Durchgängig:** i18n-Keys (Namespace `crm`), Feature-Flag-Gate (M006), Smoke-Tests +
Hot-Swap-Drill (`ui-mock`) + Visual-Regression (DoD D.18–21), Klartext-States (DoD E.22–25).
---
## Anti-Erfindungs-Vorbehalte
- Es gibt **keine** dedizierten CRM-Prisma-Modelle in der Boilerplate (verifiziert per Grep).
- Das Boilerplate-CRM ist **Entity-Builder-basiert** (`Entity`+`Row`+`RowValue`), nicht
tabellen-typisiert — verifiziert in `CrmService.ts` und `ProductCoreCrmEntities.server.ts`.
- `packages/ui-hivegrid`, `packages/db/src/search`, `packages/entitlements/src/permissions.ts`
existieren in der Boilerplate **nicht** (verifiziert per `ls`/`find`).
- `@toolctrl/ui-subframe` ist **leer** (Platzhalter) — kein gebauter Subframe-Adapter-Layer.
- Der Boilerplate-`Lead` ist operator-/marketing-scoped (`@@unique([email])`, kein `tenantId`,
Status `new|contacted|qualified|trial|customer|churned|disqualified`) — **nicht** der
HiveGrid-Tenant-CRM-Lead.
- `url`/`email` sind keine eigenen `PropertyType`-Werte (nur via TEXT+subtype gelöst).
@@ -0,0 +1,112 @@
# Boilerplate-Abgleich: M02 — Lead-Inbox / Lead-Management
| Feld | Wert |
| --- | --- |
| Modul-ID | M02 (Modul 2) |
| Name | Lead-Inbox / Lead-Management |
| Milestones | M047 (Inbox + Quellen-Adapter), M048 (Routing pro Quelle), M049 (Auto-Verkaufsfall), M050 (Spam/Duplikat + DSGVO-Auto-Lösch), M051 (Status-Pipeline + Listen/Detail-UI + Statistik + Permissions) |
| Pflichtenheft-Block | Block 2, Modul 2 (Abschnitte 2.1–2.13) |
| **Reuse-Level** | **medium** |
## Kurz-Verdikt
Die Boilerplate enthält ein **vollständiges Marketing-/Funnel-Lead-System** (`Lead`/`LeadEvent`/`TenantAcquisition`/`MarketingIntegration` + Admin-Inbox-UI + Capture/Convert-Services + Outbound-Adapter). Das ist als **Vorlage / Muster** hochwertig, aber das Daten-Modell ist **bewusst operator-/marketing-scoped (NICHT tenant-scoped)** und mit `@@unique([email])` global eindeutig — das ist **inkompatibel** mit dem tenant-isolierten CRM-Lead-Inbox aus M02. Der HiveGrid-Lead muss daher ein **eigenes, tenant-scoped Modell** sein (gehört fachlich ohnehin zu Modul 1/CRM-Kern, das das `Lead`-Datenmodell besitzt). Solider, breiter Querschnitt (Form-Widget inkl. Honeypot, Outbox, Audit, Rate-Limiter, Export, Realtime, Apps-Engine-Schema) ist 1:1 nutzbar. Die eigentliche Inbox-Domänen-Logik (tenant-scoped Listen/Filter/Triage/Routing/Statistik) ist neu zu bauen, aber mit starken Vorlagen.
> Hinweis: `HiveGrid-Cloud` ist aktuell ein reines Spec-/Docs-Repo (`_bmad`, `_bmad-output`, `docs`, `prisma` leer). Es existiert noch **kein** `packages/ui-hivegrid`, kein `apps/*`, kein HiveGrid-Lead-Modell. Alle Boilerplate-Pfade unten beziehen sich auf `/Users/raffaelsheikh/Documents/GitHub/boilerplate`.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `prisma/schema/marketingLeads.prisma` (`Lead`, `LeadEvent`) | Lead-Datenmodell + Event-Timeline-Pattern (Status-Enum, Attribution, `lastIpHash` SHA-256, Indizes auf `(status, createdAt desc)` / `(source, createdAt desc)`) | **als-vorlage** — Felder/Indizes-Muster übernehmen, aber tenant-scoped neu modellieren (siehe Lücken) |
| `apps/web/app/utils/leads/leads.db.server.ts` | DB-Helper für Inbox: `listLeads` (Filter status/source/search + Pagination), `listLeadsForExport` (Cap 50k), `getLead`, `updateLeadStatus`, `appendNote`, CSV-Export-Row-Typ | **als-vorlage** — Loader/Action-Plumbing-Muster ist direkt adaptierbar; Queries um `tenantId`-Scope + Permission-Filter (read_own/read_all) erweitern |
| `apps/web/app/routes/admin/marketing-leads/index.tsx` | Inbox-Listen-Route: Status-Badges als Filter-Shortcuts, Such-Box, Pagination, CSV-Export mit Audit-Event, `groupBy`-Status-Counts | **als-vorlage** — Strukturmuster für Loader→VM→UI; HiveGrid braucht Tenant-Route (`/{tenant}/leads`), Multi-Select-Filter, Bulk-Actions, View-Model-Adapter, i18n-Keys, Subframe-UI |
| `apps/web/app/routes/admin/marketing-leads/$id.tsx` | Lead-Detail: Status-State-Machine-Buttons (forward/back), Inline-Notes-Editor, LeadEvent-Activity-Timeline, Konversions-Flow | **als-vorlage** — Triage-Detail-Muster; HiveGrid-Übergänge (new/qualified/converted/lost/spam) + Promotion-Modal (Modul 1) abweichend |
| `apps/web/app/utils/leads/captureLead.server.ts` | Idempotenter Lead-Upsert: Trim/Blank-Fill-Semantik, `hashIp()` (SHA-256, kein Raw-IP), First-Touch-Merge, Event-Append | **als-vorlage** — Auto-Anlage-Pfad (Form/Mail/Phone/Manuell). `hashIp` 1:1; Upsert-Logik tenant-scoped + Kontakt-Match-Probe ergänzen |
| `apps/web/app/utils/leads/convertLead.server.ts` | Atomare Konversion in `$transaction` (5 Effekte: Update + Event + Acquisition + Outbox), Idempotenz via `dispatchKey`, Up-front-Check | **als-vorlage** — Promotion Lead→Kontakt (Modul 1) folgt demselben Transaktions-/Idempotenz-Muster; Ziel ist Contact statt Tenant |
| `packages/form-widget/src/honeypot.ts` (`isHoneypotTriggered`, `HONEYPOT_FIELD_KEY`) | Honeypot-Spam-Erkennung (stilles 204) | **1:1** — direkt für Anti-Spam-Heuristik „Honeypot gefüllt" (Spec 2.8.4) |
| `packages/form-widget/src/schema.ts` / `origin.ts` / `routing.ts` | Form-Definition-Zod-Schema, Origin-Allowlist, Submission-Routing (`generic`/`crm-contact`/`entity-row`) + Field-Mapping | **anpassen** — Quelle Form→Lead (2.2.1); Routing-Target um „Lead-Inbox" + Field-Mapping pro Routing-Regel (M048/M049) erweitern |
| `prisma/schema/formWidget.prisma` (`Form`, `FormSubmission`) | Eingebettetes Formular + Submission-Inbox (`payload` JSON, `ipHash`, `status` received/processed/failed/spam, `config` Anti-Spam) | **anpassen** — Form-Submission ist Hauptquelle für Leads; `status=spam` koppelt an Spam-Heuristik. Form-Builder selbst = Modul 5 (M121-M125) |
| `packages/shared-store/src/helpers/rate-limit-fixed.ts` + `rate-limit-sliding.ts` | `createFixedWindowRateLimiter` / `createSlidingWindowRateLimiter` (Redis/Memory) | **1:1** — Hard-Limit pro Quelle/h (Default 100) und pro IP/h (Default 20) aus Spec 2.8.1/2.8.2 |
| `apps/worker/src/jobs/audit-retention.ts` | Täglicher Cron mit injizierten Seams (`tenantLister`/`eventDeleter`/`auditWriter`), Retention-Schwelle, meta-auditiertes Löschen | **als-vorlage** — DSGVO-Auto-Lösch-Job (Spam→30 Tage, Lost→90 Tage konfigurierbar, Spec 2.7); selbes Cron-+Seam-+Meta-Audit-Muster |
| `apps/worker/src/jobs/dsgvo-grace-cron.ts` + `outbox-done-retention-cron.ts` | Weitere Retention-/Grace-Cron-Muster | **als-vorlage** — Referenz für Auto-Lösch-Implementierung + Anonymisierungs-Trail (Spec 2.7.4) |
| `apps/web/app/utils/leads/leadsCsv.ts` (`serializeLeadsCsv`, `leadsCsvFilename`) + `packages/exports` | CSV-Serialisierung + Streaming-Export-Framework (CSV/JSON/XLSX/PDF, Plan-Row-Caps, Spreadsheet-Injection-Schutz) | **1:1 / anpassen** — Export gefilterter Auswahl + Statistik-Export (Spec 2.3.5/2.9.6, Permission `leads.admin`) |
| `packages/marketing-outbound` (`adapterRegistry`, `resolveAdapter`, slack/discord/mailchimp/hubspot/brevo/webhook) | Outbound-Adapter-Registry, dispatched via Outbox-Events | **als-vorlage** — Muster für Apps-Engine-Quellen-Adapter (2.2.5) und Routing-Notifications; HiveGrid braucht **inbound** Lead-Adapter (Meta/Google/Funnel), nicht outbound |
| `packages/realtime/src/{client,events,server,guards}.ts` | Realtime-Pub/Sub (WebSocket) | **1:1** — Live-Aktualisierung neuer Leads in der Inbox (Spec 2.3.7 / M047 „Real-Time-Updates via packages/realtime") |
| `packages/db/src/outbox.ts` (`publishOutboxEvent`, `OUTBOX_EVENT_TYPE_PATTERN`) | Transaktionales Outbox-Pattern (`<domain>.<entity>.<verb>`, `dispatchKey @unique`) | **1:1** — `lead.created`-Event als Automation-Trigger (Spec Block I / Modul 17), Routing-Aktionen, Side-Effects |
| `packages/db/src/audit-hash.ts` + `prisma/schema/core.prisma` (`AuditEvent`) | Tamper-evidente Audit-Hash-Kette, `dataSensitivity`/`regulatoryScope[]` | **1:1** — Audit-Trail für Status-/Verantwortlichen-Wechsel, Bulk-Action, Hard-Delete (AV #6, Spec 2.11.5) |
| `prisma/schema/appsEngine.prisma` (`App`, `DataConnection`, `AppSyncRun`, `AppSyncItem`) | Integrations-/Sync-Engine mit verschlüsselten Credentials + rollback-fähigen Sync-Runs | **als-vorlage** — Quellen-Adapter aus externen Marketing-Tools (Spec 2.2.5, M047 „Quellen-Adapter via Apps-Engine M008"). Modelle vorhanden, aber kein Service-/UI-Layer in `apps/web/app/modules` gefunden |
| `prisma/schema/permissions.prisma` (`Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser`) + `apps/web/app/utils/helpers/.server/PermissionsService` (`verifyUserHasPermission`) | RBAC-Fundament + Permission-Check-Helper | **1:1 / anpassen** — Permission-Bundle `leads.*` (read_own/read_all/write/delete/admin) als Daten seeden; `verifyUserHasPermission`-Muster für Route-Gates. Owner-Check-Filter (read_own) selbst zu bauen |
| `prisma/schema/notifications.prisma` + `packages/db` notifications-Modul | `NotificationDelivery`/`UserNotificationPreference`/`DeviceToken` (In-App/Email/Push) | **1:1** — Tenant-Admin-Alerts bei Anomalie/Auto-Pause/Hard-Limit (Spec 2.8.3, Block K) via `notify()` (M011) |
| `packages/db/src/dsgvo/{user-fk-map.ts, pseudonymize.ts}` | DSGVO-FK-Karte (CI-erzwungen) + Pseudonymisierungs-Helfer | **1:1** — Pflicht-Eintrag für jeden neuen User-FK am HiveGrid-Lead; Anonymisierung vor Hard-Delete (Spec 2.7.4) |
| `prisma/schema/imports.prisma` + `packages/imports` | Import-Job + Dry-Run/Dedupe-Framework | **als-vorlage** — nur für Phase 2 (CSV/JSON-Lead-Import, Spec 2.12.1) — in Phase 1 explizit Out-of-Scope |
---
## Vorhandene Daten-Modelle
Konkret in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`:
- **`marketingLeads.prisma`** — `Lead`, `LeadEvent`, `TenantAcquisition`, `MarketingIntegration`, Enum `LeadStatus`.
- `Lead`: `email @@unique` (GLOBAL, nicht pro Tenant), **kein `tenantId`** (operator-scoped per Design-Kommentar), `status LeadStatus @default(new)`, First-Touch-UTM-Attribution, `score`, `notes Json?`, `convertedToTenantId?`, `lastIpHash` (SHA-256), Indizes `@@index([status, createdAt(sort:Desc)])`, `@@index([source, createdAt(sort:Desc)])`.
- Enum `LeadStatus`: `new | contacted | qualified | trial | customer | churned | disqualified` — **funnel-/SaaS-Akquise-Semantik**, NICHT die HiveGrid-CRM-Semantik (`new | qualified | converted | lost | spam`).
- `LeadEvent`: append-only Timeline (`type` String, `actorId?`, `payload Json`).
- **`formWidget.prisma`** — `Form`, `FormSubmission` (Quelle „Formular" für Leads; `ipHash`, `status` received/processed/failed/spam, Anti-Spam-`config`).
- **`appsEngine.prisma`** — `App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem` (externe Quellen-Adapter, Spec 2.2.5).
- **`permissions.prisma`** — `Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser` (Bundle `leads.*` + Group für Round-Robin).
- **`notifications.prisma`** — `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` (Admin-Alerts).
- **`core.prisma`** — `AuditEvent` (Audit-Trail), `IdempotencyKey`.
- **`outbox.prisma`** — `OutboxEvent` (`lead.created`-Trigger, Side-Effects).
- **`accounts.prisma`** — `Tenant`, `User`, `TenantUser` (Tenant-Scope-Anker + Verantwortlicher-FK).
**Nicht vorhanden (kritisch):** Es gibt **kein tenant-scoped CRM-`Contact`/`Company`-Modell** in der Boilerplate. Das `crm`-Modul (`apps/web/app/modules/crm/`) enthält nur `services` (z.B. `CrmService.ts`, `crmContactSearch.server.ts`) — eine Integrations-Schicht zu EXTERNEN CRMs (ConvertKit etc.), kein eigener Contact-Datastore. Das Form-Widget-Routing-Target `crm-contact` upsertet entsprechend über diese externe Service-Schicht. Promotion Lead→Kontakt (Spec Block D/2.5) zielt auf das HiveGrid-eigene Contact-Modell aus **Modul 1 (M042-M046)** — das ebenfalls neu gebaut wird.
---
## Lücken / komplett neu zu bauen
1. **Tenant-scoped HiveGrid-Lead-Modell** — Der Boilerplate-`Lead` ist operator-scoped mit globalem `@@unique([email])`. HiveGrid braucht ein neues Modell (`tenantId` pflicht, FK auf `Tenant` mit `onDelete: Cascade`, `assigned_to_user_id`, Status-Enum `new/qualified/converted/lost/spam`, `source`, `message`, `metadata` JSON für Quellen-Detail). Geplanter Ort: `prisma/schema/app.prisma` (HiveGrid-Platzhalter). FK→User in `user-fk-map.ts` eintragen (CI-Pflicht). Indizes laut Spec: `(tenant_id, status, created_at desc)`, `(tenant_id, assigned_to_user_id, status)`, Trigram auf `email`/`phone`/`last_name`.
2. **Lead-Status-State-Machine (HiveGrid-Semantik)** — Übergangs-Matrix aus Spec Block F (inkl. `converted`-Lock, Re-Engagement `lost↔qualified`, False-Positive `spam→qualified`, Begründungs-Pflicht bei `lost`). Boilerplate-State-Machine (`new→…→customer/churned`) ist semantisch anders → neu.
3. **Permissions-aware Owner-Check-Filter (`read_own` vs `read_all`)** — Server-side-Filter, der nicht-eigene Leads in Listen/Statistik/Bulk ausblendet (AV #5). RBAC-Tabellen + `verifyUserHasPermission` vorhanden, aber der scope-bewusste Query-Filter + Bundle `leads.*`-Seed sind neu.
4. **Multi-Select-Filter + Bulk-Actions + gemischte-Rechte-Pre-Check** — Boilerplate-Inbox hat nur Single-Status-Filter + Such-Box, keine Bulk-Auswahl. Neu: Multi-Select (Status/Quelle/Verantwortlicher inkl. „Nicht zugewiesen"/Tags/Custom-Felder), Date-Range, Toggles „Spam ausblenden"/„Konvertierte einblenden", Bulk-Assign/Status/Tag/Spam/Hard-Delete/Export, Pre-Check mit Klartext-Übersprungen (Spec 2.10.1).
5. **Lead-Routing pro Quelle (M048)** — Routing-Regel-DSL mit Bedingungs-Kombinatoren, Reihenfolge-Auswertung (erste Treffer-Regel), Round-Robin-State pro Gruppe, Status/Verantwortlicher/Tags-Default. Komplett neu (Apps-Engine + Outbox als Fundament).
6. **Auto-Verkaufsfall-Anlage (M049)** — Routing-Action, die aus Lead automatisch Verkaufsfall in Ziel-Pipeline anlegt mit Field-Mapping. Abhängig von Sales-Pipeline (M059/M060) — neu.
7. **Anomalie-Detection + Auto-Pause pro Quelle (M050)** — 10x-Rate-Spike-Erkennung → Quelle pausieren + Notification. Rate-Limiter (fixed/sliding) als Baustein vorhanden, aber Anomalie-Heuristik + Quellen-Pause-State + Klartext-Notification neu (M010 Apps-Engine-Webhook-Schutz als Re-Use-Referenz).
8. **DSGVO-Auto-Lösch-Job mit Lead-Semantik** — Cron-Muster (`audit-retention.ts`) als Vorlage, aber Lead-spezifisch: Spam→30 Tage, Lost→90 Tage (tenant-konfigurierbar, 0=nie), anonymisierter Activity-Trail + Audit-Eintrag, Klartext-Hinweis „wird gelöscht am [Datum]".
9. **Lead-Statistik-Seite (M051 / Spec Block H)** — KPI-Kacheln (Konversionsraten, Reaktionszeit, Spam-Quote, offene Leads), Aufschlüsselungen (Quelle/Verantwortlicher/Tag/Wochentag), Trend-Charts, Vorperioden-Vergleich, Drill-Down. Aggregat via Materialized-View/Cron-Tabelle. Komplett neu — Boilerplate hat kein Lead-Statistik-Dashboard.
10. **Quellen-Verwaltung-UI** (`/admin/lead-sources`) + **Anti-Spam-Settings-UI** (`/admin/lead-anti-spam`) — Custom-Quellen (Name/Default-Verantwortlicher/Default-Tags), Hard-Limits-Toggles, Heuristik-Schalter. Neu.
11. **Kontakt-Match-Probe + Duplikat-/Soft-Match-Marker** — Inline-Match-Check beim Tippen (Spec 2.2.4), Marker „Mögliches Duplikat"/„Spam-Verdacht"/„Auto-promoviert" (2.3.2). Abhängig vom HiveGrid-Contact-Modell (Modul 1) — neu.
12. **App-Shell-Anbindung (HiveGrid)** — Sidebar-Eintrag „Lead-Inbox" in Gruppe ARBEITEN (INTEGRATION-CONTRACTS §1), Tenant-Routen `/{tenant}/leads*`, Breadcrumb, Mobile-Tab-Bar, View-Model-Konvention (M013), Subframe-Adapter (M015), i18n-Keys (M003). `packages/ui-hivegrid` existiert noch nicht; Boilerplate `packages/ui`/`ui-app`/`ui-subframe` sind Fundament/Vorlage.
13. **Gespeicherte Ansichten** (max. 5 Filter-Kombinationen pro User, Spec 2.3.8) + user-spezifische Spalten-Konfiguration — neu (Entity-Builder `EntityView` als konzeptionelle Vorlage, aber Lead ist eigenes Modell).
14. **Inbound-Quellen-Adapter (Meta/Google/Funnel)** — `packages/marketing-outbound` ist OUTBOUND. Für 2.2.5 braucht es INBOUND-Adapter über Apps-Engine — Registry-Muster übertragbar, Implementierungen neu.
---
## Empfohlenes Vorgehen
**Phase 0 — Fundament 1:1 übernehmen (kein neuer Code, nur verdrahten):**
1. Querschnitt aktivieren: `packages/db/src/outbox.ts`, `audit-hash.ts` + `AuditEvent`, `notifications`, `permissions` + `verifyUserHasPermission`, `realtime`, `shared-store` Rate-Limiter, `exports`/CSV. Diese stehen für alle 5 Milestones bereit.
2. `hashIp()` aus `captureLead.server.ts` und `isHoneypotTriggered` aus `form-widget` direkt einziehen.
**Phase 1 — Eigenes tenant-scoped Lead-Modell (M047):**
3. HiveGrid-`Lead`-Modell in `prisma/schema/app.prisma` neu definieren (NICHT den marketing-`Lead` wiederverwenden) — `marketingLeads.prisma` als Feld-/Index-Vorlage nehmen, tenant-scopen, Status-Enum auf HiveGrid-Semantik. `user-fk-map.ts` ergänzen.
4. DB-Helper nach Muster `leads.db.server.ts` bauen, aber mit `withTenantScope()` + Owner-Check (M004).
5. Quellen-Auto-Anlage (Form/Mail/Phone/Manuell) nach Muster `captureLead.server.ts` + Apps-Engine-Schema; `lead.created`-Outbox-Event publizieren; Realtime-Push.
**Phase 2 — Listen/Detail/Triage-UI + Permissions (M051-Teil):**
6. Inbox-Route nach Muster `admin/marketing-leads/index.tsx`, aber Tenant-Route + Multi-Select-Filter + Bulk-Actions + View-Model-Adapter + Subframe + i18n.
7. Detail-/Triage-Route nach Muster `$id.tsx`; HiveGrid-State-Machine + Promotion-Modal (Modul 1) + Activity-Stream (M045).
8. Permission-Bundle `leads.*` seeden + scope-bewusste Filter.
**Phase 3 — Routing + Auto-Verkaufsfall (M048, M049):**
9. Routing-Regel-DSL + Round-Robin (neu, Outbox/Apps-Engine als Basis); Auto-Verkaufsfall als Routing-Action (nach Sales-Pipeline M059/M060).
**Phase 4 — Spam/DSGVO (M050):**
10. Rate-Limiter (fixed/sliding) für Hard-Limits; Anomalie-Detector (neu, M010-Webhook-Schutz als Re-Use); Auto-Lösch-Cron nach Muster `audit-retention.ts`/`dsgvo-grace-cron.ts` mit Lead-Semantik + Anonymisierung.
**Phase 5 — Statistik + Admin-UIs (M051-Rest):**
11. Statistik-Page (komplett neu) + Quellen-Verwaltung + Anti-Spam-Settings-UI.
**Faustregel:** Querschnitt (Outbox/Audit/Notif/Permissions/Realtime/Rate-Limit/Export/Honeypot) = 1:1. Daten-Modell + Status-Logik + Listen/Statistik/Routing/Spam-Domäne = neu, aber mit den genannten Boilerplate-Dateien als konkrete Code-Vorlage. **Nicht** den marketing-`Lead` mitbenutzen — Scope-Inkompatibilität.
@@ -0,0 +1,219 @@
# Boilerplate-Abgleich: M03 — Sales-Pipeline (Verkaufsfälle / Deals)
> Abgleich-Report: Was liefert die Boilerplate (`/Users/raffaelsheikh/Documents/GitHub/boilerplate`)
> für M03 fertig, was muss angepasst werden, was fehlt komplett.
> Quellen: Detail-Spec `_bmad-output/specs/M03-sales-pipeline.md`, Pflichtenheft `block3.md` (3.1–3.13),
> Milestones M059–M064, `AGENTS.md`, `INTEGRATION-CONTRACTS.md`, Boilerplate-Inventur.
> Eigene Greps/Reads in der Boilerplate verifiziert (keine Inventur-Blind-Übernahme).
| Feld | Wert |
| --- | --- |
| Modul-ID | M03 |
| Name | Sales-Pipeline (Verkaufsfälle / Deals) |
| Milestones | M059 (Pipeline-Konfig + SHK-Seed), M060 (Verkaufsfall-CRUD + Pflicht-Vertriebler), M061 (Kanban-UI + Drag&Drop), M062 (Verlust/Gewinn-Gründe), M063 (Auto-Auftragsprojekt + Setter-Closer), M064 (Reporting + Stuck/Auto-Archiv + KWP-Sync) |
| Block | Block 3 — Workflow-Kern (Vertrieb & Auftrag) |
| Reuse-Level | **low** (solides Querschnitt-Fundament + Bau-Vorlagen, aber **kein** Sales-Domänen-Modell vorhanden — Datenkern komplett neu) |
---
## Kernbefund (Realität)
- **Es existiert KEIN Sales-/Deal-/Pipeline-Datenmodell in der Boilerplate.** Grep über `prisma/schema/*.prisma`
nach `Deal|Pipeline|Stage|Forecast|Kanban|SalesPipeline` liefert **null Treffer** bei Modellen. Treffer für
`Lead`/`LeadEvent` betreffen das **marketing-scoped** `Lead`-Modell (operator-scoped, NICHT tenant-scoped,
`email @unique`) — das ist die M01/M02-Funnel-Welt, nicht der tenant-scoped Verkaufsfall aus M03.
- **Es existiert KEIN zentrales `Activity`-Modell** wie es M03 (`DealActivity` → zentrale `Activity` aus M1)
und `INTEGRATION-CONTRACTS §5` (`logActivity`) verlangen. Vorhanden sind nur `LeadEvent` (Lead-Timeline,
`marketingLeads.prisma`) und `RowComment`/`Entity.hasActivity`-Flag im EntityBuilder — kein generischer,
cross-entity Activity-Stream. → M045 (Activity-Service) ist Vorbedingung und muss separat existieren.
- **HiveGrid-Cloud-Repo enthält derzeit noch KEINEN Boilerplate-Code** (`packages/`, `apps/`, `prisma/schema/`
sind leer/abwesend; nur `docs/`, `_bmad*`, leeres `prisma/`). Der Abgleich erfolgt also gegen das separate
Boilerplate-Repo; die Übernahme = Code aus Boilerplate als Fundament + neue Sales-Schicht darauf.
- **Das stärkste vorhandene Sales-nahe Asset ist der EntityBuilder** (`board`-View = Kanban, `groupBy`, Row/Property/
Tag/Task/Comment/RowPermission, Custom-Felder). Er ist als **Vorlage/Pattern** sehr wertvoll, aber für ein
Kern-Vertriebsmodul mit Pflicht-FKs (Kontakt/Firma/Lead/Projekt), Stage-Probabilities, Won/Lost-Reverse-Window,
Forecast und Field-Level-Volumen-Permissions **nicht 1:1 als Datenmodell tauglich** (LP-4: Commodity sauber bauen,
kein Über-Engineering über generischen No-Code-Layer).
---
## Übernehmbar aus Boilerplate
### Querschnitt / Foundation (1:1 nutzbar)
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `packages/foundation/src/permissions.ts` | `withTenantScope()`, `withPermission()`, `withOwnerCheck()`, `requirePermission()`; 404-statt-403 bei Deny | 1:1 — deckt Block I (read_own/read_all, 404 für nicht-eigenen Deal, Stage-Change-Gate) |
| `packages/foundation/src/audit.ts` | `auditEvent()`, `withAuditMiddleware()` (`AuditEvent`-Hash-Kette) | 1:1 — AV #6 Audit-Default für Stage-Wechsel u. a. (S/W außerhalb Prisma manuell) |
| `packages/foundation/src/notifications.ts` | `notify()`, `bundleNotifications()`, Preference-Check, i18n-Key-Zwang | 1:1 — `deal.won`/`deal.stuck`-Notifications (INTEGRATION-CONTRACTS §7) |
| `packages/foundation/src/errors.ts` | `CatalogError`, `foundationErrors`, `toPinoLogObject` | 1:1 — Klartext-Diagnostik (3.13, LP-10): Stage-Validation, Stuck, Won/Lost-Confirm |
| `packages/foundation/src/idempotency.ts` | `withIdempotency()` | 1:1 — idempotente Deal-Anlage aus Lead/Form/Email/Anruf/Automation (3.7) |
| `packages/foundation/src/flags.ts` + `packages/feature-flags/*` | Feature-Flag-Engine (defineFlag/evaluateFlag, React-Hooks) | 1:1 — DoD „Modul ohne Deploy abschaltbar"; Sidebar-`feature_flag` |
| `packages/view-models/src/index.ts` | `VMState`, `ActionCallbacks`, `RegistryGate`, `SidebarRegistryItem`, `assertI18nLabel` | 1:1 — VM-Layer-Vertrag (AGENTS A.1–A.5, INTEGRATION §11/§13) |
| `packages/i18n/src/index.ts` | Locales (de/en), `formatCurrency`/`formatNumber`/`formatDate`, ICU, `useTranslate` | 1:1 — Volumen-/Datum-Formatierung, i18n-Keys `sales.*` |
| `packages/logger`, `packages/observability`, `packages/error-reporter`, `packages/metrics` | Pino-Redaction, PII-Span-Scrub, Sentry, prom-Counter | 1:1 — Logging/Tracing/Error-Querschnitt |
| `packages/url/src/canonical.ts` | `appPath`/`appUrl` host-aware | 1:1 — Deal-Routes/Links (`/deals/...`) |
| `packages/db/src/client.server.ts` | `getReadDbClient()` (Replica) / `getWriteDbClient()` / `getExtendedDbClient()` | 1:1 — Read-heavy Kanban/Listen via Read-Replica (Performance-Anforderung Block D) |
| `packages/db/src/transactions.server.ts` + `packages/db/src/outbox.ts` | `runDbTransaction()`, `publishOutboxEvent()` (atomic Side-Effects) | 1:1 — Stage-Wechsel + Activity + Notification + Automation-Trigger transaktional (`deal.stage_changed`, `deal.won`) |
| `packages/db/src/dsgvo/user-fk-map.ts` | Pflicht-Karte aller User-FKs (CI-Gate) | anpassen — neue Deal-FKs auf `User` (`created_by`, `assigned_to`) MÜSSEN ergänzt werden, sonst bricht CI |
### Apps / Infra (1:1 / als-Vorlage)
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `apps/worker/src/jobs/*` + `apps/worker/src/group-runtime.ts` | BullMQ-Cron-Pattern (single-instance, Gruppen, DI `HandlerDependencies`) | als-vorlage — Daily-Cron für Stuck-Detection (3.8.1/3.8.2), Auto-Archiv Won 30d / Lost 90d (3.8.3/3.8.4, M064) |
| `apps/worker/src/outbox/{dispatcher,event-router}.ts` | Outbox-Dispatcher (FOR UPDATE SKIP LOCKED) + Prefix-Routing | anpassen — neues Prefix `deal.` / `sales.` in `DEFAULT_OUTBOX_ROUTING` für Automation-Trigger (3.6/H/M075) |
| `packages/queue/src/*` | Queue-Namen, typisierte Producer, `buildIdempotentJobId`, Plan-Prioritäten | 1:1 — Cron-/Job-Infrastruktur für Stuck/Archiv/Forecast-Cache |
| `packages/realtime/src/{events,server,client}.ts` | SSE-Serialisierung + Client/Server-Helfer | anpassen — Live-Kanban-Updates (3.4.11, M061). **Achtung:** Boilerplate hat **nur SSE, kein WebSocket** — der „WebSocket vs SSE"-Offene-Punkt der Spec ist faktisch auf **SSE** zu entscheiden; `REALTIME_EVENT_NAMES` um Deal-Events erweitern |
| `packages/exports/src/csv/*` + `packages/exports/src/limits.ts` + Worker `export.ts` | CSV-Export (Streaming, Plan-Row-Caps, Injection-Safety) | 1:1 — CSV-Export gefilterter Deals (3.4.10, M061) |
| `packages/search/src/schemas/` (`crm-contacts.ts`, `rows.ts` als Muster) + `outbox-publisher.ts` | Meilisearch tenant-scoped Index + Outbox-Reindex | als-vorlage — neues Index-Schema `deals` (Titel/Description/Kontakt-/Firmen-Name, 3.4.5); Registrierung in `indexed-entities.ts` (INTEGRATION §8) |
### UI / App-Shell (als-Vorlage / anpassen)
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `apps/web/app/components/layouts/sidebars/shadcn/*` + `apps/web/app/application/sidebar/{AppSidebar,SidebarItem}.tsx` | Datengetriebene Sidebar mit Permission-/Feature-Gating, `data-ui-id` | als-vorlage — Sidebar-Eintrag „Verkaufsfälle" (Gruppe ARBEITEN) + „Pipelines" (Gruppe EINSTELLUNGEN); HiveGrid nutzt `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` (neu) |
| `apps/web/app/components/ui/*` (shadcn/Radix: table, tabs, dialog, sheet, drawer, badge, card, command, multi-select, pagination, calendar) | Primitive-Bibliothek (~707 Komponenten) | 1:1 / als-vorlage — Deal-Detail-Tabs, Bulk-Select, Filter-Bar, Confirm-Dialoge (Won/Lost), Listen-Tabelle |
| `apps/web/app/components/ui/tables/*` (`TableNew`, `TablePagination`, `ColumnSelector`, `RowsLoadMoreCard`) | Tabellen-Bausteine: Cursor-Pagination, Spalten-Konfig, „Mehr laden" | 1:1 — Listen-View (3.4.8 Spalten-Konfig pro User, Cursor-Pagination, Default „zuletzt geändert" |
| `apps/web/package.json` → `@dnd-kit/core` 6.3.1, `@dnd-kit/sortable`, `@dnd-kit/utilities` | Drag&Drop bereits als Dependency vorhanden | 1:1 — Kanban-D&D (3.4.3, M061) **und** Pipeline-/Stage-Reorder (3.2.7, M059). Hinweis M061 nennt `react-beautiful-dnd` — faktisch ist **@dnd-kit** vorhanden, das nutzen |
| `apps/web/app/modules/formWidget/builder/{Canvas,SortableField,palette}.tsx` | Drag&Drop-Sortier-Editor (SortableField) als konkretes @dnd-kit-Beispiel | als-vorlage — Stage-Liste-Reorder im Pipeline-Editor (M059) |
| `packages/ui-mock/src/index.ts` (`MockPage`, `generateMockArtifacts`) + `apps/web/.../hot-swap/HotSwapPageRenderer.tsx` | Hot-Swap-Drill + Visual-Snapshot-Fundament | 1:1 — DoD Hot-Swap-Drill (M017) + Visual-Regression (M016) für Kanban/Detail |
| `apps/storybook/*` (lost-pixel, axe a11y, i18n-Decorator) | Visual-Regression + a11y-Gate | 1:1 — Snapshot der Hauptansicht (M016) |
### Domänen-nahe Bau-Vorlagen (als-Vorlage)
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `prisma/schema/entityBuilder.prisma` — `EntityView` (`layout="board"`, `groupByPropertyId`, `pageSize`, `EntityViewFilter`/`Sort`/`Property`) | Generische Kanban/Board-View-Konfiguration | als-vorlage — Pattern für Stage-Spalten + Filter/Sort/Spalten-Konfig (NICHT als Deal-Datenmodell übernehmen) |
| `prisma/schema/entityBuilder.prisma` — `Property` + `PropertyOption`/`PropertyAttribute` (typisierte Custom-Felder, `applicable_to` via `entityId`) | Custom-Felder mit Typen/Optionen | anpassen — Deal-Custom-Felder „Anlagen-Typ"/„Wärmebedarf kW"/„Bestandsanlage" (3.1.14, Block H). Entweder über EntityBuilder koppeln (`applicable_to=deal`) oder eigene schlanke Deal-Custom-Field-Tabelle |
| `prisma/schema/entityBuilder.prisma` — `RowTag`/`EntityTag`, `RowTask`, `RowComment` | Tags / Aufgaben-Link / Notizen an Rows | als-vorlage — Deal-Tags (3.1.13), Deal↔Task-Link (M26/3.9.1), Notizen |
| `packages/db/src/rows.server.ts` — `getNextRowMutationNumber()` / `Row.folio` (`aggregate _max.folio + 1`, tenant+entity-scoped) | Fortlaufende Nummer pro Tenant/Entity | als-vorlage — Sequenz-Mechanik für `HiveNumberSeq` / `deal_number`. **Lücke:** kein Prefix-/Format-Layer (`D-2026-0042`), keine konfigurierbare Reset-Strategie — neu zu bauen (M060) |
| `prisma/schema/marketingLeads.prisma` — `Lead` + `LeadEvent` (Timeline, `convertedToTenantId`) | Lead-Modell + Event-Timeline (operator-scoped) | als-vorlage — Lead→Deal-Promotion-Wiring (3.7.2, Block H/M1). Achtung: dies ist das **Marketing-Lead**; M03 `lead_id` zeigt vermutlich auf das M01/M02-Lead-Konstrukt — Mapping mit M01/M02-Team klären |
| `prisma/schema/helpDesk.prisma` — `Feedback`/`SupportTicket` (`archived`-/`status`-Muster) & `prisma/schema/entityBuilder.prisma` — `Row.deletedAt`/`ExportJob` (Soft-Archiv) | Soft-Archive-Muster (`archived_at` statt Hard-Delete) | als-vorlage — Deal/Pipeline/Stage/LostReason-Archivierung (3.2.3/3.3.4/3.8) |
| `apps/api/src/routes/entity-row-routes.ts` + `route-access-inventory.ts` | REST-CRUD + Auth-Klassen-Matrix | als-vorlage — falls Deals über Public-API exponiert werden (INTEGRATION §3 `api/v1`) |
| `apps/web/app/utils/services/.server/*` + `application/dtos/*` Muster | Service-/DTO-/View-Model-Schichtung (SaasRock-Stil) | als-vorlage — Struktur für `app/domain/sales/*`, `view-models/sales/*.vm.ts` |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
Direkt für M03 **wiederverwendbar/anbindbar** (kein Deal-Modell darunter):
| Modell | Pfad | Rolle für M03 |
| --- | --- | --- |
| `Tenant`, `User`, `TenantUser` | `prisma/schema/accounts.prisma` | Tenant-Scope-Anker; `assigned_to_user_id`, `created_by_user_id`, Vertriebler-Pflicht [N6] |
| `Role`, `Permission`, `RolePermission`, `UserRole`, `Group` | `prisma/schema/permissions.prisma` | RBAC-Fundament für `deals.*`-Bundles (Block I) — neue Permission-Keys einseeden |
| `AuditEvent` | `prisma/schema/core.prisma:55` | Audit-Trail Stage-Wechsel (Hash-Kette, AV #6) |
| `IdempotencyKey` | `prisma/schema/core.prisma:84` | Idempotente Deal-Anlage aus Quellen (3.7) |
| `FileObject` / `FileUploadProgress` / `FileChunk` | `prisma/schema/core.prisma` | Verknüpfte Files am Deal (Block E, Won-Handoff-Files) |
| `NotificationDelivery`, `UserNotificationPreference` | `prisma/schema/notifications.prisma` | `deal.won`/`deal.stuck`-Notifications |
| `OutboxEvent` | `prisma/schema/outbox.prisma` | Atomic Side-Effects (Stage-Change → Activity/Notify/Automation) |
| `FeatureFlag` (+ Filter/Audit) | `prisma/schema/featureFlags.prisma` | Modul-Kill-Switch |
| `Form`, `FormSubmission` | `prisma/schema/formWidget.prisma` | Quelle „Deal aus Form" (3.7.3) + `DealForm`-Verknüpfung (Block A) |
| `Lead`, `LeadEvent` | `prisma/schema/marketingLeads.prisma` | Lead-Origin-Bezug (`lead_id`), Promotion-Pattern (3.7.2) |
| `Entity`/`EntityView`/`Property`/`Row`/`RowTag`/`RowTask`/`RowComment`/`RowPermission` | `prisma/schema/entityBuilder.prisma` | NUR als Pattern/optionale Custom-Feld-Kopplung — nicht als Deal-Kernmodell |
| `ExportJob` (+ Enum) | `prisma/schema/queue.prisma` | CSV-Export-Job-Tracking |
**Ablageort für die neuen HiveGrid-Modelle:** `prisma/schema/app.prisma` ist der vorgesehene leere Platzhalter
(`// custom database models`) — dort gehören `Pipeline`, `PipelineStage`, `Deal`, `DealActivity`/`Activity`,
`DealForm`, `LostReason`/`WinReason`, `HiveNumberSeq` hinein (bzw. eigene `sales.prisma`).
---
## Lücken / komplett neu zu bauen
**Datenmodell (komplett neu, kein Boilerplate-Äquivalent):**
- `Pipeline` (Multi-Pipeline ab Tag 1, `is_default`, `position`, `archived_at`) — 3.2, M059
- `PipelineStage` (`stage_kind open|won|lost`, `win_probability`, `auto_archive_after_days`, `color`, `position`)
+ Validierung „mind. 1 won + 1 lost" — 3.2.5/3.2.6, M059
- `Deal` (alle Pflicht-FKs: `contact_id` Pflicht, `company_id` optional, `lead_id`, `lost_reason_id`,
`won_handoff_project_id`, `volume numeric(12,2)`, `expected/actual_close_date`, `deal_number`) — 3.1, M060
- `LostReason` **und Gewinn-Gründe** (WinReason) — Tenant-pflegbar, archivierbar, Pflicht/Optional-Toggle pro
Liste [N8] — 3.3, M062 (in Boilerplate gar nichts)
- `HiveNumberSeq` mit `prefix`/`format` (`D-2026-0042`) + Reset-Strategie — 3.1.1, M060 (Boilerplate hat nur
`folio`-Counter ohne Format-Layer)
- **Zentrale `Activity`/`DealActivity`** mit Typen (`deal_stage_changed` inkl. `from/to_stage_id` +
`duration_in_previous_stage_seconds`, `deal_won`/`deal_lost`, …) — Block A/M045. **Vorbedingung M045 prüfen.**
**Domänen-Logik (neu):**
- Stage-Wechsel-Engine inkl. Vorwärts/Rückwärts (Klartext-Hinweis), Won/Lost-Confirm-Workflow,
**24h-Reverse-Window** (Soft-Revert vs. neue Stage-Activity — Offener-Punkt der Spec), `change_stage`-Gate — 3.5
- **Won-Handoff zu M4** (Projekt-Anlage, Feld-Mapping Deal→Projekt, Ziel-Pipeline-/Start-Stage-Wahl,
`won_handoff_project_id`, Mehrfach-Projekte aus einem Deal, Option A vs. B [N7], Wartung/Service kein Projekt [N9])
— 3.6, M063. Hängt an M065 (Auftragspipeline) — noch nicht vorhanden.
- **Setter-Closer-Termin-Anlage** mit konfigurierbarem Feld-Mapping Deal→Termin [N17] — 3.9.5, M063. Hängt an
M119/M25 (Calendar-Sync) — nicht vorhanden.
- **Forecast-Berechnung** (`volume × win_probability/100`, Zeitraum-Split, realisierter Umsatz),
Vertriebler-KPIs, Stage-Conversion-Rates, Lost-Reason-Verteilung + Aggregat-/Forecast-Cache — 3.10, M064.
Kein generischer KPI-Builder in Boilerplate für diese Sales-Metriken vorhanden.
- **Field-Level-Volumen-Permission-Redaction** (`read_volume_own/all`, `read_aggregates`) — Volumen „—" je Rolle
in Kanban-Karte/Detail/Liste/Aggregat — 3.11.2–3.11.4, Block I. Boilerplate kennt nur Object-Level + Row-Level
ACL (`RowPermission`), **kein** Field-Level-Redaction-Pattern → neu.
- **Stuck-Detection-Logik** + Stage-Verweildauer-Berechnung aus Stage-Change-Activities — 3.8, M064.
**UI (neu, auf Boilerplate-Primitiven aufbauend):**
- Kanban-Board-Komponente mit @dnd-kit (Spalten=Stages, Aggregat pro Spalte, Karten-Redaction, Live-SSE-Update,
„Mehr laden" pro Spalte ab 100+) — M061. (EntityBuilder-`board`-View ist nicht der HiveGrid-Kanban.)
- Pipeline-Switcher + „Nur meine Deals"-Quickfilter + Filter-Bar (Multi-Select) — M061
- Deal-Detail mit Stage-Pipeline-Komponente, Inline-Edit, Tab-Navigation, Klartext-Bannern (Won/Lost/Stuck/
Kontakt-archiviert) — Block E
- Pipeline-Editor (Stage-Liste D&D, Won/Lost-Validation-Anzeige) + Lost-/Win-Reason-Editor — M059/M062
- `packages/ui-hivegrid/` selbst existiert in der Boilerplate **nicht** (nur leerer `ui-subframe`-Stub +
Adapter-Metadaten in `@toolctrl/ui`) — der Adapter-Layer + Sidebar-/Breadcrumb-Config muss als HiveGrid-
Infrastruktur erst angelegt werden (Querschnitt M013–M017, nicht M03-spezifisch).
**Querschnitt-Wiring, das M03 voraussetzt, aber in Boilerplate fehlt:**
- `@hivegrid/activity` (`logActivity`, M045) — kein Äquivalent
- `packages/db/src/search/indexed-entities.ts` (M056) — existiert nicht (nur statische `search/schemas/*`)
- `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` / `breadcrumbs/` / `mobile/tab-bar.config.ts` /
`customer-portal/portal-shell.config.ts` (INTEGRATION §2/§4/§13/§14) — alle neu
- `packages/entitlements/src/permissions.ts`-Keys `sales.*`/`deals.*` (INTEGRATION §10) — neu zu definieren
**Integrationen mit anderen Modulen (out of M03-scope, aber harte Abhängigkeiten):**
- M4/M065 Auftragspipeline (Won-Handoff-Ziel) · M045 Activity · M042/M043 CRM-Kontakt/Firma · M048 Lead-Routing
(Default-Vertriebler) · M25/M119 Kalender (Setter-Closer) · M147 Dashboard (KPI-Widgets) · M5/M9/M12/M17
(Deal-Quellen) — keines davon in der Boilerplate fertig; M03 baut die **Hooks/Producer**, die Gegenseite separat.
---
## Empfohlenes Vorgehen (Reihenfolge)
1. **Fundament aktivieren (vor M03):** Boilerplate-Code als HiveGrid-Codebase übernehmen (AGENTS Pre-Flight),
Querschnitt-Pakete (`foundation`, `view-models`, `feature-flags`, `i18n`, `db`, `queue`, `realtime`, `exports`,
`search`, `logger`) 1:1 nutzen. Sicherstellen, dass die HiveGrid-Querschnitt-Milestones (M001 Audit,
M004 Permissions, M013–M017 UI/VM/Adapter/Hot-Swap, M045 Activity, M056/M057 Search) vorhanden sind —
M03 setzt sie laut DoD/INTEGRATION-CONTRACTS voraus.
2. **M059 — Daten + Pipeline-Konfig (neu, mit Vorlagen):** `Pipeline`/`PipelineStage` in `app.prisma`
(bzw. `sales.prisma`) neu modellieren. Stage-Reorder-UI via **@dnd-kit** (Vorlage: formWidget
`SortableField`/`Canvas`). Won/Lost-Validation als `CatalogError` (foundation/errors). SHK-Seed als
Konfig-Datei (Bauprinzip A.3) im Seed-Pfad (`prisma/seed.ts`-Muster). Soft-Archive-Pattern übernehmen.
`user-fk-map.ts` für neue User-FKs ergänzen (CI-Gate).
3. **M060 — Verkaufsfall-CRUD (neu):** `Deal` + `DealForm` modellieren; `HiveNumberSeq` neu (Sequenz-Mechanik
als-Vorlage aus `rows.server.ts:getNextRowMutationNumber`, Prefix/Format-Layer neu). Deal-Anlage-Quellen
über `withIdempotency` + Outbox. Pflicht-Vertriebler [N6] über `withOwnerCheck`/Default aus Routing.
Stage-Wechsel + 24h-Reverse transaktional (`runDbTransaction` + `publishOutboxEvent`). View-Model-Layer
(`view-models/sales/*.vm.ts`) + Smoke-Test + Hot-Swap-Drill (DoD).
4. **M061 — Kanban/Listen-UI (neu auf Primitiven):** Kanban mit @dnd-kit; Listen-View mit `TableNew`/
`ColumnSelector`/`TablePagination` (1:1). Live-Updates über `@toolctrl/realtime` (**SSE**, neue Deal-Events).
Server-side Filter (`getReadDbClient`). CSV-Export via `@toolctrl/exports`. **Field-Level-Volumen-Redaction
im VM-Builder** (neu) — Volumen „—" je Permission. Search-Index `deals` registrieren.
5. **M062 — Verlust/Gewinn-Gründe (neu, klein):** `LostReason`/`WinReason` (Soft-Archive, Pflicht/Optional-Toggle
pro Liste [N8]) + 5+5 SHK-Seeds + Aggregations-Auswertung (gehört konzeptionell zu M064-Reporting).
6. **M063 — Won-Handoff + Setter-Closer (neu, abhängig):** Erst nach M065 (Auftragspipeline) und M119/M25
(Kalender). Won-Handoff-Modal + Feld-Mapping Deal→Projekt + Option A/B [N7] + Wartung/Service-kein-Projekt [N9];
Setter-Closer-Termin mit konfigurierbarem Mapping [N17]. Automation-Trigger über Outbox-Prefix `deal.`.
7. **M064 — Reporting + Jobs (neu):** Forecast/KPIs/Conversion/Lost-Verteilung + Aggregat-/Forecast-Cache;
Daily-Cron (Stuck-Detection, Auto-Archiv Won 30d/Lost 90d) nach BullMQ-Cron-Pattern (Worker-Vorlage).
Optionaler KWP-Sync (Wartung ausgenommen [N9]) — externe Adapter-Schicht.
**Querschnitt-Entscheidungen, die die Spec offen lässt und durch Boilerplate-Realität präjudiziert sind:**
- Real-Time-Kanban: **SSE** (`@toolctrl/realtime`), **kein** WebSocket (in Boilerplate nicht vorhanden).
- Cache-Invalidierung Aggregate: über **Outbox/Pub-Sub** (`shared-store` DB-Index 2) bei Stage-Wechsel.
- Custom-Felder: Kopplung an EntityBuilder-`Property` (`applicable_to=deal`) prüfen vs. schlanke eigene Tabelle —
Entscheidung mit M044-Team (LP-4: kein Über-Engineering).
@@ -0,0 +1,148 @@
# Boilerplate-Abgleich — M04 Auftrags- / Projektpipeline
| | |
| --- | --- |
| **Modul-ID** | M04 |
| **Name** | Auftrags- / Projektpipeline (Geerkens-Kern) |
| **Milestones** | M065 (Pipeline-Konfig) · M066 (Auftrag-CRUD + KWP-Archiv) · M067 (Plantafel) · M068 (Mitglieder-Mgmt) · M069 (Abnahme-Workflow) · M070 (Material-Listen + Lager) |
| **Block** | Pflichtenheft Block 3, Abschnitt 4.1–4.14 |
| **Reuse-Level** | **low** (nur Fundament + Querschnitts-Bausteine + UI-Primitive nutzbar; KEINE Domänen-Modelle, kein Pipeline-/Kanban-/Plantafel-/Abnahme-Code vorhanden) |
| **Boilerplate-Wurzel** | `/Users/raffaelsheikh/Documents/GitHub/boilerplate` |
| **Verifiziert am** | 2026-06-04 (Greps/Reads direkt im Schema + package.json, nicht nur Inventur) |
## Kernbefund (Anti-Erfindung)
Die Boilerplate enthält **kein einziges Domänen-Modell** für dieses Modul. Grep über alle 27 `prisma/schema/*.prisma` (190 Modelle) liefert für `Project|Order|Auftrag|Pipeline|Stage|Schedule|Acceptance|Plantafel|Deal|Verkaufsfall|Contact|Kontakt|Company|Firma` → **NONE FOUND**. `prisma/schema/app.prisma` ist ein leerer Platzhalter (`// custom database models`). Alle M04-Entitäten (ProjectPipeline, ProjectPipelineStage, Project, ProjectMember, ScheduleEntry, AcceptanceProtocol, ProjectTemplate, ProjectForm) sind **komplett neu** zu modellieren.
Was vorhanden ist: ein generischer No-Code-EntityBuilder (mit `board`/`calendar`-View-Layout-Flag), die volle App-Shell + shadcn/Radix-Komponenten, DnD-/PDF-/Push-/Realtime-Libs, sowie das DB-Layer-Fundament (Outbox, Audit-Hash-Kette, Notification-Delivery, RBAC-Schema). Die HiveGrid-Querschnittsdienste (`logActivity` M045, `notify` M011, `withTenantScope`/`withPermission` M004) existieren **noch nicht** als Code — nur ihre DB-Bausteine.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `prisma/schema/app.prisma` | Leerer Platzhalter für HiveGrid-eigene Modelle | **als-vorlage** — hier kommen ALLE M04-Modelle rein, ohne Boilerplate-Domänen zu berühren |
| `packages/db/src/client.server.ts` | `getDbClient` / `getWriteDbClient` / `getReadDbClient` / `getExtendedDbClient` | **1:1** — Read-Replica wichtig für Plantafel/Kanban (read-heavy) |
| `packages/db/src/transactions.server.ts` | `runDbTransaction` / `runDbTransactionBatch` | **1:1** — Project-Anlage + Channel + ScheduleEntries atomar |
| `packages/db/src/outbox.ts` | `publishOutboxEvent(tx, …)`, Event-Pattern `<domain>.<entity>.<verb>` | **1:1** — alle Side-Effects (KWP-Sync, Notifications, Auto-Chat, Calendar-Push, Mängel→Tasks) transaktional |
| `packages/db/src/audit-hash.ts` | Tamper-evidente Hash-Kette (`computeAuditHash`, `verifyAuditChain`) | **1:1** — Pflicht für AcceptanceProtocol (rechtssichere 6-Jahre-Aufbewahrung) |
| `packages/db/src/rows.server.ts` (`getNextRowMutationNumber`, `folio`-Aggregat) | Fortlaufende Nummern-Vergabe pro Tenant/Entity via `_max`-Aggregat | **als-vorlage** — Muster für `project_number` „P-2026-0042" (eigener Sequenz-Generator, da Project kein Row ist) |
| `packages/db/src/dsgvo/user-fk-map.ts` | Zentrale FK-auf-User-Karte (CI-Test erzwingt Vollständigkeit) | **anpassen** — Pflicht-Eintrag für jedes neue Modell mit `user_id` (ProjectMember, ScheduleEntry.assigned_user_id, AcceptanceProtocol.staff_signature_user_id …), sonst bricht CI |
| `prisma/schema/permissions.prisma` (`Role`/`Permission`/`RolePermission`/`UserRole`/`Group`/`GroupUser`) | RBAC-Schema inkl. Gruppen | **1:1** — Object/Field/Plantafel/Acceptance-Permissions hängen hier dran; `Group`/`GroupUser` deckt „Team Süd"-Gruppen-Add (4.10.2 / M068) ab |
| `prisma/schema/core.prisma` (`AuditEvent`, `FileObject`/`FileUploadProgress`/`FileChunk`) | Audit-Trail-Modell + Storage-Metadaten | **1:1** — Audit auto via M001-Middleware; FileObject für Baustellen-Fotos + Acceptance-PDF (M23/M052-M055) |
| `prisma/schema/notifications.prisma` (`NotificationDelivery`, `UserNotificationPreference`, `DeviceToken`) | In-App/Email/Push-Delivery + Geräte-Token (APNs/FCM/WebPush) | **1:1** — Push bei ScheduleEntry-Zuweisung / Stage-Wechsel / Mängeln (4.6.13.5 / 4.12.12) |
| `apps/worker/src/jobs/push-notification.ts` | Fertiger Push-Job (firebase-admin FCM + node-apn APNs + WebPush, pro `DeviceToken`) | **1:1** — Mobile-Push für Monteur |
| `prisma/schema/appsEngine.prisma` (`App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem`) | Integrations-/Sync-Engine mit Rollback-fähigen Sync-Runs + Source→Destination-Mapping | **anpassen** — Träger für KWP-Plantafel-Bidi-Sync (4.6.14, M094) + KWP-Dokumenten-Archiv (M066/M095); `AppSyncItem` modelliert die cross-System-ID-Zuordnung (4.6.14.2) |
| `packages/foundation/src/adapters.ts` (`defineAdapter`/`getAdapter`, Status-Lifecycle) | Adapter-Registry (von AGENTS.md als KWP-Adapter-Pfad genannt, LP-11) | **1:1** — KWP-Adapter-Kapselung (4.12.11) |
| `packages/queue/*` (`@toolctrl/queue`: factory, producers, priority, groups, names) | BullMQ-Wrapper inkl. Idempotenz-Hilfen | **1:1** — Plantafel-Pull-Fallback-Cron (4.6.14.3), PDF-Gen, Auto-Email, Auto-Archive-Jobs |
| `packages/shared-store/*` (`@toolctrl/shared-store`) | Redis-KV: Cache, Rate-Limit, Idempotenz | **1:1** — Aggregat-Cache für Wochen-/Monats-Plantafel (Perf-Anforderung 4.6), Sync-Idempotenz |
| `@hocuspocus/server` + `@hocuspocus/provider` (`packages/hocuspocus`) | Realtime-Server (WebSocket/Y.js) | **als-vorlage** — Realtime-Plantafel-Updates (Spec G „Realtime-Updates (WebSocket)") |
| `packages/exports/src/audit-pdf.ts` + `./pdf` (`@react-pdf/renderer` ^4.3.2) | PDF-Streaming-Renderer + S3-Multipart-Sink | **anpassen** — Acceptance-Protokoll-PDF (4.7.4.5); eigenes PDF-Template nötig, aber Renderer + S3-Sink 1:1 |
| `@dnd-kit/core` + `@dnd-kit/sortable` + `react-dnd` (Root-deps) | Drag-&-Drop-Bibliotheken | **1:1** — Kanban-Stage-Move (4.5.3) + Plantafel-Block-Move/Resize (4.6.4) |
| `apps/web/app/components/ui/calendar.tsx` + `date-fns` / `date-fns-tz` | Kalender-Primitive + Datums-Utils | **als-vorlage** — Plantafel-Raster (Tag/Woche/Monat) muss eigenes Grid bauen (kein react-big-calendar/fullcalendar vorhanden) |
| `apps/web/app/components/layouts/sidebars/shadcn/*` + `apps/web/app/application/sidebar/AppSidebar.tsx` + `SidebarItem.ts` | Datengetriebene Sidebar mit Permission-/Feature-Gating | **1:1 / anpassen** — Sidebar-Eintrag „Aufträge (Projekte)" (Gruppe ARBEITEN, INTEGRATION-CONTRACTS §1) |
| `apps/web/app/components/ui/tables/*` (`TableNew`, `TablePagination`, `ColumnSelector`) | Tabellen-Komponenten | **1:1** — Listen-View `/projects/list` (4.5.1) |
| `apps/web/app/components/ui/modals/*` (`ConfirmDialog`, `ActionResultModal`, `ErrorModal`) | Bestätigungs-/Ergebnis-Dialoge | **1:1** — Klartext-Confirms (Konflikt „Trotzdem zuweisen?", Storno-Grund-Pflicht, Hard-Delete-Block) |
| `apps/web/app/components/ui/{emptyState,loaders}` | Empty-/Loading-States | **1:1** — DoD-Pflicht (Empty/Loading-State mit Klartext) |
| `apps/web/app/components/ui/forms/*` + `.../editors` (Markdown/Rich) | Formulare + Editoren | **1:1** — Stammdaten-Edit, Markdown-Description |
| `apps/web/app/components/entities/{rows,views,properties}/*` + `EntityView.layout` (`board`/`calendar`) | No-Code-Board/Calendar-View-Engine | **als-vorlage** — Kanban-/Board-Render-Muster studieren; M04 braucht aber dedizierte relationale Modelle, NICHT generische Rows |
| `prisma/schema/formWidget.prisma` (`Form`, `FormSubmission`) + `packages/form-widget` + Form-Builder UI (`apps/web/app/modules/formWidget/builder/*`) | Form-Builder + Submission-Inbox + Field-Mapping/Routing | **anpassen** — Acceptance-Form-Templates (Subtyp `acceptance_form`, 4.7.1) + „Projekt aus Formular" (4.4.4) bauen auf M5/M121 auf; Field-Mapping-Helfer (`applyFieldMapping`) wiederverwendbar |
| `prisma/schema/portals.prisma` + `apps/web/app/modules/portals/*` | Customer-Portal-System | **anpassen** — Kunden-Sicht auf Projekt-Status + Acceptance-PDF (4.12.9, M15/M141-M146) |
| `packages/view-models/*` (`VMState`, `ActionCallbacks`, `I18nLabel`, `assertI18nLabel`) | View-Model-Vertrag + i18n-Key-Zwang | **1:1** — M013-Layer-Pflicht (Loader baut VM, UI nur VM+Actions) |
| `packages/ui-app/*` (`createSidebarRegistry`/`createShellRegistries`, `generateA11yTest`) | Shell-Registry-Factories + a11y-Test-Template | **1:1** — Navigations-Wiring + a11y-Gate |
| `packages/ui-mock/*` + `packages/i18n/*` (de/en, `@toolctrl/i18n`) | Hot-Swap-Mock-Generator + i18n-Setup | **1:1** — Hot-Swap-Drill (M017) + i18n-Keys (M003), de Default |
| `prisma/schema/featureFlags.prisma` (`FeatureFlag`, `FeatureFlagFilter`, `FeatureFlagAudit`) | Feature-Flag-Engine | **1:1** — Modul ohne Deploy abschaltbar (DoD-Pflicht, M006) |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
**Direkt M04-relevant (Querschnitt + Träger):**
- `prisma/schema/app.prisma` — leerer Platzhalter, Ziel für alle M04-Modelle.
- `prisma/schema/accounts.prisma` — `User`, `Tenant`, `TenantUser`, `Group`-Bezug (Mitarbeiter, Mandanten-Scope, Mitglieder).
- `prisma/schema/permissions.prisma` — `Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser` (Berechtigungen 4.11 + Gruppen-Add 4.10.2).
- `prisma/schema/core.prisma` — `AuditEvent` (Hash-Kette → Acceptance-Compliance), `FileObject`/`FileUploadProgress`/`FileChunk` (Fotos + PDF), `IdempotencyKey`.
- `prisma/schema/notifications.prisma` — `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` (Push 4.6.13.5 / 4.12.12).
- `prisma/schema/appsEngine.prisma` — `App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem` (KWP-Bidi-Sync-Träger 4.6.14, Dokumenten-Archiv M066/M095).
- `prisma/schema/outbox.prisma` — `OutboxEvent` (transaktionale Side-Effects).
- `prisma/schema/featureFlags.prisma` — Feature-Flag-Gating.
- `prisma/schema/formWidget.prisma` — `Form`, `FormSubmission` (Acceptance-Forms / Projekt-aus-Form, via M5/M121).
**Als Pattern/Vorlage (NICHT direkt M04-Daten):**
- `prisma/schema/entityBuilder.prisma` — `Entity`, `Property`, `EntityView` (Layout-Flag `table|board|calendar|list|gallery`, Zeile 95), `EntityViewFilter/Sort`, `Row`, `RowValue`, `RowTag`, `RowTask`, `RowComment`, `RowPermission`, `RowMedia`. Generische No-Code-Engine — als Render-/Filter-/Board-Muster studierbar, ersetzt aber NICHT die dedizierten M04-Relationen.
**NICHT vorhanden (Beweis: Schema-Grep = NONE):** `Project`, `ProjectPipeline`, `ProjectPipelineStage`, `ProjectMember`, `ScheduleEntry`, `AcceptanceProtocol`, `ProjectTemplate`, `ProjectForm`, `Deal`/`Verkaufsfall`, `Contact`/`Kontakt`, `Company`/`Firma`, `Task`/`Aufgabe`.
---
## Lücken / komplett neu zu bauen
**Daten-Layer (alles neu, in `app.prisma`):**
- Alle 8 Kern-Entitäten der Spec Block A: ProjectPipeline, ProjectPipelineStage (inkl. `stage_kind`-Enum, `requires_acceptance_protocol`, `auto_archive_after_days`, `color`), Project (inkl. `project_number`-Sequenz, `source_deal_id` nullable, Baustellen-Adresse, `cancellation_reason`), ProjectMember (M:N + frei-textuelle Rolle), ScheduleEntry (Status-Enum, Index `(tenant_id, assigned_user_id, start_at, end_at)`), AcceptanceProtocol (nicht-löschbar, Signatur-Bilder, IP+Timestamp, `defects[]` JSON, `acceptance_kind`-Enum), ProjectTemplate, ProjectForm.
- Verfügbarkeits-Regeln pro MA (4.6.10 — `/admin/users/:id/availability`), neues Modell.
- Stage-Historie-Modell (4.8.1).
- Material-Listen-Modell (M070 — Position/Menge/Status; Phase 1 read-only aus KWP via M095).
**Domänen-Logik (neu, in `app/domain/**`):**
- Pipeline-Engine wird laut M065 aus M059 (Sales-Pipeline) wiederverwendet — beide sind HiveGrid-Milestones, NICHT Boilerplate. Reine Boilerplate-Wiederverwendung gibt es hier nicht.
- Stage-Wechsel-Validation (Acceptance-Pflicht-Block, Storno-Grund-Pflicht, 24h-Reverse-Window).
- Sequenz-Generator „P-YYYY-NNNN" (Muster aus `rows.server.ts` adaptierbar, aber eigene Implementierung).
- Konflikt-Detektor Plantafel (Doppel-Belegung, Urlaub/Krankheit aus M14, Verfügbarkeit, Arbeitszeit — 4.6.9).
- Acceptance-Workflow-State-Machine (Mängel-Klassifizierung → `acceptance_kind` → Folge-Aktionen 4.7.5; Mängel→Tasks-Auto-Erzeugung 4.7.5.2/4.12.15).
- Sales-Verlauf-View-Model-Composition (read-only Snapshot aus Deal, Field-Level-Volumen-Gate — 4.8.4).
- Auto-Projekt-Chat-Lifecycle (4.10.3/4.12.7, hängt an M104).
- Auto-Archive-Jobs (90/30 Tage) + Stuck-Detection.
**UI (neu, in `packages/ui-hivegrid/**` via Subframe-Adapter):**
- Kanban-Board mit Project-Karten (DnD via @dnd-kit vorhanden; Karten-/Board-Layout neu).
- **Plantafel-Grid** (Y=Mitarbeiter, X=Zeit, Resize+Move, Konflikt-Indikatoren, Zoom) — größte UI-Lücke; KEIN react-big-calendar/fullcalendar in der Boilerplate, nur `calendar.tsx`-Primitive + date-fns. Eigenes Grid (ggf. @xyflow/react oder Custom).
- Project-Detail mit Tab-/Sektions-Navigation, visueller Pipeline-Komponente, Activity-Stream.
- Mobile-first Acceptance-Workflow (mehrschrittig, Foto-Kamera-Upload, Touch-Signatur).
- Mobile „Meine Termine heute" / „Meine Woche" (4.6.13).
**Querschnitt-Dienste (noch nicht als Code in BP — HiveGrid-Milestones):**
- `logActivity` (M045) — Grep: keine Implementierung in BP.
- `notify({…})` (M011) — kein standalone `@toolctrl/notifications`-Paket; nur DB-Modelle + Worker-Job. Service-Fassade neu.
- `withTenantScope()` / `withPermission()` (M004) — Permission-Filter-Fassade neu (RBAC-Schema vorhanden, Filter-Logik nicht).
- i18n-Keys-Namespace `projects.*` neu (M003-Build-Lint blockt sonst).
**Externe Lib, die FEHLT und ergänzt werden muss:**
- **`signature_pad`** — NICHT in der Boilerplate (Grep über alle package.json = NONE). **Die Behauptung im M069-PLAN.md „`signature_pad` ist in Boilerplate-Deps" ist falsch** und muss korrigiert werden (Dependency neu hinzufügen für Touch-Signatur 4.7.4.3/4.7.4.4).
- **`firebase-admin` / `node-apn`** — der Push-Worker-Job referenziert sie; Verifizieren, dass die SDKs tatsächlich installiert sind (Job-Header nennt sie, aber als optionale Adapter).
**Integrationen, die auf andere HiveGrid-Milestones warten (keine BP-Lücke i.e.S.):**
- KWP-Bidi-Sync (M094/M095, LP-11) · Calendar-Sync M25/M119 · Email-Versand M092 · Whisper-Transkription M18 · HR-Hub-Konflikte M14/M129/M135.
---
## Empfohlenes Vorgehen
**Phase 0 — Fundament aus BP übernehmen (vor jedem Modul-Code):**
1. M04-Modelle in `prisma/schema/app.prisma` modellieren (NICHT EntityBuilder-Rows nutzen — dedizierte Relationen wegen Deal-/Contact-/User-FKs, Sequenznummer, Field-Level-Volumen, rechtlicher Aufbewahrung). Forward-Only-Migration mit Inverse-SQL-Kommentar.
2. Für jedes neue Modell mit `user_id` SOFORT Eintrag in `packages/db/src/dsgvo/user-fk-map.ts` (sonst CI-Block). AcceptanceProtocol als `retain-tombstone` (6-Jahre-Pflicht), nicht `hard-delete`.
3. DB-Zugriff über `getDbClient`/`getReadDbClient` + `runDbTransaction`; alle Side-Effects über `publishOutboxEvent`.
**Phase 1 — Pipeline-Konfig (M065):**
4. Pipeline-/Stage-Modelle + SHK-Default-Seeding (3 Pipelines, LP-3-Branchen-Template). RBAC-Permissions (`projects.*`, `plantafel.*`, `acceptance.*`) in `permissions.prisma`-Seed registrieren.
5. ProjectTemplate-Modell + Admin-UI (shadcn-Forms aus BP).
6. Sidebar-Eintrag „Aufträge (Projekte)" (Gruppe ARBEITEN) + Settings-Eintrag „Pipelines" via `sidebar.config.ts` / `AppSidebar.tsx`-Muster.
**Phase 2 — Auftrag-CRUD + Detail (M066):**
7. Project-CRUD, Sequenz-Generator (Muster `rows.server.ts`), Kanban + Liste (DnD via @dnd-kit, Tabellen aus BP). Stage-Wechsel-Validation.
8. Project-Detail-VM (View-Model-Composition inkl. read-only Sales-Verlauf mit Volumen-Gate). FileObject-Anbindung für Fotos.
9. KWP-Dokumenten-Archiv über `appsEngine` + `defineAdapter` (M095).
**Phase 3 — Mitglieder (M068, blockt Plantafel):**
10. ProjectMember + Gruppen-Add über `Group`/`GroupUser` (BP) + Auto-Chat-Channel (M104).
**Phase 4 — Plantafel (M067, größter Neubau):**
11. Eigenes Calendar-Grid (kein BP-Fertigbaustein) mit DnD-Resize/Move. Konflikt-Detektor. Read-Replica + shared-store-Aggregat-Cache für Perf. Hocuspocus für Realtime. Push via vorhandenem Worker-Job + DeviceToken.
**Phase 5 — Abnahme (M069):**
12. **Zuerst `signature_pad` als Dependency ergänzen** (fehlt!). Acceptance-Form auf Form-Builder (M5/M121). Mobile-Workflow. PDF via `@react-pdf/renderer` + `packages/exports`-S3-Sink (eigenes Template). Audit-Hash-Kette für Manipulationssicherheit. Mängel→Tasks (M073). Auto-Email (M092).
**Phase 6 — Material/Lager (M070):**
13. Material-Listen-Modell; Phase 1 read-only KWP-Daten via M095/appsEngine; Lagerist-Permission-Cluster (M023.1).
**Querschnitt durchgängig:** `logActivity` (M045), `notify` (M011), `withTenantScope`/`withPermission` (M004), i18n `projects.*` (M003), Feature-Flag (M006), VM-Layer (M013), Subframe-Adapter (M015) — alle als HiveGrid-Querschnitt nutzen, NICHT im Modul nachbauen (PR-Block sonst).
@@ -0,0 +1,118 @@
# Boilerplate-Abgleich — M05 Form-Builder
| Feld | Wert |
| --- | --- |
| Modul-ID | M05 |
| Name | Form-Builder (Formular-Werkstatt) |
| Milestones | M121 (Editor), M122 (Veröffentlichung/Sharing/Embed), M123 (Submission-Eingang/CRM-Aktionen), M124 (Conditional-Display), M125 (Rate-Limit/Captcha/DSGVO/SHK-Vorlagen) |
| Pflichtenheft-Block | Block 6, Abschnitt „Modul 5: Form-Builder" (5.1–5.13) |
| Spec | `_bmad-output/specs/M05-form-builder.md` (Size XL, „Voll Google-Forms-Niveau zu Phase 1") |
| **Reuse-Level** | **medium** |
## Reuse-Zusammenfassung
Die Boilerplate enthält ein **fertiges, aber bewusst minimales** Form-Widget (`packages/form-widget` + `prisma/schema/formWidget.prisma` + `apps/web/app/modules/formWidget/builder/*` + Submit-Route + 3 Worker-Jobs). Es ist ein **embeddable Lead-Capture-Widget**, kein Google-Forms-Klon: 8 Feldtypen, 3-Tab-Builder (Palette/Canvas/Properties/Settings/Preview), Drag-&-Drop (`@dnd-kit`), Origin-Allowlist, Honeypot, Sliding-Window-Rate-Limit, Turnstile (feature-gegated), und Routing auf `generic | crm-contact | entity-row` plus Webhook-/Notify-Email-Follow-ups über Outbox.
Das deckt das **Architektur-Skelett** von M121–M123 und den **Spam-Schutz-Kern** von M125 ab. Es fehlt jedoch der **Großteil des fachlichen Funktionsumfangs**: 9 von 17 Feldtypen (u.a. Datei, Foto, Signatur, Adresse, Datum/Zeit, Telefon-E.164, hidden), das gesamte explizite Datenmodell der Spec (`FormField`/`FormSubmissionAnswer`/`FormFile`/`FormLink`/`FormAction`/`FormActionLog`/`FormLog`/`FormTag`), die 11 Submission-Aktionen (Lead/Deal/Projekt/Task/KWP/Automation/...), Public-URL/QR/iFrame-Auto-Resize, Submissions-Verwaltung (Liste/Filter/CSV/PDF/Re-Run), Conditional-Display (M124), und die 8 SHK-Vorlagen. Querschnitt (DB-Client/Outbox/Audit/Notifications/Feature-Flags/File-Storage/i18n/View-Models/App-Shell) ist solide vorhanden und direkt nutzbar.
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `packages/form-widget/src/schema.ts` | Zod-Schemas `FormDefinitionSchema`/`FormFieldSchema`/`FormThemeSchema`/`FormSubmissionPayloadSchema`/`FieldTypeSchema`; Feldtypen-Enum, Length-Validation, Duplicate-Key-Check, `HONEYPOT_FIELD_KEY` | **als-vorlage** — Struktur 1:1, aber Feldtyp-Enum von 8 auf 17 erweitern + `field_config`/`validation_rules` (min/max_value, regex, accepted_file_types, max_file_size, custom_error_message) ergänzen |
| `packages/form-widget/src/origin.ts` | `matchAllowedOrigin`/`normalizeOrigin` (Wildcard-Subdomain, leere Allowlist = nicht submittbar) | **1:1** — direkt für Embed-/CORS-Schutz |
| `packages/form-widget/src/honeypot.ts` | `isHoneypotTriggered` (still 204/Silent-Drop) | **1:1** — deckt 5.9.1 ab |
| `packages/form-widget/src/routing.ts` | `resolveSubmissionTarget`/`applyFieldMapping` (Field-Mapping-Helper, worker-hot-path, sync) | **anpassen** — Mapping-Mechanik 1:1; Target-Set von 3 (`generic/crm-contact/entity-row`) auf die 11 Action-Typen der Spec erweitern |
| `prisma/schema/formWidget.prisma` (`Form`, `FormSubmission`) | Tenant-scoped Form-Definition (JSON) + Submission-Inbox (`payload` JSON, `ipHash` SHA-256, `status`, `result`, `allowedOrigins[]`, `notifyEmails[]`, `webhookUrl`, `config`, `key @unique`) | **als-vorlage** — gutes Fundament für `Form`+`FormSubmission`, aber Spec verlangt zusätzlich `slug`/`subtype`/`status`(draft/published/archived)/`is_public`/`requires_login`/`submission_limit`/`submission_close_at`/`success_message`/`redirect_url` und die normalisierten Tabellen (`FormField`, `FormSubmissionAnswer`, `FormFile`, `FormLink`, `FormAction`, `FormActionLog`, `FormLog`, Tags) |
| `apps/web/app/modules/formWidget/builder/FormBuilder.tsx` + `Canvas.tsx`/`SortableField.tsx`/`FieldPalette.tsx`/`FieldProperties.tsx`/`PreviewTab.tsx`/`SettingsTab.tsx`/`palette.ts`/`types.ts` | Kompletter Tab-Builder mit Drag-&-Drop-Canvas, Feld-Eigenschaften-Editor, Live-Preview, Settings (Origins/Notify/Webhook/Target/Field-Mapping), i18n-Keys durchgängig | **als-vorlage** — Editor-Grundgerüst & Drag-&-Drop-Pattern direkt übernehmbar; Palette von 8 auf 17 Feldtypen erweitern, FieldProperties um typ-spezifische Configs (Datei-Typen, Datums-Format, Adresse, Signatur, hidden-Prefill) ausbauen. ACHTUNG: liegt in `apps/web` (nicht in `packages/ui-hivegrid`) — für HiveGrid in den View-Model/Adapter-Layer umziehen (AGENTS Layer-Disziplin, M013/M015) |
| `apps/web/app/routes/api.embed.forms.$formKey.submit.tsx` | Public-Submit-Endpoint: Origin-Check → Honeypot-Silent-Drop → Sliding-Window-Rate-Limit (pro IP **und** pro Origin) → Turnstile (feature-flagged via `evaluateFlagForContext`) → `ipHash` → Payload-Validation gegen gespeicherte Definition → Outbox-Publish | **anpassen** — deckt M125 (5.9.1–5.9.4) fast vollständig ab; Rate-Limit-/Honeypot-/Captcha-Pipeline 1:1, fehlende Stücke: Datei-Upload-Annahme, Signatur+IP/Zeitstempel-Persistenz (5.5), Anomalie-Drosselung (5.9.4) |
| `apps/web/app/routes/embed.forms.$formKey.tsx` | iFrame-gerenderte Public-Form-Mini-App | **als-vorlage** — Basis für Embed; Auto-Resize via postMessage (M122) + Public-URL-Pattern `f/<tenant>/<slug>` (INTEGRATION-CONTRACTS §3) ergänzen |
| `apps/web/app/routes/app.$tenant/forms.tsx` + `forms.$id.tsx` | Tenant-Admin: Form-Liste + Create (mit Submission-Count + Embed-Snippet) und Detail/Edit-Route (Loader baut VM, Action speichert via Prisma, Permission-Gate, CSRF) | **als-vorlage** — Route-Topologie & Loader/Action-Muster übernehmbar; Permission ist aktuell `app.settings.account.update` (admin-only) statt der M05-`forms.*`-Permissions |
| `apps/worker/src/jobs/form-submission-route.ts` | Outbox-getriebener Routing-Worker (generic/crm-contact/entity-row, Idempotenz via dispatch-key `form-submission:<id>`, Status-Rückschreibung, Fehler nicht verschluckt → BullMQ-Retry + Sentry) | **anpassen** — Worker-Gerüst + Idempotenz-Muster 1:1; Target-Handler um Lead/Deal/Projekt/Task/KWP/Automation erweitern (M123, 5.7) |
| `apps/worker/src/jobs/form-submission-followups.ts` + `form-submission-webhook-delivery.ts` | Pure Follow-up-Planner (Webhook + Notify-Email als Outbox-Events) + Webhook-Delivery-Worker | **1:1** für `webhook_send` + `send_email_to_team`/`send_email_to_submitter` (5.7.1/5.7.2/5.7.8) |
| `apps/web/app/utils/turnstile.server.ts` (`verifyTurnstileToken`) + Feature-Flag `form-widget-turnstile` | Captcha-Verifikation | **anpassen** — vorhanden, aber **Cloudflare Turnstile**; M125 fordert explizit **hCaptcha (EU-konform)**. Adapter-Tausch oder zweiter Provider nötig |
| `packages/db` (`getDbClient`/`runDbTransaction`/`outbox.ts`/`audit-hash.ts`/`dsgvo/user-fk-map.ts`) | DB-Client, Transaktionen, transaktionale Outbox (`publishOutboxEvent`), Audit-Hash-Kette, DSGVO-FK-Karte | **1:1** — Querschnitt-Fundament; `user-fk-map.ts` ist Pflicht-Ergänzung bei neuen User-FKs (Submission `submitter_user_id`, `FormLog.created_by_user_id`) |
| `prisma/schema/core.prisma` (`AuditEvent`, `FileObject`/`FileUploadProgress`/`FileChunk`, `IdempotencyKey`, `Event`/`EventWebhookAttempt`) | Audit-Trail (Hash-Kette, `dataSensitivity`/`regulatoryScope`), File-Storage-Metadaten, Idempotenz, Webhook-Attempts | **1:1** — `AuditEvent` deckt `FormLog`/AV#6; `FileObject` deckt M052-Anbindung für Datei-/Foto-Upload (5.4); `IdempotencyKey` für Action-Re-Run (5.8.6) |
| `prisma/schema/notifications.prisma` + `apps/web/app/modules/notifications/services/.server/NotificationService.ts` | `NotificationDelivery`/`UserNotificationPreference`/`DeviceToken` + Service-Layer (In-App/Email/Push) | **1:1** — `notify()`-Querschnitt (M011) für „neue Submission"-Notification (5.7.2, M22-Integration) |
| `prisma/schema/featureFlags.prisma` + `evaluateFlagForContext` | Feature-Flag-Engine (scope/rules/variants, append-only Audit) | **1:1** — DoD-Pflicht „Feature-Flag-fähig" (M006); bereits im Submit-Endpoint genutzt |
| `apps/web/app/utils/integrations/storageService.ts` + `apps/web/app/utils/helpers/.server/MediaService.ts` | S3/Scaleway-Storage-Service + Media-Handling | **anpassen** — Backend für Datei-/Foto-Upload aus Submissions (5.4.1 „landen im Tenant-Datei-Speicher", M052/M053) |
| `packages/exports` (`./csv`, `./pdf`, `audit-pdf`, `EXPORT_PLAN_ROW_CAPS`, S3-Multipart-Sink) + Worker `export.ts`/`dsgvo-export.ts` | Streaming-Export CSV/PDF, Spreadsheet-Injection-Safety, DSGVO-Export | **anpassen** — CSV-Export der Submissions (5.8.4) + PDF-Export einer Submission (5.8.7) + DSGVO-Löschung (5.8.8) |
| `packages/view-models` (`VMState`, `ActionCallbacks`, `I18nLabel`, `assertI18nLabel`) | View-Model-Statusmodell + i18n-Key-Zwang | **1:1** — Konvention M013; Loader baut VM, UI empfängt nur VM+Actions (DoD Layer-Disziplin) |
| `apps/web/app/application/sidebar/{AppSidebar.tsx,SidebarItem.ts}` + `packages/ui-app` Registry-Factories | Datengetriebene Sidebar mit Permission-/Feature-Gating | **als-vorlage** — Sidebar-Eintrag „Forms" (Gruppe gem. INTEGRATION-CONTRACTS §1, Zeile 53 verweist bereits auf M121–M125) |
| `apps/web/app/components/ui/*` (shadcn/Radix: tables, forms, modals, tabs, multi-select, empty/loading states, calendar) | Komponenten-Bibliothek | **1:1** — Submissions-Liste (TableNew/ColumnSelector/TablePagination → 5.8.1–5.8.3), Filter, Empty-/Loading-States, Datums-Picker (`calendar.tsx` für `date`-Feldtyp) |
| `packages/db/src/notifications.server.ts` + `OutboxEvent` (`outbox.prisma`) | Transaktionales Side-Effect-Muster | **1:1** — `form.submitted`-Event (M17-Integration 5.7.9) + alle Actions als Outbox-Events |
| `qrcode.react` (Root-Dependency `^4.2.0`) | QR-Code-Generator-Lib | **1:1** — QR-Code für Print-Material (M122, 5.6.5) — Lib bereits vorhanden, kein neuer Dep |
| `@dnd-kit/{core,sortable,utilities}` (Root-Dependencies) | Drag-&-Drop | **1:1** — Editor-Sortierung (M121, 5.3.1); bereits in Builder verwendet |
## Vorhandene Daten-Modelle
| Modell | Pfad | Deckt Spec-Entität | Lücke |
| --- | --- | --- | --- |
| `Form` | `prisma/schema/formWidget.prisma:8` | `Form` (teilweise) | Fehlen: `slug`, `subtype`, `status`(draft/published/archived) + `published_at`/`archived_at`, `is_public`, `requires_login`, `submission_limit`, `submission_close_at`, `success_message`, `redirect_url`, `description`, `created_by_user_id`. Vorhanden statt Felder: `definition` JSON, `theme`, `targetType`/`fieldMapping` (1 Target), `allowedOrigins[]`, `notifyEmails[]`, `webhookUrl`, `config`, `key` |
| `FormSubmission` | `prisma/schema/formWidget.prisma:40` | `FormSubmission` (teilweise) | Vorhanden: `payload` JSON, `sourceOrigin`, `ipHash`, `userAgent`, `status`, `result`. Fehlen: `submitter_user_id`/`submitter_email`/`submitter_phone`, `linked_entity_type`/`linked_entity_id`, `is_complete`, `archived_at`, `submitted_at` (nur `createdAt`) |
| `AuditEvent` | `prisma/schema/core.prisma` | `FormLog` (AV#6) | Generisches Audit mit Hash-Kette; reicht als `FormLog` oder als Vorlage für dediziertes `FormLog`-Modell |
| `FileObject`/`FileUploadProgress`/`FileChunk` | `prisma/schema/core.prisma` | `FormFile` (Storage-Seite) | `FormFile`-Join (`submission_id`/`field_key`/`file_id`) selbst fehlt; Storage-Metadaten vorhanden |
| `OutboxEvent` | `prisma/schema/outbox.prisma` | Action-/Event-Dispatch | Trägt `form.submission.*`-Events; `FormAction`/`FormActionLog` als eigene Modelle fehlen |
| `NotificationDelivery` etc. | `prisma/schema/notifications.prisma` | M22-Notification bei Submission | 1:1 |
| `FeatureFlag` | `prisma/schema/featureFlags.prisma` | Feature-Flag-Querschnitt | 1:1 (`form-widget-turnstile` existiert bereits) |
| `app.prisma` (leerer Platzhalter) | `prisma/schema/app.prisma` | — | Vorgesehener Ort für die neuen HiveGrid-Form-Modelle (`FormField`, `FormSubmissionAnswer`, `FormFile`, `FormLink`, `FormAction`, `FormActionLog`, `FormLog`, `FormTag`) |
**Nicht vorhandene Spec-Modelle (komplett neu):** `FormField` (normalisiert — BP speichert Felder als JSON in `Form.definition`), `FormSubmissionAnswer` (pro Feld/Submission — BP speichert alles als `payload` JSON), `FormFile`, `FormLink` (personalisierte Tracking-Links mit Token/expires/usage_limit), `FormAction`, `FormActionLog`, `FormLog` (dediziert), `FormsTag`/`FormTag`.
## Lücken / komplett neu zu bauen
**Feldtypen (M121, 5.1):** 9 von 17 fehlen — `number` ist da, aber nicht `date`/`datetime`/`time`, `multiselect`, `boolean`(toggle), `url`, `phone` (E.164-Norm), `file`, `photo` (Mobile-Kamera-Direktzugriff), `signature` (Touch — **kein** `signature_pad`/Signatur-Komponente in der Boilerplate vorhanden), `address` (Auto-Complete via Maps-Adapter M25), `section_header`, `description_block` (Markdown), `hidden` (URL-Param-Prefill). BP hat nur text/email/number/textarea/select/radio/checkbox/honeypot.
**Validation (M121, 5.2):** BP hat `min/max_length` + `required`. Fehlen: `min_value`/`max_value`, `regex_pattern`, `accepted_file_types[]`/`max_file_size_mb`, `custom_error_message` (Klartext pro Feld, 5.13).
**Veröffentlichung/Sharing/Embed (M122, 5.6):** Public-URL-Pattern `app.hivegrid.de/f/<tenant>/<slug>` (INTEGRATION-CONTRACTS §3) — BP nutzt `embed.forms.$formKey` (Key, kein slug/tenant-Pfad). `is_public`/`requires_login`-Gating fehlt. iFrame-**Auto-Resize** (postMessage) fehlt. **QR-Code-Generator** fehlt (Lib `qrcode.react` vorhanden, Integration neu). **FormLink** (personalisierte Tracking-Links mit Token/expires/usage_limit, 5.6.2) komplett neu. JS-Snippet/`embed.js` (5.6.4) — BP hat Embed-Script-Base, aber CORS-konformes Inline-Snippet neu zu verifizieren.
**Submission-Aktionen (M123, 5.7):** BP routet nur auf `crm-contact`/`entity-row` + Webhook + Notify-Email. Fehlen: `create_lead` (M2), `create_deal` mit Pipeline-Wahl (M3), `create_project` (M4), `create_task` (M26), `link_to_existing_entity` (Auto-Match Email/Phone), `trigger_automation` (M17), `kwp_push` (M19). **Mehrere Actions in Reihenfolge** pro Form + **Conditional Actions** (`filter_rules`) + per-Action-Field-Mapping als Konfig — BP kennt nur 1 Target pro Form.
**Datei-/Foto-/Signatur-Submission (M123, 5.4/5.5):** Datei-Upload-Annahme im Submit-Endpoint fehlt (Schema schließt File explizit aus). Foto-Upload mit Kamera (mobile). Touch-Signatur **mit IP+Zeitstempel-Persistenz** (rechtliche Beweiskette, 5.5.2). File-→-Tenant-Storage-Verknüpfung (`FormFile`).
**Submissions-Verwaltung (M123, 5.8):** BP zeigt nur Submission-Count auf der Liste. Komplett neu: Submission-Liste mit Spalten-Konfiguration (5.8.3), Filter nach Datum/Submitter/Entität, Detail-Ansicht mit allen Antworten + Metadaten + Action-Status + File-Vorschau, **Action-Re-Run** (5.8.6), CSV-Export (5.8.4 — `packages/exports` als Basis), PDF-Export (5.8.7 — `packages/exports/pdf`), Archivieren/DSGVO-Löschen (5.8.8).
**Conditional-Display (M124, 5.12.1-as-Phase-1-Variante):** Show/Hide-Logik basierend auf anderem Feldwert (ODER/UND), im Editor setzbar + im Renderer ausgewertet — **komplett neu** (BP-Schema kennt kein `conditional_visibility`). KEIN Branching (Anti-Liste).
**Spam-Schutz (M125, 5.9):** Honeypot ✅, Rate-Limit pro IP ✅, Origin-Check ✅, Captcha ✅ (aber **Turnstile statt hCaptcha** — Provider-Tausch nötig, 5.9.2). Fehlt: Anomalie-Drosselung bei verdächtigen Mustern (sehr schnelles Ausfüllen / mehrfach gleicher Inhalt, 5.9.4).
**SHK-Branchen-Vorlagen (M125, 5.10):** Alle 8 Vorlagen (Kontakt-Anfrage, Sales-Aufnahme, Foto-Checkliste, Acceptance-Protokoll, Wartungs-Buchung, Krankmeldung, Urlaubsantrag, Customer-Feedback) als deklarative Seed-Konfiguration (LP-3/AV#3) — **komplett neu**, an M158-Provisionierung gekoppelt.
**Permissions (M05 Block J):** Die 8 atomaren `forms.*`-Permissions + 4 Bundles + Default-Rollen-Mapping (SHK-Rollen aus M023) — neu zu seeden. BP-Forms nutzen ein einzelnes `app.settings.account.update`.
**i18n:** BP-Builder nutzt `settings.forms.builder.*`-Keys (Namespace `settings`/`translations`). HiveGrid-eigene Keys (de/en) für alle neuen Felder/Texte (M003-Build-Lint blockt sonst).
**Layer-Disziplin:** Boilerplate-Builder + Routen liegen in `apps/web/app` (gemischter Loader/UI-Layer). HiveGrid verlangt UI in `packages/ui-hivegrid` (kein Prisma-Import), Loader baut VM, Subframe via Adapter (M013/M015, DoD). Beachte MEMORY: jede Route MUSS `mcp__subframe__design_page` nutzen; `.server.ts`-Re-Export-Blocker → nur Component-Extraktion.
## Empfohlenes Vorgehen
1. **Datenmodell zuerst (M121-Basis):** `formWidget.prisma` als Vorlage nehmen, aber das volle Spec-Schema in `prisma/schema/app.prisma` bauen — `Form` (alle Stammdaten-Felder + `slug` unique pro Tenant + `subtype`/`status`), normalisierte `FormField`, `FormSubmission` + `FormSubmissionAnswer`, `FormFile`, `FormLink`, `FormAction`, `FormActionLog`, `FormLog`, `FormTag`. Entscheidung normalisiert-vs-JSON bewusst treffen (Spec listet beide normalisierten Tabellen UND `submission_data` JSON — Hybrid wie BP ist legitim, aber `FormField`-Normalisierung ist für Editor/Spalten-Konfig/Conditional-Display sinnvoll). User-FKs in `dsgvo/user-fk-map.ts` ergänzen.
2. **Zod-Schema erweitern (M121):** `packages/form-widget/src/schema.ts` als Startpunkt kopieren/erweitern (eigenes HiveGrid-Paket oder erweitern), Feldtyp-Enum auf 17 erweitern, typ-spezifische `field_config` + volle `validation_rules` + Klartext-`custom_error_message`.
3. **Editor übernehmen + ausbauen (M121):** `FormBuilder.tsx` + Canvas/`@dnd-kit`-Drag-&-Drop + FieldProperties + PreviewTab als Vorlage in den View-Model/Adapter-Layer ziehen (Layer-Disziplin!). Palette auf 17 Feldtypen, FieldProperties um typ-spezifische Configs, Live-Preview + Klartext-Validation (5.13.1/5.13.2). Neue Components: Signatur-Pad (neuer Dep, z.B. `signature_pad`), Foto/Kamera, Adress-Autocomplete (M25), Datums-Picker (`calendar.tsx` vorhanden).
4. **Submit-Endpoint + Spam-Schutz (M125 vorziehen, da BP es fast fertig hat):** `api.embed.forms.$formKey.submit.tsx` als Vorlage — Honeypot/Rate-Limit/Origin/ipHash 1:1, Turnstile → **hCaptcha** tauschen, Anomalie-Drosselung ergänzen. Public-URL auf `f/<tenant>/<slug>` (INTEGRATION-CONTRACTS) umstellen, `is_public`/`requires_login` gaten.
5. **Sharing/Embed (M122):** iFrame-Auto-Resize (postMessage), QR-Code (`qrcode.react`), `FormLink`-Tracking-Links (Token/expires/usage_limit). `embed.forms.$formKey.tsx` als Render-Basis.
6. **Submission-Routing/Actions (M123):** `form-submission-route.ts`-Worker + `applyFieldMapping` + Outbox-Idempotenz-Muster als Skelett; Action-Registry auf alle 11 Typen erweitern, jede Action als eigener Outbox-Event-Handler (`create_lead`/`create_deal`/`create_project`/`create_task`/`kwp_push`/`trigger_automation`/...). Webhook + Notify-Email-Follow-ups 1:1. `FormActionLog` + Re-Run (5.8.6) über `IdempotencyKey`.
7. **Datei/Foto/Signatur-Persistenz (M123):** Submit-Endpoint um Multipart-/File-Annahme erweitern, in `FileObject`/Storage (`storageService.ts`/M052) ablegen, `FormFile`-Join schreiben; Signatur als File + IP/Zeitstempel in Submission.
8. **Submissions-Verwaltung (M123):** Liste (shadcn `TableNew`/`ColumnSelector`/`TablePagination`) + Filter + Detail-Ansicht; CSV-Export via `packages/exports/csv`, PDF via `packages/exports/pdf`, DSGVO-Löschen via `dsgvo-export`/Hard-Delete-Muster.
9. **Conditional-Display (M124):** `conditional_visibility`-JSON pro Feld im Editor setzbar, Show/Hide-State im Renderer (kein Routing-Pfad-Wechsel). Komplett neu.
10. **SHK-Vorlagen + Permissions (M125):** 8 Vorlagen als deklarative Seed-Konfig (an M158 gekoppelt). `forms.*`-Permissions + Bundles + Rollen-Mapping seeden.
11. **Querschnitt-Wiring (alle, DoD):** Sidebar-Eintrag „Forms" (INTEGRATION-CONTRACTS §1, Zeile 53), Breadcrumbs, `AuditEvent`/`FormLog`, `notify()` (M011) bei Submission, Search-Index (M24, falls suchbar), Feature-Flag pro Modul (M006), View-Model-Konvention (M013), Customer-Portal-Sichtbarkeit explizit konfigurieren.
## Notizen / Vorbehalte
- **„Reif" in der Inventur ist relativ:** Die Domain-Packages-Inventur listet Form-Widget als „Reif" — das stimmt für das **embeddable Widget**, aber gemessen am M05-„Google-Forms-Niveau"-Anspruch ist es ein **Bruchteil** des Scopes. Daher Reuse-Level **medium**, nicht high.
- **Provider-Diskrepanz Captcha:** BP nutzt Cloudflare **Turnstile**, M125 fordert explizit **hCaptcha (EU-konform statt Google-OAuth-Trigger)**. Architektur ist feature-flagged → Adapter-Tausch machbar, aber bewusste Entscheidung nötig.
- **Keine Signatur-Komponente:** Trotz Spec-Hinweis „Touch-Signatur via signature_pad (Boilerplate-Deps)" in M121-PLAN ist **kein** `signature_pad`/Signatur-Code in der Boilerplate vorhanden (nur Treffer in unrelated Files). Neuer Dependency erforderlich.
- **Speicher-Modell-Bruch:** BP speichert Felder als `definition`-JSON und Antworten als `payload`-JSON; Spec listet normalisierte `FormField`/`FormSubmissionAnswer`. Für Spalten-Konfiguration (5.8.3), Conditional-Display (M124) und CSV-Export pro Feld ist die normalisierte Variante deutlich tragfähiger — Architektur-Entscheidung früh treffen.
- **Layer-Umzug Pflicht:** BP-Builder/Routen sind nicht layer-getrennt (UI + Prisma im selben `apps/web`-Modul). HiveGrid-DoD blockt das — Component-Extraktion in `packages/ui-hivegrid` + VM-Loader nötig (MEMORY: `.server.ts`-Re-Export-Blocker beachten).
@@ -0,0 +1,142 @@
# Boilerplate-Abgleich: M06 — Booking / Termin-Buchung (Calendly-Style)
| Feld | Wert |
| --- | --- |
| **Modul-ID** | M06 (Block 6, Modul 6) |
| **Name** | Booking / Termin-Buchung (Calendly-Style) |
| **Milestones** | M126 (Buchungs-Seite + Time-Slot-Engine), M127 (Verfügbarkeits-Berechnung), M128 (Bestätigungs-Flow + Verknüpfungen), M129 (Stornierung + Verschiebung + No-Show) |
| **Pflichtenheft-Block** | block6.md §6.1–6.12 |
| **Detail-Spec** | `_bmad-output/specs/M06-booking.md` |
| **Reuse-Level** | **low** |
## Reuse-Verdikt (Kurzfassung)
Das **gesamte Booking-Domänen-Modell fehlt** in der Boilerplate. Es gibt **kein** Prisma-Modell für `BookingPage`, `BookingPageProvider`, `AvailabilityRule`, `AvailabilityException` oder `Appointment` — verifiziert mit `grep '^model (Booking|Appointment|Availability|Slot|Calendar|...)'` über alle 27 `prisma/schema/*.prisma`-Dateien (Treffer: 0). Auch die im Spec als Dependency genannte **Calendar-Sync (Modul 25, Google/MS)** existiert in der Boilerplate NICHT (kein `googleapis`, kein `@microsoft/microsoft-graph`, kein Calendar-Service). Die **Slot-Berechnungs-Engine** und der **Doppelbuchungs-Schutz** (`EXCLUDE USING gist`/`tstzrange`/`btree_gist` — 0 Treffer in `prisma/migrations`) sind komplett neu zu bauen.
Was die Boilerplate liefert, ist **ausschließlich das Querschnitt-Fundament** (Permissions/Audit/Notify/Idempotenz/Adapter/CatalogError/View-Models/Feature-Flags/i18n), der **Mail-Stack** (für Bestätigungs-/Storno-Mails inkl. `handlebars` als Template-Engine), das **Form-Widget** (für Intake-Form-Renderer/Submissions, deckt M5-Kopplung), das **Lead-Modell** (für die „neuer Lead bei unbekannter Email"-Verknüpfung) und Infrastruktur (Queue/Outbox/Redis-Cache für die Slot-Cache-TTL, Public-Site-Pattern, Storage). Die Geschäftslogik des Moduls — Slots, Verfügbarkeit, Provider-Zuteilung, Public-Booking-Flow, iFrame-Embed, .ics, Kalender-Sync — ist vollständig HiveGrid-Neubau.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `packages/foundation/src/permissions.ts` | `withTenantScope()`, `withPermission()`, `withOwnerCheck()`, `requirePermission()` — RLS-Tenant-Scope, Permission-Gate, Owner-Check für `bookings.read_own` (A.5/K.5); 404-statt-403 bei Denied | **1:1** |
| `packages/foundation/src/audit.ts` | `auditEvent()`, `withAuditMiddleware()` — Audit-Trail für Termin-/Booking-Page-Änderungen (AV #6); Hash-Kette über `AuditEvent` | **1:1** |
| `packages/foundation/src/notifications.ts` | `notify()`, `notifyMany()`, `bundleNotifications()` — interne Notifications (z.B. „Bestätigungsmail fehlgeschlagen — bitte manuell kontaktieren", I.3) statt Direkt-Mail | **1:1** |
| `packages/foundation/src/idempotency.ts` | `withIdempotency()` — replay-sicherer Booking-Submit (verhindert Doppel-POST des Public-Flows) | **1:1** |
| `packages/foundation/src/errors.ts` | `CatalogError`, `foundationErrors` (`notFound` 404, `forbidden`, `idempotencyConflict` 409) — Klartext-Diagnostik (LP-10): 404 für nicht-publizierte Page (G.4), 409 bei Slot-Race (G.3) | **als-vorlage** (HiveGrid-Catalog-Errors ergänzen, z.B. Slot-Konflikt, Sync-Fehler, Slug-Konflikt) |
| `packages/foundation/src/adapters.ts` | `defineAdapter()`, `getAdapter()` mit Status — Adapter-Pattern für Calendar-Sync (LP-11: Sync über Modul-25-Adapter, nicht direkt gegen Google/MS) | **als-vorlage** (Calendar-Adapter selbst existiert nicht) |
| `packages/view-models/src/index.ts` | `VMState<T>`, `ActionCallbacks<T>`, `RegistryGate`, `SidebarRegistryItem`, `ready/empty`, `assertI18nLabel` — VM-Status/Action-Vertrag für Listen-View (C), Detail-View (D), Editor (E), Verfügbarkeits-Page (F) | **1:1** |
| `packages/feature-flags/src/*` + `packages/foundation/src/flags.ts` | `evaluateFlag()` / `featureFlag()` — Modul ohne Deploy abschaltbar (DoD „Feature-Flag-fähig") | **1:1** |
| `packages/i18n/src/*` | Locale-Config (de/en), `formatDate`/`formatNumber`/ICU, `useTranslate` — alle UI-Texte + Datums-/Zeitformat in Endkunden-/Tenant-Timezone (A.3/G) | **1:1** |
| `packages/mail/src/mailersend.ts` + `src/queue.ts` + `src/templates-react/*` | MailerSend-Provider (EU) + Queued-Mail + React-Email-Layout-Primitives (BaseLayout/Header/Footer/Button mit `assertSafeUrl`) — Bestätigungs-/Storno-/Update-Mail (I, 6.5, 6.8.4/6.8.5) | **anpassen** (Booking-spezifische Templates neu; Layout 1:1) |
| `handlebars` (Root-Dependency, `package.json`) | Template-Engine für `confirmation_email_subject`/`_body` + `internal_calendar_event_title` mit `{{...}}`-Variablen (I.1/I.2) — löst die im Spec offene Frage „Liquid vs. Handlebars vs. Mustache" zugunsten Handlebars (bereits vorhanden) | **1:1** (Engine), **anpassen** (Variablen-Resolver für Booking-Kontext) |
| `packages/shared-store/src/*` (`createCacheStore`, TTL, Cross-Instance-Invalidation) | Redis-KV für Slot-Cache (A.6: 60s-TTL pro `(booking_page_id, date)` + Invalidation bei neuer/gelöschter Appointment etc.) | **1:1** (Engine), **anpassen** (Booking-Cache-Keys/Invalidation) |
| `packages/queue/src/*` + `packages/db/src/outbox.ts` (`OutboxEvent`) | BullMQ + transaktionaler Outbox-Publisher — async Kalender-Sync-Re-Try-Queue (J.3, 6.4.4), Outbox für Activity-/Sync-Side-Effects in der Submit-Transaktion | **1:1** (Infra), **anpassen** (Booking-Jobs/Event-Typen neu) |
| `packages/form-widget/src/*` + `prisma/schema/formWidget.prisma` (`Form`, `FormSubmission`) | Form-Definition/Renderer/Submission-Inbox inkl. `ipHash`, Honeypot, Origin-Allowlist — Intake-Form (G.2): Standard-Mini-Form ODER `intake_form_id`→Modul-5-Form; `FormSubmission` ist das von `Appointment.form_submission_id` referenzierte Modell | **anpassen** (Renderer wiederverwenden; Booking-Submit erzeugt `FormSubmission` + Activity `form_submitted`, J.5) |
| `apps/web/app/modules/formWidget/builder/*` (`FormBuilder.tsx`, `PreviewTab.tsx`, `SettingsTab.tsx`) | Editor-/Preview-/Settings-Tab-Muster — Vorlage für Booking-Page-Editor (E.2: sektionierte Anzeige, Live-Preview, Publish-Toggle, Embed-Generator) | **als-vorlage** |
| `prisma/schema/marketingLeads.prisma` (`Lead`, `LeadEvent`) | Lead-Modell — „unbekannte Email → neuer Lead mit Quelle Buchung" (J.1, 6.9.1). Achtung: `Lead` ist **operator-scoped, NICHT tenant-scoped** (`email @unique` global). HiveGrid braucht hier ggf. einen tenant-scoped Lead — Abgleich mit M047 (Lead-Inbox) nötig | **anpassen** / Klärung mit M047 |
| `prisma/schema/sites.prisma` (`Site`, `type = "booking"`, `serverResolution domain/path/subdomain`) | Multi-Site-Konfig-Record mit Branding/Copy + Host-Resolution — Vorlage für Public-Booking-Site-Auflösung (`/b/{tenant}/{slug}`, B/G). **Kein** Booking-Domänenmodell, nur Site-Routing/Branding-Hülle | **als-vorlage** |
| `packages/url/src/canonical.ts` | host-aware `appPath`/`appUrl` — Public-URL-Bau für Page-Liste (E.1 Copy-to-Clipboard) + Embed-Src (H.1) | **1:1** |
| `apps/web/app/components/ui/calendar.tsx` (`react-day-picker`) | Kalender-Primitive (date-fns-Locale, i18n) — Datum-Auswahl im Public-Slot-Picker (G.2) + Date-Range-Filter (C) | **anpassen** (Verfügbarkeits-Indikator/Ausgrauen pro Tag ergänzen) |
| `apps/web/app/components/ui/*` (shadcn: `table.tsx`, `tabs.tsx`, `multi-select.tsx`, `stepper.tsx`, `pagination.tsx`, `dialog.tsx`, `command.tsx`, `tables/ColumnSelector.tsx`, `tables/TablePagination.tsx`) | UI-Primitives — Listen-View (C: sortierbare Tabelle, Tabs, Filter, Spalten-Config, Cursor-Pagination), Multi-Step-Flow (G: Stepper), Provider-Multi-Select (E.2) | **1:1** (Primitives), **anpassen** (Booking-Pages bauen) |
| `apps/web/app/components/layouts/sidebars/shadcn/*` + `application/sidebar/SidebarItem.ts` + `AppSidebar.tsx` | Datengetriebene Sidebar mit Permission-/Feature-Gating — Sidebar-Gruppe „Arbeiten" → „Buchungs-Seiten" (INTEGRATION-CONTRACTS §1, M126-M129) | **1:1** (Muster); HiveGrid-Ziel ist `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` |
| `packages/exports/src/csv/*` + Worker `export.ts` | CSV-Export-Framework mit Plan-Row-Caps + Injection-Safety — CSV-Export gefilterter Buchungs-Liste (C: „Export als CSV") | **1:1** |
| `packages/storage/src/*` (S3/Scaleway, Signed-URLs) | EU-Storage — Datei-Uploads aus verknüpftem Intake-Form (falls Custom-Form Upload-Felder hat) | **1:1** |
| `packages/db/src/client.server.ts` + `transactions.server.ts` (`runDbTransaction`) | Prisma-Client-Boundary + Transaktionen — atomarer Submit (Appointment + FormSubmission + Lead/Contact-Link + Outbox-Event in einer Tx, J/G.3) | **1:1** |
| `packages/db/src/dsgvo/user-fk-map.ts` | DSGVO-FK-Karte — **Pflicht-Eintrag** für jeden neuen User-FK (`Appointment.provider_user_id`, `AvailabilityRule.tenant_user_id`, `BookingPageProvider.tenant_user_id`); CI-Test bricht sonst | **1:1** (erweitern, kein Skip) |
| `packages/db/src/search/indexed-entities.ts`-Pattern + `packages/search/*` (Meilisearch, tenant-scoped) | Search-Index-Registry — falls Buchungen global suchbar sein sollen (DoD „Search-Index, falls suchbar") | **als-vorlage** |
| `packages/ui-app` + `packages/ui-mock` + `apps/web/app/components/ui/hot-swap/*` | Shell-Registry-Factories + Hot-Swap-/Visual-Mock — DoD „Hot-Swap-Drill grün" + „Visual-Regression-Snapshot" | **1:1** (Test-Infra) |
---
## Vorhandene Daten-Modelle (konkret)
**Booking-Domäne: NICHTS vorhanden.** Verifiziert: kein `model BookingPage|BookingPageProvider|AvailabilityRule|AvailabilityException|Appointment` in irgendeiner `prisma/schema/*.prisma`-Datei (`grep '^model'`-Treffer 0). `prisma/schema/app.prisma` ist der leere Platzhalter (`// custom database models`) — vorgesehener Ort für die neuen HiveGrid-Booking-Modelle.
Indirekt nutzbare / referenzierte Bestands-Modelle:
| Modell | Pfad | Rolle für M06 |
| --- | --- | --- |
| `User`, `TenantUser`, `Tenant` | `prisma/schema/accounts.prisma` | Provider = `TenantUser`; Tenant-Scope-Anker; `User`-FK für `provider_user_id`/`tenant_user_id` |
| `Form`, `FormSubmission` | `prisma/schema/formWidget.prisma` | `intake_form_id`-Ziel (Custom-Intake) + `Appointment.form_submission_id` (Intake-Daten der Buchung) |
| `Lead`, `LeadEvent` | `prisma/schema/marketingLeads.prisma` | „neuer Lead bei unbekannter Email" (operator-scoped — Klärung mit M047 nötig) |
| `Site` | `prisma/schema/sites.prisma` | `type="booking"`-Site-Config + Host-Resolution (Routing/Branding-Hülle, KEIN Booking-Modell) |
| `AuditEvent`, `IdempotencyKey` | `prisma/schema/core.prisma` | Audit-Trail + idempotenter Submit |
| `OutboxEvent` | `prisma/schema/outbox.prisma` | transaktionale Side-Effects (Sync/Activity/Mail) |
| `NotificationDelivery`, `UserNotificationPreference` | `prisma/schema/notifications.prisma` | interne Hinweise an Provider (Mail-Fehler etc.) |
| `FeatureFlag` | `prisma/schema/featureFlags.prisma` | Modul-Abschaltung ohne Deploy |
| `Email`, `OutboundEmail` | `prisma/schema/email.prisma` | E-Mail-Infrastruktur (Bestätigungs-/Storno-Mail-Tracking; Tenant-Sending-Domain = Modul 9 separat) |
**Neu anzulegen (in `app.prisma`):** `BookingPage`, `BookingPageProvider`, `AvailabilityRule`, `AvailabilityException`, `Appointment` — komplett gemäß Spec A.1–A.5, inkl. Indizes und Enums (`assignment_strategy`, `status`, `cancelled_by_kind`, `kind`, `day_of_week`).
---
## Lücken / komplett neu zu bauen
1. **Komplettes Booking-Daten-Modell** (A.1–A.5): `BookingPage`, `BookingPageProvider` (M:N), `AvailabilityRule`, `AvailabilityException`, `Appointment` — inkl. aller Felder/Enums/Indizes. Forward-Only-Migration mit Inverse-SQL als Kommentar.
2. **Doppelbuchungs-Schutz auf DB-Ebene** (G.3): `EXCLUDE USING gist` mit `tstzrange` pro Provider — `btree_gist`-Extension muss aktiviert werden (pgvector-Migration zeigt das Muster für Extension-Aktivierung; gist/tstzrange selbst: 0 Treffer in `prisma/migrations`).
3. **Slot-Berechnungs-Engine** (A.6, M127): Layer-Modell Standard-Regel → Exceptions → bestehende Appointments (+Buffer) → externe Calendar-Events → Plantafel → min/max-Vorlauf → Slot-Increment → Assignment-Strategy-Merge. Server-side, gecacht 60s in Redis (`shared-store` liefert nur die Cache-Primitive, nicht die Logik).
4. **Provider-Zuteilungs-Strategien** (A.2): `round_robin` (wenigste Buchungen letzte 30 Tage), `specific_provider_choice`, `first_available` — reine HiveGrid-Logik.
5. **Public-Booking-Flow** (G, M126): Multi-Step-Wizard (Slot → Intake → Bestätigung), Verfügbarkeits-Kalender mit ausgegrauten Tagen, Timezone-Wechsler, Race-Condition-Handling mit Klartext + aktualisierten Alternativen, .ics-Add-to-Calendar. Kein Bestandscode.
6. **Public-Route ohne Login + Embed** (B/H): `/b/{tenant}/{slug}` und `/b/{tenant}/{slug}/embed`; Embed ohne Branding-Header + `X-Frame-Options: ALLOWALL`/CSP für `/embed`. Boilerplate-Public-Routes existieren (Forms `/f/...`), aber Booking-Routen + Embed-CSP-Setup sind neu.
7. **Embed-Code-Generator** (H.3): iFrame-Snippet + Copy-to-Clipboard + Klartext-Hilfe. Optional JS-Snippet-Loader (`booking.js`) — Phase-2-Tendenz.
8. **Calendar-Sync (Modul 25)** — **fehlt komplett in der Boilerplate**: kein interner Kalender (`CalendarEvent`), kein Google/MS-Adapter, kein `googleapis`/MS-Graph-Dep, kein bidirektionaler Sync, keine externen Events als Slot-Blocker (A.6/J.2/J.3). M118–M120 (Calendar) sind harte Vorbedingung von M126/M127/M128 und müssen vorher gebaut sein. M06 konsumiert sie nur über den Adapter (LP-11).
9. **Bestätigungs-/Storno-/Update-Mail-Inhalt** (I, M128/M129): Booking-spezifische Handlebars-Templates + Variablen-Resolver (`appointment.*`, `booking_page.*`, `provider.*`, `contact.*`, `tenant.company_name`); Default-Seeds (SHK-Branche); Versand über Tenant-Sending-Domain (Modul 9) mit Fallback `noreply@hivegrid.de`. Mail-Stack/Layout vorhanden, Templates neu.
10. **.ics-Generator** (G.3/6.5.3): Add-to-Calendar-Datei für Endkunde — keine `ics`/`ical-generator`-Dependency vorhanden.
11. **Verknüpfungs-Logik** (J, M128): Email-Match gegen Contact → `contact_id` ODER neuer Lead; `FormSubmission`-Erzeugung; Activity-Typen (`appointment_scheduled/held/cancelled/rescheduled/no_show`, `form_submitted`) via `logActivity` (Activity-Service `@hivegrid/activity` ist HiveGrid-M045, nicht Boilerplate). Deal-Verknüpfung wenn Contact in Deal.
12. **Interne Termin-Verwaltung** (C/D/M129): Listen-View (Filter/Tabs/Bulk-Cancel/CSV-Export/Spalten-Config/Cursor-Pagination), Detail-View (Sektionen, Inline-Notes, Sync-Status-Badge, Action-Buttons), Status-Transitions (`held`/`no_show`/`cancelled`/`rescheduled`), Reschedule mit Slot-Picker, Provider-Re-Assignment mit Kalender-Resync + Update-Mail.
13. **Verfügbarkeits-Verwaltung-UI** (F, M127): Wochen-Editor (mehrere Fenster/Tag, Auto-Merge überlappender Fenster), Ausnahmen-Editor, Timezone pro Regel, Bulk „diese Woche nicht verfügbar" (7 Exceptions).
14. **Booking-Page-Editor** (E, M126): sektionierter Editor, Live-Slug-Validation (Eindeutigkeit + Reserved-Words + Auto-Vorschlag), Markdown-Editor, Provider-Multi-Select mit Drag&Drop-`display_order`, Intake-Form-Dropdown, Publish-Toggle, Duplizieren/Archivieren/Hard-Delete mit Klartext-Warnungen.
15. **Booking-Permission-Set** (K): `bookings.read_own / read_all / write / admin` + Bundle-Mapping in `packages/entitlements/src/permissions.ts` (HiveGrid-Konvention) — Boilerplate hat RBAC-Modelle (`Permission`/`Role`), aber keine Booking-Keys.
16. **`packages/ui-hivegrid` + `packages/ui-subframe`-Booking-Pages**: In der Boilerplate ist `ui-hivegrid` nicht vorhanden und `ui-subframe` ist leerer Platzhalter (`{ status: "empty" }`). Subframe-Design + Adapter-Integration sind komplett neu (User-Memory: jede Route via Subframe).
---
## Empfohlenes Vorgehen (Reihenfolge)
**Phase 0 — Vorbedingungen prüfen (blockierend):**
- M118–M120 (Calendar: interner `CalendarEvent` + Google/MS-Sync-Adapter) müssen existieren, sonst sind A.6 (externe Events als Slot-Blocker) und J.2/J.3 nicht baubar. Calendar-Sync ist **nicht** in der Boilerplate — vor M06 sicherstellen.
- M042/M047 (Contact/Lead-Inbox) für die Submit-Verknüpfung (J.1); Lead-Scope (operator vs. tenant) mit M047 klären.
- M5 (Form-Builder) für `intake_form_id`-Renderer (G.2) — Boilerplate-`form-widget` ist die Basis.
**Phase 1 — Querschnitt 1:1 übernehmen (kein Neubau):**
1. `foundation` (permissions/audit/notify/idempotency/errors/adapters) + `view-models` + `feature-flags` + `i18n` + `url` als Service-Imports verdrahten.
2. `db`-Boundary (`client.server`, `runDbTransaction`, Outbox-Publisher) + `queue` + `shared-store` (Cache) als Infra einziehen.
3. Mail-Stack (`@toolctrl/mail`) + `handlebars` als Template-Engine festschreiben (löst offene Spec-Frage).
**Phase 2 — Daten-Modell + DB-Schutz (M126-Fundament):**
4. Neue Modelle in `prisma/schema/app.prisma` (A.1–A.5) + Forward-Only-Migration; `user-fk-map.ts` für alle neuen User-FKs ergänzen (CI-Pflicht).
5. `EXCLUDE USING gist`/`tstzrange` + `btree_gist`-Extension für Doppelbuchungs-Schutz (G.3) — Muster aus pgvector-Extension-Migration.
6. Booking-Permission-Keys + Bundles (K) in `entitlements/permissions.ts`.
**Phase 3 — Engine + Public-Flow (M126/M127, der Neubau-Kern):**
7. Slot-Berechnungs-Engine (A.6) als pure Domain-Logik in `app/domain/**` (keine React/Prisma-UI-Kopplung) + Redis-Cache via `shared-store` + Invalidation.
8. Provider-Zuteilungs-Strategien (A.2).
9. Verfügbarkeits-Verwaltung (F) und Booking-Page-Editor (E) — Editor/Preview-Muster aus `formWidget/builder` als Vorlage.
10. Public-Booking-Flow (G) inkl. Race-Condition-Handling + .ics-Generator (neue `ics`/`ical-generator`-Dependency); Public-Route `/b/{tenant}/{slug}` (+`/embed` mit CSP), Site-Resolution via `sites.prisma`-Muster.
**Phase 4 — Verknüpfungen + Bestätigung (M128):**
11. Submit-Transaktion: Appointment + FormSubmission (`form-widget`) + Contact-Match/Lead-Erzeugung + Outbox-Events (Activity/Sync/Mail) atomar.
12. Bestätigungs-Mail-Templates (Handlebars + Booking-Variablen-Resolver) über Tenant-Sending-Domain (Modul 9) mit Fallback; Kalender-Eintrag intern + extern (Modul-25-Adapter) + Re-Try-Queue.
**Phase 5 — Interne Verwaltung (M129):**
13. Listen-View (C) mit shadcn-Tabelle/Tabs/Filter/CSV-Export (`exports`).
14. Detail-View (D) + Status-Transitions + Storno/Reschedule/Re-Assignment mit Auto-Mails (Storno/Update) und Kalender-Resync.
**Querschnitt durchgehend (DoD/INTEGRATION-CONTRACTS):** Sidebar-Eintrag „Buchungs-Seiten" (Gruppe Arbeiten), Breadcrumbs, alle Texte als i18n-Keys, `withTenantScope`+`withPermission`, Audit auto, `logActivity` (M045), `notify` (M011), Feature-Flag, Permission-Denied → 404, Unit-Test (VM + Zod), Smoke-Test, Hot-Swap-Drill, Visual-Snapshot, Forward-Only-Migration mit Inverse-SQL. Subframe-Design pro Route (User-Memory) + Adapter in `packages/ui-hivegrid/src/adapters/`.
---
## Anti-Erfindungs-Hinweise (faktisch verifiziert)
- **Kein** Booking/Appointment/Availability/Calendar/Slot-Prisma-Modell in der Boilerplate (`grep '^model ...'` über alle 27 Schemas: 0).
- **Kein** Calendar-Sync (Google/MS): kein `googleapis`/`@microsoft/microsoft-graph`-Dep, kein Calendar-Service in `apps/web`/`apps/worker`.
- **Kein** `ics`/`ical-generator`-Dep (Add-to-Calendar neu).
- **Kein** `EXCLUDE USING gist`/`tstzrange`/`btree_gist` in `prisma/migrations` (Doppelbuchungs-Schutz neu).
- **`handlebars` IS vorhanden** (Root-`package.json`) — direkt als Mail-/Title-Template-Engine nutzbar.
- `Site.type = "booking"` in `sites.prisma` ist nur ein Site-Config/Routing-Record, **kein** Booking-Domänenmodell.
- `core.prisma.Event` ist ein generisches Webhook-/Domain-Event, **kein** Kalender-Event.
- `packages/ui-subframe` ist leer; `packages/ui-hivegrid` existiert in der Boilerplate nicht — beides HiveGrid-Neubau.
- `Lead` ist operator-scoped (`email @unique` global), nicht tenant-scoped — Verknüpfungs-Scope mit M047 klären.
@@ -0,0 +1,117 @@
# Boilerplate-Abgleich: M07 — Angebot + Signatur
| Feld | Wert |
| --- | --- |
| **Modul-ID** | M07 (Modul 7) |
| **Name** | Angebot + elektronische Signatur |
| **Milestones** | M130 (Angebot-Erstellung / PDF-Upload + Send-Wizard), M131 (PandaDoc/DocuSign via Apps-Engine + Status-Webhooks), M132 (Status-Tracking + Auto-Stage-Wechsel + Detail-Sicht) |
| **Pflichtenheft-Block** | Block 6, Modul 7 (`_bmad-output/pflichtenheft/blocks/block6.md` §7.1–7.9) |
| **Detail-Spec** | `_bmad-output/specs/M07-angebot-signatur.md` |
| **Reuse-Level** | **low** |
## Reuse-Verdikt (Kurzfassung)
Das **Signatur-Fachdomäne selbst existiert NICHT** in der Boilerplate: kein PandaDoc/DocuSign-Adapter, kein `DealSignatureDocument`/`-Signer`/`-StatusTransition`, kein `SignatureProviderConnection`, kein `SignatureWebhookEvent`, und — fundamental — **kein Deal/Sales/Pipeline-Modell** (M3), an dem das Angebot überhaupt hängt. Verifiziert per Grep über alle `*.prisma`/`*.ts`/`*.tsx`: die einzigen `signature`-Treffer sind (a) HMAC-Webhook-Verify für Stripe/MailerSend, (b) Ed25519-Plugin-Signaturen — beides themenfremd.
Wiederverwendbar ist ausschließlich das **Querschnitt-Fundament + Infrastruktur-Pattern**: Adapter-Registry, SSRF-sicherer Outbound-HTTP-Client, Inbound-Webhook-Handler-Vorlage (Stripe/MailerSend), HMAC-Verify, Idempotenz, Transactional-Outbox, File-Storage (Source-/Signed-PDF), `appsEngine.prisma` (`App`/`DataConnection`) als OAuth-Connection-Träger, sowie der komplette UI-/View-Model-/i18n-/Notification-Stack. Damit ist M07 **kein "Felder einfüllen", sondern ein Neubau auf solidem Fundament** — daher `low` (nicht `none`, weil die Querschnitt-Hülle und das Adapter-/Webhook-Pattern erheblich Arbeit sparen).
---
## Übernehmbar aus Boilerplate
> Boilerplate-Wurzel (NUR LESEN): `/Users/raffaelsheikh/Documents/GitHub/boilerplate`
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `packages/foundation/src/adapters.ts` | `defineAdapter()` / `getAdapter()` + `AdapterStatus` — Registry für den `SignatureProvider`-Interface-Pattern (PandaDoc/DocuSign) | **1:1** als Registry-Mechanik; die konkreten Provider-Adapter (`createDocument`/`sendDocument`/`getDocumentStatus`/`revokeDocument`/`handleWebhook`) sind neu zu schreiben |
| `packages/foundation/src/outbound-http.ts` | `fetchOutboundHttpWithRedirects()` mit Timeout, Redirect-Limit, SSRF-Schutz (Blockliste interner Hosts), Response-Byte-Cap | **1:1** für alle PandaDoc/DocuSign-REST-Calls (PDF-Upload, Send, Status-Poll, Revoke, Reminder) |
| `packages/foundation/src/idempotency.ts` | `IdempotencyClient`/`IdempotencyResponse` + Catalog-Errors, gestützt auf `IdempotencyKey`-Modell | **1:1** für idempotente Webhook-Verarbeitung (Spec verlangt M007-Idempotenz im Webhook-Empfänger) |
| `packages/foundation/src/errors.ts` | `CatalogError` / `foundationErrors` — Klartext-Diagnostik-Basis (AGENTS.md DoD §22, LP-10/AV#1) | **als-vorlage** — HiveGrid-Klartext-Texte (PDF >25 MB, Signer ohne Email, Provider nicht erreichbar) als eigene Catalog-Einträge ergänzen |
| `packages/foundation/src/{audit,notifications,permissions,flags}.ts` | Querschnitt-Hooks: Audit, `notify`, Permission-Checks, Feature-Flags | **1:1** als Service-Aufrufe (DoD-Pflicht: Audit, Notification via `notify()`, Permission-Filter, Feature-Flag-fähig) |
| `apps/api/src/billing/stripe-webhook-route.ts` | Vollständiger **Inbound-Webhook-Handler**: Signatur-Header lesen → HMAC-Verify → 400/503 bei invalid/missing → `$transaction` mit Outbox-Publish atomar → Metrik | **als-vorlage** — exakte Blaupause für `/api/v1/webhooks/pandadoc/{tenant_id}/{connection_id}` und `…/docusign/…` (Provider-spezifischer Verify + Event-Mapping einsetzen) |
| `apps/api/src/email-sequences/provider-webhook-route.ts` | Inbound-Provider-Webhook mit Event-Type-Mapping (`MAILERSEND_BOUNCE_EVENT_TYPES`-Set → Domain-Aktion) | **als-vorlage** — gleiche Mechanik für `document.viewed/signed/declined/completed/expired` → `DealSignatureDocument`/`-Signer`-Update |
| `apps/api/src/webhooks/mailersend-signature.ts` | `verifyMailerSendWebhookSignature()` (Header-Kandidaten, konstantzeit) | **als-vorlage** für provider-spezifischen Webhook-Verify (PandaDoc/DocuSign HMAC) |
| `packages/api-client-ts/src/webhooks.ts` | `verifyWebhookSignature()` / `signWebhookPayload()` — generisches konstantzeit-HMAC-SHA256 mit Replay-Window | **1:1**/**anpassen** als HMAC-Verify-Helper, falls Provider HMAC-SHA256 nutzen |
| `apps/api/src/route-access-inventory.ts` | Auth-Klasse `webhook-signature` für Inbound-Webhook-Routen (Quelle der Wahrheit der API-Auth-Matrix) | **1:1** — Signatur-Webhook-Routen unter `webhook-signature` registrieren |
| `prisma/schema/appsEngine.prisma` — `App`, `DataConnection`, `AppSyncRun` | Apps-Engine-Daten-Sync-Modell: `App` (`@@unique([tenantId, type])`, `config`-JSON), `DataConnection` (verschlüsselte Credentials, `type` "oauth2"/"webhook"), `AppSyncRun` (Status/Mode/Metriken/Rollback) | **anpassen** — `SignatureProviderConnection` (Spec Block A) kann auf `App`/`DataConnection` aufsetzen (`data_connection_id` referenziert M33a). Verschlüsselte OAuth-Tokens passen in `DataConnection.config` |
| `packages/storage/src/index.ts` + `signed/index.ts` | `createS3StorageAdapter()` (Scaleway-S3), `createSignedDownloadUrl`/`createSignedUploadUrl`, Virus-Scan, Quarantäne | **1:1** für Source-PDF-Upload (M23) + automatischen Download des signierten PDF nach `document.completed` (Spec §7.4.6) |
| `prisma/schema/core.prisma` — `FileObject` | Datei-Metadaten (provider/bucket/key/contentType/byteSize/checksum/signedUrl-Timestamps) | **1:1** als Speicher-Referenz für `source_pdf_file_id` / `signed_pdf_file_id` (sofern M23/Files dieses Modell adoptiert) |
| `prisma/schema/outbox.prisma` + `packages/db/src/outbox.ts` (`publishOutboxEvent`) + `apps/worker/src/outbox/event-router.ts` | Transactional-Outbox: atomares Event-Publish in `$transaction`, longest-prefix-Routing (`webhook.`, `notification.`, `mail.`, …) | **1:1** für `signature.sent/signed/declined/expired`-Trigger an M17 (Spec Block G) und Notification-Fan-out an Vertriebler |
| `apps/worker/src/jobs/form-submission-webhook-delivery.ts` + `packages/queue` (`WebhookDeliveryJob`, `enqueueJob`, `buildIdempotentJobId`) | Worker-Job-Pattern für (a) Reminder-Versand über Provider, (b) Status-Refresh-Poll, (c) Signed-PDF-Download als Hintergrund-Job | **als-vorlage** — neuen Worker-Job + Queue-Payload für Signatur-Async-Arbeit anlegen |
| `packages/queue/src/{names,payloads,producers,priority}.ts` | BullMQ-Wrapper: typisierte Job-Payloads, `enqueueJob()`, `buildIdempotentJobId()`, plan-basierte Prioritäten | **1:1**/**anpassen** — neue `signature.*`-Job-Payloads in die Map ergänzen |
| `prisma/schema/notifications.prisma` (`NotificationDelivery`, `DeviceToken`, `UserNotificationPreference`) + `packages/realtime` + `apps/worker` notification-Job | Push/In-App-Notification-Stack (Spec §7.4: Push an Vertriebler bei Status-Änderung) | **1:1** via `notify()`-Service (M011), NIE direkt Mail/Push |
| `packages/view-models/src/index.ts` | `VMState<T>`, `ActionCallbacks<T>`, `SidebarRegistryItem`, `BreadcrumbRegistryItem`, `RegistryGate{permission,flag}` — die VM-/Action-Callback-Konvention (M013/M014) | **1:1** — Signatur-VMs (Document-Liste, Send-Wizard, Detail) bauen auf diesen Typen |
| `packages/ui-app/src/index.ts` | AppShell + `createSidebarRegistry`/`createBreadcrumbRegistry`/`createMobileRegistry`/`createPortalRegistry`/`createShellRegistries`, `EmptyPage` | **1:1** — Reachability-Wiring (Sidebar-Eintrag, Breadcrumb, Empty-State) erfolgt über diese Registries. **Hinweis:** Paket heißt `@toolctrl/ui-app`, nicht das in AGENTS.md genannte `ui-hivegrid` — Naming-Diskrepanz (siehe Lücken) |
| `packages/ui-subframe` + `packages/ui-mock` | Subframe-Adapter-Ziel + Hot-Swap-Mock (M015/M017) | **als-vorlage** — Send-Wizard/Detail-Sicht via Adapter integrieren; Mock-Variante für Hot-Swap-Drill |
| `packages/i18n/src/index.ts` + `useTranslate.ts` (ICU) | i18n-Engine mit ICU + `useTranslate` (M003) | **1:1** — alle UI-Texte + Klartext-Fehler als i18n-Keys |
| `packages/feature-flags/src/{evaluate,cache,publisher}.ts` + `prisma/schema/featureFlags.prisma` | Feature-Flag-Engine (M006) — Modul ohne Deploy abschaltbar (DoD-Pflicht) | **1:1** — `featureFlag('signature', tenant)`-Gate |
| `packages/db/src/{client.server,transactions.server,audit-hash}.ts` + `prisma/schema/app.prisma` | DB-Boundary, `runDbTransaction`, Audit-Hash-Kette; `app.prisma` = leerer Platzhalter für HiveGrid-eigene Modelle | **1:1** Infrastruktur; **`app.prisma` ist der vorgesehene Ort** für alle neuen M07-Modelle |
| `prisma/schema/permissions.prisma` (`Role`/`Permission`/`RolePermission`/`UserRole`) | RBAC-Fundament | **1:1** — `signature.read/send/revoke/admin` (Spec Block H) als `Permission`-Einträge + Rollen-Defaults seeden |
| `apps/api` Error-Envelope + Request-ID-Middleware (`middleware/error-envelope.ts`, `request-id.ts`) | Konsistentes API-Fehler-Envelope (`code`/`message`/`requestId`) für Webhook-Routen | **1:1** |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
**Direkt relevant / als Träger nutzbar:**
- `prisma/schema/appsEngine.prisma` → `App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem` — Apps-Engine-Connection + Sync-Lauf-Tracking. Kandidat-Basis für `SignatureProviderConnection` (über `data_connection_id`) und ggf. Status-Sync-Runs.
- `prisma/schema/core.prisma` → `FileObject` (Source-/Signed-PDF-Metadaten), `IdempotencyKey` (Webhook-Idempotenz), `AuditEvent` (tamper-evidente Hash-Kette, Audit-Trail für Status-Transitions — Spec AV#6), `Event` + `EventWebhookAttempt` (**outbound**-Webhook-Delivery, NICHT inbound — Achtung), `Credential`.
- `prisma/schema/notifications.prisma` → `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` — Push/In-App an Vertriebler.
- `prisma/schema/outbox.prisma` → `OutboxEvent` — atomare `signature.*`-Events.
- `prisma/schema/permissions.prisma` → `Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser` — Permissions Block H.
- `prisma/schema/featureFlags.prisma` → `FeatureFlag`, `FeatureFlagFilter`, `FeatureFlagAudit`.
- `prisma/schema/queue.prisma` → `QueueFailedJob` (DLQ), `ExportJob`.
- `prisma/schema/app.prisma` → **leerer Platzhalter** (`// custom database models`) — vorgesehener Ort für alle neuen M07-Modelle.
**NICHT vorhanden (Kern der Lücke):**
- Kein `Deal` / `Sale` / `Pipeline` / `Verkaufsfall` / `Stage` / `Opportunity`-Modell (verifiziert per Grep). M07 hängt komplett an M3 (M059–M064), das ebenfalls neu gebaut wird. → `deal_id`, Auto-Stage-Wechsel und Activity-Stream-Anker existieren noch nicht.
- Kein `SignatureProviderConnection`, `DealSignatureDocument`, `DealSignatureSigner`, `DealSignatureStatusTransition`, `SignatureWebhookEvent` (alle aus Spec Block A) — komplett neu.
- Kein `Contact`/`Kontakt`-Modell (für `linked_contact_id` / Signer-Vorbefüllung aus M1) — neu in M042–M046.
- Kein Activity-Stream-Modell (`logActivity` / M045) — neu.
---
## Lücken / komplett neu zu bauen
1. **Daten-Modell (Block A) — komplett neu** in `prisma/schema/app.prisma`:
- `SignatureProviderConnection` (tenant, `data_connection_id`→`DataConnection`, `provider` pandadoc|docusign, `is_default`)
- `DealSignatureDocument` (deal_id, provider_connection_id, provider_document_id, title, status-Enum draft|sent|viewed|signed|declined|expired|revoked, sent/signed/expires_at, source_pdf_file_id, signed_pdf_file_id, archived_at)
- `DealSignatureSigner` (document_id, position, email/name/role, linked_contact_id, status pending|viewed|signed|declined, signed_at, ip_at_signing, user_agent_at_signing)
- `DealSignatureStatusTransition` (from/to_status, transitioned_at, triggered_by user|provider_webhook|system_timeout, metadata) — Audit-Historie
- `SignatureWebhookEvent` (provider_connection_id, received_at, event_type, payload, signature_validation_status, processed_at) — Inbound-Log
- Pflicht-Begleitung: `packages/db/src/dsgvo/user-fk-map.ts` ergänzen (sonst CI-Bruch), Forward-Only-Migration mit Inverse-SQL-Kommentar.
2. **PandaDoc-Provider-Adapter** — neu: `createDocument(pdf, signers)`, `sendDocument`, `getDocumentStatus`, `revokeDocument`, `handleWebhook`, OAuth2-Connect über M33a, Reminder-Email-Trigger. Nur die Adapter-Registry-Mechanik (`defineAdapter`) + Outbound-HTTP-Client sind vorhanden.
3. **DocuSign-Provider-Adapter** — neu (analog).
4. **Inbound-Webhook-Routen** `/api/v1/webhooks/pandadoc/{tenant_id}/{connection_id}` + `…/docusign/…` — neu, aber Stripe/MailerSend-Route als 1:1-Blaupause. Provider-spezifischer HMAC-Verify + Event-Mapping (`document.viewed|signed|declined|completed|expired`) neu.
5. **Send-Flow-Wizard (Block C, 5 Schritte)** im Deal-Detail — neue UI (PDF-Picker reuse aus M046, Signer-Konfig, Sequenz seriell/parallel, Provider-Auswahl, Send) + View-Models + Action-Callbacks.
6. **Status-Tracking-Verarbeitung (Block D)** — neuer Worker-/Service-Pfad: Webhook → Status-Update + StatusTransition + Activity-Stream + Notification + (bei completed) Signed-PDF-Download in M23 + `signature.signed`-Trigger für M17.
7. **Auto-Stage-Wechsel (M132)** — neu, hängt an M3-Deal-Stage (existiert noch nicht); Tenant-Konfig-Toggle „Auto-Stage-Wechsel" neu.
8. **Multi-Signer-Sequenz-Logik (Block E)** seriell/parallel — neu.
9. **Detail-Sicht im Deal (Block D/§7.5)**: Document-Liste, Pro-Signer-Status, Original-/Signed-PDF-Download, Reminder/Revoke/Resend/Refresh-Operationen — neue UI.
10. **Permissions seeden**: `signature.read/send/revoke/admin` + Rollen-Defaults (GF→admin; Vertrieb→read+send+revoke).
11. **Activity-Stream-Wiring (`logActivity`/M045)** — Service existiert in der Boilerplate nicht als HiveGrid-`@hivegrid/activity`; HiveGrid baut M045 selbst (außerhalb M07-Scope, aber Abhängigkeit).
12. **Naming-Diskrepanz Boilerplate↔HiveGrid-Contracts**: AGENTS.md/INTEGRATION-CONTRACTS referenzieren Pakete `@hivegrid/*` (`packages/ui-hivegrid`, `@hivegrid/notifications`, `@hivegrid/activity`, `@hivegrid/audit`) und `sidebar.config.ts`; in der Boilerplate heißen sie `@toolctrl/*` (`ui-app`, `foundation`, `view-models`) mit Registry-Funktionen statt `sidebar.config.ts`. → Vor M07 klären, ob die EU-Sterilisierung/Rebranding `@toolctrl/*`→`@hivegrid/*` bereits erfolgt ist oder M07 die Boilerplate-Namen nutzt.
---
## Empfohlenes Vorgehen (Reihenfolge)
**Vorbedingungen (außerhalb M07, müssen stehen):** M3-Deal-Modell (M059–M064), M23-Files (M052–M055), M33a-Apps-Engine-Adapter (M008–M010), M045-Activity-Stream, M001-Audit, M011-Notifications, M002-Error-Catalog. M07 dockt daran an — ohne Deal-Modell kein `deal_id`.
1. **Daten-Layer zuerst (M130-Basis):** Die 5 Block-A-Modelle in `prisma/schema/app.prisma` definieren; `SignatureProviderConnection` auf `appsEngine.prisma`-`DataConnection` aufsetzen (OAuth-Token in `config` verschlüsselt). `FileObject` für Source-/Signed-PDF referenzieren. `user-fk-map.ts` ergänzen, Forward-Only-Migration + Inverse-SQL-Kommentar. Permissions seeden.
2. **Querschnitt 1:1 übernehmen, nicht nachbauen:** `foundation` (adapters/outbound-http/idempotency/errors/audit/notifications/permissions/flags), `db` (client/transactions/outbox), `view-models`, `ui-app`-Registries, `i18n`, `feature-flags`, `storage`, `queue`, `notifications`. Feature-Flag `signature` als erstes Gate.
3. **M130 — Send-Flow-UI:** 5-Schritt-Wizard im Deal-Detail. PDF-Picker aus M046 reusen. VMs nach `view-models`-Konvention, Action-Callbacks, Subframe-Adapter, i18n + Klartext-Catalog (25-MB-/Email-/Provider-Hinweise). Sidebar/Breadcrumb-Wiring via `ui-app`-Registries (bzw. Inline-Verlinkung aus Deal — Modul ist deal-kontextuell, kein eigener Top-Level-Sidebar-Eintrag nötig, das im Milestone dokumentieren).
4. **M131 — Provider-Adapter + Webhooks:** PandaDoc zuerst (Standard), DocuSign analog, je via `defineAdapter` + `fetchOutboundHttpWithRedirects`. Inbound-Webhook-Routen nach Stripe-/MailerSend-Blaupause (`webhook-signature`-Auth-Klasse, HMAC-Verify, `$transaction`+Outbox, Idempotenz via `IdempotencyKey`). `SignatureWebhookEvent`-Log schreiben.
5. **M132 — Status-Tracking + Auto-Aktionen:** Worker-Job (nach `form-submission-webhook-delivery`-Vorlage) für Signed-PDF-Download (`storage` signed-URL → `FileObject` → M23), StatusTransition-Audit, `notify()` an Vertriebler, `signature.*`-Outbox-Events für M17, optional Auto-Stage-Wechsel (Tenant-Toggle, hängt an M3-Stage). Detail-Sicht im Deal mit Reminder/Revoke/Resend/Refresh.
6. **DoD-Abschluss:** Unit-Tests für VM-Builder (Zod), Smoke-Test (`e2e/smoke/signature.spec.ts`), Hot-Swap-Drill gegen `ui-mock`, Visual-Regression-Snapshot, Permission-Denied→404, Empty-/Loading-States mit Klartext, Rollback-Pfad dokumentiert.
---
## Quellen (verifiziert per Read/Grep am 2026-06-04)
- Boilerplate: `prisma/schema/{appsEngine,core,notifications,outbox,permissions,featureFlags,queue,app}.prisma`; `packages/foundation/src/{adapters,outbound-http,idempotency,errors}.ts`; `packages/{view-models,ui-app,i18n,feature-flags,storage,queue}/src/`; `apps/api/src/billing/stripe-webhook-route.ts`, `apps/api/src/email-sequences/provider-webhook-route.ts`, `apps/api/src/webhooks/mailersend-signature.ts`, `apps/api/src/route-access-inventory.ts`; `packages/api-client-ts/src/webhooks.ts`; `apps/worker/src/outbox/event-router.ts`.
- Negativ-Befund (Grep, kein Treffer als Domäne): `pandadoc|docusign|esignature|DealSignature|SignatureProviderConnection`; `model (Deal|Sale|Pipeline|Verkauf|Opportunity|Stage)`.
- HiveGrid: `_bmad-output/specs/M07-angebot-signatur.md`, `_bmad-output/pflichtenheft/blocks/block6.md`, `docs/milestones/M13{0,1,2}-*/PLAN.md`, `AGENTS.md`, `docs/INTEGRATION-CONTRACTS.md`, `docs/boilerplate-inventory/{domain-packages,integrations-ki,data-layer,apps-infra}.md`.
</content>
@@ -0,0 +1,125 @@
# Boilerplate-Abgleich: M09 — Email-Inbox + Sync
| Feld | Wert |
|---|---|
| Modul-ID | M09 |
| Name | Email Inbox + Sync (Gmail/Outlook) |
| Milestones | M096 (Gmail/Outlook-OAuth + Sende-Domain), M097 (Sync + Inbox-UI + Threading + Auto-Verknüpfung), M098 (Versand + Vorlagen), M099 (Tracking + Delivery-Status + Bounces) |
| Block | Block 5 · Pflichtenheft 9.1-9.7 |
| Spec | `_bmad-output/specs/M09-email-inbox.md` |
| **Reuse-Level** | **medium** (solides E-Mail-/Worker-/Adapter-Fundament vorhanden; der Kern von M09 — User-Mailbox-OAuth-Sync, Push-Webhooks, DKIM-Sende-Domain, Auto-Match — fehlt komplett und ist neu zu bauen) |
> Quelle Boilerplate (NUR GELESEN): `/Users/raffaelsheikh/Documents/GitHub/boilerplate`. Verifiziert per Grep/Read, nicht nur aus Inventur übernommen.
## Kurz-Verdict
Die Boilerplate liefert ein **Sending-/Marketing-/Transaktions-Mail-Fundament** (MailerSend-Provider, Queue, Templates, Outbox-Mail-Routing, Bounce-Webhook-Pattern, Open/Click-Tracking) und ein generisches **App-/DataConnection-/Sync-Run-Modell** (`appsEngine.prisma`) plus ein vorbildliches **Provider-Adapter-Pattern** (`marketing-outbound`). Das ist **gut für M098/M099** und als **Vorlage** für das Adapter-Interface aus Block J nutzbar.
Der **fachliche Kern von M09 fehlt aber vollständig**: Es gibt **keine** Gmail-/Microsoft-Mailbox-OAuth-Anbindung (mit Mail-Scopes), **keine** Pub/Sub- oder Graph-Subscription-Push-Sync, **keine** DKIM/SPF/DMARC-Sende-Domain-Verifikation, **keine** Auto-Match-Logik (Mail→Kontakt/Deal/Projekt) und das vorhandene `email.prisma`-Modell ist viel zu dünn (kein Tenant-Scope auf `Email`, kein Thread, keine Labels, kein gespiegelter Mailbox-Zustand).
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `packages/mail/src/types.ts` | `MailProvider`-Interface, `SendMailInput` (to/cc/bcc/subject/html/text/attachments/`inReplyTo`/`references`/`replyTo`/`sendAt`/`idempotencyKey`/`tenantId`/`priorityOverride`), `SendMailResult` | **als-vorlage** für den Outbound-Compose-Send-Pfad (M098). Versand geht in M09 jedoch über die **User-Mailbox** (Gmail-API `messages.send` / Graph `sendMail`), nicht über MailerSend → Interface-Form übernehmen, Implementierung neu |
| `packages/mail/src/queue.ts`, `packages/mail/src/mailersend.ts` | `createQueuedMailProvider()` (Versand async via Queue) + MailerSend-Provider | **als-vorlage** für transaktionalen Versand über System-Domain `noreply@hivegrid.de` (Spec Block C, Fallback). MailerSend-Provider zeigt das Provider-Adapter-Muster für SMTP-Side |
| `packages/mail/src/templates-react/` (`render.ts`, `_layout/`, `transactional/`) | React-Email-Templates + Layout-Primitives (BaseLayout, Header, Footer, Button mit `assertSafeUrl`) | **1:1** für transaktionale HiveGrid-Mails (Booking-Confirmation etc. = `Email`-Modell der Spec); `assertSafeUrl`/`InvalidActionUrlError` direkt für sichere Action-Links |
| `packages/marketing-outbound/src/types.ts` | `MarketingOutboundAdapter<TConfig>`-Interface (`type`/`parseConfig`/`send`), `adapterRegistry`/`resolveAdapter`, `adapterFetch()` (Timeout + SSRF-Allowlist), `maskEmailIfRequested()` (PII) | **als-vorlage** für `EmailProviderInterface` (Spec Block J / LP-11): identisches "Adapter-Interface + Registry + `resolveAdapter`"-Pattern für `GmailAdapter`/`MicrosoftAdapter`. SSRF-/PII-Helfer-Muster übernehmen |
| `packages/queue/src/payloads.ts`, `names.ts`, `producers.ts` | `queueNames.email`, `TransactionalMailJob`, `WebhookDeliveryJob`, `enqueueJob()`, `buildIdempotentJobId()`, `buildRetryJobOptions()` | **1:1/anpassen** — Basis für M09-Jobs (Initial-Sync, Push-Verarbeitung, Webhook-Renewal, Token-Refresh, DNS-Probe, Bounce). Neue typisierte Job-Payloads + ggf. neue Queue-Namen ergänzen |
| `packages/queue/src/priority.ts` | `resolveJobPriorityForTenant()` (plan-basiert), Prioritäten CRITICAL/HIGH/NORMAL/LOW | **1:1** für Quotenschutz/Drosselung pro Tenant (M098 9.6, M099) |
| `apps/worker/src/jobs/transactional-mail-outbox*` (Gruppe `mail`) | Outbox→Mail-Versand-Job, Worker-Gruppe `mail` (Concurrency 5) | **als-vorlage** für die M09-Worker-Jobs; Gruppen-/Concurrency-Modell direkt übernehmen |
| `apps/worker/src/outbox/event-router.ts` | `DEFAULT_OUTBOX_ROUTING` mit Prefix→Queue (u.a. `mail.`, `webhook.`, `notification.`, `audit.`, longest-prefix-wins) | **anpassen** — neue Prefixes (z.B. `email.sync.`, `email.match.`) registrieren für M09-Outbox-Events |
| `apps/worker/src/index.ts` + `apps/worker/src/jobs/*` (Cron-Muster, z.B. `dsgvo-grace-cron.ts` 24h, `workflow-execution-stalled-reconciler` 5m) | Single-instance Cron-Scheduler-Registrierung | **als-vorlage** für die M09-Cron-Jobs: DNS-Verifikation alle 5 Min (Block C), Microsoft-Subscription-Renewal täglich (Block D), Token-Refresh alle 30 Min (Block B), Fallback-Polling-Watchdog 4h (Block D) |
| `apps/api/src/email-sequences/provider-webhook-route.ts` + `apps/api/src/webhooks/mailersend-signature.ts` | Inbound-Provider-Webhook-Route mit Signaturverifikation + Bounce-Event-Mapping (`hard_bounced`/`soft_bounced`/`rejected`/`spam_complaint`) | **als-vorlage** für die Push-/Bounce-Webhook-Endpunkte: `/api/v1/webhooks/email/gmail`, `/api/v1/webhooks/email/microsoft` (Block D) und Delivery-/Bounce-Webhooks (M099). Hard-/Soft-Bounce-Mapping direkt wiederverwendbar |
| `apps/api/src/route-access-inventory.ts` (Klasse `webhook-signature`) | Auth-Klassen-Matrix inkl. `webhook-signature` für signierte Provider-Webhooks | **1:1** — neue M09-Webhook-Routen unter `webhook-signature` einordnen |
| `apps/web/app/modules/admin-sso/client.server.ts` + `state.server.ts` + `apps/web/app/routes/auth.admin.callback.tsx` | `openid-client`-Wrapper: Issuer-Discovery, PKCE/state/nonce-Generierung, Code-Exchange im Callback | **als-vorlage** für den OAuth-Connect-Flow (Block B). ACHTUNG: existierend nur für **Login-SSO (Authentik OIDC)**, NICHT für Gmail/Graph mit Mail-Scopes → Flow-Skelett übernehmen, Provider/Scopes/Token-Storage neu |
| `packages/storage/src/index.ts` + `signed/` + `scan/` | S3/Scaleway-Adapter, Signed-Upload/Download-URLs, ClamAV-/magic-bytes-Virus-Scan, Quarantäne | **1:1** für `EmailAttachment`-Storage (Spec: `tenants/{tenantId}/emails/{messageId}/{filename}`, LP-13 EU-Storage), Lazy-Download von Provider-Side, Download-Links im Detail-View |
| `packages/db/src/outbox.ts` (`publishOutboxEvent`) + `prisma/schema/outbox.prisma` | Transaktionales Outbox-Pattern (atomarer Side-Effect mit DB-Write, `dispatchKey`-Idempotenz) | **1:1** — jeder M09-Side-Effect (neue Mail gespiegelt → Auto-Match → Activity → Notification) läuft über Outbox |
| `packages/db/src/client.server.ts`, `transactions.server.ts` | `getWriteDbClient`/`getReadDbClient`, `runDbTransaction` | **1:1** Fundament; Read-Replica für Inbox-Listen-Heavy-Queries |
| `packages/db/src/dsgvo/user-fk-map.ts` | Pflicht-Karte aller User-FKs (CI-Test bricht sonst) | **anpassen (Pflicht)** — neue M09-Modelle mit User-FK (`owner_user_id`, `manual_override_user_id`, `EmailRead.reader…`) eintragen |
| `packages/realtime/src/*` (SSE) | SSE-Event-Serialisierung + Notification-Stream | **1:1** für Live-Inbox-Badge (Sidebar `badge: 'count'` für ungelesene Mails, Integration-Contracts §2) + Sync-Status-Live-Update |
| `packages/search/src/*` (Meilisearch, tenant-scoped, Outbox-Reindex) + `schemas/` | Such-Index-Pattern, `bulkIndex`, `searchInIndex`, Outbox-Publisher | **als-vorlage** für Inbox-Volltext-Suche (M097: Subject+Body+Absender). Spec nennt zusätzlich Trigram-Index in Postgres als Alternative — Meili-Pattern ist die saubere Querschnitt-Variante |
| `packages/i18n/*` + `apps/web/app/locale/*` (Namespace `mail` existiert) | i18n-Setup de/en, Klartext-Keys, `assertI18nLabel`-Zwang | **1:1** — alle M09-Klartext-Diagnose-Texte (LP-10) als i18n-Keys; vorhandener `mail`-Namespace erweiterbar |
| App-Shell: `packages/view-models`, `packages/ui-app` (Registry-Factories), `apps/web/app/application/sidebar/*`, `uiRegistry.ts` | Datengetriebene Sidebar mit Permission/Flag-Gating, `VMState`-Statusmodell, Breadcrumb-Registry, stabile `data-ui-id` | **1:1/anpassen** — Inbox-Route + Sidebar-Eintrag (Gruppe KOMMUNIKATION, Integration-Contracts §1) + Breadcrumb + View-Model-Vertrag |
| `packages/ui-mock` + Hot-Swap (`HotSwapPageRenderer`) + `apps/storybook` (lost-pixel, axe) | Hot-Swap-Drill, Visual-Regression, a11y-Gates | **1:1** für die DoD-Pflichten (Hot-Swap-Drill, Visual-Snapshot) |
| `prisma/schema/notifications.prisma` (`NotificationDelivery`, prefs, `DeviceToken`) + Notification-Wiring (Contracts §7 `notify()`) | Notification-Querschnitt In-App/Email/Push | **1:1** für OAuth-Disconnect-/revoked-/Send-Fehler-Benachrichtigungen (Block B/D, LP-10) |
| `prisma/schema/core.prisma` (`AuditEvent` Hash-Kette) + Audit-Wiring (Contracts §6, auto via Middleware; `auditEvent()` manuell für Nicht-Prisma) | Tamper-evidenter Audit-Trail | **1:1** — manueller Match-Override loggt `audit_event` (Spec AV #6); externe Provider-Calls manuell auditieren |
| `prisma/schema/featureFlags.prisma` + Feature-Flag-Querschnitt | Modul ohne Deploy abschaltbar (DoD-Pflicht) | **1:1** |
| `packages/entitlements` / Permissions (`permissions.prisma`, Contracts §10) | RBAC-Fundament + Permission-Key-Konvention | **anpassen** — neue Keys `email.read_own`/`email.read_all`/`email.write`/`email.admin` + Bundle-Mapping (Spec Block I) ergänzen |
---
## Vorhandene Daten-Modelle (Prisma) — Bestand vs. Bedarf
Schema-Pfad: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`
### `email.prisma` (10 Modelle) — teilweise relevant, aber dünn
- **`Email`** (`messageId @unique`, `type` inbound/outbound, `date`, `subject`, `fromEmail/fromName`, `toEmail/toName`, `textBody`, `htmlBody`) — deckt grob die HiveGrid-eigene `Email`-Entität (Spec Block A, transaktionale Mails) ab. **Lücken ggü. Spec:** **kein `tenantId`** (nur indirekt über `TenantInboundAddress`), kein `direction`-Enum, keine `cc/bcc`-Arrays am Modell (CC via separatem `EmailCc`-Modell), kein `delivery_status`/`bounce_type`/`bounce_reason`, keine `related_contact/company/deal/project`-FKs, kein `archived_at`, kein `provider_message_id` vom SMTP-Provider getrennt.
- **`EmailRead`** (`emailId`, `userId`, `createdAt`) — existiert, aber semantisch **anders**: trackt "von HiveGrid-User gelesen", nicht "vom Empfänger gelesen" (Spec `EmailRead` = Pixel-Tracking mit `reader_ip`/`reader_user_agent`). → **anpassen/erweitern**.
- **`EmailCc`** — separate CC-Tabelle. Spec modelliert cc/bcc als Arrays direkt am Message-Modell. → Designentscheidung.
- **`EmailAttachment`** (`name`, `type`, `length`, `content`, `publicUrl`, `storageBucket`, `storageProvider`) — gute Basis für Spec-`EmailAttachment`. **Lücken:** kein `parent_kind`/`parent_id`-Polymorphismus (hängt fest an `Email`), kein `provider_attachment_id`/`download_status` für Lazy-Download.
- **`EmailSender`** (`provider`, `stream`, `apiKey`, `fromEmail`, `replyToEmail`) — SMTP-Sender-Config. Nutzbar als Anker für transaktionalen Versand, **aber** das ist NICHT die DKIM-verifizierte `TenantSendingDomain` der Spec (kein `dkim_*`, kein `verification_status`).
- **`TenantInboundAddress`** (`address @unique`) — Name kollidiert mit Spec-`TenantInboundAddress`, ist aber simpler (kein `local_part`/`full_address`/`purpose`-Enum/`archived_at`). → **anpassen** oder neues Modell.
- **`Campaign`, `OutboundEmail`, `OutboundEmailOpen`, `OutboundEmailClick`** — Marketing-Kampagnen + Open/Click-Tracking. **Gehört primär zu M10 (Marketing)**, aber `OutboundEmail` (mit `sentAt`/`deliveredAt`/`bouncedAt`/`spamComplainedAt`/`unsubscribedAt`) ist ein **direkter Template-Bestand für M099** (Delivery-Status + Bounce-Tracking) und für Spec-`Email.delivery_status`.
### `appsEngine.prisma` (5 Modelle) — Template für OAuth-Connection + Sync-Runs
- **`App`** (`@@unique([tenantId, type])`, `config` JSON, `isActive`) — "installierte App pro Tenant". **als-vorlage** für die Email-Integration-Aktivierung pro Tenant.
- **`DataConnection`** (`type` "oauth2"/"rest-api"/"webhook", `config` = **encrypted JSON credentials**) — **direkt relevant** als Speicherort-Muster für OAuth-Tokens. Spec verlangt aber dedizierte Felder (`oauth_access_token_encrypted`, `oauth_refresh_token_encrypted`, `oauth_token_expires_at`, `oauth_scope_granted`, `sync_status`, `webhook_subscription_id`, `webhook_expires_at`) → entweder `DataConnection.config` als JSON nutzen ODER (empfohlen) dediziertes `EmailIntegrationMailbox`-Modell bauen.
- **`AppSyncRun`** (`status` pending/running/completed/partial/failed, `executionMode` manual/scheduled/webhook, `itemsProcessed/created/updated/failed`, `error`) — **als-vorlage** für `EmailIntegrationSyncCursor.initial_sync_status`/`progress` (Block A/D) und für Sync-Run-Observability.
- **`AppSyncRunLog`**, **`AppSyncItem`** (Source→Destination-Mapping + Rollback) — nützliches Muster für Per-Message-Sync-Logging, eher als Vorlage denn 1:1.
### `accounts.prisma` — OAuth-Bestand
- **`User.googleId`/`User.githubId` (`@unique`)** — Login-OAuth-Identitäten. **NICHT** das, was M09 braucht (das sind Auth-Login-IDs, keine Mailbox-Connections mit `gmail.modify`/`Mail.ReadWrite`-Scopes + Refresh-Tokens). Nur als Hinweis, dass Google-OAuth-Login schon existiert.
### `emailSequences.prisma` (3) + `marketingLeads.prisma` (`MarketingIntegration`)
- Bounce-/Open-/Click-Felder + verschlüsselte `configJson` — Template-Bestand für M099 und für verschlüsselte Provider-Config (relevanter für M10).
**Vorgesehener Ort für neue HiveGrid-Modelle:** `prisma/schema/app.prisma` (leerer Platzhalter `// custom database models`) — hier kommen die M09-eigenen Modelle hinein, ohne Boilerplate-Domänen zu verändern.
---
## Lücken / komplett neu zu bauen
Verifiziert: kein Treffer in `packages/`, `apps/`, `prisma/` für Gmail-API/`googleapis`, Pub/Sub, `graph.microsoft`/Graph-Subscriptions, `gmail.modify`/`Mail.ReadWrite`-Scopes, DKIM/SPF/DMARC, Sending-Domain-Verifikation. (Grep-Treffer auf "gmail/google/dkim" waren ausschließlich Falsch-Positive in feature-flags, config-catalog, Email-Template-Texten und `node_modules`.)
1. **Mailbox-OAuth-Connect (Block B) — KOMPLETT NEU.** Gmail-OAuth (`gmail.modify`/`gmail.send`/`gmail.labels`/`userinfo.email`) und Microsoft-OAuth (`Mail.ReadWrite`/`Mail.Send`/`User.Read`/`offline_access`). Vorhandener `openid-client`-Flow (Admin-SSO) nur als Skelett. Token-Refresh-Worker, Disconnect+Token-Revoke, `sync_status`-Lifecycle.
2. **Push-Sync (Block D) — KOMPLETT NEU.** Gmail-Watch-API + Pub/Sub-Push-Endpoint, Microsoft-Graph-Subscription auf `/me/messages` + tägliches Renewal (3-Tage-Lifetime), `historyId`/`deltaToken`-Cursor-Sync, Fallback-Polling-Watchdog (4h). Webhook-Routen `/api/v1/webhooks/email/{gmail,microsoft}` (Signatur-/Token-Verifikation analog `mailersend-signature`).
3. **Provider-Adapter `EmailProviderInterface` (Block J / LP-11) — NEU (Pattern vorhanden).** `connect/disconnect/fetchMessages/getMessage/sendMessage/subscribeToWebhook/renewWebhook/revokeWebhook/markRead/downloadAttachment` + `GmailAdapter`/`MicrosoftAdapter`. Interface-/Registry-Muster aus `marketing-outbound` als Vorlage; Implementierungen neu. Klartext-Fehler-Übersetzung pro Adapter (LP-10).
4. **Sende-Domain DKIM/SPF/DMARC (Block C) — KOMPLETT NEU.** `TenantSendingDomain`-Modell (Spec-Version), DKIM-Schlüsselpaar-Generierung (2048 Bit, encrypted Private-Key), DNS-TXT-Record-Bau (DKIM/SPF/DMARC), DNS-Lookup-Verification-Cron (alle 5 Min, 7-Tage-Timeout), Copy-to-Clipboard-UI. Plattform-Fundament für M10.
5. **Auto-Match-Logik (Block E) — KOMPLETT NEU.** Mail→Kontakt (Email-Match gegen `ContactEmail`/`Lead.email`), Mail→Deal/Projekt (Subject-Pattern `[Deal #X]`/`[Auftrag #X]`/`[Lead #X]`), Heuristik (genau-1-offener-Deal → medium), manueller Override (hält stand), Edge-Cases (Mehrfach-Contacts, eigene Adresse, Newsletter/Bulk via List-Unsubscribe-Header). Setzt M042 (Kontakt), M060/M066 (Deal/Auftrag) voraus.
6. **Vollständiges M09-Datenmodell — NEU (Teil-Bestand).** `EmailIntegrationMailbox`, `EmailIntegrationMessage` (mit Thread/Labels/Match-Feldern/Trigram-Suche), `EmailIntegrationSyncCursor`, `EmailIntegrationWebhookEvent` (mit Retention 30/90 Tage). Bestehende `Email`/`EmailAttachment`/`EmailRead`/`TenantInboundAddress` müssen erweitert oder zugunsten Spec-konformer Modelle ergänzt werden (Tenant-Scope, direction, Match-FKs, delivery_status, polymorphe Attachments).
7. **Inbox-UI (Block F) — NEU (Shell-/Komponenten-Fundament vorhanden).** Listen-View mit Filtern (Mailbox/Direction/Read/Verknüpfung/Datum/Anhang), Volltext-Suche, Bulk-Actions, Cursor-Pagination; Detail-View mit Thread-Aggregat, CSP-gehärtetem HTML-Body-Render ("Bilder anzeigen"-Toggle = Tracking-Pixel-Schutz), Attachment-Liste, Verknüpfungs-Editor, Reply/Reply-All/Forward, Provider-Deep-Link. shadcn-Tabellen/Modale/Forms aus `apps/web` wiederverwendbar, aber inbox-spezifische Komponenten neu.
8. **Outbound-Compose (Block G) — NEU.** Rich-Text-Editor (TipTap laut Milestone-Hinweis in Deps; verifizieren), To/CC/BCC mit Contact-Autocomplete, Mailbox-Auswahl, Anhang-Upload (≤25 MB), Send über User-Mailbox (nicht MailerSend), Save-as-Draft, Compose-Aufruf aus Kontakt-/Deal-/Projekt-/Inbox-Kontext mit Pre-Fill + Subject-Pattern. BCC-Logging-Adresse.
9. **OAuth-Token-Encryption (KMS) — NEU/zu klären.** Kein dediziertes Crypto-/KMS-Paket gefunden (nur `…Encrypted`-Spalten-Konvention + verschlüsselte `configJson` in marketing/appsEngine). Konkretes KMS-Setup (Scaleway-managed-Key) ist offener Detail-Punkt der Spec.
10. **Vorlagen + Variable-Resolution (M098 9.7) — NEU.** Quick-Reply-Templates + `{{first_name}}`-Resolver gegen Kontakt-Properties. React-Email/Render vorhanden, aber Variable-Resolver gegen CRM-Daten neu.
11. **Bounce-/Sperrliste (M099) — NEU (Teil-Bestand).** Hard-Bounce→Sperrliste-Auto-Add pro Tenant, Soft-Bounce 3×-Retry/24h. Bounce-Event-Mapping (hard/soft/rejected/spam) aus `email-sequences/provider-webhook-route.ts` wiederverwendbar; Sperrlisten-Modell + Kontakt-Detail-Hinweis neu.
12. **Activity-Stream-Integration (Block H) — Wiring NEU.** `email_received`/`email_sent`/`email_match_changed` via `logActivity` (Contracts §5, Service kommt aus M045). Permission-gegateter Activity-Inhalt (`email.read_own` vs. nur "Email-Aktivität (nicht einsehbar)").
---
## Empfohlenes Vorgehen (Reihenfolge)
**Phase 0 — Fundament aus BP übernehmen (1:1), bevor M09-Logik beginnt:**
1. Datenmodell in `prisma/schema/app.prisma` anlegen (`EmailIntegrationMailbox/Message/SyncCursor/WebhookEvent`, Spec-`TenantSendingDomain`, erweitertes `Email`/`EmailAttachment`/`EmailRead`/`TenantInboundAddress`). Forward-Only-Migration mit Inverse-SQL-Kommentar (DoD). **`user-fk-map.ts` zwingend ergänzen** (sonst CI-Bruch).
2. Permission-Keys + Bundle-Mapping (Block I) in `packages/entitlements` ergänzen; Sidebar-Eintrag (Gruppe KOMMUNIKATION) + Route + Breadcrumb + Feature-Flag verdrahten (Integration-Contracts §1-4, §10).
3. Outbox-Routing-Prefixes + neue typisierte Queue-Jobs + Worker-Gruppe `mail`/neue Gruppe registrieren (aus `packages/queue` + `apps/worker`-Mustern).
**Phase 1 — M096 (Connect-Fundament):**
4. OAuth-Connect-Flow aus `admin-sso/client.server.ts`-Skelett ableiten, aber Gmail-/Graph-Provider + Mail-Scopes + verschlüsseltes Token-Storage neu. Token-Refresh-Cron (Muster aus `apps/worker/src/jobs`).
5. `EmailProviderInterface` + `GmailAdapter`/`MicrosoftAdapter` nach `marketing-outbound`-Adapter-Pattern (Registry + `resolveAdapter` + `adapterFetch`-Härtung).
6. DKIM/SPF/DMARC-Sende-Domain komplett neu (Schlüsselgenerierung, DNS-Record-Bau, 5-Min-Verifikations-Cron, 7-Tage-Timeout).
**Phase 2 — M097 (Sync + Inbox):**
7. Push-Webhook-Routen (Gmail Pub/Sub, Microsoft Graph) nach `provider-webhook-route.ts`-Muster + Signatur-/Token-Verifikation; Initial-Sync-Job mit Cursor; Renewal-/Watchdog-Cron.
8. Auto-Match-Logik (neu) + Activity-Wiring (`logActivity`) + Outbox.
9. Inbox-UI (Liste+Detail) auf App-Shell/shadcn-Fundament; Volltext-Suche via `packages/search`-Pattern; Attachments via `packages/storage` (signed URLs, EU-Bucket); SSE-Badge via `packages/realtime`.
**Phase 3 — M098/M099 (Versand + Tracking):**
10. Outbound-Compose + Send über User-Mailbox (Adapter `sendMessage`), Spiegelung als `direction=outbound`-Message; Vorlagen + Variable-Resolver; transaktionaler Fallback-Versand über MailerSend/`packages/mail` für System-Mails.
11. Delivery-Status/Bounce: Bounce-Webhook + hard/soft-Mapping (aus `email-sequences` übernehmen), Sperrliste pro Tenant, Quotenschutz via `queue/priority.ts`-Plan-Prioritäten.
**Querschnitt durchgängig (DoD):** i18n-Keys (Namespace `mail` erweitern), Klartext-Diagnose (LP-10) aus M002-Catalog, View-Model-Pflicht (Contracts §11), Audit auto/manuell, Notifications via `notify()`, Smoke-Test + Hot-Swap-Drill + Visual-Snapshot, Customer-Portal-Sichtbarkeit explizit (Default: nicht sichtbar), Permission-Denied→404.
**Vor Bau zu validieren (Spec-Hinweis):** Microsoft-3-Tage-Renewal-Lifecycle und DKIM-DNS-Check mit echter Domain; konkretes KMS-Setup für Token-Verschlüsselung; ob TipTap tatsächlich in den Boilerplate-Deps liegt (Milestone-Annahme).
@@ -0,0 +1,147 @@
# Boilerplate-Abgleich: M10 — Email-Marketing
| Feld | Wert |
| --- | --- |
| **Modul-ID** | M10 (Modul 10) |
| **Name** | Email-Marketing |
| **Milestones** | M100 (Kampagnen-Editor / Block-Style + Personalisierung + Vorschau + Test-Mail), M101 (Empfänger-Segmente / CRM-Filter), M102 (Versand-Engine + DKIM/SPF + Daily-Cap + Frequenz-Schutz + Pflicht-Footer), M103 (A/B-Tests Phase 2 + Reporting/Stats), M104 (Unsubscribe + Bounce-Handling + Suppression) |
| **Pflichtenheft-Block** | Block 5, Modul 10 (`_bmad-output/pflichtenheft/blocks/block5.md` §10.1–10.12) |
| **Detail-Spec** | `_bmad-output/specs/M10-email-marketing.md` |
| **Reuse-Level** | **medium** |
## Reuse-Verdikt (Kurzfassung)
M10 ist das **best-vorbereitete Kommunikations-Modul** der Boilerplate. Es existiert ein **vollständiger Email-Marketing-Vertikalschnitt** im Web-App (`apps/web/app/modules/emailMarketing/**`) mit Tenant- UND Admin-Scope, hinter Feature-Flag `email-marketing` (`packages/entitlements`): Kampagnen-CRUD, Sender-Verwaltung, Outbound-Email-Liste, Summary-Dashboard, Test-Mail/Preview, Personalisierungs-Token-Replace, Versand über MailerSend mit Open/Click-Tracking-Settings. Die Datenmodelle `Campaign`, `OutboundEmail`, `OutboundEmailOpen`, `OutboundEmailClick`, `EmailSender` (`prisma/schema/email.prisma`) decken den Kern der Spec-Block-A-Entitäten **teilweise 1:1** ab.
**Aber** — und das drückt das Verdikt von `high` auf `medium`:
1. **Vier zentrale Spec-Modelle fehlen komplett:** `Unsubscribe`, `EmailSuppressionList`, `RecipientSegment`, `EmailTemplate`. Verifiziert per Grep über alle `*.prisma`: kein Treffer. Unsubscribe lebt heute nur als `OutboundEmail.unsubscribedAt`-Flag bzw. `EmailSequenceRun.status='unsubscribed'` — **keine tenant-weite, CRM-Lösch-persistente Sperrliste** (Spec Block H/G + 10.5.5).
2. **Zwei parallele, NICHT verbundene Email-Systeme:** (a) Tenant-`Campaign`/`OutboundEmail` (Broadcast) und (b) operator-seitige Drip-Funnel (`EmailSequence`/`EmailSequenceRun`/`EmailSequenceStepDelivery`, `packages/email-sequences`). **Die öffentlichen Tracking-/Unsubscribe-Routen (`/unsubscribe/$token`, `/email/track-open`, `/email/track-click`) operieren ausschließlich auf dem SEQUENCE-System, NICHT auf `OutboundEmail`.** Für M10 muss Tracking/Unsubscribe auf `Campaign`/`OutboundEmail` umverdrahtet bzw. neu gebaut werden (die HMAC-Token-Mechanik + Pixel-Route sind aber 1:1-Vorlagen).
3. **Anti-Spam-/Versand-Robustheit fehlt fast vollständig:** kein Daily-Cap pro Plan, kein Frequenz-Cap/Empfänger/Woche, kein Spam-Score-Check, kein Throttling pro Tenant/Empfänger-Domain, **kein geplanter Versand**, **kein BullMQ-Versand-Worker für Kampagnen** (Versand läuft heute synchron in-process via `mapWithConcurrency`, Konkurrenz 5 — Spec K + E verlangen Queue + Cron + Throttle).
4. **DSGVO-Opt-In-Dokumentation fehlt:** kein `marketing_opt_in_at/_source/_reference/_text_snapshot` (Spec Block H 10.7). Heute nur ein CRM-Boolean `marketingSubscriber` (Custom-Field via `CrmService`).
5. **Provider:** Boilerplate nutzt **MailerSend** (`packages/mail`) — Spec/LP-13 verlangt EU-SMTP (Scaleway TEM/Brevo EU/Mailjet EU). Provider-Adapter-Pattern existiert (`MailProvider`-Interface), aber MailerSend ist kein EU-Anbieter → Adapter-Austausch nötig (offener Spec-Punkt).
6. **Keine M9-Sending-Domain-Verknüpfung:** `EmailSender` hält `apiKey`/`fromEmail`/`provider` direkt, **kein FK auf `TenantSendingDomain`** (existiert in M9 noch nicht). DKIM-Verifikations-Gate vor Versand fehlt.
7. **Kein Segment/Filter-Tree:** Empfänger werden heute über `EntityViewsApi` (CRM-Entity-Views) als Quelle gezogen — solide Wiederverwendung als Basis für M101, aber das spezifizierte `RecipientSegment` mit verschachteltem AND/OR-JSONB-Filter-Tree + Static-Snapshot + Auto-Exclusion-Aufschlüsselung ist neu.
8. **Layer-Disziplin:** Bestehender Code folgt dem ToolCTRL-Muster (Service direkt aus Route, `db.server` in Modul-Code), NICHT der HiveGrid-DoD (View-Model-Layer `packages/view-models`, `packages/ui-hivegrid`, Subframe-Adapter, kein Prisma in UI). → **Übernahme erfordert Re-Layering** in die HiveGrid-Struktur.
Fazit: Solide Fachbasis (Campaign-Send + Tracking-Datenmodell + MailerSend-Adapter + UI-Bausteine + i18n) spart erheblich Arbeit, aber die DSGVO-/Anti-Spam-/Segment-/Suppression-/Scheduling-Hälfte ist Neubau. Daher `medium`.
---
## Übernehmbar aus Boilerplate
> Boilerplate-Wurzel (NUR LESEN): `/Users/raffaelsheikh/Documents/GitHub/boilerplate`
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `prisma/schema/email.prisma` (`Campaign`, `OutboundEmail`, `OutboundEmailOpen`, `OutboundEmailClick`, `EmailSender`) | Kern-Datenmodell: Kampagne (name/subject/html/text/status/track/sentAt), Outbound-Email pro Empfänger (email, sentAt/deliveredAt/bouncedAt/spamComplainedAt/unsubscribedAt, FK auf `Row` als Kontakt), Open/Click-Tracking-Tabellen, Sender | **anpassen** — Felder fehlen massiv gegenüber Spec Block A (preheader, recipient_segment_id, recipient_list_snapshot, scheduled_at, *_count-Aggregate, tracking_token/unsubscribe_token unique, bounce_type, personalization_payload, delivery_status-Enum, Indizes). `tenantId` ist `String?` (operator-nullable) → für RLS auf NOT-NULL-Tenant-Scope härten |
| `apps/web/app/modules/emailMarketing/services/EmailMarketingService.ts` | `getSummary` (Open/Click-Rate-Aggregation), `createCampaignDraft`, `sendCampaign` (mapWithConcurrency), `sendCampaignTest` (max 5), `sendPreview`/`sendContactPreview`, `replaceVariables` (`{{firstName}}`/`{{lastName}}`/`{{email}}`/`{{job_title}}`/`{{company_name}}`) | **als-vorlage** — Logik-Gerüst für Versand + Personalisierung übernehmbar; muss aber in `app/domain/**`-Layer + BullMQ-Worker (Spec E) statt synchron, Token-Format auf `{{first_name}}` (Spec C) angleichen, Custom-Field-Token `{{custom.x}}` ergänzen, Auto-Exclusion + Footer-Injection + Cap-Checks fehlen |
| `apps/web/app/modules/emailMarketing/services/EmailMarketingSenderService.server.ts` | `sendBroadcast()` Provider-Switch → `sendMailerSendBroadcast` (MailerSend-Send mit `settings.trackClicks/trackOpens`, replyTo, tags) | **als-vorlage** — Send-Adapter-Pattern; Provider muss auf EU-SMTP (LP-13) gewechselt/ergänzt werden; DKIM-/Domain-Gate fehlt |
| `apps/web/app/modules/emailMarketing/db/campaigns.db.server.ts` | `getAllCampaigns`/`getCampaign` (mit recipients+opens+clicks+contactRow-Include), `createCampaign` (nested recipients), `updateCampaign` (recipient-replace), `deleteCampaign`, `groupCampaigns` (by status) | **anpassen** — tenant-scoped Queries direkt verwendbar als Basis; auf `withTenantScope()` (M004) + neue Felder/Snapshot anheben |
| `apps/web/app/modules/emailMarketing/db/outboundEmails.db.server.ts` | `getAllOutboundEmails` (paginiert), `createOutboundEmail(Test)`, `updateOutboundEmail` (sentAt/deliveredAt/bouncedAt/spamComplainedAt/unsubscribedAt), `openedOutboundEmail`/`clickedOutboundEmail`, `unsubscribeCampaignRecipient`, `countOutboundEmails` | **anpassen** — Pro-Empfänger-Liste + Status-Updates (Spec F „Pro-Empfänger-Anzeige") fast vollständig; Bounce-Klassifikation (hard/soft), Suppression-Add, Activity-Wiring fehlen |
| `apps/web/app/modules/emailMarketing/db/emailSender.ts` + `services/EmailMarketingSenderService.server.ts` + `components/EmailSenderForm.tsx` + `routes/Senders_*.ts` + `components/senders/Senders{List,New,Edit}Route.tsx` | Sender-Verwaltung End-to-End (CRUD + Form + List/Edit/New) | **als-vorlage** — Sender-UI/-Flow direkt nachnutzbar; Validierung „from_email-Domain muss zu Sending-Domain passen" + M9-`sending_domain_id`-FK + DKIM-Status-Anzeige + is_default + archived_at ergänzen |
| `apps/web/app/modules/emailMarketing/components/campaigns/CampaignsNewRoute.tsx` + `CampaignsEditRoute.tsx` + `CampaignsListRoute.tsx` + `campaignDefaults.ts` | Kampagnen-Editor (Name/Sender/Empfänger-View/Subject/HTML-Body via `InputContent` WYSIWYG+Markdown), Test-Mail-Button, Default-HTML-Body, Liste | **als-vorlage** — Editor-Grundgerüst; der spezifizierte 6-Schritt-Wizard (Basis→Inhalt→Empfänger→Test→Zeitpunkt→Bestätigung mit Pflicht-Checks), Mobile/Desktop-Vorschau, Spam-Score, Auto-Exclusion-Anzeige fehlen. Re-Layering nach `ui-hivegrid`/Subframe-Adapter nötig |
| `apps/web/app/modules/emailMarketing/components/OutboundEmailsTable.tsx` + `outboundEmails/OutboundEmailsListRoute.tsx` + `EmailMarketingSummaryRoute.tsx` + `routes/EmailMarketing_Summary.ts` | Outbound-Tabelle (Status/Opens/Clicks/Kontakt), Summary-Dashboard (avg Open/Click-Rate, sent/delivered) | **als-vorlage** — deckt Spec F „Stats-Aggregation" + „Pro-Empfänger-Liste" teilweise; CTR, Bounce-Raten, Unsubscribe-Rate, Complaint-Rate, Click-Map, Filter nach Status ergänzen |
| `apps/web/app/routes/app.$tenant/email-marketing.tsx` + `email-marketing/{index,activity,senders,campaigns/*}.tsx` | Tenant-Route-Tree mit `SidebarIconsLayout` (Overview/Campaigns/Activity/Senders), `requireTenantProductAccess(featureKeys.emailMarketing)` | **als-vorlage** — Route-Struktur + Feature-Flag-Gate + Sub-Nav; an HiveGrid-Route-Pattern (`/{tenant}/email-marketing/...`, INTEGRATION-CONTRACTS §3) + `sidebar.config.ts`-Eintrag (Gruppe „Kommunikation") + Permission `email_marketing.*` (M004) angleichen |
| `apps/web/app/routes/unsubscribe.$token.tsx` + `app/utils/email/unsubscribeToken.server.ts` + `packages/email-sequences/src/unsubscribe-token.ts` (`signUnsubscribeToken`/`verifyUnsubscribeToken`/`buildUnsubscribeUrl`) | Öffentliche Unsubscribe-Route (GET+POST, kein Login, Token=Auth, idempotent) + HMAC-SHA256-Token-Mechanik (`MARKETING_TOKEN_SECRET`, timing-safe) | **als-vorlage** — exakte Blaupause für Spec Block H Unsubscribe-Page; **aber** Token zeigt heute auf `EmailSequenceRun`, muss auf `OutboundEmail.unsubscribe_token`/`Unsubscribe`-Modell + `EmailSuppressionList`-Add + Grund/Feedback-Formular + Activity-Eintrag umgebaut werden. List-Unsubscribe-One-Click-Flow (RFC-8058) ist bereits berücksichtigt |
| `apps/web/app/routes/email/track-open.$deliveryId[.gif].tsx` + `track-click.$deliveryId.tsx` | 1×1-GIF-Pixel-Route (Cache-Control no-store, idempotenter First-Open via `updateMany`), Click-Redirect-Route | **als-vorlage** — Pixel-/Redirect-Mechanik 1:1; Ziel-Modell von `EmailSequenceStepDelivery` auf `OutboundEmail`/`OutboundEmailOpen`/`OutboundEmailClick` umverdrahten; `link_url`/`link_label`/`reader_ip`(anonymisiert)/`reader_user_agent`/`is_first_open` ergänzen (Spec Block F) |
| `apps/api/src/webhooks/mailersend-signature.ts` (`verifyMailerSendWebhookSignature`) | Inbound-Webhook-HMAC-Verify (timing-safe, Timestamp-Toleranz, Header-Kandidaten) | **1:1** für Bounce-/Complaint-Webhook-Empfänger (Spec Block G `/api/v1/webhooks/email/bounce`); Provider-Wechsel ggf. anderer Verify |
| `apps/api/src/email-sequences/provider-webhook-route.ts` (`postMailerSendEmailSequenceWebhookRequest`, `parseMailerSendSequenceWebhookPayload`, `MAILERSEND_BOUNCE_EVENT_TYPES`) | Vollständiger Bounce-Webhook-Handler: Signatur→Parse Event-Type (bounced/hard/soft/rejected/spam_complaint)→`providerMessageId`-Lookup→`markDeliveryBounced` | **als-vorlage** — exaktes Pattern für Spec Block G Bounce-Verarbeitung; Ziel-Update muss auf `OutboundEmail` + Hard/Soft-Klassifikation + Suppression-Auto-Add + Soft-to-Hard-Conversion + Complaint→Auto-Unsubscribe + Activity gehen |
| `packages/mail/src/{index,types,mailersend,queue}.ts` (`MailProvider`-Interface, `SendMailInput` mit `listUnsubscribe`, `precedenceBulk`, `settings.trackOpens/Clicks`, `priorityOverride`, `sendAt`, `idempotencyKey`, `tenantId`, `tags`; `createQueuedMailProvider` = BullMQ-Versand mit Prioritäts-Resolver) | Provider-Abstraktion + Queue-Provider. `SendMailInput` deckt List-Unsubscribe-Header, Bulk-Precedence, Tracking-Settings, geplanten Versand (`sendAt`), Idempotenz, Tenant-Priorität bereits ab | **anpassen** — `MailProvider`-Interface 1:1 als LP-11-Adapter-Schicht; **EU-Provider-Adapter (LP-13) neu implementieren** (heute nur `mailersend` + `queue`); `QueuedMailProvider` ist Basis für den Spec-E-Versand-Worker mit Drossel |
| `packages/email-templates/src/{render,theme}.ts` + `components/{Layout,primitives}.tsx` + `sequences/*.tsx` | React-Email-Render (`renderEmail`→`{html,text}`, `htmlToPlainText` mit Anchor-URL-Erhaltung), Layout/Primitives, Sequenz-Templates (welcome/trial-reminder/re-activation/generic-step) | **als-vorlage** — Render-Pipeline + `htmlToPlainText` 1:1; SHK-Branchen-Templates („Wartungs-Erinnerung", „Newsletter Q4", „Service-Aktion", Spec Block I / 10.1.4) sind neu zu erstellen; das spezifizierte `EmailTemplate`-DB-Modell fehlt komplett |
| `packages/mail/src/templates-react/_layout/{Footer,BaseLayout,Button}.tsx` (`assertSafeUrl`, `InvalidActionUrlError`) | Mail-Footer-Layout + URL-Safety-Guard für Action-Links | **als-vorlage** — Basis für Pflicht-Footer (Spec G/10.7.5: Impressum-Adresse + Unsubscribe-Link auto-injiziert + Tracking-Pixel-Hinweis); Footer-Pflicht-Inhalt + Auto-Injection-Logik fehlen |
| `apps/web/app/utils/api/.server/EntityViewsApi` + `CrmService` (`getContactsInRowIds`, `getContact`, `marketingSubscriber`-Boolean, ContactDto firstName/lastName/email/jobTitle/company) | CRM-Entity-Views als Empfänger-Quelle (gefilterte Kontakt-Listen mit pageSize, Row-IDs) + Kontakt-Auflösung + Marketing-Subscriber-Flag | **als-vorlage** — Brücke CRM↔Marketing; Basis für M101-Segmente, aber das spezifizierte `RecipientSegment` (JSONB-Filter-Tree AND/OR, static/dynamic, Auto-Exclusion-Aufschlüsselung, 50-Sample-Vorschau) ist neu zu bauen |
| `packages/marketing-outbound/**` (Adapter-Registry slack/discord/mailchimp/hubspot/brevo/webhook, `resolveAdapter`, `adapterFetch` mit SSRF-Schutz/Timeout) + `apps/worker/src/jobs/marketing-outbound.ts` | Outbound-Integrations-Adapter + isolierter Dispatch-Worker (Outbox-getriggert, Idempotenz, per-Integration-Fehlerisolation) | **als-vorlage** für das LP-11-Adapter-Muster (SMTP-Provider als austauschbarer Adapter) + Worker-Job-Struktur. **Inhaltlich themenfremd** (CRM-Sync zu Drittanbietern, nicht Email-Versand) — nur Pattern/SSRF-Client nachnutzen |
| `packages/email-sequences/src/{engine,step-runner,conditions}.ts` + `apps/worker/src/jobs/email-sequence-{trigger,scheduler,runtime,prisma-store}.ts` | Drip-Sequence-Engine (start/advance Run, delayHours-Scheduling, Conditions-DSL, BullMQ-Step-Scheduler, Prisma-Store-Adapter, Idempotenz `@@unique([sequenceId,triggerEventId])`) | **als-vorlage** — relevant für Spec Block J Trigger-Mails über M17 + (Phase 2) Sequenzen; das injizierbare Store/Queue-Engine-Pattern (testbar) ist HiveGrid-DoD-kompatibel und als Architektur-Vorbild für den Kampagnen-Versand-Worker nutzbar |
| `packages/entitlements/src/features.ts` (`featureKeys.emailMarketing = "email-marketing"`) + `requireTenantProductAccess` | Produkt-/Feature-Gate für Email-Marketing | **anpassen** — Feature-Flag-fähig (DoD M006) vorhanden; um HiveGrid-Permissions `email_marketing.read/write/admin` (M004, Spec Block L) erweitern |
| `apps/web/public/locales/{de,en}/emailMarketing.json` | i18n-Keys für das gesamte Email-Marketing-UI (DE+EN) | **anpassen** — Basis-Wortschatz vorhanden; HiveGrid-i18n-Namespace + Klartext-Texte (Spec „Klartext-Hinweise"-Querschnitt, M002/M003) ergänzen |
| `apps/web/app/modules/emailMarketing/routes/emailMarketingAccess.server.ts` + `email-marketing-auth-boundary.test.ts` | Mutation-Access-Gate (`requireSuperUserOrAdmin` bei Tenant-Scope) + Auth-Boundary-Test | **anpassen** — auf HiveGrid-Permission-Bundles (`email_marketing.write`/`.admin`) umstellen; Test-Pattern für Permission-Denied→404 (DoD §25) nachnutzen |
---
## Vorhandene Daten-Modelle
> Pfad: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`
**Direkt M10-relevant (`email.prisma`):**
- `Campaign` — `email.prisma:72` — `tenantId?`, `emailSenderId`, `name`, `subject`, `htmlBody`, `textBody?`, `status` (default "draft", Freitext-String — nicht das Spec-Enum draft/scheduled/sending/sent/paused/failed/cancelled), `track`, `sentAt?`, Relation `recipients OutboundEmail[]`. **Fehlt:** preheader, template_id, recipient_segment_id, recipient_list_snapshot, scheduled_at, sending_started/finished_at, total_recipients, alle *_count-Aggregate, archived_at, created_by_user_id, Spec-Indizes.
- `OutboundEmail` — `email.prisma:88` — `tenantId?`, `campaignId?`, `contactRowId?` (FK auf `Row`), `email`, `fromSenderId`, `isPreview?`, `error?`, `sentAt?`/`deliveredAt?`/`bouncedAt?`/`spamComplainedAt?`/`unsubscribedAt?`, Relations `opens`/`clicks`. **Fehlt:** recipient_email-Snapshot-Semantik (vorhanden als `email`), recipient_first/last_name-Snapshot, personalization_payload (JSONB), delivery_status-Enum, provider_message_id, bounce_type/bounce_reason_text, tracking_token + unsubscribe_token (unique), Spec-Indizes.
- `OutboundEmailOpen` — `email.prisma:111` — `outboundEmailId`, `firstOpen`, `createdAt`. **Fehlt:** reader_ip (anonymisiert), reader_user_agent, tenantId, `opened_at`-Semantik (= createdAt).
- `OutboundEmailClick` — `email.prisma:119` — `outboundEmailId`, `link`, `createdAt`. **Fehlt:** link_label, reader_ip/user_agent, tenantId.
- `EmailSender` — `email.prisma:58` — `tenantId?`, `provider`, `stream`, `apiKey`, `fromEmail`, `fromName?`, `replyToEmail?`, Relations `campaigns`/`outboundEmails`. **Fehlt:** sending_domain_id (M9-FK), is_default, archived_at, created_at. Hält `apiKey` direkt am Sender (→ DKIM/Domain-Gate fehlt).
**Verwandt, aber paralleles operator-seitiges System (`emailSequences.prisma` / `marketingLeads.prisma`):**
- `EmailSequence`, `EmailSequenceRun`, `EmailSequenceStepDelivery` — `emailSequences.prisma` — Drip-Funnel mit `triggerEvent`, `stepsJson` (linear), Run-State, Step-Delivery mit sentAt/openedAt/clickedAt/bouncedAt/providerMessageId. **Wichtig:** Die öffentlichen Tracking-/Unsubscribe-Routen hängen an DIESEN Modellen, nicht an `OutboundEmail`. Status-Enum kennt `unsubscribed` (terminal).
- `Lead`, `LeadEvent`, `TenantAcquisition`, `MarketingIntegration` — `marketingLeads.prisma` — Marketing-Lead-Funnel (operator-org, nicht tenant-scoped) + Outbound-Integrations-Config. **Themenfremd für M10** (CRM-Lead-Akquise / Drittanbieter-Sync), nicht der Tenant-Newsletter-Versand.
**Inbound-Email (themenfremd, M9):** `Email`, `EmailRead`, `EmailCc`, `EmailAttachment`, `TenantInboundAddress` — `email.prisma:1–56`.
---
## Lücken / komplett neu zu bauen
**Datenmodelle (4 Spec-Modelle existieren NICHT — per Grep verifiziert):**
1. `Unsubscribe` (Spec Block H) — Pro Contact+Tenant dauerhaft, scope-Enum (all_marketing/specific/global), unsubscribed_via, email_address-Snapshot, reason. Heute nur Flag `OutboundEmail.unsubscribedAt`.
2. `EmailSuppressionList` (Spec Block G/H) — Tenant-weite Sperrliste (hard_bounce/complaint/manual/invalid_format), CRM-Lösch-persistent (10.5.5), `(tenant_id, email_address)` unique. **Existiert nicht.**
3. `RecipientSegment` (Spec Block A/D) — JSONB-Filter-Tree (verschachteltes AND/OR), entity_type, is_static_snapshot + static_recipient_ids[], archived_at. **Existiert nicht** (heute nur CRM-Entity-Views als Ad-hoc-Quelle).
4. `EmailTemplate` (Spec Block A/I) — Wiederverwendbare Vorlagen mit subject/preheader-Default, body_html/text, is_template_origin, archived_at. **Existiert nicht** (nur hardcodierte React-Sequenz-Templates).
**Opt-In-Dokumentation (Spec Block H 10.7):** `marketing_opt_in_at/_source/_reference/_text_snapshot` am Contact + Double-Opt-In-Flow (Bestätigungs-Mail + 7-Tage-Token). Heute nur CRM-Boolean `marketingSubscriber`. **Neu.**
**Versand-Engine-Robustheit (Spec Block E/K):**
- Geplanter Versand (`scheduled` + Cron `WHERE status=scheduled AND scheduled_at<=NOW()` jede Minute) — **fehlt**.
- BullMQ-Versand-Worker für Kampagnen mit Batch (1.000) + Retry (3× exp. Backoff) — **fehlt** (heute synchron `mapWithConcurrency(5)`).
- Throttling: 100 Mails/Min/Tenant, 30 Mails/Min/Empfänger-Domain — **fehlt**.
- Daily-Cap pro Plan (Cloud 10K / Cloud+ 50K / Trial 200, via SubscriptionFeature) + Auto-Pause + Resume 00:00 Tenant-Zeit — **fehlt**.
- Frequenz-Cap (max 5 Mails/Empfänger/Woche, tenant-konfigurierbar) + Auto-Exclusion-Reason — **fehlt**.
- Pause/Cancel-Lebenszyklus (paused re-aktivierbar, cancelled final) — **fehlt** (Status ist Freitext-String).
**Anti-Spam (Spec Block K):** Spam-Score-Check 0–10 (Großschrift/Ausrufezeichen/Phrasen/Bild-Text-Verhältnis) mit Block-Option ab >7 — **fehlt komplett**.
**Auto-Exclusion-Engine (Spec Block D):** beim Segment-Auflösen Ausschluss von Unsubscribe/Suppression/ohne-Email/archiviert/ohne-Opt-In + Aufschlüsselung „X aus Y ausgeschlossen (pro Grund)" — **fehlt**.
**Pflicht-Validierungen vor Versand (Spec Block C/L):** Sending-Domain-DKIM-grün-Gate (M9), Unsubscribe-Link-Auto-Injection, Impressum-Adress-Erkennung, Empfänger>0-nach-Exclusion, Daily-Cap-Check, Bestätigungs-Modal mit Pflicht-Check-Anzeige — **fehlt**.
**6-Schritt-Kampagnen-Wizard + Mobile/Desktop-Vorschau + Custom-Field-Token (`{{custom.x}}`)** — Editor existiert nur als einstufiges Formular; Wizard/Vorschau/Custom-Token **neu**.
**Bounce-Klassifikation + Auto-Folgeaktionen (Spec Block G):** Hard/Soft-Klassifikation (5.x.x/4.x.x), Soft-to-Hard nach 3 Versuchen, Hard→Suppression-Add + `Contact.email_invalid_at` + Activity, Complaint→Auto-Unsubscribe — Webhook-Empfang-Pattern existiert, **Domain-Folgeaktionen fehlen**.
**Stats-Vollausbau (Spec Block F):** CTR (Klick/Öffnung), Hard/Soft-Bounce-Raten, Unsubscribe-/Complaint-Raten, Click-Map (URL→Anzahl), Status-Filter, Aktivitäts-Detail pro Empfänger (Opens/Clicks chronologisch) — Summary/Tabelle existieren teilweise, Rest **neu**.
**Trigger-Mail-Action `send-marketing-email` (Spec Block J, M17):** Mini-Kampagne (1 Empfänger, sofort) aus Automation-Engine + Template/Sender/Empfänger-Resolution + Auto-Exclusion + System-Actor — **neu** (Sequence-Engine ist Vorbild).
**Querschnitt-Wiring (HiveGrid-DoD):** Activity-Stream-Typen (`marketing_email_sent/opened/clicked/bounced/unsubscribed/complained` via `logActivity`), Aggregat „Marketing-Engagement" am Contact, `notify()`-Integration, View-Model-Layer (`packages/view-models`), `ui-hivegrid`/Subframe-Adapter-Re-Layering, `sidebar.config.ts`-Eintrag (Gruppe „Kommunikation"), HiveGrid-Permissions (`email_marketing.*`), Search-Index — **alles neu zu verdrahten** (bestehender Code folgt ToolCTRL-, nicht HiveGrid-Layer-Konvention).
**EU-SMTP-Provider (LP-13):** MailerSend ist kein EU-Anbieter → Scaleway-TEM-/Brevo-EU-/Mailjet-EU-Adapter über das vorhandene `MailProvider`-Interface **neu implementieren** (offener Spec-Punkt).
**M9-Sending-Domain-Verknüpfung:** `EmailSender.sending_domain_id` FK auf `TenantSendingDomain` + DKIM-Status-Anzeige + Sender-Auto-Deaktivierung bei Domain-Widerruf — **fehlt** (M9 existiert noch nicht).
---
## Empfohlenes Vorgehen
**Phase 0 — Re-Layering-Entscheidung + Schema-Erweiterung (Fundament):**
1. Bestehende `email.prisma`-Modelle in eine HiveGrid-Migration übernehmen und **härten**: `Campaign.status`→Spec-Enum, `tenantId` NOT-NULL/RLS, Spec-Felder (preheader, scheduled_at, *_count, recipient_list_snapshot, archived_at, created_by_user_id, tracking_token/unsubscribe_token unique) + Indizes ergänzen. Forward-Only-Migration mit Inverse-SQL (DoD §28).
2. **Vier fehlende Modelle neu anlegen:** `Unsubscribe`, `EmailSuppressionList`, `RecipientSegment`, `EmailTemplate` (Spec Block A). Opt-In-Felder am Contact ergänzen.
3. Provider-Schicht: `MailProvider`-Interface aus `packages/mail` 1:1 übernehmen, EU-SMTP-Adapter (LP-13) als neuen Adapter daneben (Scaleway TEM evaluieren — offener Spec-Punkt vor Bau klären).
**Phase 1 — M100 Editor (höchster 1:1-Reuse):**
4. `EmailMarketingService` + `campaigns.db.server` + `EmailSenderForm`/Sender-Routes + i18n + Feature-Flag **als Vorlage** in HiveGrid-Layer (`app/domain` + `packages/view-models` + `ui-hivegrid`/Subframe-Adapter) re-layern. Permissions `email_marketing.*` + Sidebar-Eintrag „Kommunikation" verdrahten.
5. `replaceVariables` übernehmen, Token auf `{{first_name}}` + `{{custom.x}}` angleichen; `EmailTemplate`-CRUD + SHK-Branchen-Templates (aus `email-templates`-Render-Pipeline) neu.
6. 6-Schritt-Wizard + Mobile/Desktop-Vorschau + Test-Mail (vorhandenes `sendCampaignTest` als Basis) + Spam-Score-Check neu.
**Phase 2 — M101 Segmente:**
7. `RecipientSegment` mit JSONB-Filter-Tree neu bauen; `EntityViewsApi`/`CrmService` als CRM-Auflösungs-Brücke nachnutzen; Static/Dynamic + Auto-Exclusion-Engine + 50-Sample-Vorschau + Aufschlüsselung neu.
**Phase 3 — M102 Versand-Engine (Architektur-Kern):**
8. `QueuedMailProvider` + `email-sequences`-Engine-Pattern (injizierbarer Store/Queue) als Vorbild für einen **BullMQ-Kampagnen-Versand-Worker** mit Batch/Retry/Throttle (M007-Idempotenz). Cron für geplanten Versand. Daily-Cap (SubscriptionFeature) + Frequenz-Cap + Pause/Cancel-Lebenszyklus + Pflicht-Footer-Auto-Injection (`Footer.tsx`-Basis) + Pflicht-Validierungen + DKIM-Gate (M9) neu.
**Phase 4 — M104 Unsubscribe + Bounce (starke Vorlagen):**
9. Token-Mechanik (`unsubscribe-token.ts`) + Pixel-/Click-Routen + `unsubscribe.$token.tsx` + `mailersend-signature.ts` + `provider-webhook-route.ts` **als Vorlagen** auf `OutboundEmail`/`Unsubscribe`/`EmailSuppressionList` umverdrahten. Bounce-Klassifikation + Suppression-Auto-Add + Complaint→Auto-Unsubscribe + Activity-Wiring + Grund/Feedback-Formular neu.
**Phase 5 — M103 Reporting (+ A/B Phase 2):**
10. `getSummary`/`OutboundEmailsTable`/`EmailMarketingSummaryRoute` ausbauen: CTR, Bounce/Unsub/Complaint-Raten, Click-Map, Pro-Empfänger-Aktivitäts-Detail, Status-Filter. A/B-Tests sind Phase 2 (Spec 10.3/10.11.1).
**Querschnitt (durchgängig):** Activity-Stream (`logActivity` + Marketing-Engagement-Aggregat am Contact), `notify()`, Audit (auto M001), Klartext-Catalog (M002), View-Model-Unit-Tests + Smoke-Tests + Hot-Swap-Drill + Visual-Regression (DoD D).
**Trigger-Mails (Spec J):** koordiniert mit M17 — `send-marketing-email`-Action auf Mini-Kampagnen-Pfad (1 Empfänger, sofort) aufsetzen; Sequence-Engine als Architektur-Vorbild.
@@ -0,0 +1,110 @@
# Boilerplate-Abgleich: M12 — Telefonie (AirCall + HalloPetra)
| Feld | Wert |
|---|---|
| **Modul-ID** | M12 |
| **Name** | Telefonie (AirCall + HalloPetra) |
| **Milestones** | M105 (HalloPetra-Adapter), M106 (AirCall-Adapter), M107 (Anruf-Eingang + Auto-Match), M108 (Anruf-Ausgang / Click-to-Call), M109 (Voicemail + Notizen + Automation-Trigger) |
| **Block** | Block 5 — Kommunikation (Pflichtenheft Modul 12, [N16, 09.05.2026]) |
| **Sidebar-Gruppe** | KOMMUNIKATION → Telefonie (INTEGRATION-CONTRACTS §1) |
| **Reuse-Level** | **medium** |
> **Verifikations-Hinweis:** Alle Befunde wurden per Grep/Read direkt in `/Users/raffaelsheikh/Documents/GitHub/boilerplate` geprüft, nicht nur aus der Inventur übernommen. Das Kern-Ergebnis: **Es gibt KEINE Telefonie-, Call-, Phone-, VoIP-, AirCall- oder HalloPetra-Artefakte** in der Boilerplate (weder Prisma-Modell, noch Service, noch Route, noch UI). Die gesamte Telefonie-Domäne ist neu zu bauen. Was die Boilerplate liefert, sind **starke Querschnitts-Fundamente und ein direkt passendes Adapter-Registry-Pattern** (`marketing-outbound`), das als Vorlage für den `TelephonyProviderInterface` dient.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `packages/marketing-outbound/src/types.ts` (`MarketingOutboundAdapter`, `parseConfig`/`send`, `AdapterContext`, `AdapterResult`, `MarketingAdapterConfigError`) + `packages/marketing-outbound/src/index.ts` (`adapterRegistry`, `resolveAdapter(type)`, `ADAPTER_TYPES`) | Adapter-Interface + Registry + Resolver — strukturell exakt das, was der Spec-Block H `TelephonyProviderInterface` (connect/disconnect/listProviderUsers/triggerOutboundCall/subscribeToWebhooks/verifyWebhookSignature/parseWebhookEvent/getRecordingUrl) braucht | **als-vorlage** (Form 1:1 übernehmbar, AirCall-/HalloPetra-spezifische Methoden/Configs ergänzen) |
| `packages/foundation/src/adapters.ts` (`defineAdapter()`, `getAdapter()`, `AdapterStatus`) | Globales Adapter-Register-Primitiv (von AGENTS.md M008-M010 als Apps-Engine-Seam referenziert); `AdapterStatus` = `not_configured/configured/healthy/degraded/unavailable` deckt `connection_status` + Health-Check ab | **1:1** (Status-Enum auf Telephony-Connection mappen) |
| `apps/web/app/utils/api/.server/CryptoApi.ts` (`encrypt`/`decrypt`, AES-256-GCM, versioniert, `CRYPTO_SECRET`) | Verschlüsselung der OAuth-Tokens + Webhook-Secret (`oauth_access_token_encrypted`, `oauth_refresh_token_encrypted`, `webhook_secret_encrypted`) | **1:1** |
| `packages/api-client-ts/src/webhooks.ts` (`verifyWebhookSignature`, `signWebhookPayload`, HMAC-SHA256 über `${timestamp}.${payload}`, 5-Min-Replay-Fenster, `timingSafeEqual`) | Webhook-Signatur-Verifikation für eingehende AirCall/HalloPetra-Webhooks (Spec Block D, `signature_valid`) | **anpassen** (AirCall nutzt eigenes Signatur-Schema/Header — Logik als Vorlage, Provider-Format einsetzen) |
| `apps/web/app/routes/oauth/github/start.server.ts` (`createGitHubOAuthPkceChallenge`, state/verifier/PKCE) + `apps/web/app/routes/oauth/github/callback.tsx` (state-Validierung, code→token-Exchange, Session-Roundtrip) | OAuth-Authorization-Code-Flow-Template für AirCall-Connect (Spec Block B, Schritt 1) | **als-vorlage** (GitHub-spezifisch; AirCall-OAuth-Endpunkte/Scopes `public_api`, Token-Storage encrypted ersetzen) |
| `apps/worker/src/jobs/push-notification.ts` (provider-pluggable FCM/APNs/WebPush-Fan-out, `revokedAt`-Invalidierung) + `prisma/schema/notifications.prisma` (`DeviceToken`, `NotificationDelivery`, `UserNotificationPreference`) + `packages/foundation/src/notifications.ts` (`NotificationInput`-Typen) | Push-Benachrichtigung bei eingehendem Anruf (`call.ringing_on_agent`, Spec Block D + 12.5.7) + verpasster-Anruf-Notification — **kein Direkt-Push, sondern über M011-Pipeline** | **1:1** (Notify-Aufruf gemäß INTEGRATION-CONTRACTS §7; Telephony liefert nur `notify({...})`) |
| `apps/worker/src/jobs/workflow-trigger.ts` + `workflow-trigger-store.ts` + `workflow-trigger-native-executor.ts` (`WorkflowTriggerJobData`, `findLiveEventWorkflows`, Outbox-Queue `outbox.workflow`, Filter/Condition-Auswertung) + `prisma/schema/workflows.prisma` (`Workflow`, `WorkflowBlock`, `WorkflowBlockCondition`) | Automation-Engine-Trigger-Auslösung für `call.received/answered/completed/missed` (Spec Block F) — M12 publiziert nur Outbox-Events, der Trigger-Apparat existiert | **1:1** (Trigger-Event-Typen + Payload aus M12 in bestehende Workflow-Trigger-Pipeline einspeisen) |
| `packages/db/src/outbox.ts` (`publishOutboxEvent(tx, args)`, `OUTBOX_EVENT_TYPE_PATTERN` `<domain>.<entity>.<verb>`) + `prisma/schema/outbox.prisma` (`OutboxEvent`, `dispatchKey @unique`) | Transaktionale Event-Auslieferung für Webhook-Verarbeitung → Activity / Notification / Automation-Trigger (atomarer Side-Effect) | **1:1** |
| `packages/queue/*` (`queueNames`, `enqueueJob`, `buildIdempotentJobId`, `createWorker`, Prioritäten) | Async-Verarbeitung: Webhook-Event-Processing, Token-Refresh-Worker (24h-Cron, Spec Block B Reconnect), Health-Check | **1:1** (neue Queue-Namen + Job-Payloads ergänzen) |
| `apps/web/app/utils/leads/marketingIntegrations.db.server.ts` + `apps/web/app/routes/admin/marketing-integrations.tsx` (verschlüsselte `configJson`-Integration + Setup-/Verbindungs-UI) | Vorlage für Integration-Setup-Route `/settings/integrations/telephony/` (Connect, Test, Disconnect, encrypted Config) | **als-vorlage** |
| `prisma/schema/appsEngine.prisma` (`App` mit `@@unique([tenantId, type])`, `DataConnection` mit encrypted `config`, `AppSyncRun`/`AppSyncRunLog`/`AppSyncItem`) | Datenmodell-Muster für „1 Connection pro Tenant" + verschlüsselte Credentials + Sync-Lauf-Log. **Achtung:** nur Schema, KEIN Service/Worker — siehe Lücken | **als-vorlage** (Muster für `TelephonyConnection` `@@unique([tenant_id, provider])`; nicht direkt verwendbar) |
| `apps/web/app/application/sidebar/AppSidebar.tsx` + `SidebarGroupDto.ts` + `SidebarItem.ts` + `packages/ui/src/navigation.ts` (`AppSidebar`-Adapter, shadcn/radix `SidebarPrimitives`) | Sidebar-Wiring (KOMMUNIKATION → Telefonie) mit Permission-Gate (DoD §B-6/7) | **anpassen** (HiveGrid nutzt eigene `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` gemäß AGENTS.md — Boilerplate-Sidebar ist Referenz, nicht Ziel-Ort) |
| `prisma/schema/accounts.prisma` (`TenantUser`, `User`, `Tenant`) | FK-Anker für `TelephonyUserBinding.tenant_user_id`, `TelephonyCallSession.tenant_user_id`, `connected_by_user_id` | **1:1** (existierende Identity-Modelle referenzieren) |
| `prisma/schema/core.prisma` (`AuditEvent` Hash-Kette) + `packages/db/src/audit-hash.ts` + `packages/foundation/src/audit.ts` | Audit-Trail für Connection-Setup, User-Bindings, Notiz-Edits (Spec AV #6, DoD §C-13) — automatisch via M001-Prisma-Middleware | **1:1** |
| `packages/db/src/dsgvo/user-fk-map.ts` | Pflicht-Eintrag für jeden neuen User-FK der Telephony-Modelle (`tenant_user_id`, `manual_override_user_id`, `note_updated_by_user_id`, `connected_by_user_id`) — CI bricht sonst | **1:1** (Wartung beim Modellbau) |
| `packages/foundation/src/idempotency.ts` + `apps/worker/src/jobs/legacy-event-receiver.ts` (`withOutboxIdempotency`) | Idempotenz für Webhook-Events (`TelephonyWebhookEvent.provider_event_id`, Spec Block A) | **1:1** |
| `apps/web/app/routes/api.embed.forms.$formKey.submit.tsx` (public inbound-POST-Endpoint mit Origin/Anti-Spam-Verarbeitung) | Strukturvorlage für Webhook-Endpoint `/api/v1/webhooks/telephony/aircall/{tenant_slug}` (Spec Block D) | **als-vorlage** |
---
## Vorhandene Daten-Modelle (konkret)
**Direkt referenzierbar (existieren):**
- `prisma/schema/accounts.prisma` — `Tenant`, `TenantUser`, `User` → FK-Ziele für alle Telephony-Modelle. (Hinweis: `Tenant.country` existiert **nicht** — Default-Country für E.164-Normalisierung muss neu modelliert/aus Tenant-Setting kommen.)
- `prisma/schema/notifications.prisma` — `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` (APNs/FCM/WebPush) → Push bei Inbound/Missed.
- `prisma/schema/workflows.prisma` — `Workflow`, `WorkflowBlock`, `WorkflowBlockCondition*` → Automation-Trigger-Konsum.
- `prisma/schema/outbox.prisma` — `OutboxEvent` → transaktionale Event-Auslieferung.
- `prisma/schema/core.prisma` — `AuditEvent`, `IdempotencyKey`, `Credential` (globaler Secret-Store) → Audit + Idempotenz.
- `prisma/schema/appsEngine.prisma` — `App`/`DataConnection`/`AppSyncRun*` → **nur als Schema-Muster** (kein Service-/Worker-Layer dahinter).
**Telephony-spezifische Modelle — KOMPLETT NEU (existieren nicht in der Boilerplate):**
- `TelephonyConnection` — neu (Spec Block A). Muster aus `App`/`DataConnection` übernehmbar (`@@unique([tenant_id, provider])`, encrypted config).
- `TelephonyUserBinding` — neu (AirCall/HalloPetra-User ↔ TenantUser).
- `TelephonyCallSession` — neu (zentrales Anruf-Modell, inkl. `matched_contact_id/lead_id/deal_id/project_id`).
- `TelephonyWebhookEvent` — neu (append-only Webhook-Roh-Log, Retention 30/90 Tage).
- Enums (`provider`, `direction`, `call status`, `match_confidence`, `event_type`, `processing_status`) — neu.
- Alle neuen Telephony-Modelle gehören in `prisma/schema/app.prisma` (HiveGrid-Platzhalter) oder ein neues `prisma/schema/telephony.prisma`.
---
## Lücken / komplett neu zu bauen
1. **Gesamte Telefonie-Domäne** — Verifiziert: 0 Treffer für `telephon|aircall|hallopetra|sipgate|voicemail|click-to-call|call-session` in `prisma/schema/`, `apps/web/app`, `packages/ui*`, `packages/view-models`. Datenmodelle, Domain-Logik, Routes, View-Models, UI: alles neu.
2. **AirCall-Adapter** (M106) — AirCall-OAuth (`public_api`-Scope), `POST /users/{id}/calls`, `GET /users`, Webhook-Subscription-Anlage, Recording-URL-Abruf, AirCall-Signatur-Verifikation. Kein AirCall-SDK/Code vorhanden.
3. **HalloPetra-Adapter** (M105) — kein Code, keine Referenz. Eigene Implementierung des `TelephonyProviderInterface`; Click-to-Call-Detection mit `tel:`-Fallback; Vorqualifikations-Tag → Lead/Activity.
4. **Telefonnummer-Normalisierung (E.164)** — `libphonenumber`/`awesome-phonenumber` ist **keine** Dependency (verifiziert: leer in allen package.json). Library + Normalisierungs-Helfer komplett neu; Default-Country-Quelle ebenfalls neu (kein `Tenant.country`).
5. **Activity-Stream** — Kein `Activity`-Modell, kein `logActivity`-Service in der Boilerplate (M045 ist HiveGrid-eigener Querschnitt, noch nicht gebaut). Anruf-Activities (`call_inbound/outbound/missed/note_added/note_edited`) hängen an M045. **Vorbedingung, nicht aus BP übernehmbar.**
6. **CRM-Entitäten zum Matchen fehlen** — `Contact`, `ContactPhone`, `Deal`, `Project` existieren **nicht** in der Boilerplate (nur operator-scoped `Lead` aus `marketingLeads.prisma`, ohne `phone`-Feld). Auto-Match (Spec Block D) hängt vollständig an HiveGrid-CRM (M042-M046 Kontakte, M059-M064 Sales, M065-M070 Aufträge, Lead-Inbox M047-M051). **Harte Vorbedingung.**
7. **appsEngine ist Schema-only** — Verifiziert: kein Worker-Job/Service konsumiert `AppSyncRun`/`DataConnection`. Die „Apps-Engine" mit Code ist das `marketing-outbound`-Pattern + WASM-`plugin-runtime` (in Prod nur `log`/`audit`-Capabilities aktiv, `http.outbound` ist `productionAvailable: false`). → Provider-Adapter müssen als regulärer Server-/Worker-Code laufen (Pattern: `marketing-outbound`), **nicht** im WASM-Sandbox.
8. **Outbound HTTP zu Provider-APIs** — `adapterFetch()` (SSRF-Guard, Timeout) existiert in `marketing-outbound`, ist aber auf das interne Adapter-Set gemünzt. AirCall/HalloPetra-API-Clients (Auth-Header, Rate-Limit-/Quota-Handling, Klartext-Diagnose pro HTTP-Code laut Spec Block H) neu.
9. **Token-Refresh-Worker** (24h-Cron, <72h-Restlaufzeit → erneuern; Refresh-Fehler → `revoked` + Notify) — Cron-Worker-Muster vorhanden (`*-cron.ts`), Logik neu.
10. **Notiz-Editor + Markdown** (M109, Spec Block E) — UI + `note_text`-Persistenz + Audit-on-Edit; kein vorgefertigter Anruf-Notiz-Baustein.
11. **Permissions** — `telephony.*`-Keys (`click_to_call`, `read_own/all`, `write_notes_own/all`, `admin`) + Bundle-Mapping neu in `packages/entitlements/src/permissions.ts`; RBAC-Fundament (`permissions.prisma`, `withTenantScope`/`withPermission`) existiert.
12. **i18n-Keys** — Keine `telephony`-Locale-Datei; i18n-Runtime (`packages/i18n`) vorhanden, aber `packages/i18n/src/locales/de/telephony.json` neu (DoD §B-10).
13. **View-Models** — `packages/view-models/src/` ist Skelett (nur `index.ts`). Alle Telephony-VMs (Connection-Status, Call-Liste, Call-Detail, User-Binding-Liste) neu.
14. **Subframe-UI** — Spec nennt Designs explizit als offen; UI komplett zu entwerfen + via Adapter integrieren (DoD §A-4, Subframe-Pflicht laut User-Memory).
---
## Empfohlenes Vorgehen (Reihenfolge)
**Phase 0 — Vorbedingungen prüfen (nicht aus BP, hängt an anderen HiveGrid-Modulen):**
- CRM-Kontakt/`ContactPhone`/Lead/Deal/Project (M042-M070) müssen existieren, bevor Auto-Match (M107) sinnvoll ist.
- Activity-Stream-Querschnitt M045 (`logActivity`) muss stehen (M107/M109 schreiben darauf).
- Notification-Service M011 (`notify`) + Permission-Querschnitt M004 + i18n M003 sind harte Querschnitt-Voraussetzungen (laut AGENTS.md vorhanden bzw. zu bauen).
**Phase 1 — Fundament aus BP übernehmen (zuerst):**
1. `TelephonyProviderInterface` definieren — Form aus `packages/marketing-outbound/src/types.ts` + `foundation/adapters.ts` (`defineAdapter`) übernehmen, Telephony-Methoden ergänzen (Spec Block H).
2. Datenmodelle (`TelephonyConnection/UserBinding/CallSession/WebhookEvent` + Enums) in `prisma/schema/` anlegen, Muster `@@unique([tenant_id, provider])`/encrypted-config aus `appsEngine.prisma`; FK auf `TenantUser`; `user-fk-map.ts` ergänzen; Forward-Only-Migration mit Inverse-SQL (DoD §F-28).
3. Token-Verschlüsselung 1:1 aus `CryptoApi.ts`; Webhook-HMAC-Verify-Logik aus `api-client-ts/src/webhooks.ts` als Vorlage.
4. Permissions (`telephony.*`) + Bundles in `packages/entitlements`; i18n-Keys; Sidebar-Eintrag (KOMMUNIKATION → Telefonie) mit Gate.
**Phase 2 — Adapter + Connect-Flow (M106 AirCall, dann M105 HalloPetra):**
5. AirCall-Adapter implementieren; OAuth-Flow nach Vorlage `oauth/github/start.server.ts`/`callback.tsx` (PKCE/state/Session); Setup-Route nach Vorlage `routes/admin/marketing-integrations.tsx`; Token-Refresh-Cron nach `*-cron.ts`-Muster.
6. HalloPetra-Adapter (gleiches Interface), Click-to-Call-Detection + `tel:`-Fallback.
**Phase 3 — Anruf-Verarbeitung:**
7. Webhook-Endpoint (Vorlage `api.embed.forms.$formKey.submit.tsx`) → `TelephonyWebhookEvent` (idempotent via `withOutboxIdempotency`) → `publishOutboxEvent` → Worker (Queue-Pattern).
8. E.164-Normalisierung (neue `libphonenumber`-Dependency) + Auto-Match (M107) gegen CRM; Push via `notify()` (M011); verpasste Anrufe + Rückruf-Aufgabe.
9. Click-to-Call (M108) aus Kontakt/Deal/Lead über Adapter `triggerOutboundCall`; ausgehender Anruf auto-protokolliert.
**Phase 4 — Notizen, Voicemail, Automation (M109):**
10. Notiz-Editor + Audit-on-Edit; Recording-Link (kein Re-Hosting); Automation-Trigger als Outbox-Events in die bestehende `workflow-trigger`-Pipeline einspeisen.
**Querschnitt durchgehend:** View-Models (M013), Klartext-Fehler (M002), Empty/Loading-States, Smoke- + Hot-Swap-Tests, Visual-Regression, Feature-Flag (M006) — alle gemäß DoD.
---
## Reuse-Level-Begründung: medium
Die **Telefonie-Domäne selbst existiert zu 0%** (kein Modell, kein Service, kein UI). Aber die Boilerplate liefert **mehr als nur Fundament**: ein strukturell direkt passendes Adapter-Registry-Pattern (`marketing-outbound`), einen verwendbaren OAuth-Flow, fertige Verschlüsselung, HMAC-Webhook-Verify, eine komplette Push-/Notification-Pipeline, die Automation-Trigger-Pipeline und das Outbox/Queue/Audit/Idempotenz-Querschnitt-Set. Das hebt M12 über „low" (nur Querschnitt) — die nennenswerten Lücken (Provider-Adapter, CRM-Match-Abhängigkeit, E.164-Normalisierung, Activity-Stream-Abhängigkeit, gesamtes UI) verhindern „high". → **medium**.
@@ -0,0 +1,129 @@
# Boilerplate-Abgleich — M13 Team-Chat (intern)
- **Modul-ID:** M13 (Modul 13)
- **Name:** Team-Chat (intern) — Quiply-Ablösung, drei Ebenen (Projekt-Channel / Gruppen-Channel / DM)
- **Milestones:** M110 (Channel-Modell + Permissions), M111 (Nachrichten + Threads + Pinned + Reactions), M112 (Datei/Foto-Anhänge), M113 (Modul-Verknüpfungen / Auto-Lifecycle / Mark-as-Task), M114 (Mentions + Push + Lese-Bestätigungen)
- **Block:** Block 5 (`_bmad-output/pflichtenheft/blocks/block5.md`), Abschnitte 13.1–13.3
- **Reuse-Level:** **low** — kein einziges Chat-Datenmodell in der Boilerplate; das Modul ist im Datenkern komplett neu zu bauen. Wiederverwendbar sind ausschließlich **Querschnitts-Bausteine** (Notifications, Push/DeviceToken, Storage+AV, Audit, Permissions, i18n, Feature-Flags, Search, View-Model-Konvention, App-Shell) und **konzeptionelle Vorlagen** (`RowComment`/`RowCommentReaction` als Message/Reaction-Muster, `realtime`-SSE-Paket als Streaming-Vorlage, `apps/hocuspocus` als Standalone-Realtime-Service-Muster).
> **Wichtigster Befund:** Die im Auftrag erwähnten „packages/realtime + apps/hocuspocus + collabDocs" lösen **nicht** die Chat-Echtzeit-Anforderung aus Block H. `@toolctrl/realtime` ist ein **SSE-only Notification-Stream** (4 Event-Namen, alle notification-bezogen), kein generischer WebSocket-Room-/Pub-Sub-Layer mit `message.new`/`typing.*`/`presence.*`/`reaction.*` (Block H.2). `apps/hocuspocus` ist ein **Y.js/CRDT-Collab-Doc-Server** (für Live-Editing geteilter Dokumente), kein Chat-Channel-Broadcast. `CollabDoc` (collabDocs.prisma) speichert CRDT-Binärzustand pro Resource, **kein** Message-/Thread-/Reaction-Modell. Beide sind als *Architektur-Vorlage und Infrastruktur-Bauteil* nützlich, decken die Chat-Domäne fachlich aber nicht ab.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `prisma/schema/notifications.prisma` (`NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` + Enums `NotificationDeliveryStatus`, `NotificationPreferenceChannel`) | In-App- + E-Mail-Notification-Delivery, per-Channel/Category-Preference, Push-Device-Token (APNs/FCM/WebPush, `revokedAt`) | **1:1** — deckt Block I.2 (Notification-Trigger/Status), Block J.2 (Push-Token-Registry) und Quiet-Hours-Speicherort-Vorbild ab. `category`/`messageKey`/`sourceKey` direkt für Chat-Notification-Bündelung nutzbar |
| `packages/foundation/src/notifications.ts` (`notify`, `notifyMany`, `bundleNotifications`, `isNotificationEnabled`, `assertI18nKey`) | Tenant-pflichtige Notifications mit Preference-Check, Upsert per `sourceKey`, Metadata-Redaction, **Bündelung pro Tenant/User/Channel/Category** | **1:1** — entspricht DoD-Pflicht „Notifications via `notify()` (M011), nicht direkt"; `bundleNotifications` deckt Block I.2 Anti-Spam-Bündelung (>5 Msg/Min) direkt ab |
| `apps/worker/src/jobs/push-notification.ts` | Worker-Job für Push-Versand (DeviceToken-basiert) | **1:1 / anpassen** — Push-Pipeline für Block J.2 wiederverwenden; Chat-Payload-Datenschutz (nur Channel-Name + Sender-Vorname + Snippet 100 Zeichen, Block J.2) als Payload-Builder ergänzen |
| `apps/mobile/src/pushRegistration.ts` + `apps/mobile/capacitor.config.ts` (Plugin `push-notifications`, `local-notifications`) | Capacitor-Push-Registration gegen Backend + native Shell (WebView um Web-Build) | **als-vorlage** — PWA-Web-Push (Phase 1) + späteres Native (Phase 2). Block J Mobile-First-Hülle |
| `packages/storage/src/scan/*` (`createMagicBytesScanAdapter`, `clamav-scanner`, `DEFAULT_ALLOW_LIST`/`resolveAllowList`, `s3-quarantine`, `ScanVerdict`) + `apps/worker/src/scanService.ts` | Pre-Upload Magic-Bytes-Check, ClamAV-Scan, S3-Quarantäne, Datei-Typ-Allowlist | **1:1** — Block G.1 Anti-Virus-Scan + Datei-Typ-Whitelist (überlappt mit M23) |
| `packages/storage/src/index.ts` + `./signed` + `./access` | S3-Adapter, Signed-Upload/Download-URLs, Access-Helper | **1:1** — Block G.1 Chat-File-Upload-Pipeline (`tenants/{tenantId}/chat/{chatId}/...`) |
| `prisma/schema/core.prisma` `FileObject` (+ `FileUploadProgress`, `FileChunk`) | Storage-Metadaten-Modell (provider/bucket/key/contentType/byteSize/checksum) | **anpassen** — Datei-Records für Chat-Attachments. **Caveat:** `FileObject` ist **nicht** tenant-scoped (kein `tenantId`/`userId`-Feld) — für Chat-Attachment-Backlinks (`TeamChatMessage.attachments[]` → File-Record mit Backlink auf Source-Message, Block A/G) braucht es ein tenant-scoped Join-/Attachment-Modell (M23-Abhängigkeit) |
| `prisma/schema/entityBuilder.prisma` `RowComment` + `RowCommentReaction` (+ `RowTag`/`EntityTag`, `RowTask`) | Comment-mit-Reactions-Muster: `value`, `createdByUserId`, `isDeleted`, `reactions[]` (emoji als `reaction String`); `RowTask` als „Aufgabe aus Eintrag"-Muster | **als-vorlage** — direktes konzeptionelles Vorbild für `TeamChatMessage` + `TeamChatReaction` (Soft-Delete-Flag, Reaction als String, Author-FK). NICHT 1:1 übernehmbar: es fehlen Thread/`parent_message_id`, `mentions[]`, `attachments[]`, `link_previews[]`, System-Events, Read-Receipts, Channel-Membership |
| `packages/realtime/src/{events,server,client}.ts` (`REALTIME_EVENT_NAMES`, `validateRealtimeEvent`, `serializeRealtimeEvent`, `subscribeToRealtime` mit Reconnect-Backoff + Diagnostics) | SSE-Event-Contract + deterministische Frame-Serialisierung + Browser-Subscriber **mit Exponential-Backoff-Reconnect, Cursor-Resume, Status-Transitions, Redaction** | **als-vorlage** — das **Reconnect-/Cursor-/Diagnostics-Muster** (`client.ts`) ist direkte Vorlage für Block H.3 (Reconnect-Strategie, Cursor-Resume) und Block M (Verbindungs-Banner). Die Event-Union ist chat-fremd und muss um `message.*`/`typing.*`/`presence.*`/`reaction.*`/`thread.reply`/`read.receipt`/`link_preview.ready` (Block H.2) erweitert oder ersetzt werden. Für viele Channels + bidirektionales Typing/Presence ist SSE allein unzureichend → echter WebSocket-Layer nötig |
| `apps/hocuspocus/` (`Server`-Bootstrap, `createAuthExtension` mit JWT HS256 + Tenant-Parity-Check, eigenes `Dockerfile`, Standalone-Container-Pattern) | Standalone-Node-Realtime-Service: JWT-Handshake-Auth, Tenant-Isolation pro Connection, eigener Container | **als-vorlage** — Architektur-Blueprint für den **`/ws/chat`-WebSocket-Server** (Block H.1): separater EU-gehosteter Container (LP-13), Connection-Auth bei Handshake, Tenant-Scope. JWT-Auth-Extension + Tenant-Parity-Logik wiederverwendbar. Yjs/CRDT-Teil ist NICHT relevant |
| `packages/shared-store/src` (`createSharedStore`, `RedisAdapter`, Pub/Sub-Channel, `createSlidingWindowRateLimiter`, `createFixedWindowRateLimiter`, `createIdempotencyStore`) | Redis-KV mit Pub/Sub-Channel + Rate-Limiter + Idempotenz; DB-Index-Allokation (2 = Pub/Sub) | **1:1 / anpassen** — Block H.1 Redis-Pub/Sub als Message-Broker zwischen WS-Server-Instanzen (Multi-Pod); Rate-Limiter für Anti-Spam (Reactions/Messages); Presence-State (`online/away/offline`, Block H.5) und Typing-Throttle (Block H.4) als KV mit TTL |
| `packages/foundation/src/permissions.ts` (`requirePermission`, `withTenantScope`, `withPermission`, `withOwnerCheck`) | Tenant-Scoping + Permission-/Owner-Gating, Fail = `CatalogError(notFound)` (404 statt 403) | **1:1** — Block L (alle Chat-Permissions), DoD „Permission-Filter via `withTenantScope()` + `withPermission()`"; `withOwnerCheck` für Membership-/Author-Checks (Block L.6); 404-statt-403 deckt Block M („Du bist kein Mitglied dieses Channels") |
| `packages/foundation/src/audit.ts` (`auditEvent`, `withAuditMiddleware`) + `prisma/schema/core.prisma` `AuditEvent` (Hash-Chain `previousHash`/`eventHash`, `dataSensitivity`, `regulatoryScope[]`) | Audit-Middleware für mutierende Aktionen, manipulationssichere Event-Kette | **1:1** — Block L.7 (Tenant-Admin-DM-Audit mit Begründungs-Pflicht), Block H.6 (audit-relevante Events: member_added, channel_archived), AV #6 (Audit-Default für schreibende Aktionen) |
| `packages/foundation/src/errors.ts` (`CatalogError`, `catalogRegistry`, `foundationErrors`) | Klartext-Fehler mit `code`/`status`/`i18nKey`/`details` | **1:1** — Block M (alle Klartext-Hinweise/-Fehler), DoD „Fehler-Texte aus M002-Catalog" |
| `packages/foundation/src/flags.ts` + `packages/feature-flags/src` (`evaluateFlag`, `FeatureFlagProvider`, `useFeatureFlagEnabled`) + `prisma/schema/featureFlags.prisma` | Feature-Flag-Engine (Targeting, Cache, React-Hooks, Audit-Trail) | **1:1** — DoD „Feature-Flag-fähig (M006) — Modul ohne Deploy abschaltbar"; Chat-Modul als gegateter Block |
| `packages/i18n/src` (`useTranslate`, Namespaces, `defaultLocale="de"`) + `assertI18nKey`/`assertI18nLabel` | Locale-Config, ICU-MessageFormat, i18n-Key-Disziplin (verify:i18n-Gate) | **1:1** — DoD „Alle UI-Texte als i18n-Keys"; neuer Namespace `chat` analog zu `crm`/`billing` |
| `packages/view-models/src/index.ts` (`VMState`/`VMStatus`, `SidebarRegistryItem`, `MobileRegistryItem`, `RegistryGate`, `assertI18nLabel`) | VM-State-Maschine (idle/loading/ready/empty/error mit i18n-Key) + Shell-Navigations-Registry-Typen (permission+flag-gegated) | **1:1** — DoD „Loader baut View-Model; UI-Component empfängt nur VM"; Sidebar-/Mobile-Tab-Eintrag „Chat" mit Unread-Badge (Block B) via `SidebarRegistryItem` + `RegistryGate` (Permission `chat.read_own_channels`) |
| `packages/ui-app/src/index.ts` (`AppShell`, `createSidebarRegistry`, `createMobileRegistry`, `createShellRegistries`, `EmptyPage`) | App-Shell + Sidebar-/Mobile-Registry-Factories + Empty-Page-Pattern | **1:1** — Erreichbarkeits-DoD (Sidebar §1, Mobile-Tab-Bar §13); Empty-State-Pattern für Block M („Du bist noch in keinem Channel") |
| `packages/search/src` (`createSearchClient`, `createSearchIndex`, Tenant-Filter-Zwang, Meilisearch-Wrapper) + Worker `search-indexer*` + Outbox-Prefix `search.index.` | Tenant-scoped Volltext-Suche (Meilisearch), Index-Registrierung via Outbox | **1:1 / anpassen** — Block B/D Cross-Channel-Suche (`/{tenant}/chat/search`), DoD „Search-Index registriert in `indexed-entities.ts`". Trigram-In-Channel-Suche (Block A Indizes) ist DB-seitig (Postgres) separat |
| `apps/worker/src/outbox/{dispatcher,event-router}.ts` (`resolveOutboxQueueName`, Prefixe `notification.`/`audit.`/`search.index.`/`workflow.`) + `prisma/schema/outbox.prisma` | Transactional-Outbox (FOR UPDATE SKIP LOCKED) + Event-Routing nach Prefix | **1:1 / anpassen** — zuverlässige Event-Auslieferung für Chat→Notification/Audit/Activity/Search; neuer Prefix `chat.` für Activity-Stream-Integration (Block K) |
| `apps/worker/src/handlers.ts` (`buildHandlerRegistry`, `HandlerDependencies`, `createRegisteredProcessor`) + `apps/worker/src/queues.ts` (Cron-Muster) | Worker-Handler-Registry + Cron-/Queue-Muster (DI-Seams) | **als-vorlage** — Block C.1 Auto-Archive-Cron (Projekt-Chat 7d nach Abschluss), Block G.2 Link-Preview-Async-Job, Read-Receipt-Batch |
---
## Vorhandene Daten-Modelle (konkret)
**Direkt nutzbare Querschnitts-Modelle:**
- `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` — `prisma/schema/notifications.prisma` (Block I.2 + J.2)
- `AuditEvent` — `prisma/schema/core.prisma:213`-Umfeld bzw. core (Hash-Chain) (Block L.7, H.6)
- `FileObject`, `FileUploadProgress`, `FileChunk` — `prisma/schema/core.prisma:213` ff. (Block G.1, **nicht tenant-scoped** → Caveat)
- `FeatureFlag`, `FeatureFlagFilter`, `FeatureFlagAudit` — `prisma/schema/featureFlags.prisma` (DoD Feature-Flag)
- `Outbox` — `prisma/schema/outbox.prisma` (Event-Auslieferung)
**Als konzeptionelle Vorlage (Muster, nicht 1:1):**
- `RowComment` (`value`, `createdByUserId`, `isDeleted`, `reactions[]`) + `RowCommentReaction` (`reaction String`, `createdByUserId`) — `prisma/schema/entityBuilder.prisma:409`/`:424` → Vorbild für `TeamChatMessage` + `TeamChatReaction`
- `RowTask` (`title`, `description`, `assignedToUserId`, `deadline`, `completed`) — `prisma/schema/entityBuilder.prisma` → Vorbild für „Mark as Task" (Block G.3), aber Ziel-Modell ist M26-Task
- `CollabDoc` (`ydocState Bytes`, `version`, `@@unique([tenantId, resourceType, resourceId])`) — `prisma/schema/collabDocs.prisma` → **nicht** für Chat verwendbar (CRDT-Binär, kein Message-Modell)
- `Chat`/`ChatMessage`/`ChatMessagePart` — `prisma/schema/ai.prisma:1`/`:31`/`:44` → **AI-Assistent-Chat** (LLM-Konversation), namentlich kollidierend aber fachlich irrelevant für Team-Chat. **Vorsicht:** Modell-Namen `Chat`/`ChatMessage` sind in der Boilerplate bereits belegt → HiveGrid-Chat-Modelle brauchen anderen Namespace (z.B. `TeamChat`/`TeamChatMessage` gemäß Spec Block A)
---
## Lücken / komplett neu zu bauen
**Datenmodell (Block A) — komplett neu, 7 Modelle (kein Boilerplate-Äquivalent):**
- `TeamChat` (Channel mit `kind` project/group/dm, `project_id`, `is_private`, `archived_at`/`archived_reason`, `last_message_at`, `notification_default`, Partial-Unique-Index pro Projekt)
- `TeamChatMember` (M:N, `role_in_chat`, `notification_pref`, `last_read_message_id`, `is_pinned`, `left_at`, Last-Admin-Logik)
- `TeamChatMessage` (`body` Markdown, `body_format` markdown/system, `parent_message_id` für Threads, `mentions[]` JSON, `attachments[]`, `link_previews[]`, `edited_at`/`deleted_at`, `system_event_kind`, GIN-Index auf mentions, Trigram-Index auf body)
- `TeamChatPinnedMessage` (max 50/Channel, `note`)
- `TeamChatReaction` (Unique `(message_id, user_id, emoji)`, Anti-Spam-Limits) — Muster aus `RowCommentReaction`, aber eigenes Modell
- `TeamChatThread` (denormalisiert `reply_count`/`last_reply_at`/`participant_user_ids[]`)
- `TeamChatRead` (Read-Receipt pro User/Message, Aggregat „X von Y gelesen")
**Echtzeit (Block H) — größte Lücke:**
- Generischer **WebSocket-Server `/ws/chat`** mit Rooms-pro-Channel-Modell, bidirektionalem Typing/Presence — existiert NICHT. `realtime` (SSE-Notification) + `hocuspocus` (CRDT) sind nur Vorlagen.
- Chat-Event-Union (`message.new/edited/deleted`, `reaction.added/removed`, `thread.reply`, `read.receipt`, `typing.start/stop`, `channel.*`, `presence.*`, `link_preview.ready`) — komplett neu (Block H.2)
- Typing-Indikator (Block H.4), Presence online/away/offline (Block H.5) — neu (shared-store als KV-Fundament)
- Per-Event-Auth-Revalidation (Block H.6, AV) — neu
- Optimistic-UI + Initial-50 + Cursor-Infinite-Scroll (Block E) — neu
**Mentions mit Auto-Verknüpfung (Block F.4) — neu:**
- Mention-Resolver über User/Project(M4)/Deal(M3)/Contact/Company(M1) mit Permission-Filter, Auto-Complete-Lookup, `@channel`/`@here` Spezial-Trigger, Mass-Mention-Confirm, Mention-Markdown-Syntax-Rendering. Kein Boilerplate-Baustein.
**Modul-Verknüpfungen (Block C/K, M113) — neu (Cross-Modul-Wiring zu noch zu bauenden HiveGrid-Modulen):**
- Auto-Projekt-Channel-Anlage bei Project-Anlage (M4), Member-Sync, Auto-Archive-Cron (7d, pipeline-konfigurierbar), Reaktivierung
- Activity-Stream-Erzeugung `chat.message`/`chat.mention`/… (Block K) — Ziel-Activity-Modul (M045) noch nicht in Boilerplate (kein `Activity`-Modell gefunden)
- „Mark as Task" → M26-Task mit Backlink (Block G.3)
**Link-Previews (Block G.2) — neu:**
- EU-self-hosted OG-Tag/oEmbed-Microservice (LP-13), Async-Job, `link_previews[]`-Speicherung. Kein Boilerplate-Baustein (api-client-ts ist nur SDK-Wrapper).
**Quiet-Hours / Override-Hierarchie (Block I) — teils neu:**
- `UserNotificationPreference` deckt per-Category-Enable; Quiet-Hours-Zeitfenster + 4-stufige Override-Hierarchie (Channel-Mute > Quiet-Hours > User-Default > Channel-Default) ist neu (gehört zu M116 User-Settings).
**UI (Block D/E/F/J) — komplett neu:**
- Kein Chat-UI-Component-Set in `packages/ui`/`ui-app`/`ui-subframe` (nur generische Primitive: Avatar, Toast, Textarea, EmptyState). Channel-Index, Chat-Verlauf mit Date-Buckets, Compose-Editor mit @-Mention/Emoji-Picker/Drag-Drop, Thread-Panel, Reactions-Badges, Read-Receipt-Anzeige, Mobile-First-Foto/Voice-Note — alles neu zu designen (Subframe, separat).
**HiveGrid-Repo-Status:** `/Users/raffaelsheikh/Documents/GitHub/HiveGrid-Cloud` hat aktuell **kein** `packages/` oder `apps/` (nur `docs`, `_bmad-output`, `prisma`). Die Boilerplate-Pakete sind Quelle/Vorlage; das Chat-Modul wird gegen die noch zu portierende HiveGrid-Code-Basis gebaut.
---
## Empfohlenes Vorgehen
**Phase 0 — Querschnitt aus Boilerplate übernehmen (vor Chat-Logik):**
1. `@toolctrl/foundation` 1:1 nutzen: `permissions` (Block L), `audit` (L.7/H.6), `notifications` (`notify`/`bundleNotifications` für I.2), `errors`/`CatalogError` (Block M), `flags`. Diese sind DoD-Pflicht.
2. `@toolctrl/i18n` + Namespace `chat` anlegen; `@toolctrl/view-models` (`VMState`, `SidebarRegistryItem`, `RegistryGate`) als Loader-/Registry-Fundament. `@toolctrl/feature-flags` für Modul-Gate.
3. `notifications.prisma` (NotificationDelivery/UserNotificationPreference/DeviceToken) 1:1 ins HiveGrid-Schema übernehmen — deckt Block I.2 + J.2.
**Phase 1 — M110 Datenmodell + Permissions (neu, mit Mustern):**
4. 7 `TeamChat*`-Prisma-Modelle neu schreiben (Block A) — `RowComment`/`RowCommentReaction` als strukturelles Vorbild für Message/Reaction; **eigener Namespace** (`Chat`/`ChatMessage` sind in ai.prisma belegt). Forward-Only-Migration mit Inverse-SQL-Kommentar (DoD).
5. Channel-Lifecycle + Membership + Last-Admin-Block über `withTenantScope`/`withPermission`/`withOwnerCheck`. Sidebar-Eintrag „Chat" via `createSidebarRegistry` + `RegistryGate` (Permission `chat.read_own_channels`).
**Phase 2 — M111 Echtzeit (größter Eigenbau):**
6. Redis-Pub/Sub-Broker via `@toolctrl/shared-store` aufsetzen (DB-Index 2). Presence/Typing als KV mit TTL.
7. WebSocket-Server `/ws/chat` neu bauen — **`apps/hocuspocus` als Container-/Auth-Blueprint** (JWT-Handshake + Tenant-Parity), aber eigene Room-/Event-Logik (Chat-Event-Union). EU-Hosting (LP-13).
8. Browser-Subscriber: Reconnect-/Cursor-/Diagnostics-Muster aus `packages/realtime/src/client.ts` übernehmen (Exponential-Backoff, Status-Banner Block M).
9. Messages/Threads/Reactions/Pinned mit Optimistic-UI.
**Phase 3 — M112 Anhänge:**
10. `@toolctrl/storage` (`signed`-URLs) + `scan/*` (ClamAV + Magic-Bytes + Allowlist + Quarantäne) 1:1 für Upload-Pipeline. `FileObject` als Metadaten-Basis, **tenant-scoped Attachment-Join neu** (FileObject hat kein tenantId). Image-Compression client-seitig (browser-image-compression, Block J.3).
**Phase 4 — M114 Mentions + Push + Read-Receipts:**
11. Mention-Resolver neu (Block F.4) — permission-aware Lookup über CRM-Module.
12. Push via vorhandenem `apps/worker/src/jobs/push-notification.ts` + `DeviceToken`; Chat-Payload-Builder mit Datenschutz-Snippet (Block J.2) ergänzen.
13. `notify`/`bundleNotifications` für Notification-Trigger + Anti-Spam-Bündelung; Quiet-Hours-Override-Hierarchie neu (mit M116).
**Phase 5 — M113 Modul-Verknüpfungen (zuletzt, da Cross-Modul-abhängig):**
14. Auto-Projekt-Channel + Member-Sync + Auto-Archive-Cron (Worker-Handler-Registry-Muster) — abhängig von M4-Projekt-Modul.
15. Activity-Stream-Integration (Block K) via Outbox-Prefix `chat.` — abhängig von Activity-Modul (M045, in Boilerplate noch nicht vorhanden).
16. „Mark as Task" → M26.
**Link-Previews (Block G.2):** EU-Microservice separat (Phase-2-nah), Async-Job über Worker-Muster.
> **Reihenfolge-Kernaussage:** Zuerst alle Foundation-Querschnitte 1:1 ziehen (sofortiger DoD-Gewinn), dann das Datenmodell + die Echtzeit-Schicht als Hauptaufwand neu bauen (mit hocuspocus/realtime/shared-store als Infrastruktur-Vorlagen, nicht als fertige Lösung), zuletzt die Cross-Modul-Verzahnung (abhängig von M4/M3/M1/M26/M45).
@@ -0,0 +1,177 @@
# Boilerplate-Mapping: M14 — HR-Hub / Mitarbeiterplattform
> Abgleich-Report. Stellt fest, was fuer M14 aus der Boilerplate
> (`/Users/raffaelsheikh/Documents/GitHub/boilerplate`) uebernommen werden kann,
> was angepasst werden muss und was komplett fehlt.
> Boilerplate wurde NUR gelesen. Alle Pfade absolut.
## Kopf
| Feld | Wert |
| --- | --- |
| Modul-ID | M14 |
| Name | HR-Hub / Mitarbeiterplattform |
| Spec | `/Users/raffaelsheikh/Documents/GitHub/HiveGrid-Cloud/_bmad-output/specs/M14-hr-hub.md` |
| Milestones | M133 (Stammdaten+Standorte), M134 (Dokumente), M135 (Urlaub), M136 (Krankmeldungen), M137 (Kommunikation/Announcements/Quick-Links/Resources/Password-Safe), M138 (Onboarding-Drittanbieter), M139 (Anträge+Surveys+Lohn-Export+DSGVO), M140 (MA-Selbstbedienung/Hub) |
| Pflichtenheft-Block | Block 6, Modul 14 (`_bmad-output/pflichtenheft/blocks/block6.md` Abschnitte 14.1–14.17) |
| Size | XL |
| **Reuse-Level** | **low** (kein einziges HR-Domänen-Modell vorhanden; aber sehr starke Querschnitt-/Infrastruktur-Basis) |
### Begründung Reuse-Level = low
Die HR-Fachdomäne ist in der Boilerplate **vollständig abwesend**: es gibt **keine** der 21 in der Spec genannten Entitäten als Prisma-Modell und **kein** HR-Modul in `apps/web`. Verifiziert per Grep gegen `prisma/schema/*.prisma` (keine Treffer auf `Hr*`, `Employee*`, `Vacation*`, `Sick*`, `Payslip*`, `PasswordSafe*`, `Announcement*`, `Survey*`, `Leave*`) und gegen `apps/web/app/**` (kein `hr/`, `employee/`, `payslip/`, `vacation/`, `announcement/`, `directory/`-Verzeichnis).
Trotzdem ist der Wiederverwendungs-Hebel auf der **Querschnitt-/Infrastruktur-Ebene hoch**: Verschlüsselung (AES-256-GCM-Helper), Storage+Signed-URLs, Audit-Hash-Kette, Notifications (In-App/E-Mail/Push), Permissions/Owner-Check, Export (CSV/DSGVO/Audit-PDF), Import (Bulk/Dedupe — für Payslip-Bulk-Upload), Form-Widget, Worker/Cron (Reminder-Engine), Feature-Flags und das View-Model-/Sidebar-Registry-Fundament sind alle vorhanden. M14 baut sein Datenmodell + Service-Layer + UI neu, klinkt sich aber durchgängig in fertige Querschnitte ein.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/utils/api/.server/CryptoApi.ts` | AES-256-GCM Encrypt/Decrypt (`aes-256-gcm`, 12-Byte-IV, AuthTag, versioniertes `v1:iv:tag:ct`-Format, Key = SHA-256 von `CRYPTO_SECRET`) | **als-vorlage** — direkt für Password-Safe-Werte (`username`/`password`/`notes_encrypted`) und Sick-Cert-Encryption. ACHTUNG: nutzt **einen** Env-Key, **nicht** Per-Tenant-KMS/per-Eintrag-DEK wie die Spec (Block J) fordert → Architektur-Lücke (s.u.) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/utils/leads/marketingIntegrations.db.server.ts` | Muster „selektive Feld-Verschlüsselung at-rest in JSON + nie Re-Display im UI" (`encryptSensitive`/`decryptSensitive` mit `CRYPTO_TAG`-Prefix, Merge+Re-Encrypt bei Patch) | **als-vorlage** — exakt das Reveal/Edit-Verhalten für Password-Safe-Einträge (UI zeigt entschlüsselten Wert nie automatisch) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/index.ts` + `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/signed/` | `putObject`, Signed-Upload/-Download, S3/Scaleway-Provider | **1:1** — für alle Datei-Anhänge (Payslips, Employee-Docs, Sick-Certs, Announcement-/Request-Attachments). Tenant-isolierte Pfade (`tenants/{tenantId}/payslips/{userId}/…`) als Key-Konvention selbst setzen |
| `prisma/schema/core.prisma` → `model FileObject` (`/Users/raffaelsheikh/.../boilerplate/prisma/schema/core.prisma`) | Storage-Metadaten-Modell (`provider`/`bucket`/`key`/`contentType`/`byteSize`/`checksumSha256`, signed-Timestamps) | **1:1** — File-Referenzen aller HR-Dokumente (Spec verweist auf „File M23"; M23 = dieses FileObject) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/foundation/src/notifications.ts` | `notify`/`notifyMany`/`bundleNotifications`, Preference-Check, `sourceKey`-Upsert, `assertI18nKey` | **1:1** — alle M14-Notifications (Approver/Antragsteller/HR-Admin/Reminder/Announcement-Push). INTEGRATION-CONTRACTS §6 verlangt `notify()` statt Direkt-Versand |
| `prisma/schema/notifications.prisma` → `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` | In-App + E-Mail + Push-Token-Modelle (APNs/FCM/WebPush) | **1:1** — Push für Krankmeldung/Urlaub/Payslip/Reminder (Spec Block N M22); Mobile-Push bereits modelliert |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/foundation/src/permissions.ts` | `withTenantScope`, `withPermission`, `withOwnerCheck`, `requirePermission`; Fehlschlag = `CatalogError(notFound)` (404, nicht 403) | **1:1** — deckt Block O exakt: Owner-Check (`read_own`), 404-statt-403 (LP-9 Information-Hiding), Tenant-Scope. HR-Permission-IDs selbst registrieren |
| `prisma/schema/permissions.prisma` → `Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser` | RBAC-Fundament; `Group`/`GroupUser` für Audience-Auflösung (`audience_group_ids`) | **1:1** — HR-Permission-Bundles (`hr.employee`…`hr.admin`) als `Permission`-Sätze + Default-Rollen-Zuweisung. Audience „groups" nutzt `Group` |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/foundation/src/audit.ts` + `/Users/raffaelsheikh/.../packages/db/src/audit-hash.ts` | `auditEvent`/`withAuditMiddleware`; tamper-evidente Hash-Kette (`previousHash`/`eventHash`), `dataSensitivity`, `regulatoryScope[]` | **1:1** — Block P „Audit": Pflicht-Audit für sensible Reads (Payslip-Download, Sick-Cert-Einsicht, **Password-Safe-Reveal**), `regulatoryScope=gdpr`, `dataSensitivity=pii` |
| `prisma/schema/core.prisma` → `AuditEvent` | Audit-Trail-Tabelle (actor, target, outcome, Hash-Chain) | **1:1** — alle schreibenden HR-Aktionen + sensible Reads |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/foundation/src/idempotency.ts` + `core.prisma:IdempotencyKey` | `withIdempotency` (scope+key, In-Process-Lock, 409 bei Konflikt) | **1:1** — Antrags-Submit, Payslip-Bulk-Upload, Reveal-Endpoint (Replay-Schutz) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/exports/` (`./csv`, `./json`, `./pdf`, `./audit-pdf`, `./s3-multipart`, `./limits`) | Streaming-Export CSV/JSON/PDF + Audit-Compliance-PDF + Plan-Row-Caps | **1:1** — M139 Lohn-Export-CSV (Steuerberater), Survey-CSV-Export, DSGVO-MA-Datenexport, Audit-Reports |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/worker/src/jobs/dsgvo-export.ts` + `/Users/raffaelsheikh/.../packages/db/src/dsgvo/user-fk-map.ts` | DSGVO-Personal-Data-Export-Job (aggregiert alle User-FK-Rows, JSON-Bundle → S3 → Signed-URL → AuditEvent) | **anpassen** — neue HR-Modelle in `USER_FK_MAP` eintragen (Pflicht, sonst CI-Bruch), dann deckt der Job M14 Block P „DSGVO-Auskunftsrecht" + M139 14.16.5 ab |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/imports/` (`createImportDryRunPreview`, `dedupe`, Diagnose-Redaction) | Bulk-Import-Framework mit Dry-Run-Preview + Dedupe + PII-Redaction | **als-vorlage** — Payslip-Bulk-Upload (Block F): Match-Vorschau (Hard/Soft/No-Match), Doppel-Upload-Erkennung. Dateiname-Pattern-Match (`{personal_number}-{yyyy-mm}.pdf`) ist HR-spezifisch und neu |
| `prisma/schema/imports.prisma` → `ImportJob` | Job-Tracking (accepted/rejected rows, error-report) | **anpassen** — Bulk-Upload-Status + Aktivitäts-Bericht (Block F Schritt 7) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/form-widget/` + `prisma/schema/formWidget.prisma` (`Form`, `FormSubmission`) + `apps/web/app/modules/formWidget/builder/` | Form-Definition (Zod), Submission-Routing, Field-Mapping, Form-Builder-UI | **anpassen** — Urlaubsantrag/Krankmeldung/Request laufen laut Spec (Block E/H, Block N M5) als M5-Forms; `form_submission_id`-FKs zeigen darauf. Neue Form-Action-Typen (`create_hr_vacation_request`…) sind HR-spezifisch zu ergänzen |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/outbox.ts` + `apps/worker/src/outbox/event-router.ts` | Transaktionaler Outbox-Publisher + Event-Routing (Prefix→Queue) | **1:1** — alle HR-Side-Effects (Activity, Notification, Automation-Trigger `hr.*`) atomar mit DB-Write. Block M Activity-Typen als Outbox-Events |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/worker/` (Cron-Scheduler, BullMQ-Gruppen, `HandlerDependencies`) | Worker-Runtime mit single-instance Cron + Queue-Consumer | **anpassen** — Reminder-Engine (Block G: täglicher Cron `once`/`yearly`/`every_n_months`/`on_expiry`), Überfälligkeits-Eskalation, Aufbewahrungs-Hard-Delete-Cron (Block P) als neue Jobs nach Muster von `dsgvo-grace-cron.ts`/`audit-retention.ts` |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/feature-flags/` + `packages/foundation/src/flags.ts` | Feature-Flag-Engine v2 (Targeting) + leichtgewichtige Gates | **1:1** — AV #7 „Feature-Flag-Hülle" (`hr.feature_resturlaub_tracking`), Modul ohne Deploy abschaltbar (DoD) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/view-models/src/index.ts` | `VMState`, `ActionCallbacks`, `RegistryGate`, `I18nLabel`, `SidebarRegistryItem`, `assertI18nLabel` | **1:1** — Loader-baut-VM-Disziplin (DoD „Code-Layer-Disziplin"), Sidebar-Gating per Permission/Flag |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/components/layouts/sidebars/shadcn/` + `app/application/sidebar/AppSidebar.tsx`, `SidebarItem.ts` | App-Shell (App/Admin/Docs), datengetriebene Sidebar mit Permission-/Feature-Gate, Breadcrumb-Header, Mobile-Sidebar | **als-vorlage** — HR-Hub-Hauptpunkt + kontextabhängige Sub-Punkte (Block B); HiveGrid registriert real in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` (INTEGRATION-CONTRACTS §1) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/components/ui/` (table, tabs, dialog, sheet, form, calendar, chart, multi-select, pagination, command, badge, card, drawer, stepper, modals, emptyState, loaders, pdf-viewer, editors) | shadcn/Radix-Primitives + Tabellen/Forms/Modale/Empty-/Loading-States/PDF-Viewer/Markdown-Editoren | **als-vorlage** — Feed-Listen, Detail-Views, Reveal-Dialoge, Payslip-PDF-Viewer, Leave-Kalendersicht (`calendar.tsx`/`chart.tsx`), Survey-Auswertung (`chart.tsx` für Bar/NPS) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/i18n/` + `apps/web/app/locale/` | i18n (de/en), Intl-Formatter (Datum/Zahl), ICU-Plurale, `useTranslate` | **1:1** — alle Klartext-Hinweise (Block Q) als i18n-Keys (DoD + INTEGRATION-CONTRACTS §10). Locale-File neu: `packages/i18n/src/locales/de/hr.json` |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/components/entities/` (Entity-Builder: Properties, Views, RowComment/RowCommentReaction, RowTask, RowTag) | No-Code-Entity-Engine inkl. Kommentare+Reaktionen+Tags-Muster | **als-vorlage** — Announcement-Comments/-Reactions und Request-Comments können das `RowComment`/`RowCommentReaction`-Muster adaptieren; Phase-2-Custom-Felder pro `HrEmployeeProfile` (Block R) per Entity-Builder |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/appsEngine.prisma` (`App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem`) + `apps/web/app/modules/onboarding/` | Integrations-/Sync-Engine (verschlüsselte Credentials, Rollback-fähige Sync-Runs) + Onboarding-Flow-Modelle | **anpassen** — M138 Onboarding-Drittanbieter (Memberspot/Boobora) via Apps-Engine: Read-Sync Modul-Abschlüsse/Zertifikate/Quiz/Fortschritt = `AppSyncRun`/`AppSyncItem`-Muster |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/client.server.ts` + `transactions.server.ts` | `getDbClient`/`getReadDbClient`/`getExtendedDbClient`, `runDbTransaction` | **1:1** — DB-Boundary für alle HR-Repositories; Read-Replica für Hub-/Verzeichnis-Dashboards |
| `prisma/schema/app.prisma` (`// custom database models`) | Leerer Platzhalter für projekt-eigene Modelle | **1:1** — vorgesehener Ort für ALLE 21 HR-Modelle (alternativ HiveGrid-eigene Schema-Datei `hr.prisma`) |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/search/indexed-entities.ts` (per INTEGRATION-CONTRACTS §7) | Such-Index-Registry | **anpassen** — MA-Verzeichnis-Suche, Announcement-Volltextsuche registrieren |
---
## Vorhandene Daten-Modelle (konkrete Prisma-Modelle, die M14 nutzt/erweitert)
**Wichtig: KEIN HR-Domänen-Modell existiert. Folgende Boilerplate-Modelle sind Anbindungs-/Querschnitt-Modelle, auf die M14 verweist:**
| Modell | Pfad | M14-Bezug |
| --- | --- | --- |
| `TenantUser`, `User`, `Tenant` | `prisma/schema/accounts.prisma` | `HrEmployeeProfile.user_id` (1:1 mit TenantUser), `manager_user_id`; Auto-Seed leeres Profil bei Tenant-User-Anlage (Spec A „Sonderfall", 14.1.5). `User.totpSecretEncrypted` zeigt das Encrypted-Suffix-Muster für 2FA (Block P 2FA-Empfehlung) |
| `Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser` | `prisma/schema/permissions.prisma` | Permission-Bundles Block O; Audience-Auflösung `groups` |
| `FileObject` | `prisma/schema/core.prisma` | Datei-Backing für `file_id`/`attached_file_ids[]`/`certificate_file_id`/`avatar_file_id` |
| `AuditEvent`, `IdempotencyKey`, `Event` | `prisma/schema/core.prisma` | Audit-Trail; Idempotenz; generischer `Event` (kein Calendar-Event!) |
| `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` | `prisma/schema/notifications.prisma` | Notifications/Push |
| `OutboxEvent` | `prisma/schema/outbox.prisma` | Activity-/Automation-/Notification-Fanout (`hr.*`-Events) |
| `Form`, `FormSubmission` | `prisma/schema/formWidget.prisma` | `form_submission_id` für Urlaub/Krank/Request |
| `ImportJob` | `prisma/schema/imports.prisma` | Payslip-Bulk-Upload-Tracking |
| `ExportJob` | `prisma/schema/queue.prisma` | Lohn-/Survey-/DSGVO-Export |
| `App`, `DataConnection`, `AppSyncRun`, `AppSyncItem` | `prisma/schema/appsEngine.prisma` | M138 Drittanbieter-Onboarding-Sync |
| `FeatureFlag`, `FeatureFlagFilter`, `FeatureFlagAudit` | `prisma/schema/featureFlags.prisma` | AV #7 Feature-Flags |
| `Onboarding*` (7 Modelle) | `prisma/schema/onboarding.prisma` | Produkt-Onboarding-Tour (NICHT MA-Onboarding) — nur als Muster für M138 |
---
## Lücken / komplett neu zu bauen
### A. Alle 21 HR-Domänen-Modelle (komplett neu, in `app.prisma` / `hr.prisma`)
`HrEmployeeProfile`, `HrAnnouncement`, `HrAnnouncementRead`, `HrAnnouncementReaction`, `HrAnnouncementComment`, `HrLocation`, `HrEmployeeDocument`, `HrResource`, `HrQuickLink`, `HrSurvey`, `HrSurveyItem`, `HrSurveySubmission`, `HrSurveySubmissionAnswer`, `HrRequest`, `HrRequestTemplate`, `HrRequestComment`, `EmployeePayslip`, `EmployeeUploadReminder`, `EmployeeUploadReminderDispatch`, `EmployeeVacationRequest`, `EmployeeSickLeaveReport`, `EmployeePasswordSafeVault`, `EmployeePasswordSafeEntry`, `EmployeePasswordSafeAccess` — **kein einziges existiert**. Inkl. aller Indizes (Spec A „Indizes") und der zusätzlichen App-Code-Permission-Layer über RLS hinaus (Spec A „RLS / Datenisolation").
> Pflicht beim Bau: jedes Modell mit User-FK in `packages/db/src/dsgvo/user-fk-map.ts` eintragen (Cascade-Strategie `hard-delete`/`pseudonymize`/`retain-tombstone` + `includeInExport`) — der CI-Test bricht sonst. HR hat gesetzliche Aufbewahrungsfristen (Block P: 10/4 Jahre) → meist `retain-tombstone`/anonymisieren statt hard-delete.
### B. Per-Tenant-KMS / Per-Eintrag-DEK für Password-Safe (Architektur-Lücke)
Die Spec (Block J „Verschlüsselung") fordert **Per-Tenant-Master-Key im KMS (Scaleway-KMS) + Per-Eintrag-DEK gewrappt mit Master-Key**. Die Boilerplate liefert nur `CryptoApi.ts` mit **einem** Env-abgeleiteten Key (`CRYPTO_SECRET`) und **keine** KMS-Integration, **kein** DEK-Wrapping, **keine** Key-Rotation. → Das KMS-/DEK-Wrapping-Layer ist komplett neu zu bauen (Master-Key-Rotation explizit Phase 2). CryptoApi taugt nur als AES-256-GCM-Primitive darunter.
### C. Leave-/Approval-Workflow-Engine (komplett neu)
Genehmigungs-Workflow (submitted→approved/rejected/withdrawn), Default-Approver-Auflösung (`manager`/`fixed_user`/`role`/`group`), Approver-Rückfrage, `working_days_count`-Berechnung. Es gibt KEINE generische Approval-Engine in der Boilerplate. Der `workflows.prisma`-Builder ist node-basierte Automation, kein menschlicher Genehmigungs-Workflow.
### D. DE-Feiertagskalender / Bundesland-genaue Arbeitstage-Berechnung (komplett neu)
`working_days_count` netto ohne Wochenende + Bundesland-Feiertage (aus `HrLocation`). Keine Feiertags-Library/-Logik in der Boilerplate (offener Detail-Punkt in der Spec).
### E. Calendar-Integration (Modell fehlt)
Spec Block N M25: genehmigter Urlaub → Calendar-Event. Es gibt **kein** Calendar-/CalendarEvent-Modell in der Boilerplate (nur generischer `Event` in `core.prisma` = Webhook-Event, kein Kalendereintrag). `calendar_event_id`-FK zeigt auf ein erst zu bauendes M25.
### F. Plantafel-Konflikt-Detection (Modul-übergreifend, neu)
Spec Block N M4: genehmigte Urlaube + aktive Krankmeldungen als Konflikt-Indikatoren in Plantafel/ScheduleEntries. Kein ScheduleEntry-Modell vorhanden — gehört zu M4, ist hier nur Integrationspunkt.
### G. Automation-Trigger/-Actions `hr.*` (neu)
Spec Block N M17: `hr.vacation_requested`, `hr.sick_leave_reported`, Actions `hr.send_announcement` etc. Die Outbox/Workflow-Infra existiert, aber die HR-spezifischen Trigger/Action-Definitionen + Block-Verträge sind neu.
### H. Reminder-Engine-Cron-Logik (neu, Muster vorhanden)
Block G: Dispatch-Erzeugung pro Cadence, Überfälligkeits-Eskalation, `reminder_count`-Wiederholung. Worker-Cron-Muster vorhanden (s.o.), aber die konkrete Reminder-Logik + `on_expiry`-Lookahead auf `valid_until` ist neu.
### I. SHK-Default-Seeding (neu)
Block H/LP-3: Default Request-Templates (Spesen/Hardware/Schulung/Zeugnis/Sonstiges), Default-Reminder, Default-Announcements. Seeding-Infra vorhanden (`packages/db/src/seed`), HR-Inhalte neu.
### J. HR-spezifische UI-Surfaces (neu, in `packages/ui-hivegrid`)
Announcement-Feed + Detail (Reaction-Picker, Read-Receipt-Tabelle), Leave-Übersicht (Tabs + Kalendersicht), Password-Safe Reveal-UI (Copy-mit-Auto-Clear-30s), Survey-Editor + Auswertung, MA-Verzeichnis (Karten/Liste + Click-to-Call/Mail/Chat), Bulk-Upload-Match-Vorschau, Hub-Startseite mit Widgets. Primitives vorhanden (Tabelle/Tabs/Chart/Calendar/Dialog), HR-Komposition neu. Subframe-Adapter-Layer ist Boilerplate-seitig leer (`@toolctrl/ui-subframe` = Stub) — HiveGrid muss diese Grenze selbst füllen (MEMORY: Subframe für alle Routes Pflicht).
### K. Permission-IDs `hr.*` (neu registrieren)
Alle 18 atomaren Permissions + 5 Bundles (Block O) als TypeScript-Konst in `packages/entitlements/src/permissions.ts` (INTEGRATION-CONTRACTS §8) + Default-Rollen-Zuweisung (SHK-Template).
### L. `@hivegrid/activity` `logActivity` (HiveGrid-Querschnitt, kein BP-1:1)
INTEGRATION-CONTRACTS §5 verlangt `logActivity` aus `@hivegrid/activity` (M045) — das ist ein HiveGrid-eigenes Paket, nicht die Boilerplate-Foundation. Block M Activity-Typen `hr.*` dort registrieren.
---
## Empfohlenes Vorgehen (Reihenfolge)
**Phase 1 — Fundament aus BP einklinken (vor jedem Feature):**
1. HR-Permission-IDs + Bundles in `packages/entitlements/src/permissions.ts` registrieren (Block O). Voraussetzung für jede Route.
2. Datenmodelle in `prisma/schema/app.prisma` (oder neue `hr.prisma`) anlegen — **gestaffelt pro Milestone**, nicht alle 21 auf einmal. M133 zuerst: `HrEmployeeProfile`, `HrLocation`. Jeweils sofort `USER_FK_MAP` ergänzen (CI-Gate).
3. DB-Repositories über `@toolctrl/db/client.server` + `runDbTransaction`; Side-Effects ausschließlich via `publishOutboxEvent` (INTEGRATION-CONTRACTS §6/Outbox).
4. Sidebar-Eintrag in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` + Routes unter `app.$tenant/hr/*` und `admin/hr/*` (Block B), gegated via `withTenantScope`+`withPermission` (404-statt-403).
**Phase 2 — Stammdaten/Verzeichnis/Dokumente (M133, M134):**
5. `HrEmployeeProfile`/`HrLocation` + Verzeichnis-UI (Click-to-Call M12/Mail/Chat M13). i18n-Keys `hr.*`.
6. `HrEmployeeDocument` + Upload über `@toolctrl/storage` (`putObject`+signed-download) auf `FileObject`; tenant-isolierte Key-Konvention. Visibility-Gating + Audit auf sensible Reads (`withAuditMiddleware`).
**Phase 3 — Leave + Payslips (M135, M136, M139-Teil):**
7. Form-Templates Urlaub/Krank über `@toolctrl/form-widget` + `FormSubmission`; neue Form-Actions `create_hr_vacation_request`/`create_hr_sick_leave`.
8. **Neu bauen:** Approval-Workflow-Engine, `working_days_count`+DE-Feiertage, Calendar-Event-Hook (sobald M25 da).
9. Payslip-Bulk-Upload: `@toolctrl/imports` Dry-Run/Dedupe als Vorlage + HR-Dateiname-Pattern-Match; `ImportJob`-Tracking; Lohn-Export-CSV via `@toolctrl/exports/csv`.
**Phase 4 — Kommunikation + Password-Safe (M137):**
10. Announcements (Feed/Detail/Reactions/Comments) — `RowComment`/`RowCommentReaction`-Muster adaptieren; Read-Receipts neu. Push via `notify()`.
11. **Password-Safe: KMS/DEK-Layer komplett neu** (Scaleway-KMS + per-Eintrag-DEK über AES-256-GCM-Primitive aus `CryptoApi.ts`); Reveal-Endpoint mit Audit + Idempotenz; Copy-Auto-Clear-UI. Quick-Links/Resources (Audience-Gating).
**Phase 5 — Reminder-Engine + Surveys + Onboarding-Drittanbieter (M138, M139, M140):**
12. Reminder-Cron als neuer Worker-Job nach Muster `dsgvo-grace-cron.ts`/`audit-retention.ts`.
13. Surveys (Items als Form-Renderer-Wiederverwendung) + Auswertung (`chart.tsx`) + CSV-Export.
14. M138 Drittanbieter-Onboarding via `App`/`AppSyncRun`/`AppSyncItem` (Apps-Engine).
15. Hub-Startseite (M140) — aggregiert vorhandene Widgets; DSGVO-Export via `dsgvo-export.ts` (USER_FK_MAP muss vollständig sein).
16. AV #7 Feature-Flag-Hülle + SHK-Default-Seeding.
**Querschnitt durchgängig (DoD/INTEGRATION-CONTRACTS):** Loader-baut-VM (`@toolctrl/view-models`), keine Prisma-Imports in `packages/ui-hivegrid/**`, i18n-Keys (`hr.json`), `logActivity` (M045) + auto-Audit (M001), Smoke-Test + Hot-Swap-Drill + Visual-Snapshot.
---
## Anti-Erfindungs-Hinweise (faktische Grenzen)
- **Es gibt KEIN HR-Datenmodell und KEIN HR-Modul in der Boilerplate.** Verifiziert per Grep gegen `prisma/schema/*.prisma` und `apps/web/app/**`.
- **CryptoApi nutzt EINEN Env-Key, KEIN KMS/DEK.** Die Per-Tenant-KMS-Architektur der Spec ist neu.
- **Kein Calendar-/CalendarEvent-Modell.** `Event` in `core.prisma` ist ein Webhook-/Domain-Event, kein Kalendereintrag.
- **`@toolctrl/ui-subframe` ist ein leerer Stub** — kein gebauter Subframe-Adapter-Layer.
- **Keine generische Human-Approval-Workflow-Engine** (nur node-basierte `workflows.prisma`-Automation).
- **`packages/ui-hivegrid`, `@hivegrid/activity`, `packages/entitlements/src/permissions.ts` (HiveGrid-Variante)** sind HiveGrid-Repo-Konventionen aus INTEGRATION-CONTRACTS — in der Boilerplate so nicht vorhanden (Boilerplate hat `@toolctrl/*`).
@@ -0,0 +1,137 @@
# Boilerplate-Abgleich: M15 — Customer-Portal (Endkunden-Login)
| Feld | Wert |
| --- | --- |
| **Modul-ID** | M15 |
| **Name** | Customer-Portal (Endkunden-Login) |
| **Milestones** | M141 (Auth), M142 (Projekt-Sicht + Status-Mails), M143 (Datei-Upload + Reklamations-Forms), M144 (Subunternehmer-Zugang), M145 (Skalierung / Service-Inbox), M146 (Lebensdauer + DSGVO + Konfig) |
| **Block** | Block 6 — Spezial-Module (Pflichtenheft `block6.md` §15) |
| **Reuse-Level** | **low** |
| **Stand** | Boilerplate read-only verifiziert (`/Users/raffaelsheikh/Documents/GitHub/boilerplate`) |
## Verdict in einem Satz
Der namensgleiche Boilerplate-Baustein (`prisma/schema/portals.prisma` + `apps/web/app/modules/portals` + `apps/web/app/routes/app.$tenant/__portals`) ist ein **White-Label-SaaS-Marketing-Portal** (Subdomain-Hosting, öffentliche Signup-Seiten, Stripe-Connect-Abos, SEO, Analytics) und deckt das HiveGrid-Customer-Portal (Endkunden-Sicht auf eigene Projekte/Dokumente/Status) fachlich **nicht** ab. Nutzbar ist fast ausschließlich die Querschnitt-Hülle (Auth-Primitiven, Storage, Mail, Notifications, Audit, DSGVO-Export, i18n, Outbox, Cron) — die fachliche Datenebene und alle Endkunden-Views sind **neu zu bauen**.
---
## WICHTIG: Eignung von `portals.prisma` (vom Task explizit zu prüfen)
**Befund: nicht geeignet für 1:1-Übernahme, allenfalls als Namens-/Struktur-Vorlage.**
Verifiziert in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/portals.prisma` (12 Modelle):
- `Portal` ist Subdomain-gehostet (`subdomain @unique`, optional `domain`), trägt SEO-Felder (`seoTitle`/`seoDescription`/`seoImage`/`seoKeywords`...), Analytics-Toggles (Simple/Plausible/Google), eigenen `stripeAccountId` (Stripe Connect) und White-Label-Branding. Es gibt **kein** `tenantId @unique` (P1 verlangt ein Default-Portal pro Tenant), **kein** `enabled`/`archived_at`/`is_default`/`slug=portal`.
- `PortalUser` hat `passwordHash` als **Pflichtfeld** (HiveGrid: nullable für Magic-Link-Only), OAuth-Felder `githubId`/`googleId` (gegen EU-Stack AGENTS.md — Apple/Google/Microsoft-Login in P1 verboten), **kein** `status`-Enum (`pending_setup|active|disabled|deleted`), **kein** `linked_contact_id`/`linked_company_id`, **kein** `last_login_at`, **kein** `language`-Konzept passend zu Auto-Mails.
- `PortalUserRegistration` existiert (Token + Email + Name + Company), kommt der HiveGrid-`PortalUserRegistration` strukturell am nächsten — aber an die SaaS-Signup-Logik (`selectedSubscriptionPriceId`) gekoppelt.
- **Komplett fehlend** in `portals.prisma`: `CustomerPortalAccount`, `CustomerPortalMagicLinkToken` (mit `purpose`-Enum), `CustomerPortalSession` (server-side Postgres-Session für PortalUser), `CustomerPortalProjectAccess`, `CustomerPortalProjectDocument`, `PortalConfiguration` (Phasen-Sichtbarkeit/Doc-Auto-Freigabe/Pflicht-Links), `PortalEmailTemplate`. Es gibt **kein** `Session`-Prisma-Modell für PortalUser im gesamten Schema (`grep "model Session" prisma/schema/` ist leer).
- 6 der 12 Modelle sind reines Portal-**Abo-Subsystem** (`PortalSubscriptionProduct/Price/Feature`, `PortalUserSubscription(Product)(Price)`, `PortalCheckoutSessionStatus`) — entspricht direkt dem Anti-Listen-Punkt 15.8.6 (keine Wartungs-Abos/Service-Pakete im Portal in P1) und ist für M15-P1 **nicht** zu verwenden.
Service-Layer `apps/web/app/modules/portals/services/` enthält ausschließlich `Portal.server.ts`, `PortalPricing.server.ts`, `PortalStripe.server.ts`, `PortalSubscription.server.ts`, `StripeConnect.server.ts` — **keinen** PortalUser-Login-/Magic-Link-/Session-Flow. Die Routen `apps/web/app/routes/app.$tenant/__portals/**` sind Tenant-Admin-CRUD über das Portal (Pages, Pricing, Domains, Users-Verwaltung), **kein** Endkunden-Frontend mit eigener Auth.
**Empfehlung:** `portals.prisma` und das `portals`-Web-Modul NICHT erweitern. HiveGrid-Portal-Modelle neu in `prisma/schema/app.prisma` (vorgesehener Platzhalter) als `CustomerPortal*`-Familie anlegen, um Namenskollision mit dem bestehenden `Portal`/`PortalUser` zu vermeiden. Das bestehende Schema bleibt als Struktur-/Feld-Vorlage für `PortalConfiguration` (Branding/Auth-Flags) und `PortalUserRegistration` (Token-Einladung) nutzbar.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `prisma/schema/portals.prisma` (Modelle `Portal`, `PortalUser`, `PortalUserRegistration`) | Feld-/Struktur-Vorlage für `CustomerPortalConfiguration` (Branding/Auth-Flags) + Registration-Token-Muster | **als-vorlage** (NICHT 1:1 — siehe Abschnitt oben) |
| `prisma/schema/accounts.prisma` → `MagicLinkToken` (Z.98) | Magic-Link-Token-Pattern (`tokenHash @unique`, `expiresAt`, `consumedAt`, Index `(userId, expiresAt)` + `(expiresAt, consumedAt)`) | **als-vorlage** — neue `CustomerPortalMagicLinkToken` braucht zusätzlich `purpose`-Enum (login/setup/password_reset/email_verification) + FK auf `PortalUser` statt `User` |
| `apps/web/app/routes/auth.magic-link.tsx` (+ `.test.tsx`), `login.tsx`/`login.server.ts`, `forgot-password.tsx`, `reset.tsx`, `verify.$id.tsx` | Bestehende TenantUser-Auth-Flows (Magic-Link, Login, Pwd-Reset, Email-Verify) als Referenz-Implementierung | **als-vorlage** — analog für PortalUser nachbauen mit getrenntem Cookie (`hg_portal_session`) und getrennter Middleware (`requirePortalUser`); KEIN Code-Reuse 1:1 (andere User-Klasse, andere Routes) |
| `apps/web/app/utils/auth/passwordHasher.server.ts` | Passwort-Hashing (für optionales PortalUser-Pwd, ≥12 Zeichen, C.2) | **1:1** (Hasher) — HIBP-Check ist zusätzlich zu verdrahten |
| `apps/api/src/auth/login-rate-limit.ts`, `apps/api/src/auth/cookie/session-cookie.ts`, `packages/config/src/session-cookie.ts` (`resolveSessionCookiePolicy`) | Login-Rate-Limit + sichere Cookie-Policy (HTTP-only/Secure/SameSite=Lax) | **1:1** für Policy-Helper; Rate-Limit-Pattern **anpassen** für Portal-Email/IP-Limits (C.1: 3/15min pro Email, 10/h pro IP) |
| `packages/shared-store` (`createSlidingWindowRateLimiter`, `createFixedWindowRateLimiter`, `createSessionStore`, `createIdempotencyStore`) | Redis-KV für Rate-Limit (Magic-Link-Anfragen), Session-Cache, Idempotenz | **1:1** als Infrastruktur |
| `packages/storage/src` (`signed/`, `access/`, `scan/` mit ClamAV + `magic-bytes-adapter` + `allow-list`, `s3-quarantine`) | Datei-Upload-Pipeline für D.5: signierte URLs, Mime-Type-Allowlist, Virus-Scan, Quarantäne | **1:1** als Infrastruktur — Upload-Pfad `tenants/{tenantId}/projects/{projectId}/portal-uploads/...` + erlaubte Mime-Types (Bilder/PDF/Office/Audio) konfigurieren |
| `prisma/schema/core.prisma` → `FileObject`/`FileUploadProgress`/`FileChunk` | Storage-Metadaten-Modelle | **anpassen** — HiveGrid M23-Files (M052-M055) liefert die eigentliche File-Entität; `CustomerPortalProjectDocument` referenziert diese, nicht direkt `FileObject` |
| `packages/mail/src` (`mailersend.ts`, `queue.ts`, `templates-react`, `types.ts`) | Transaktionaler Mail-Versand (Setup-/Login-/Status-/Doc-Released-/Upload-Received-Mails, Block G) | **1:1** als Versand-Layer; Sending-Domain kommt aus HiveGrid M9 (Tenant-Default) |
| `packages/foundation/src/notifications.ts` (`notify`, `notifyMany`, `bundleNotifications`, `isNotificationEnabled`) + `prisma/schema/notifications.prisma` (`NotificationDelivery`, `UserNotificationPreference`, `DeviceToken`) | Notification-Querschnitt für Push/Email-Digest an PL bei Upload/Form (M22) + Anti-Spam-Bündelung (M145 / 15.13.2.5) | **1:1** — `bundleNotifications` deckt die 30-Min-Bündelung (15.13.2.5) direkt ab |
| `packages/foundation/src/audit.ts` (`auditEvent`, `withAuditMiddleware`) + `prisma/schema/core.prisma` → `AuditEvent` (Hash-Kette) | Portal-Audit (Logins, Downloads, Uploads, Access-Änderungen, Status-Mails — Block A E.6, AV #6) | **1:1** — S3-Upload/Download manuell auditieren (INTEGRATION-CONTRACTS §6 Ausnahme) |
| `packages/foundation/src/permissions.ts` (`withTenantScope`, `withPermission`, `requirePermission`, `withOwnerCheck`) | Tenant-seitige Portal-Permissions (`portal.read/manage_users/manage_access/delete_user/admin`, Block I.1) + 404-statt-403-Pattern (I.3) | **1:1** als Pattern; Permission-Keys neu in `packages/entitlements/src/permissions.ts` definieren |
| `packages/foundation/src/errors.ts` (`CatalogError`, `foundationErrors.notFound` 404) | Klartext-Fehler + Existenz-Versteckung (404 bei fehlendem Project-Access, I.3 / D.3) | **1:1** |
| `packages/foundation/src/flags.ts` / `packages/feature-flags` + `prisma/schema/featureFlags.prisma` | Feature-Flag „Modul ohne Deploy abschaltbar" (DoD), Portal aktivieren/deaktivieren | **1:1** |
| `packages/view-models/src/index.ts` (`VMState`, `ActionCallbacks`, `RegistryGate`, `PortalRegistryItem`, `ShellRegistries`) | View-Model-Vertrag + Portal-Navigations-Registry-Typ (passt zu INTEGRATION-CONTRACTS §14 `portal-shell.config.ts`) | **1:1** als Vertrag |
| `packages/i18n` (`defaultLocale="de"`, `supportedLocales=["de","en"]`, ICU, `formatDate/Currency`) | i18n für alle Portal-Texte (DE/EN je PortalUser.language) + Klartext-Diagnostik (Block K) | **1:1** — neuer Namespace `customerPortal` o.ä. |
| `packages/db/src/dsgvo/user-fk-map.ts` + `packages/db/src/dsgvo/pseudonymize.ts` + Worker `apps/worker/src/jobs/dsgvo-export.ts`, `dsgvo-deletion.ts`, `dsgvo-grace-cron.ts` (24h) | DSGVO-Lösch-Workflow (24h-Reverse-Window, Anonymisierung) + JSON-Daten-Export (Block J / 15.11) | **anpassen** — DSGVO-Infra ist auf `User`-FK ausgelegt; PortalUser-FKs müssen in `user-fk-map.ts` (bzw. analogem Portal-FK-Map) ergänzt werden; Export-Job-Muster (`@toolctrl/exports` JSON) wiederverwenden |
| `packages/exports` (`./json`, `./s3-multipart`) | DSGVO-JSON-Export-Bundle für PortalUser (J.4) | **1:1** als Serializer + S3-Sink |
| `prisma/schema/outbox.prisma` (`OutboxEvent`) + `packages/db/src/outbox.ts` (`publishOutboxEvent`) + Worker `apps/worker/src/outbox/event-router.ts` | Transaktionale Side-Effects (Status-Mail-Trigger bei `project.stage_changed`, M17-Trigger `portal.*`, Mail-Debounce) | **1:1** als Event-Backbone; neue `portal.*` Event-Prefixe im `DEFAULT_OUTBOX_ROUTING` ergänzen |
| `apps/worker/src` (Cron-Scheduler-Muster, `group-runtime.ts`, `handlers.ts`, single-instance Cron wie `dsgvo-grace-cron`) | Lifecycle-Cron-Jobs (M146): Auto-Deaktivierung nach 24 Monaten Inaktivität, Pro-Projekt-Auto-Schließung nach 12 Monaten, Status-Mail-Debounce (5-Min-Window) | **als-vorlage** — neue Portal-Lifecycle-Jobs nach demselben Muster |
| `apps/api/src/route-access-inventory.ts` (Auth-Klassen, u.a. `public-*`) + `apps/api/src/middleware/error-envelope.ts` | Öffentlich erreichbare Portal-Routen (Login/Setup/Reset sind ohne Session erreichbar) als Auth-Klassen-Muster | **als-vorlage** |
| `apps/web` App-Shell (`AppLayout`, `ShadcnSidebarLayout`, `components/ui/*` shadcn/Radix: Tabellen/Forms/Modale/Empty/Loading) + Theme (`Tenant.themeConfig`, `parseThemeConfig`) | Branding-Vererbung P1 (Tenant-Logo + Farbe, 15.8.10) + UI-Primitiven für Tenant-Admin-Konfig-Seiten (E.1–E.6) | **anpassen** — Tenant-Admin-Seiten reuse die App-Shell; Endkunden-Portal-Shell ist eine **eigene, minimale** Shell (INTEGRATION-CONTRACTS §1: „Customer-Portal hat eigene minimale Navigation"), mobile-first (LP-2) |
| `packages/marketing-outbound` (Bündelungs-/Routing-Adapter-Muster) + Lead-Routing-Pattern (M048-Referenz) | Routing-Regeln für Endkunden-Aktivitäten (M145 / 15.13.2.4, „analog Lead-Routing") | **als-vorlage** für die Service-Inbox-Routing-Logik |
| CI-Gates: `verify:phase-c-boundaries`, `verify:i18n`, `verify:tenant-isolation-report`, `verify:sidebar-routes`, `test:hot-swap`, `verify:ui-visual`, Storybook + lost-pixel | Erfüllung der DoD-Checkliste (Layer-Trennung, i18n, Visual-Regression, Hot-Swap-Drill) | **1:1** erben/erweitern |
---
## Vorhandene Daten-Modelle (Boilerplate, konkrete Prisma-Pfade)
| Modell | Pfad | Relevanz für M15 |
| --- | --- | --- |
| `Portal`, `PortalUser`, `PortalUserRegistration`, `PortalPage` | `prisma/schema/portals.prisma` | **Nur als Struktur-Vorlage** — fachlich SaaS-White-Label-Portal, nicht Endkunden-Projekt-Portal (siehe Eignungs-Abschnitt) |
| `PortalSubscription*` (6 Modelle) | `prisma/schema/portals.prisma` | **Nicht verwenden** in P1 (Anti-Liste 15.8.6) |
| `MagicLinkToken` (FK auf `User`) | `prisma/schema/accounts.prisma:98` | Token-Pattern-Vorlage; neuer `CustomerPortalMagicLinkToken` mit `purpose`-Enum + FK auf PortalUser |
| `User`, `Tenant`, `TenantUser`, `Registration`, `TenantUserInvitation` | `prisma/schema/accounts.prisma` | TenantUser-Seite (strikt getrennt von PortalUser, LP-1 AV#2); `Tenant.status`/`Tenant.themeConfig` für Tenant-Lifecycle-Wirkung + Branding |
| `AuditEvent` (Hash-Kette), `IdempotencyKey`, `FileObject`/`FileUploadProgress`/`FileChunk` | `prisma/schema/core.prisma` | Audit, Idempotenz, Storage-Metadaten — Querschnitt |
| `Role`, `Permission`, `RolePermission`, `UserRole`, `Group` | `prisma/schema/permissions.prisma` | RBAC-Fundament für Tenant-seitige `portal.*`-Permissions (Block I.1) |
| `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` | `prisma/schema/notifications.prisma` | Notifications/Push an PL (M22) + Anti-Spam-Bündelung |
| `OutboxEvent` | `prisma/schema/outbox.prisma` | Event-Backbone (Status-Mail-Trigger, M17-Trigger) |
| `FeatureFlag`, `FeatureFlagFilter`, `FeatureFlagAudit` | `prisma/schema/featureFlags.prisma` | Feature-Flag-Abschaltbarkeit (DoD) |
| `ExportJob`, `ImportJob` | `prisma/schema/queue.prisma`, `imports.prisma` | DSGVO-Export-Job-Tracking |
| `app.prisma` (leerer Platzhalter `// custom database models`) | `prisma/schema/app.prisma` | **Ziel-Ort für alle neuen `CustomerPortal*`-Modelle** |
**NICHT vorhanden (HiveGrid-Domäne, von vorgelagerten Modulen geliefert, nicht von der Boilerplate):**
`Project`/`Auftrag`, `Contact`, `Company`, `Deal`, `Pipeline`, `Stage`, HiveGrid-`File` — `grep "model (Project|Contact|Company|Deal|Pipeline|Stage)"` in `prisma/schema/` ist **leer**. Diese kommen aus M042-M046 (CRM), M059-M064 (Sales), M065-M070 (Auftrag), M052-M055 (Dateien) und sind harte Vorbedingungen (M142-Abhängigkeiten: M066, M052-M055, M092).
---
## Lücken / komplett neu zu bauen
### Daten-Layer (neu in `app.prisma`, alle als `CustomerPortal*`-Familie)
- `CustomerPortal` (ein Default-Portal pro Tenant: `tenantId @unique`, `slug=portal`, `is_default`, `enabled`, `archived_at`)
- `PortalUser` als **eigene Endkunden-User-Klasse** mit `status`-Enum (`pending_setup|active|disabled|deleted`), `linked_contact_id`/`linked_company_id`, `language`, `last_login_at`, optionalem Subunternehmer-Discriminator (M144 / 15.13.2.2)
- `CustomerPortalAccount` (nullable `password_hash`, `terms_accepted_at`, `marketing_opt_in`, `failed_login_attempts`/`locked_until`)
- `CustomerPortalMagicLinkToken` (`purpose`-Enum, Lebenszeiten login=15min/setup=7d/reset=1h/verify=24h, `requested_ip`/`user_agent`)
- `CustomerPortalSession` (**server-side Postgres-Session** für PortalUser — existiert für keine User-Klasse in der Boilerplate; eigener Cookie `hg_portal_session`, 30-Tage-Sliding-Window, 7-Tage-Inaktivität)
- `PortalUserRegistration` (Setup-Token + `welcome_message`)
- `CustomerPortalProjectAccess` (`access_level`-Enum `read_status_only|read_full`, `granted_by`, Soft-Revoke; Indizes `(tenant_id, portal_user_id)`, `(tenant_id, project_id)`)
- `CustomerPortalProjectDocument` (FK auf HiveGrid-File, `release_kind`-Enum, `customer_visible_filename`)
- `PortalConfiguration` (`visible_pipeline_stages` JSON, `stage_display_labels` JSON, `auto_release_doc_types`, `auto_status_mail_enabled`, Pflicht-Links `imprint_url`/`privacy_url`, `support_email/phone`, `welcome_message_default`)
- `PortalEmailTemplate` (`template_kind`-Enum, `subject`/`body` mit Platzhaltern, `from_email_address_id` → M9)
- Subunternehmer-Strukturen (M144): Projekt-Subunternehmer-Liste (Firma/Rolle/Zeitraum), per-Projekt-Sichtbarkeits-JSON
- Service-Inbox / Aktivitäts-Aggregation + Routing-Regeln (M145)
### Auth-Layer (M141) — größte Einzel-Lücke
- Komplette **PortalUser-Auth-Schicht parallel zu M27**: `requirePortalUser`-Middleware, getrennte Cookie-Domäne, Magic-Link-Flow (Anfrage → Token → Validierung → Session), Setup-Flow, Pwd-Reset, Email-Verification — in der Boilerplate existiert NUR die TenantUser-Auth (als Referenz, nicht 1:1 nutzbar). Cross-Permission-Test (Endkunde auf Tenant-Route → 404) ist Erfolgskriterium.
- HIBP-Check für PortalUser-Passwörter (Boilerplate hat Hasher, aber HIBP separat zu verdrahten — vgl. M020)
- hCaptcha ab 3. Magic-Link-Anfrage (C.1)
### UI / Views — komplett neu
- **Endkunden-Portal-Shell** (eigene minimale, mobile-first Navigation; NICHT die Tenant-App-Shell) + `packages/ui-hivegrid/src/customer-portal/portal-shell.config.ts` (INTEGRATION-CONTRACTS §14)
- Alle Endkunden-Views: Dashboard (D.1), Projekte-Liste (D.2), Project-Detail mit Phasen-Fortschritt (D.3), Dokumente-Cross-View (D.4), Upload (D.5), Forms (D.6 — über HiveGrid M5, NICHT `@toolctrl/form-widget`, das nur text/email/number/select kann — kein File/Foto/Signatur), Settings (D.7)
- Alle Tenant-Admin-Konfig-Seiten (E.1–E.6: Portal-Konfig, PortalUser-Verwaltung/-Detail, Access-Matrix, Email-Templates, Portal-Audit) — reuse App-Shell + shadcn-Primitiven, Inhalte neu
- Querschnitt-Erweiterungen in bestehenden HiveGrid-Modulen: Project-Detail-Portal-Sektion (M4), Contact-Detail-Portal-Zugang (M1), Deal-Detail-Sales-Phase-Toggle (M3) — abhängig von M042-M070
### Domänen-Logik / Integration
- Auto-Status-Mail-Engine bei `project.stage_changed` mit Debounce (5-Min-Window), Mehrfach-Empfänger, Reverse-Stage-Stumm-Logik (Block G) — Outbox + Mail vorhanden, Logik neu
- Doc-Auto-Freigabe-Regeln pro Doc-Typ + manuelle Freigabe/Sperre (15.5)
- M17-Automation-Trigger/-Actions (`portal.*` — Block H), M22-Notifications-Wiring
- Service-Inbox + Routing-Regeln + Stille-Modi + Service-Team-Rolle (M145)
- Pro-Kunden- vs. Pro-Projekt-Lebensdauer-Logik + Lifecycle-Cron (M146)
- DSGVO-Lösch-Workflow für PortalUser inkl. Portal-FK-Map (Export-/Deletion-Infra vorhanden, PortalUser-Verdrahtung neu)
---
## Empfohlenes Vorgehen (Reihenfolge)
1. **Querschnitt zuerst sicherstellen (1:1 aus Boilerplate):** foundation (`auditEvent`, `withTenantScope`/`withPermission`, `notify`, `CatalogError`, flags), `packages/mail`, `packages/storage` (signed/scan/access), `packages/shared-store` (Rate-Limit/Session/Idempotenz), Outbox + Worker-Cron-Muster, i18n, view-models. Diese sind nutzbar wie sie sind.
2. **Daten-Layer neu in `prisma/schema/app.prisma`** als `CustomerPortal*`-Familie — `portals.prisma` als Feld-Vorlage heranziehen, aber NICHT erweitern (Namenskollision + falsche Fachlichkeit). `MagicLinkToken` (accounts.prisma:98) als Token-Pattern, ergänzt um `purpose`-Enum. Forward-Only-Migration mit Inverse-SQL (DoD).
3. **M141 Auth-Schicht neu bauen** (kritischster Block) — TenantUser-Auth-Routen (`auth.magic-link.tsx`, `login.server.ts`, `forgot-password.tsx`, `reset.tsx`, `verify.$id.tsx`) als Referenz, aber als eigene PortalUser-Klasse mit getrenntem Cookie/Middleware. passwordHasher 1:1, HIBP + hCaptcha + Rate-Limits ergänzen. Cross-Permission-404-Test als Gate.
4. **M142 Projekt-Sicht** auf bereits gebauten HiveGrid-Modulen (M042-M070, M052-M055, M092 sind Vorbedingungen) — Endkunden-Views + Permission-aware Filter (`CustomerPortalProjectAccess`) + Status-Mail-Engine (Outbox + Mail 1:1, Debounce-Logik neu).
5. **M143 Upload + Forms** — `packages/storage`-Pipeline 1:1 (signed/scan/mime-allowlist/quarantine), Upload-Pfad-Konvention, Notification an PL via `notify` 1:1; Portal-Forms über HiveGrid M5 (M121-M125), nicht über `@toolctrl/form-widget`.
6. **M144 Subunternehmer** — als Discriminator/Untergruppe der PortalUser-Klasse + per-Projekt-Sichtbarkeits-JSON; Datenleck-Schutz (Subunternehmer sehen Endkunden nicht).
7. **M145 Skalierung** — Service-Inbox + Routing (Lead-Routing-Muster M048 als Vorlage) + `bundleNotifications` 1:1 für 30-Min-Bündelung.
8. **M146 Lebensdauer + DSGVO + Konfig** — Lifecycle-Cron nach `dsgvo-grace-cron`-Muster; DSGVO-Export/Deletion-Infra (`@toolctrl/exports` JSON + `dsgvo-*`-Jobs) wiederverwenden, PortalUser-FK-Map ergänzen; Tenant-Admin-Konfig-Seiten mit App-Shell + shadcn.
9. **Reachability/DoD verdrahten:** `portal-shell.config.ts` (§14, Default unsichtbar), Permission-Keys in `entitlements/src/permissions.ts`, i18n-Namespace, Smoke-/Hot-Swap-/Visual-Tests, Feature-Flag. CI-Gates erben.
**Anti-Listen-Wächter (nicht bauen in P1):** Portal-eigenes Branding (15.8.10), Chat/bidirektionale Kommunikation (15.8.1/15.8.4), In-App-Notifications im Portal (15.8.3), Wartungs-Abos/`PortalSubscription*` (15.8.6 — das gesamte Abo-Subsystem aus `portals.prisma` bewusst ignorieren), Bulk-ZIP-Download (15.8.9), 2FA-Pflicht (15.8.8), Blog/CMS (15.8.7), eigene Domain (15.14.2), Mehrfach-Portale (15.14.3).
@@ -0,0 +1,106 @@
# Boilerplate-Abgleich — M17 Automation Engine
| Feld | Wert |
| --- | --- |
| Modul-ID | M17 |
| Name | Automation Engine (USP-Säule) |
| Milestones | M075 (Schema + Auslöser-Registry), M076 (Aktion-Registry + Steuerung), M077 (Bedingungen), M078 (Visueller Editor), M079 (Test-Lauf/Schwellenwert-Vorschau), M080 (24h-Undo/Reversibilität), M081 (Schleifen-/Webhook-/Anomalie-Schutz + Resume), M082 (Klartext-Logs + Re-Run + Trigger-Volumen-Metering) |
| Pflichtenheft-Block | Block 4 (USP-Säulen), Modul 17, Abschnitte 17.1–17.9 + Anhang H.1.1 (Metering) |
| Spec | `_bmad-output/specs/M17-automation-engine.md` (USP-Säule, „größte und wichtigste") |
| **Reuse-Level** | **high** |
## Reuse-Zusammenfassung
Die Boilerplate enthält eine **vollständige, produktionsreife, graph-basierte Workflow-Engine** — exakt die in der Spec geforderte Architektur (Knoten + Edges, parallele Branches, If/Switch). Sie umfasst:
- **Datenmodell** (`prisma/schema/workflows.prisma`, 10 Modelle): `Workflow`, `WorkflowBlock`, `WorkflowBlockToBlock` (Edges), `WorkflowBlockConditionGroup`/`WorkflowBlockCondition` (AND/OR), `WorkflowExecution`, `WorkflowBlockExecution`, `WorkflowInputExample`, `WorkflowVariable`, `WorkflowCredential` — tenant-scoped + `appliesToAllTenants` (Plattform-Templates), Execution-Tracking pro Block, alle nötigen Performance-Indizes vorhanden.
- **Visueller Editor** (`apps/web/app/modules/workflowEngine/`, ~120 Dateien): `@xyflow/react`-Canvas mit Mini-Map/Zoom/Pan, `dagre`-Auto-Layout, Knoten-Catalog, pro-Knoten-Config-Panel, Custom-Edges, Pre-Save-Validierung mit Klartext-Fehlern, Block-Errors-Anzeige, Execution-Visualisierung, Streaming-Progress.
- **Engine + Worker** (`apps/worker/src/jobs/workflow-*.ts`, ~16 Dateien): Outbox-getriebener `event`-Trigger-Fanout, Block-Executor-Factory (native + HTTP), Stalled-Execution-Reconciler-Cron, Preflight-Check, Resume-from-block (`fromBlockId`), Continuation/`waitForInput`, BullMQ-Backoff.
- **Bedingungs-Engine** (`WorkflowsConditionsService.ts` + `WorkflowConditionUtils.ts`): Operatoren `=`, `!=`, `>`, `<`, `>=`, `<=`, `startsWith`, `endsWith`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty`, AND/OR-Gruppen, `{{$vars}}`/`{{$params}}`-Variablen-Auflösung.
- **Querschnitt**: AES-Credential-Verschlüsselung (`CryptoApi.decrypt`) + Credential-Redaction in Logs, AJV-JSON-Schema-Validierung für Trigger-Payloads, Plugin-Hooks (`workflow.executed`/`workflow.failed`), Idempotenz (`@toolctrl/foundation` + Outbox-Receiver-Idempotency), Default-Templates-Registry (code-basiert, i18n).
Damit ist das **gesamte Architektur-Skelett** von M075–M082 abgedeckt: Graph-Datenmodell, Editor-Library, Execution-Engine, Conditions, Resume-Mode, Trigger-Fanout, Templates-Registry. Was **fehlt**, sind durchweg **HiveGrid-fachliche Inhalte und USP-Mechaniken**: die ~60 domänen-spezifischen Trigger/Actions (Deal/Project/Lead/CRM/Booking/Telefonie/KWP/Portal …), das 24h-Undo/Reverse-Window, Anomalie-Detection + Loop-Guard mit Klartext, KI-Erstellung per Sprache, Dry-Run-mit-Empfänger-Sample + >100-Schwelle, Trigger-Volumen-Metering, sowie die HiveGrid-Permission-Bundles.
**Wichtiger Architektur-Vorbehalt:** Der gesamte Editor- und Service-Code liegt in `apps/web/app/modules/workflowEngine/` als RR7-Route-Module mit `.db.server.ts`/`.api.server.ts`-Dateien — das **kollidiert mit der HiveGrid-Layer-Disziplin** (AGENTS §A: View-Models in `packages/view-models`, UI in `packages/ui-hivegrid`, Subframe via Adapter, keine `useLoaderData` in UI-Components). Plus User-Memory-Blocker: **`.server.ts`-Re-Export ist verboten — nur Component-Extraktion**. Der Code ist daher als **Vorlage** zu behandeln (Logik 1:1 übernehmen, Layer-Struktur umbauen), nicht als 1:1-Import. Datenmodell, Engine/Worker und reine Helper/Services sind dagegen sehr nah an 1:1.
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `prisma/schema/workflows.prisma` | Komplettes graph-basiertes Datenmodell: `Workflow`, `WorkflowBlock`, `WorkflowBlockToBlock` (Edges + `condition`), `WorkflowBlockConditionGroup`/`Condition`, `WorkflowExecution`, `WorkflowBlockExecution`, `WorkflowInputExample`, `WorkflowVariable`, `WorkflowCredential`; tenant-scoped + `appliesToAllTenants`, Status/Duration/Error/`waitingBlockId` pro Execution, Indizes | **als-vorlage** — Struktur deckt `AutomationRule`/`RuleStep`/`Execution`/`StepExecution`/`Credential`/`Template` der Spec (Block A). Ergänzen: `created_via` (manual/template/ki_natural_language), `last_executed_at`, `executions_count_30d`, `archived_at`, `dry_run`, `idempotency_key`, `reverse_window_expires_at`, `summary_text`, `parent_step_ids[]`/`position`, sowie ein dediziertes `AutomationTemplate`-Modell (Boilerplate-Templates sind code-only, kein DB-Modell) |
| `apps/web/app/modules/workflowEngine/components/workflows/WorkflowBuilder.tsx` + `LazyWorkflowBuilder.tsx`, `WorkflowBlockEditor.tsx`, `WorkflowEditorSidebar.tsx`, `WorkflowExecutionsSidebar.tsx`, `components/nodes/*`, `components/edges/CustomEdge.tsx` | Vollständiger `@xyflow/react`-Editor: Canvas, Knoten-Catalog-Sidebar, pro-Knoten-Config, Custom-Edges, Trigger-/If-/Switch-Nodes, Lazy-Load | **als-vorlage** — Editor-Grundgerüst + Drag/Connect/Config-Muster 1:1 übernehmbar (deckt M078, Block D). MUSS in `packages/ui-hivegrid` + Adapter-Layer umziehen (AGENTS §A, M015); UI darf kein `useLoaderData` nutzen |
| `apps/web/app/modules/workflowEngine/helpers/WorkflowLayoutUtils.ts` | `dagre`-basiertes Auto-Layout (`getLayoutedElements`, rankdir) | **1:1** — deckt „Auto-Layout"-Toolbar (Block D) |
| `apps/web/app/modules/workflowEngine/helpers/WorkflowUtils.ts` | `buildExecutionOrder` mit `visited`-Set (DFS-Traversal über `true/false/default/case/loopNext/loopEnd`-Branches), Block-Graph-Helfer | **anpassen** — Traversal-Basis; für Loop-Guard/Cycle-Pre-Save-Validation (Block G) als Cycle-Detektor erweitern (vorhandenes `visited` ist Traversal-Schutz, KEINE explizite Cycle-Warnung) |
| `apps/web/app/modules/workflowEngine/services/WorkflowPreflightService.ts` + `helpers/WorkflowReferenceUtils.ts` | Pre-Save/Pre-Run-Validierung: fehlende Variablen/Credentials, fehlende Pflicht-Inputs pro Block, native-Capability-Errors, alles als i18n-Klartext; `throwIfFailed` mit `status:400` + `preflight`-Detail | **als-vorlage** — deckt 17.5.6 „Validator listet alle Probleme" (Block D) sehr gut; ergänzen: „Knoten nicht verbunden / wird nie ausgeführt" + Cycle-Warnung (Klartext aus Spec Block D) |
| `apps/web/app/modules/workflowEngine/services/WorkflowsValidationService.ts` | AJV-JSON-Schema-Compile + `validate`/`parseJsonSchema` für Trigger-Payload-Validierung | **1:1** — Schema-Validation für Trigger-Payloads/manuelle Trigger |
| `apps/web/app/modules/workflowEngine/services/WorkflowsConditionsService.ts` + `helpers/WorkflowConditionUtils.ts` + `dtos/WorkflowConditionDtos.ts` | Bedingungs-Auswertung: 12 Operatoren, AND/OR-Gruppen, `parseVariable`-Auflösung, i18n-Labels, Operator-Validierung | **anpassen** — deckt M077/Block C Conditions (Field-Equals/Contains/Greater/Less, Tag-Set via contains). Ergänzen: `Time-Of-Day`/`Day-Of-Week`, `User-In-Group`, type-safe Feld-Picker, der Trigger-Felder kennt (Spec 17.4 / M077-Hinweis) |
| `apps/web/app/modules/workflowEngine/services/WorkflowsExecutionsService.ts` | Execution-Orchestrierung: Live-Check, Preflight, Execution-Record, Trigger-Block-Suche, **Resume ab `fromBlockId`**, Continuation-Sourcing, Outbox-Publish (`workflow.execution.requested`), Plugin-Hook-Emission, Credential-Redaction | **als-vorlage** — Kern der Engine; Resume-Mode (Block H) bereits angelegt. `.server`-Logik in HiveGrid-Service-Layer (`app/domain/**` / loader-pfad) extrahieren |
| `apps/web/app/modules/workflowEngine/services/blocks/WorkflowBlockService.ts` + `WorkflowBlockProService.ts` + `dtos/WorkflowBlockTypes.ts` | Block-Type-Registry + Executor: `trigger(manual/event)`, `if`/`switch` (flow), `httpRequest`, `log`, `alertUser`, `doNothing`, `waitForInput`, `gpt` (ai), `rowGet/Create/Update/Delete` (entities) — als deklarative `WorkflowBlockTypeDto[]` mit Inputs/Outputs/Conditions | **als-vorlage** — die Registry-Architektur (deklarative Node-Typen + Executor) ist genau das M075/M076-Muster. Inhalte sind aber generisch (`row*`/`httpRequest`/`gpt`) — die ~60 HiveGrid-Trigger/Actions (deal/project/lead/contact/booking/call/kwp/portal/…) müssen NEU als Plug-Registry-Einträge ergänzt werden |
| `apps/web/app/modules/workflowEngine/utils/DefaultWorkflowTemplates.ts` + `WorkflowsTemplatesService.ts` + `workflowTemplateI18n.ts` | Code-basierte Template-Registry mit i18n-Keys, Import-in-Tenant, `adminOnly`/`appliesToAllTenants` | **als-vorlage** — deckt Template-Mechanik (Block I, M082-Templates). Inhalte sind Demo (`sampleHttpRequests`); die SHK-Templates („Lead-Begrüßung", „Won-Handoff", „Stuck-Deal-Eskalation", „Mängel-Tasks-Generator" …) neu schreiben |
| `apps/worker/src/jobs/workflow-trigger.ts` + `workflow-trigger-store.ts` + `workflow-trigger-executor-factory.ts` + `workflow-trigger-native-executor.ts` + `workflow-trigger-http-executor.ts` + `workflow-trigger-bootstrap.ts` | Outbox-getriebener Event→Workflow-Fanout: `findLiveEventWorkflows` (matcht `event`-Trigger gegen `eventName` + Condition-Filter), Idempotenz via Outbox-Receiver, Executor-Factory (native/HTTP), Rejected-Execution-Recording | **anpassen** — exakt die „Auslöser-Registry als Plug-Architektur"-Mechanik (M075-Hinweis). HiveGrid-Module müssen ihre Domain-Events in den Outbox-Bus emittieren (`workflow.*`-Prefix); Event-Liste erweitern |
| `apps/worker/src/jobs/workflow-execution-stalled-reconciler.ts` + `-cron.ts` | Cron: findet hängende Executions (>30 min, `waiting`/`waitingBlock`), gleicht gegen Failed-Outbox/Queue-Failed-Jobs ab, repariert/re-published | **1:1** — Resilienz-Baustein für Resume-Mode (Block H, M081); zusätzlich als Muster für den Stage-Stuck-Daily-Cron (Block K) nutzbar |
| `packages/queue/src/producers.ts` (`retryJobOptionsByQueueAndJobName`, `buildRetryJobOptions`) + `types.ts` (Default `attempts:3`, exponential backoff) | BullMQ-Retry/Backoff-Konfiguration pro Queue/Job, exponential | **anpassen** — Basis für die Auto-Retry-Strategie 1m/5m/15m/1h/6h/24h (Block H, M081); konkrete Backoff-Stufen + „nach 6 Retries failed + Admin-Mail" ergänzen |
| `packages/foundation/src/idempotency.ts` (`withIdempotency`, `IdempotencyClient`) + `prisma/schema/core.prisma` (`IdempotencyKey @@unique([scope,key])`) | Idempotenz-Helper + DB-Modell | **1:1** — `idempotency_key` pro Action-Step (Block H / AV#8); deckt „skip mit Status skipped, wenn schon erledigt" |
| `apps/worker/src/outbox/event-router.ts` (`DEFAULT_OUTBOX_ROUTING`, inkl. `{ prefix: "workflow.", queue: "outbox.workflow" }`) + `outbox/dispatcher.ts` | Outbox→Queue-Routing (longest-prefix-wins), Dead-Letter | **1:1** — der `workflow.`-Pfad ist bereits verdrahtet; HiveGrid-Domain-Events nur emittieren |
| `apps/web/app/modules/workflowEngine/services/WorkflowVariablesAndCredentialsService.ts` + `WorkflowsVariablesService.ts` (`CryptoApi.decrypt`, `redactWorkflowCredentials`, `REDACTED_WORKFLOW_CREDENTIAL`) | AES-verschlüsselte Tenant-Credentials at-rest + Credential-Redaction in Outputs/Logs | **1:1** — deckt `AutomationCredential` (Block A) + Klartext-Logs-Sicherheit (Block J). App-Credentials aus M33a separat anbinden |
| `apps/web/app/modules/workflowEngine/components/workflows/stream/WorkflowStreamProgress.tsx` + Routes `workflows.$id.run.stream.*` + `apps/web/app/modules/workflowEngine/routes/.../run.manual.*` | SSE-Streaming der laufenden Execution + manueller Run-Trigger | **als-vorlage** — Live-Streaming-Antwort (Block E) + Test-Run-UX (Block F/M079); Layer-Umbau nötig |
| `apps/web/app/modules/workflowEngine/utils/workflowExecutionErrorText.ts` + `components/workflows/misc/WorkflowBlockErrors.tsx` + `WorkflowResultBadge.tsx` | Klartext-Fehler-Mapping + Block-Error-/Status-Badges | **anpassen** — Basis für Klartext-Diagnostik (Block J/LP-10); Spec-Klartext-Beispiele (17.9.3) ergänzen |
| `packages/ai` (`@toolctrl/ai`): `src/index.ts`, `prompts/index.ts` (versionierte Prompt-Registry), `tools/index.ts` (scoped Tool-Registry), `streaming/index.ts`, `guards/index.ts` (`assertAICostBudget`, `createFixedWindowAIRateLimiter`, `createReviewRequiredResult`, PII-Redaction), `fallback.ts` (Multi-Provider EU: Mistral/Scaleway/Aleph-Alpha), `cost/*` | KI-Provider-Layer + Guards + Streaming + Cost-Tracking | **anpassen** — Fundament für KI-Erstellung-per-Sprache (Block E/LP-5). `createReviewRequiredResult` deckt Action-Boundary (EC-5); Rate-Limiter/Cost-Guards für KI-Aufrufe. NEU: Intent-Klassifikation + Entity-Extraction + Flow-Graph-Generierung-Prompts |
| `packages/ai/src/promptfoo-provider.ts` + `packages/ai/test/evals/` + Repo-Root `promptfoo.config.yaml` | Promptfoo-Eval-Harness (Baseline-Test) als CI-Gate | **als-vorlage** — deckt AV#14 / EC-5 „Promptfoo-Eval-Suite als CI-Pflicht" für die KI-Flow-Generierung (Block E); eigene Eval-Cases neu |
| `packages/plugin-runtime/src/hooks/registry.ts` (Hooks `workflow.created/updated/deleted/executed/failed`) | zod-validierte Workflow-Lifecycle-Hooks | **1:1** — Audit-Trail-/Activity-Hooks für Rule-Änderungen (AV#6) + Notification-Trigger |
| `prisma/schema/core.prisma` (`AuditEvent` Hash-Kette, `Event`/`EventWebhookAttempt`) + `packages/db` (`publishOutboxEvent`, `runDbTransaction`, `audit-hash.ts`, DSGVO-FK-Map) | Audit-Trail, transaktionale Outbox, signierte Webhooks | **1:1** — Querschnitt für AV#6 (Rule-Change-Audit), `webhook.send (signiert)` (Block C), Reverse-Audit-Eintrag (Block F) |
| `prisma/schema/notifications.prisma` + `apps/web/app/modules/notifications/.../NotificationService.ts` | In-App/Email/Push-Delivery + Preferences + DeviceToken | **1:1** — `notification.send_push`/`send_in_app` Actions (Block C) + Anomalie-/Loop-Guard-Notifications (Block G) via `notify()` (M011) |
| `prisma/schema/featureFlags.prisma` + `evaluateFlagForContext` | Feature-Flag-Engine | **1:1** — DoD-Pflicht „Feature-Flag-fähig" (M006) für das gesamte Modul |
| `packages/view-models` (`VMState`, `ActionCallbacks`, `I18nLabel`, `assertI18nLabel`) | View-Model-Statusmodell + i18n-Key-Zwang | **1:1** — Konvention M013; Loader baut VM, UI empfängt nur VM+Actions (DoD Layer-Disziplin) |
| `package.json` Root-Dep `@xyflow/react ^12.10.0` + `dagre` | Graph-Editor-Library bereits installiert | **1:1** — beantwortet die offene Spec-Frage „Flow-Editor-Library-Wahl"; **xyflow ist gesetzt** |
## Vorhandene Daten-Modelle (konkret)
`prisma/schema/workflows.prisma` — Mapping zur Spec (Block A):
| Spec-Entität | Boilerplate-Modell | Lücke |
| --- | --- | --- |
| `AutomationRule` | `Workflow` (`name`, `description`, `status`, `tenantId?`, `createdByUserId`, `appliesToAllTenants`) | fehlt: `flow_data`-JSON (BP nutzt normalisierte `WorkflowBlock`-Tabellen statt eines JSON-Blobs — beides valide; Spec nennt JSON, BP-Ansatz ist sauberer), `created_via`, `last_executed_at`, `executions_count_30d`, `archived_at` |
| `AutomationRuleStep` | `WorkflowBlock` (`type`, `description`, `isTrigger`, `input`, `conditionsGroups`) + `WorkflowBlockToBlock` (Edges) | fehlt: explizites `step_kind`-Enum, `parent_step_ids[]` (BP modelliert Edges als eigene Tabelle — funktional äquivalent für Multi-Parent), `position` für Editor-Layout |
| `AutomationExecution` | `WorkflowExecution` (`type`, `status`, `input`, `output`, `duration`, `endedAt`, `error`, `waitingBlockId`, `createdByUserId`) | fehlt: `summary_text`, `dry_run`, `idempotency_key`, `reverse_window_expires_at`, `trigger_payload` (steckt in `input`) |
| `AutomationStepExecution` | `WorkflowBlockExecution` (`status`, `startedAt`, `endedAt`, `input`, `output`, `duration`, `error`) | fehlt: `summary_text`, `error_text` (Klartext getrennt vom techn. `error`), `idempotency_key` pro Step |
| `AutomationCredential` | `WorkflowCredential` (`name`, `value`, `@@unique([tenantId,name])`, AES-verschlüsselt) | weitgehend vollständig; `description`-Feld + App-Credential-Bridge zu M33a ergänzen |
| `AutomationTemplate` | (kein DB-Modell) — Templates sind code-basiert in `DefaultWorkflowTemplates.ts` | **komplett neu**: DB-Modell für Tenant-eigene Templates (`is_published`, `category`, `branchen_template_origin`) |
Zusätzlich nutzbar: `WorkflowVariable` (tenant-scoped Vars), `WorkflowInputExample` (Sample-Payloads für Dry-Run-Quelle, Block F offener Punkt), `IdempotencyKey` (core.prisma).
## Lücken / komplett neu zu bauen
1. **Domänen-Trigger-Library (~45 Trigger, Block B / M075):** Boilerplate kennt nur generische Trigger (`manual`, `event` mit `row.created/updated/deleted` + Account/Member/Subscription/Email-Events). Alle HiveGrid-Trigger fehlen: `deal.*` (created/stage_changed/won/lost/stuck/assignee_changed/volume_changed), `project.*`, `lead.*`, `contact.*`, `booking.*`, `call.*` (Telefonie), `task.*`, `form.submitted`, `email.received_inbox`/`subject_pattern_matched`, `kwp.*`, `app.*`, `hr.*`, `portal.*`, `schedule.cron`/`delay`. → Als Plug-Registry-Einträge + Domain-Event-Emission in den jeweiligen Modulen.
2. **Domänen-Action-Library (~40 Actions, Block C / M076):** BP hat nur `httpRequest`, `log`, `alertUser`, `gpt`, `rowGet/Create/Update/Delete`. Fehlen alle CRM/Lead/Sales/Project/Communication/Task/KWP/Calendar/File-Actions inkl. **Pipeline-Targeting** (`deal.create` mit Ziel-Pipeline+Start-Stage), `project.create_from_won_deal` (Won-Handoff mit Default-Mapping), `email.send` (Template+Vars), `notification.send_push/in_app`, `webhook.send (signiert)`, `ai.summarize/classify`. Flow-Control `if`/`switch` vorhanden; `delay`/`parallel`/`merge` ergänzen; **`loop` bewusst NICHT** (Anti-Liste). KI-Actions auf `packages/ai` mappen.
3. **24h-Undo / Reverse-Window (Block F / M080):** Komplett neu. Kein Reverse-Mechanismus in BP. Benötigt: `reverse_window_expires_at`, Snapshot der reversiblen Wirkungen pro Run, Reverse-Handler (Hard-Delete angelegter Entitäten / Status-Reset / Tag-Rückgängig / Empfänger-Liste bei Mails), BullMQ-delayed Cleanup-Job nach 24h, Multi-Step-Confirm + Audit-Eintrag pro Reverse.
4. **Anomalie-Detection + Loop-Guard mit Klartext (Block G / M081):** Komplett neu. Hard-Limit 1.000/h pro Trigger (tenant-konfigurierbar), 10×-Rate-Anomalie→Pause+Notification, Loop-Guard max. 5 Auto-Trigger/Datensatz/60s, Pre-Save-Cycle-Detection mit Klartext-Visualisierung. (BP `createFixedWindowAIRateLimiter` ist NUR für AI-Calls pro userId — nicht für Trigger-Volumen.)
5. **KI-Erstellung per natürlicher Sprache (Block E / wird in M086 referenziert):** Engine-Bausteine (`packages/ai`, Prompt-/Tool-Registry, Streaming, Promptfoo, `createReviewRequiredResult`) vorhanden, aber die **KI-Pipeline selbst neu**: Intent-Klassifikation, Entity-Extraction (Trigger/Filter/Actions), Schema-Validation gegen existierende Pipelines/User/Forms (EC-4), Rückfrage-bei-Mehrdeutigkeit, Flow-Graph-Generierung als JSON, Risiko-Hinweise, Edit-Mode-Diff, Voice via Whisper-EU (M18). Hinweis Spec: M078 listet M086 (KI-Sprache) als „benötigt von" — KI-Erstellung ist eigener Milestone-Strang.
6. **Dry-Run mit Empfänger-Sample + >100-Schwelle (Block F / M079):** BP hat Test-Run/manual-Run + SSE-Streaming, aber **keinen** Side-Effect-freien Dry-Run mit „würde X Personen treffen, erste 5: …", keine konfigurierbare Schwellenwert-Bestätigung (Default 100), kein Mock-Adapter-Layer für Actions.
7. **Trigger-Volumen-Metering (Block L / M082):** Komplett neu. Counter `AutomationExecution` pro Tenant/Monat, Soft-Limit-Warnung bei 80%, Hard-Limit→Queue+Klartext, Visualisierung in `/{tenant}/admin/usage/`. (BP hat `MetricLog`/AI-Cost-Aggregate als Muster, aber kein Trigger-Volumen.)
8. **Stage-Stuck-Eskalation als Daily-Cron (Block K):** Neu — Cron checkt `stage.auto_archive_after_days`, emittiert `deal.stuck.in_stage`/`project.stuck.in_stage`. Reconciler-Cron-Muster (`workflow-execution-stalled-reconciler-cron.ts`) als Vorlage.
9. **HiveGrid-Permissions + Bundles (Block M):** Neu — `automation.read/test/write/activate/delete/reverse/use_ki/admin` + Bundles `basic/builder/publisher/admin` + SHK-Default-Rollen. BP-Workflow-Routes nutzen generische Auth-Boundary (`workflowAccess.server.ts`), nicht diese Permissions. Über M004 `withPermission()` verdrahten.
10. **Klartext-Step-Diagnostik nach Spec (Block J / M082):** BP hat Execution-Detail-Views + Error-Text-Mapping, aber die konkreten Spec-Klartext-Texte (17.9.3: „Email an X gesendet, Dauer 0.8s", Halb-Erfolg, Skipped-mit-Grund) + „Detail-Logs für Plattform-Admin (`tech_details`)" + Re-Run-aus-Log neu formulieren.
11. **Layer-Umbau (querschnittlich, PR-Block-relevant):** Der gesamte `apps/web/app/modules/workflowEngine/`-Editor-/Service-Stack muss in die HiveGrid-Drei-Schichten überführt werden: View-Models → `packages/view-models/src/automation/*.vm.ts`, UI → `packages/ui-hivegrid/src/...` (kein `useLoaderData`, kein Prisma-Import), Subframe-Output via Adapter. **`.server.ts`-Re-Export verboten (User-Memory) → Component-Extraktion.** Sidebar-Eintrag „Automation-Studio" in Gruppe `automation-ki` (INTEGRATION-CONTRACTS §1), Routen unter `/{tenant}/automations/...` (Spec-Pfade) statt BP-`__workflows/workflow-engine`.
## Empfohlenes Vorgehen
**Phase 1 — Fundament aus Boilerplate übernehmen (M075):**
1. `prisma/schema/workflows.prisma` als Basis nach HiveGrid übernehmen, um Spec-Felder erweitern (`created_via`, `archived_at`, `dry_run`, `idempotency_key`, `reverse_window_expires_at`, `summary_text`/`error_text`, `position`) + neues `AutomationTemplate`-DB-Modell. Forward-Only-Migration.
2. Outbox→Workflow-Fanout-Worker (`workflow-trigger*.ts`) + `event-router.ts` (`workflow.`-Prefix ist schon verdrahtet) 1:1 übernehmen.
3. Block-Type-Registry-Architektur (`WorkflowBlockTypes.ts` + `WorkflowBlockService.ts`) als Plug-Registry übernehmen; HiveGrid-Domain-Trigger als erste Einträge ergänzen + Domain-Events in CRM/Lead/Sales/Project emittieren.
**Phase 2 — Actions + Conditions (M076/M077):**
4. Action-Registry mit Idempotency-Key (`foundation/idempotency.ts`) ausbauen; Domänen-Actions inkl. Pipeline-Targeting + Won-Handoff neu; Flow-Control `delay/parallel/merge` ergänzen (kein `loop`).
5. `WorkflowsConditionsService`/`WorkflowConditionUtils` übernehmen, um `Time-Of-Day`/`Day-Of-Week`/`User-In-Group` + type-safe Feld-Picker erweitern.
**Phase 3 — Editor (M078):**
6. `WorkflowBuilder`-Editor (xyflow + dagre) als Vorlage in `packages/ui-hivegrid` + Adapter überführen (Layer-Disziplin, Component-Extraktion statt `.server`-Re-Export). `WorkflowPreflightService` übernehmen + Cycle-/„nicht-verbunden"-Klartext-Validierung ergänzen. Pflicht-Aktivierung als separater Schritt.
**Phase 4 — USP-Mechaniken neu (M079–M082):**
7. M079: Dry-Run-Mode mit Mock-Action-Adapter + Empfänger-Sample + >100-Schwelle (BP Test-Run/Streaming als UX-Vorlage).
8. M080: 24h-Undo/Reverse-Window komplett neu (Snapshot + Reverse-Handler + delayed Cleanup-Job + Audit).
9. M081: Anomalie-Detection + Loop-Guard + Resume/Backoff — Resume ist in `WorkflowsExecutionsService` (`fromBlockId`) + Stalled-Reconciler vorhanden; Rate-Limit/Anomalie/Loop-Guard neu; Backoff-Stufen 1m/5m/15m/1h/6h/24h via `queue/producers.ts` konfigurieren.
10. M082: Klartext-Logs (BP Error-Text-Mapping + Execution-Detail als Vorlage) + Re-Run + Trigger-Volumen-Metering (neu, `MetricLog`/AI-Cost-Aggregate als Muster).
**Querschnitt parallel:** Permissions/Bundles (M004), Feature-Flag (M006), Audit-Hooks (`plugin-runtime` workflow-Hooks + `AuditEvent`), `notify()` (M011), Sidebar/Routes (INTEGRATION-CONTRACTS), Promptfoo-Eval-Gate für die spätere KI-Erstellung (`packages/ai/test/evals` als Vorlage). Templates-Registry (`DefaultWorkflowTemplates.ts`-Muster) mit SHK-Templates befüllen.
@@ -0,0 +1,193 @@
# Boilerplate-Abgleich: M18 — KI-Layer / LLM-Gateway
> Faktischer Abgleich der HiveGrid-Detail-Spec (M18) gegen die Boilerplate.
> Boilerplate-Wurzel (NUR LESEN): `/Users/raffaelsheikh/Documents/GitHub/boilerplate`
> Spec-Quelle: `/Users/raffaelsheikh/Documents/GitHub/HiveGrid-Cloud/_bmad-output/specs/M18-ki-layer.md`
> Alle Pfade absolut. Nichts erfunden — jeder Eintrag verweist auf konkrete Datei(en); fehlt etwas, ist es klar als Lücke markiert.
## Kopf
| Feld | Wert |
| --- | --- |
| Modul-ID | M18 |
| Name | KI-Layer / LLM-Gateway |
| Milestones | M083 (EU-Provider-Layer), M084 (Multi-Provider-Fallback), M085 (Promptfoo-Eval-Suite), M086 (Sprach-zu-Automation), M087 (RAG + Aktions-Grenze), M088 (Token-Limits + Pay-as-you-go) |
| Block | Block 4 — Integrationen, Automation & KI (`_bmad-output/pflichtenheft/blocks/block4.md`, Modul 18) |
| Reuse-Level | **HIGH** |
**Kernaussage:** Das ist die USP-Säule mit dem höchsten Wiederverwendungs-Grad im KI-Bereich. Die Boilerplate liefert den kompletten **EU-Provider-Layer** (Mistral/Scaleway/Aleph Alpha — exakt der von der Spec geforderte Stack, KEIN OpenAI/Anthropic im aktiven Pfad), den **30-Min-Multi-Provider-Fallback** (M084 quasi fertig), die **RAG/Embeddings-Pipeline + pgvector-Store** (M087-Retrieval), die **Promptfoo-Eval-Suite mit OWASP-Injection-Vektoren + Gitea-CI-Workflow** (M085 quasi fertig), das **Cost-/Quota-Tracking inkl. Pay-as-you-go-Overage-Logik** (M088-Engine fertig) und die **Guards/Action-Boundary-Bausteine** (M087-Aktions-Grenze). Die Hauptlücken liegen bei: **HiveGrid-spezifische Daten-Modelle** (LlmGatewayProvider/Model, AiSettings, Conversation als HiveGrid-Variante, TenantEmbedding-Tabelle), **Whisper-EU-Transkription** (komplett nicht vorhanden), die **Use-Case-Orchestrierung** (Lead-Zusammenfassung, Sprach→Automation, Stuck-Deal, Email-Klassifizierung) und das **gesamte UI** (Chat, Usage-Dashboard, Settings, Voice-Assistant).
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `packages/ai/src/index.ts` | Provider-Adapter-Interface + Registry: `AIProviderName = "mistral"\|"scaleway"\|"aleph-alpha"`, `getAIProviderAdapter()`, `getAIProviderAdapterForModel()`, `getPrimaryProvider()`, `getAIProviderDiagnostics()`, Modell-Katalog `aiModels` (mit Preisen pro Mio Tokens), `defaultAIModel = "mistral-large-latest"` | **1:1** — exakt der von Spec Block B geforderte EU-Stack. Deckt M083 Erfolgskriterium (Mistral-Aufruf, Switch via Env-Var, keine US-Endpunkte) |
| `packages/ai/src/providers/mistral.ts` | Mistral-Provider via `@ai-sdk/mistral`, `resolveMistralModel(env)`, `MISTRAL_API_KEY`/`MISTRAL_MODEL` | **1:1** — Primary-Provider (Spec: Mistral Large Chat / Mistral Small) |
| `packages/ai/src/providers/scaleway.ts` | Scaleway-Provider (OpenAI-kompatibel), Default `mistral-small-3.2-24b-instruct-2506`, `SCALEWAY_*`-Env | **1:1** — Secondary-Provider (France-EU) |
| `packages/ai/src/providers/aleph-alpha.ts` | Aleph-Alpha-Provider (OpenAI-kompatibel), Default `pharia-1-llm-7b-control`, `ALEPH_ALPHA_*`-Env | **1:1** — Tertiary-Provider (Germany-EU) |
| `packages/ai/src/providers/compatible.ts` | `createCompatibleLanguageModel()` (gemeinsame OpenAI-kompatible Basis für Scaleway + Aleph Alpha) | **1:1** |
| `packages/ai/src/fallback.ts` | `AIProviderHealthTracker` (30-Min-Failure-Window = `fallbackWindowMs`), `getFallbackProviderSelection()`, `fallbackProviderOrder = ["mistral","scaleway","aleph-alpha"]` | **1:1 / anpassen** — deckt M084 (Auto-Fallback + 30-Min-Window) fast vollständig. Spec verlangt zusätzlich 1-Min-Health-Ping + 5-Min-Sperre (Block H / 18.6.3) — der Health-Tracker ist reaktiv (failure-on-call), nicht aktiv-pingend → **kleine Erweiterung nötig** |
| `packages/ai/src/cost/executeLLMCall.ts` | LLM-Wrapper: wrappt jeden Provider-Call, leitet Token/Kosten ab, emittiert Outbox-Event `ai.cost.event.persist`, Quota-Pre-Check + Soft-Degradation + Concurrency-Slot; Killswitch `AI_COST_TRACKING_ENABLED=false`; Capabilities `chat`/`chat-stream`/`embedding`/`completion`/`rerank`/`tool-call` | **1:1** — zentraler Gateway-Aufruf-Wrapper (Spec Block B Interface + Block F Token-Tracking) |
| `packages/ai/src/cost/assertCredit.ts` | `assertTenantAICreditAccess()` — Pre-Call-Quota-Assertion (3 Outcomes: allow / allow+degradeTo / throw `AIQuotaExceededError`), Concurrency-Counter, Soft-Degradation (90%-Threshold), per-Capability-Caps, Killswitch `AI_QUOTA_ENFORCEMENT_ENABLED=false` | **1:1 / anpassen** — M088-Kern. **WICHTIG:** Default-Verhalten ist Hard-Deny (HTTP 402) bei Limit. HiveGrid [N13] verlangt **Pay-as-you-go statt Pause** → die Hard-Deny-Pfade durch Overage-Pfad ersetzen (Overage-Logik existiert separat, s.u.) |
| `packages/entitlements/src/ai-overage.ts` | Pay-as-you-go-Overage: `AI_OVERAGE_DEFAULT_MULTIPLIER = 1.5`, `computeMarkedUpCostCents()`, `roundUpToBlock()`, `resolveTenantAIOverage()`, `standardAIOverageDefaults` pro Plan, `TenantAIOverageOverride` (parse/resolve) | **anpassen** — genau das von M088/[N13] geforderte Aufschlags-Tarif-Modell. Markup-Faktor + Block-Rundung fertig; HiveGrid-Plan-Werte + Notbremse-Threshold (18.8.7) einsetzen |
| `packages/entitlements/src/ai-quotas.ts` | `aiQuotaCapabilityKeys`, `AIQuotaPlanDefaults` (aiQuotaCentsPerMonth / aiPerCapabilityCaps / aiMaxConcurrentCalls / aiDegradeTo), `standardAIQuotaDefaults` (starter/pro/business), `resolveTenantAIQuota()`, `parseTenantAIQuotaOverride()` | **anpassen** — Inklusiv-Kontingent + per-Capability-Caps pro Plan. HiveGrid-Token/Cent-Werte einsetzen (Spec: „konkrete Mengen mit Pricing-Festlegung", noch offen) |
| `packages/ai/src/cost/spend-cache.ts` | `createSpendCache()` — Monatsspend-Caching (TTL) als Watermark für 80%-Banner und Forecast | **1:1** — Datenbasis für 80%-Soft-Limit-Banner + Email (18.8.2) und Usage-Dashboard-Forecast (18.8.9) |
| `packages/ai/src/cost/tokens.ts` | `countTokens`/`countMessageTokens` (gpt-tokenizer + Heuristik-Fallback) | **1:1** — Token-Zählung für Budget pro Use-Case (Spec C: ~2000 in / 800 out etc.) |
| `packages/ai/src/cost/pricing/v2026-05-16.ts` + `pricing/index.ts` | Versionierte Pricing-Tabelle (`getPricingTable`, `LATEST_PRICING_VERSION`) | **anpassen** — EU-Provider-Preise sind als `aiModels[].costPerM*` bereits hinterlegt; HiveGrid-Aufschlags-Tarife ergänzen |
| `packages/ai/src/guards/index.ts` | `assertAICostBudget()`, `createFixedWindowAIRateLimiter()` (pro userId), `createAIAuditEvent()`/`createAI{Prompt,ToolCall,Mutation}AuditEvent()`, `redactAIAuditPayload()` (PII-Redaction), `createReviewRequiredResult()`, `AIGuardError` | **1:1 / anpassen** — `createReviewRequiredResult()` + Mutation-Audit-Events sind das Fundament der **Action-Boundary** (M087, Spec Block G/18.5.3: KI generiert nur Vorschläge, User bestätigt). Audit-Redaction deckt AV#6 |
| `packages/ai/src/embeddings/factory.ts` + `embeddings/types.ts` | `createEmbeddingProvider()`, `registerEmbeddingProviderAdapter()`, `resolveEmbeddingProviderName(env)`, `resolveEmbeddingDimensions(env)`, `EmbeddingProvider`-Interface | **1:1** — RAG-Schreib-/Leseseite-Fundament (M087 / Spec Block D) |
| `packages/ai/src/embeddings/providers/scaleway.ts` | Scaleway-Embeddings-Adapter, Default `bge-multilingual-gemma2` (1024 dim), Batch 64, max 4096 Tokens | **1:1** — Spec Block D: „Scaleway-Embeddings, 1024-Dim Default" — exakter Match |
| `packages/ai/src/embeddings/embed-and-upsert.ts` | `embedAndUpsert()` — chunked Embed + Upsert in `VectorUpsertSink`; erzwingt `tenantId` im Payload; deterministische `buildChunkId()`; optionale Kostenverfolgung über `executeLLMCall` | **1:1 / anpassen** — Embedding-Pipeline (Spec D: bei Anlage/Update Embedding). Re-Embed-bei-Text-Änderung via Hash (Spec D) muss als Trigger-Wiring ergänzt werden |
| `packages/ai/src/embeddings/retrieve.ts` | `retrieveRelevantContext()` — Query embedden, Fan-out über Collections, Rank-Merge, min-Score 0.7, top-K 8; tenant-scoped via `RagRetrievalStore.searchForTenant` | **1:1** — RAG-Leseseite. Spec D Permission-Pre-Filter andocken |
| `packages/ai/src/embeddings/chunking.ts` | `splitForEmbedding()`, `countTokens()`, `DEFAULT_CHUNK_TOKENS`/`...OVERLAP_TOKENS` | **1:1** |
| `packages/ai/src/embeddings/outbox-publisher.ts` | `publishRagUpsertEvent`/`publishRagDeleteEvent`, `ragEventType()` → `rag.index.<entity>.<verb>` (BullMQ-Queue `outbox.rag-index`), Idempotenz via `dispatchKey` | **1:1** — async Re-Index bei jedem Domain-Write (Spec D Embedding-Pipeline) |
| `packages/vector-store/src/factory.ts` + `adapters/pgvector.ts` | `createVectorStore()` (pgvector Default / Qdrant Upgrade), `PgvectorAdapter` mit HNSW-Index (`m`/`efConstruction`), Payload-Filter | **1:1** — Spec D: „pgvector mit HNSW-Index" — exakter Match. Offener Detail-Punkt der Spec (IVFFlat vs. HNSW) ist durch BP bereits auf HNSW entschieden |
| `packages/vector-store/src/wrappers/tenant-scoped.ts` | `withTenantScope()` → `TenantScopedVectorStore`; `searchForTenant()` erzwingt `tenantId`-must-Filter; bare `searchVectors()` wirft | **1:1** — Spec D: „kein Cross-Tenant!" + M087 Permission-aware. Tenant-Isolation built-in |
| `prisma/migrations/20260517085146_04_enable_pgvector/migration.sql` | Aktiviert `pgvector`-Extension | **1:1** — Voraussetzung für TenantEmbedding-Tabelle |
| `packages/ai/src/prompts/index.ts` | `createAIPromptRegistry()` mit Versionierung, `render(selection, variables)`, `{{var}}`-Templates, `AIPromptRegistryError` | **1:1 / als-vorlage** — System-Prompt-Trennung (Spec G/18.5.2) + Use-Case-Prompts. Strikte System/User-Daten/Frage-Trennung als Template-Konvention |
| `packages/ai/src/tools/index.ts` | `createAIToolRegistry()` (scoped Tools für Vercel AI SDK `ToolSet`), `toToolSet(selection)`, Secret-Redaction | **1:1** — Tool-Calling-Architektur (Spec G: Tools nur per Tool-Call-Output, nicht aus Text geparst). Basis für Sprach→Automation-Tools (M086) |
| `packages/ai/src/streaming/index.ts` | `createAITextStream()` via `streamText`, Guard-Evaluator, Prompt-/Tool-Registry-Integration, `getAITextStreamDiagnostics()` mit Redaction | **1:1** — Streaming-Chat-UX für Programm-Hilfe-Chat (Use-Case 3). Guard-Evaluator-Seam = Action-Boundary-Andock-Punkt |
| `packages/ai/src/promptfoo-provider.ts` + `promptfoo.config.yaml` (Repo-Root) + `tests/promptfoo/injection-vectors.yaml` + `.gitea/workflows/promptfoo.yml` | Promptfoo-Eval-Harness mit OWASP-LLM-Vektoren (LLM01/LLM02/LLM06…), Mistral-als-Primary-Label, deterministischer Test-Provider, CI-Workflow | **1:1 / anpassen** — M085 quasi fertig: `description: EU-only OWASP LLM injection-vector eval suite`. Pro neuem HiveGrid-Prompt-Flow eigene Eval-Vektoren ergänzen (Spec G: „pro Prompt-Flow eigene Eval-Suite") |
| `packages/ai/test/evals/promptfoo-baseline.test.ts` + `prompt-registry-cases.yaml` | Eval-Baseline-Vitest + Prompt-Registry-Testfälle | **als-vorlage** — Muster für HiveGrid-Prompt-Flow-Evals |
| `prisma/schema/ai.prisma` → `Chat`, `ChatMessage`, `ChatMessagePart` | Chat-Datenmodell (Typ tenant/admin/marketing, Scope, anonym via `sessionId`, Multi-Part-Messages) | **als-vorlage / anpassen** — deckt Spec `Conversation`/`ConversationMessage` strukturell. `Chat.kind`/HiveGrid-`kind`-Enum (programm_help/automation_builder) ergänzen; `lastMessageAt`/`archivedAt` bereits vorhanden |
| `prisma/schema/ai.prisma` → `GeneratedObject` | Strukturierte AI-Outputs mit Token/Cost-Feldern, `status` pending/success/error, `model`, `chatId` | **1:1 / anpassen** — Spec `GeneratedObject` (JSON-Schema-validierte Outputs). `schema_version`-Feld ergänzen |
| `prisma/schema/ai.prisma` → `AICostEvent` | Append-only Cost-Log (`requestId @unique`, KEINE Prompt-Bodies, `capability`/`provider`/`model`/Tokens/Cents, `isOverage`, `markedUpCostCents`, `stripeReportedAt`) | **1:1** — Spec `AiTokenUsage` (Verbrauch pro Tenant/User/Use-Case). Overage-Felder + Stripe-Watermark für M088 bereits da |
| `prisma/schema/ai.prisma` → `TenantAICostSummary` | Vor-aggregierte Cost-Summary pro Periode (`@@unique([tenantId, periodStart])`, `breakdown` JSON) | **1:1** — Spec `AiCredits`-Aggregat + Usage-Dashboard O(1)-Top-Line (18.8.9) |
| `prisma/schema/accounts.prisma` → `Tenant` AI-Felder | `aiQuotaOverrideJson` (Json, geformt als `TenantAIQuotaOverride`), `aiCostLastAlertedThreshold`, `aiCostLastAlertedPeriod` | **1:1 / anpassen** — Spec `AiSettings.usage_limit_tokens_per_month` + idempotente 80%-Alert-State. HiveGrid-`AiSettings` braucht zusätzlich `enabled_use_cases[]`, `default_models`, `data_residency` (s. Lücken) |
| `prisma/schema/promptFlows.prisma` → `PromptFlow`, `PromptTemplate`, `PromptFlowExecution`, `PromptFlowOutput*`, `PromptFlowInputVariable` | LLM-Prompt-Pipelines an Entity/Property gekoppelt, `executionType` sequential/parallel, Streaming-Flag, Execution + Result-Log, Output→Row-Property-Mapping | **als-vorlage** — Phase-2-`PromptFlow`/`PromptTemplate` (visueller Prompt-Editor, Spec Block J) bereits strukturell vorhanden. Für Phase 1 optional als Use-Case-Prompt-Definitionen |
| `packages/queue/src/payloads.ts` → `AiJob` | Typisierte AI-Jobs: `ai.chat-completion` / `ai.embedding` / `ai.browser-task`; Queue `toolctrl.ai-chatbot` (`names.ts`) | **1:1 / anpassen** — async KI-Verarbeitung. **Whisper-Transkriptions-Job-Typ fehlt** → ergänzen (`ai.transcribe`) |
| `packages/queue/src/*` (producers/priority/connection/runtime) | `enqueueJob()`, Idempotenz (`buildIdempotentJoben`), Plan-basierte Prioritäten (`resolveJobPriorityForTenant`), BullMQ-Runtime-Factory | **1:1** — Whisper-Async-Pipeline (Spec E: Audio-Upload → Job-Queue → Whisper → Resultat) baut hierauf |
| `packages/storage/src/index.ts` + `signed/index.ts` + `scan/*` | Scaleway-S3-Adapter, Signed-Upload/Download-URLs, ClamAV + magic-bytes Virus-Scan, Quarantäne | **1:1** — Audio-Upload für Whisper-Transkription (Spec E) + DSGVO-EU-Storage |
| `packages/db/src/outbox.ts` + `prisma/schema/outbox.prisma` | `publishOutboxEvent(tx, args)` transaktional, `OutboxEvent`-Modell | **1:1** — alle KI-Side-Effects (Cost-Persist, RAG-Reindex, Notifications) transaktional |
| `prisma/schema/notifications.prisma` → `NotificationDelivery`, `UserNotificationPreference` | In-App + Email + Push Notification-Delivery | **1:1** — 80%-Budget-Email an Tenant-Admin (18.8.2), Notbremse-Warnung (18.8.7) — via `notify()` (M011), nicht direkt |
| `prisma/schema/permissions.prisma` → `Role`, `Permission`, `RolePermission`, `UserRole`, `Group` | RBAC-Fundament | **1:1** — die 6 `ai.*`-Permissions (Spec Block I) als `Permission`-Rows + Bundle-Mapping + Default-Rollen-Zuweisung |
| `prisma/schema/featureFlags.prisma` → `FeatureFlag` | Feature-Toggles (scope global/tenant/user) | **1:1** — DoD verlangt Feature-Flag-Abschaltbarkeit; `ai-quota-soft-degradation`-Flag wird von `assertCredit.ts` bereits erwartet |
| `prisma/schema/knowledgeBase.prisma` → `KnowledgeBaseArticle` etc. | Help-Center-Datenmodell | **als-vorlage** — RAG-Quelle für Programm-Hilfe-Chat (Use-Case 3) + `HelpArticleEmbedding` (M38-Kopplung) |
| `packages/search/src/*` (Meilisearch) + `schemas/kb-articles.ts` | Tenant-scoped Volltext-Index | **1:1** — Spec D Hybrid-Suche (Volltext + Embeddings) + Graceful-Degradation-Fallback bei Provider-Down (Spec D/H: „Standard-Volltext-Suche aktiv") |
---
## Vorhandene Daten-Modelle (konkret)
Alle in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`:
| Spec-Modell (M18 Block A) | Boilerplate-Entsprechung | Datei | Bewertung |
| --- | --- | --- | --- |
| `Conversation` | `Chat` (type tenant/admin/marketing, scope, sessionId, title, `lastMessageAt`, `archivedAt`, metadata) | `ai.prisma` | strukturell vorhanden — `kind`-Werte (programm_help/automation_builder/tenant_assistant) als HiveGrid-Anpassung |
| `ConversationMessage` | `ChatMessage` + `ChatMessagePart` (role, Multi-Part content, order) | `ai.prisma` | vorhanden — Token-Felder (`tokens_input/output`) pro Message ergänzen (aktuell auf `GeneratedObject`/`AICostEvent`-Ebene) |
| `GeneratedObject` | `GeneratedObject` (object/type/model, userPrompt/fullPrompt, generatedData JSON, Token+Cost, status, chatId) | `ai.prisma` | nahezu 1:1 — `schema_version` ergänzen |
| `AiTokenUsage` | `AICostEvent` (capability, provider, model, in/out/total Tokens, costCents, requestId @unique, isOverage, markedUpCostCents, stripeReportedAt) | `ai.prisma` | 1:1 — append-only, KEINE Prompt-Bodies (PII-konform) |
| `AiCredits` (Aggregat-Teil) | `TenantAICostSummary` (periodStart/End, totalCostCents, total Tokens, breakdown JSON) | `ai.prisma` | 1:1 für vor-aggregierte Sicht |
| `AiSettings` (Teil) | `Tenant.aiQuotaOverrideJson` + `aiCostLastAlertedThreshold/Period` | `accounts.prisma` | teilweise — Quota-Override + Alert-State da; restliche AiSettings-Felder fehlen |
| Plan-Defaults / Inklusiv-Kontingent | `standardAIQuotaDefaults` + `AIQuotaPlanDefaults` (Code, kein DB-Modell) | `packages/entitlements/src/ai-quotas.ts` | 1:1 (Code-Registry-Stil) |
| Overage-/Aufschlags-Tarif | `standardAIOverageDefaults` + `resolveTenantAIOverage` (Code) | `packages/entitlements/src/ai-overage.ts` | 1:1 (Code-Registry-Stil) |
| `PromptFlow`/`PromptTemplate` (Phase 2) | `PromptFlow`, `PromptTemplate`, `PromptFlowExecution`, `PromptFlowOutput(+Mapping)`, `PromptFlowInputVariable`, `PromptFlowGroup(+Template+Entity)` | `promptFlows.prisma` | 12 Modelle bereits vorhanden (visueller Prompt-Editor Phase 2) |
**Provider-Registry als Code, nicht als DB:** Die Spec modelliert `LlmGatewayProvider` / `LlmGatewayModel` als „Plattform-weit, Code-Registry". Die Boilerplate setzt das **genau so** um: `providerMetadata` + `aiModels`-Katalog in `packages/ai/src/index.ts` (mit `costPerMInputTokens`/`costPerMOutputTokens`/`maxOutputTokens` pro Modell). Es gibt **keine** Prisma-Modelle `LlmGatewayProvider`/`LlmGatewayModel` — das ist konform mit der Spec (Code-Registry) und kein Defizit.
---
## Lücken / komplett neu zu bauen
### Daten-Layer (in `prisma/schema/app.prisma` — leerer HiveGrid-Platzhalter)
1. **`AiSettings` (HiveGrid-spezifisch)** — kein Modell vorhanden. Benötigt: `enabled_use_cases[]`, `default_models` (Use-Case→Modell-Key JSON), `data_residency` (Default `eu`, Hard-Constraint). Teilweise durch `Tenant.aiQuotaOverrideJson` gedeckt; HiveGrid-Settings-Modell neu.
2. **`TenantEmbedding`-Tabelle** — als Prisma-Modell **nicht vorhanden**. Die Boilerplate legt Vektoren über pgvector in `vec_<namespace>_<collection>`-Tabellen per Raw-SQL an (kein Prisma-Modell). Spec verlangt zusätzlich Metadaten-Tracking (`entity_type`, `entity_id`, `text_source` für Re-Embed-Hash, `embedding_model_id`, `last_embedded_at`) — **Metadaten-Begleittabelle neu bauen** (die Vektoren selbst kommen aus dem vorhandenen pgvector-Adapter).
3. **`HelpArticleEmbedding`** (M38-Kopplung) — nicht vorhanden; analog zu TenantEmbedding über pgvector + Begleit-Metadaten.
4. **Conversation-`kind`-Enum** + **per-Message-Token-Felder** — kleine Schema-Erweiterung an `Chat`/`ChatMessage`.
5. **Whisper-Transkriptions-Modell** (Job-Status, Audio-Ref, Transkript, Sprache, Edit-State) — komplett neu.
### Whisper-EU-Transkription (Spec Block E) — KOMPLETT FEHLT
- **Kein** Transcription-Adapter, **keine** `transcribe()`-Methode in `packages/ai`, **kein** Audio/Whisper-Code (grep über `packages/ai/src` ergab 0 Treffer für transcri/whisper/audio/speech).
- Queue-Payload `AiJob` kennt nur `ai.chat-completion`/`ai.embedding`/`ai.browser-task` — **`ai.transcribe` fehlt**.
- Neu zu bauen: Whisper-auf-Scaleway-GPU-Adapter (DSGVO-konform), Async-Pipeline (Storage-Upload → Queue → Whisper → Resultat — Bausteine `storage` + `queue` vorhanden), Auto-Sprache-Erkennung (DE/EN), Mikrofon-Aufnahme-UI, Edit-Funktion, Klartext-Fehler.
### Use-Case-Orchestrierung (Spec Block C) — Gateway-Bausteine da, Use-Cases neu
- **Lead-Zusammenfassung / Pre-Termin-Brief** (Use-Case 2): Activity-Stream + Custom-Felder + Form-Submissions als Kontext sammeln, Cache-Hit-Logik (gleiche Datenbasis), Widget im Kontakt-Detail. Neu.
- **Programm-Hilfe-Chat** (Use-Case 3): RAG über Help-Articles + Conversation-Historie. Streaming-Chat-Baustein vorhanden, Orchestrierung + UI neu.
- **Klartext-Diagnostik-Fallback** (Use-Case 4): KI nur Fallback bei unbekannten Fehler-Codes, klar markiert. Neu.
- **Stuck-Deal-Vorschläge** (Use-Case 5): wöchentlicher Cron-Job, Widget. Neu (Cron via M-Infra).
- **Email-Klassifizierung** (Use-Case 7): `lead|customer_question|invoice|spam|newsletter|other`, Inbox-Auto-Verknüpfung (M9). Neu.
- **Sprach-zu-Automation (M086, USP-Hauptstütze)**: Sprach-Befehl → Wenn-Dann-Vorschlag im visuellen Editor, Rückfrage bei Mehrdeutigkeit, Existenz-Probe referenzierter Pipelines/Mitarbeiter/Templates, Risiko-Warnungen, „Vorher/Nachher"-Diff, NIE Auto-Aktivierung. Tool-Registry + Streaming + Prompt-Registry als Fundament; die gesamte Orchestrierung + Editor-Integration (M078) + Voice-Input (Web-Speech-API) neu.
### Sicherheits-Layer (Spec Block G) — teilweise
- **Input-Sanitization (Runtime)**: **FEHLT** als Modul. Die Boilerplate hat Promptfoo-**Test**-Vektoren (`tests/promptfoo/injection-vectors.yaml`, deterministischer Test-Provider), aber **keine produktive Sanitizer-Funktion**, die Marker (`---SYSTEM:`, `[INST]`, `<|im_start|>`) in externen Daten neutralisiert oder Eingabe-Länge cappt (grep ergab 0 Runtime-Treffer). Neu bauen.
- **System-Prompt-Trennung**: Prompt-Registry (`{{var}}`-Templates) ist da; strikte 3-Block-Trennung (System / User-Daten „nicht als Anweisung" / Frage) als Konvention/Template neu definieren.
- **Action-Boundary (M087)**: Bausteine vorhanden (`createReviewRequiredResult()`, Mutation-Audit-Events, Streaming-Guard-Evaluator) — die **Bestätigungs-Workflow-Verdrahtung im UI** + Tool-Output-only-Policy neu.
### Multi-Provider-Fallback (M084) — fast fertig, 2 Lücken
- Aktiver **1-Min-Health-Ping** + **5-Min-Sperre** (Spec 18.6.3): vorhandener `AIProviderHealthTracker` ist reaktiv (failure-on-call, 30-Min-Window). Aktives Pingen + 5-Min-Re-Check-Fenster ergänzen.
- **Deterministischer Response-Cache** (Spec H/18.7: Cache-Hit auch bei KI-Down): **nicht vorhanden** — es gibt `spend-cache` (Kosten), aber keinen Output-Cache für deterministische Prompts. Neu (Hash-basiert).
### Pay-as-you-go (M088) — Engine da, Verdrahtung + Notbremse neu
- Overage-Berechnung (`ai-overage.ts`) + Cost-Felder (`AICostEvent.markedUpCostCents`/`stripeReportedAt`) fertig.
- **Umstellung Hard-Deny → Overage**: `assertCredit.ts` wirft aktuell bei Limit (HTTP 402). [N13] verlangt **weiterlaufen + Aufschlag**. Pfad umbauen.
- **Notbremse** (18.8.7): absolute Euro-Obergrenze pro Tenant → Drosselungs-Modus (nur geschäftskritische KI). Neu (Threshold + Capability-Whitelist).
- **Stripe-Metered-Billing-Reporter** (18.8.6): `stripeReportedAt`-Watermark + nightly Cron vorgesehen; Stripe-Anbindung kommt aus M153/M156. Verdrahtung neu.
- **Upsell-Logik** (18.8.5): Vergleich Overage-Kosten vs. Plan-Differenz → Soft-Recommendation. Neu.
### UI — KOMPLETT NEU (Boilerplate liefert KEIN HiveGrid-UI)
- Programm-Hilfe-Chat-Oberfläche (Streaming)
- KI-Sprach-Assistent (M086, Sidebar-Gruppe „AUTOMATION & KI" → laut INTEGRATION-CONTRACTS §1)
- Usage-Dashboard `/{tenant}/admin/usage/ai` (Pie pro Use-Case, Verbrauch über Zeit, Top-User, Forecast, Tarif, Notbremse-Stand)
- AI-Settings-Seite (EU-DSGVO-Klartext, Modell-Defaults pro Use-Case, Token-Override) — Sidebar „EINSTELLUNGEN → Mandant"
- Voice-Aufnahme-Komponente (Mikrofon-Tap/Toggle, Edit, Klartext-Fehler)
- Lead-Zusammenfassungs-Widget, Stuck-Deal-Widget
- Action-Boundary-Bestätigungs-Dialog
- **DoD-Pflicht** (alle Milestones): View-Model-Loader-Trennung, keine Prisma-Imports in `packages/ui-hivegrid/**`, Subframe-Adapter, i18n-Keys, Smoke-Tests, Hot-Swap-Drill gegen `packages/ui-mock`, Visual-Regression — alles HiveGrid-seitig neu.
### Querschnitt-Wiring (HiveGrid-seitig)
- Sidebar-Registrierung in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` (M086 → Gruppe „AUTOMATION & KI"; Usage/Settings → „EINSTELLUNGEN")
- Routes unter Tenant-URL-Pattern, Breadcrumbs, Customer-Portal-Sichtbarkeit
- i18n-Keys für alle Klartext-Fehler (Spec 18.12 — 5 konkrete Beispiele) aus M002-Catalog
- Activity-Stream (`logActivity`) + Notifications (`notify()`) Anbindung
- Permission-Seed der 6 `ai.*`-Permissions + 3 Bundles + Default-Rollen-Zuweisung
- Feature-Flag pro KI-Modul
---
## Empfohlenes Vorgehen (Reihenfolge)
**Phase 0 — Voraussetzung (vor M083):** EU-Sterilisation der Boilerplate prüfen. Der aktive Provider-Pfad ist bereits EU-only (Mistral/Scaleway/Aleph Alpha); **Anthropic/OpenAI existieren nur als Embedding-Stub bzw. `@ai-sdk/openai-compatible`-Bibliothek** (`packages/ai/src/embeddings/providers/{openai,anthropic}.ts`). Sicherstellen, dass diese nicht aktiviert/registriert werden (DSGVO-Hard-Constraint, Spec 18.2.2).
**M083 (EU-Provider-Layer) — überwiegend ÜBERNEHMEN:**
1. `packages/ai` Provider-Registry + Mistral/Scaleway/Aleph-Alpha-Adapter 1:1 nutzen.
2. `executeLLMCall` als zentralen Gateway-Wrapper verdrahten (Boundary in apps/api injiziert Env-Snapshots + Cost-Publisher + Prisma-Adapter).
3. EU-DSGVO-Klartext in AiSettings (Spec 18.2.4) als i18n-Key.
**M084 (Fallback) — ÜBERNEHMEN + 2 Erweiterungen:**
1. `fallback.ts` (30-Min-Window, Mistral→Scaleway→Aleph-Alpha) 1:1.
2. NEU: aktiver 1-Min-Health-Ping + 5-Min-Sperre.
3. NEU: deterministischer Output-Cache (Hash-basiert, Cache-Hit bei KI-Down).
4. Graceful-Degradation-Klartexte + Editor-Fallback-Hinweis (UI).
**M085 (Promptfoo) — fast fertig ÜBERNEHMEN:**
1. `promptfoo.config.yaml` + `tests/promptfoo/injection-vectors.yaml` + `.gitea/workflows/promptfoo.yml` 1:1.
2. NEU: pro HiveGrid-Prompt-Flow eigene Eval-Vektoren ergänzen; CI-Gate verifizieren.
**M087 (RAG + Aktions-Grenze) — Bausteine ÜBERNEHMEN, Wiring neu:**
1. pgvector-Migration aktivieren; `vector-store` tenant-scoped + `embeddings`-Pipeline 1:1.
2. NEU: `TenantEmbedding`-Metadaten-Modell in `app.prisma` (Re-Embed-Hash, embedding_model_id, last_embedded_at).
3. NEU: Permission-Pre-Filter vor Vector-Search (Spec 18.3.3).
4. NEU: Embedding-Trigger bei Entity-Anlage/Update (Outbox `rag.index.*` vorhanden — Domain-Writes andocken).
5. Action-Boundary: `createReviewRequiredResult()` + Mutation-Audit + UI-Bestätigungs-Dialog.
6. NEU: Runtime-Input-Sanitizer (Marker-Neutralisierung + Längen-Cap).
**M086 (Sprach-zu-Automation, USP) — meiste Eigenleistung:**
1. Tool-Registry + Streaming + Prompt-Registry als Fundament.
2. NEU: Sprach→Wenn-Dann-Orchestrierung, Existenz-Probe, Rückfrage-Loop, Risiko-Warnung, Vorher/Nachher-Diff, Voice-Input (Web-Speech-API), Editor-Integration (M078). NIE Auto-Aktivierung.
3. Abhängig von M087 (Aktions-Grenze) + M075-M077 (Automation-Schema).
**M088 (Token-Limits + Pay-as-you-go) — Engine ÜBERNEHMEN, Policy umbauen:**
1. `ai-quotas.ts` + `ai-overage.ts` + `AICostEvent`/`TenantAICostSummary` 1:1 (HiveGrid-Plan-Werte einsetzen, sobald Pricing fixiert).
2. UMBAU: `assertCredit.ts` Hard-Deny → Pay-as-you-go-Overage-Pfad ([N13]).
3. NEU: Notbremse (absolute Euro-Obergrenze → Drosselung statt Pause), Upsell-Logik, 80%-Banner+Email, Usage-Dashboard.
4. Stripe-Metered-Reporter andocken (M153/M156).
**Whisper-EU (Spec Block E) — separater Neubau-Strang (parallel/danach):**
1. NEU: Whisper-auf-Scaleway-GPU-Adapter + `transcribe()`-Methode in `packages/ai`.
2. NEU: `ai.transcribe`-Queue-Job + Async-Pipeline (storage + queue vorhanden).
3. NEU: Voice-Aufnahme-UI + Edit + Klartext-Fehler.
**Querschnitt zum Schluss jedes Milestones (DoD):** Sidebar-Wiring, Routes/Breadcrumbs, Permission-Seed (6 `ai.*` + Bundles + Default-Rollen), i18n-Klartexte (18.12), Activity/Notifications, Feature-Flags, View-Model-Loader-Trennung, Smoke- + Hot-Swap- + Visual-Regression-Tests.
@@ -0,0 +1,142 @@
# Boilerplate-Abgleich: M19 — KWP-Integration
| Feld | Wert |
|---|---|
| **Modul-ID** | M19 |
| **Name** | KWP-Integration |
| **Milestones** | M089 (Sync-Programm), M090 (Heartbeat+Recovery), M091 (Warteschlange+Replay), M092 (Schema-Drift-Sentinel), M093 (Konflikt-Auflösung „KWP gewinnt"), M094 (Plantafel-Sync), M095 (Dokumenten-Archiv-Sync) |
| **Block** | Block 4 — USP-Säulen (Modul 19), sitzt auf M33a Apps-Engine (M008-M010) |
| **Reuse-Level** | **medium** |
> **Verifikations-Basis:** Eigene Greps/Reads direkt in `/Users/raffaelsheikh/Documents/GitHub/boilerplate` (nicht nur Inventur). Wo Inventur und Realität abweichen, ist das markiert.
---
## Kernbefund in einem Satz
Die Boilerplate hat **kein KWP-, MSSQL-, Heartbeat-, Schema-Drift- oder Windows-Sync-Code** (grep über `*.ts`/`*.prisma` leer außer einem Test-Match in `packages/cli`). Was sie liefert, ist ein **solides Querschnitt-Fundament**: ein generisches Data-Sync-Datenmodell (`appsEngine.prisma`), eine Public-API-Runtime mit Tenant-API-Key/Webhook-Auth (`apps/api`), eine Cron+Queue-Worker-Runtime mit Transactional-Outbox (`apps/worker`), gehashte API-Keys (`core.prisma`), Idempotenz, Storage/Signed-URLs und Notifications. Die **gesamte KWP-Fachlogik** (Queue-Semantik, Mapping-Registry, Drift-Diff, Konflikt-Default „KWP gewinnt", Projektnummern-Sequenz/Reservierung) ist **neu zu bauen**.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `prisma/schema/appsEngine.prisma` | `App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem` — generische Data-Sync-Engine mit Status/Mode/Metriken + Source→Destination-Mapping + Rollback-Daten | **anpassen** — deckt `KwpSettings` (als `App`/`DataConnection`), `KwpProjectQueue`/Sync-Lauf (als `AppSyncRun`) und `KwpRegistry` (als `AppSyncItem`) teilweise ab; KWP-spezifische Felder fehlen (s.u.) |
| `prisma/schema/core.prisma` → `ApiKey` + `ApiKeyLog` | Gehashte Keys (`keyHash`/`keyPrefix`/`keyLastFour`/`keyVersion`/`hashedAt`/`expires`/`active`), `@@unique([tenantId, alias])`, Request-Logging | **1:1** für `KwpApiKey` (Token-Auth des kwpsync.exe bzw. KWP-API). Rotation/Expiry-Felder vorhanden |
| `prisma/schema/core.prisma` → `IdempotencyKey` | `@@unique([scope, key])`, Response-Caching, `expiresAt` | **1:1** für die in der Spec geforderte `idempotency_key`-Doppel-Push-Erkennung (Block D) |
| `prisma/schema/core.prisma` → `FileObject` (+ `FileUploadProgress`/`FileChunk`) | Storage-Metadaten (provider/bucket/key/checksum/byteSize/metadata Json) | **anpassen** — Basis für `KwpDocument`/Dokumenten-Archiv-Sync (M095); KWP-Herkunfts-Felder (`direction`, `kwp_path`, gelöscht-Badge) ergänzen |
| `apps/api` (`@toolctrl/api`, Hono) + `apps/api/src/route-access-inventory.ts` | Public-API-Runtime mit Auth-Klassen `tenant-api-key`, `service-token`, `webhook-signature`, `optional-api-key-validation`; `apps/api/src/auth.ts` (`extractApiKeyFromRequest`, `extractBearerToken`, `createServicePrincipal`); Error-Envelope + Request-ID | **als-vorlage / anpassen** — exakt die Ingress-Schicht für `/api/v1/integrations/kwp/queue/{token}/...` (Pull/Push/Heartbeat). Neue Routes + Auth-Klasse für den kwpsync-Token andocken |
| `apps/api/src/webhooks/mailersend-signature.ts`, `billing/stripe-webhook-route.ts`, `email-sequences/provider-webhook-route.ts` | Bestehende inbound-Webhook-Handler mit Signatur-Verifikation | **als-vorlage** — Muster für `handleWebhook` (kwpsync.exe pusht Delta + Heartbeat) bzw. KWP-API-Webhook |
| `apps/worker` (`@toolctrl/worker`) + `apps/worker/src/jobs/*-cron.ts` | Cron+Queue-Runtime, single-instance Cron-Scheduler (Bsp. `dsgvo-grace-cron.ts` 24h, `plugin-audit-retention` nächtlich), DI-`HandlerDependencies`, Health/Admin-Server | **als-vorlage** — für Daily-Schema-Drift-Check (M092), Heartbeat-Watchdog (M090), Rate-Limited-Replay-Worker (M091). Neuen Queue-Namen + Handler registrieren |
| `apps/worker/src/outbox/{dispatcher,event-router}.ts` + `prisma/schema/outbox.prisma` | Transactional-Outbox: Event in DB-Tx committen, Worker fan-out per Prefix-Routing (longest-prefix-wins), Exponential-Backoff-Retry, DLQ | **anpassen** — Push aus HiveGrid → KWP-Queue als Outbox-Event (`kwp.push.*` Prefix-Routing ergänzen). Garantiert „nichts geht verloren" (Spec 19.3.3) ohne eigene Queue-Mechanik |
| `packages/queue` (`@toolctrl/queue`) | BullMQ-Wrapper: `enqueueJob`, `buildIdempotentJobId()`, plan-basierte Prioritäten (`resolveJobPriorityForTenant`), Idempotenz | **1:1 / anpassen** — Cloud-Queue-Verarbeitung + Idempotenz. Neuer Queue-Name `toolctrl.kwp-sync` o.ä. hinzufügen (Queue-Namen sind hartcodiert in `packages/queue/src/names.ts`) |
| `packages/foundation/src/adapters.ts` | `defineAdapter()`/`getAdapter()` + `AdapterStatus = not_configured\|configured\|healthy\|degraded\|unavailable` | **anpassen** — die 5 Status-Werte mappen direkt auf die M33a-/Spec-Status (`wartet auf Setup`/`aktiv`/`eingeschränkt`/`defekt`/`getrennt`). **ACHTUNG:** das ist nur eine 18-Zeilen-Status-Registry, **kein** Sync-Connector-Framework — siehe „Lücken" |
| `packages/storage` (`@toolctrl/storage`) | S3/Scaleway-Adapter, Signed-Upload/Download-URLs, Virus-Scan (ClamAV + magic-bytes), Quarantäne | **1:1** für Dokumenten-Archiv-Sync (M095) — KWP-Dokumente in Scaleway-S3 ablegen, Signed-URLs zum Lesen/Download |
| `packages/marketing-outbound/src/types.ts` → `adapterFetch()` + SSRF-Guard | Timeout + SSRF-Allowlist (nur HTTPS, kein RFC1918/metadata) für ausgehende HTTP-Calls | **als-vorlage** — falls die KWP-API direkt von HiveGrid Cloud aufgerufen wird (API-Variante, s.u.), als sicherer HTTP-Egress |
| `packages/realtime` (`@toolctrl/realtime`) | SSE-Event-Serialisierung (`notification.snapshot/update`) | **1:1** für sichtbaren Re-Sync-Fortschritt („847/1.230 abgearbeitet") und Live-Status-Indicator |
| `packages/db/generated/prisma/client/models/{App,DataConnection,AppSyncRun,AppSyncRunLog,AppSyncItem}.ts` | Generierter Prisma-Client für die Apps-Engine-Modelle (bereits vorhanden) | **1:1** — Modelle sind generiert und tenant-relation-verdrahtet; sofort nutzbar |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
Direkt für M19 relevante, **bereits existierende** Prisma-Modelle:
- `prisma/schema/appsEngine.prisma`
- `App` (`tenantId`, `type`-Identifier, `config` JSON-String, `isActive`, `installedByUserId`, `@@unique([tenantId, type])`) → Basis für „KWP-App ist installiert" + `KwpSettings.enabled_features`
- `DataConnection` (`tenantId`, `name` (sprechender Name „Geerkens Hauptbüro KWP"), `type` z.B. "rest-api"/"webhook", `config` = **verschlüsselte** JSON-Credentials) → Basis für `KwpSettings`/`data_connection_id`
- `AppSyncRun` (`status` pending/running/completed/partial/failed/cancelled/**reverted**, `executionMode` manual/scheduled/webhook, `itemsProcessed/Created/Updated/Failed`, `startedAt/completedAt`, `error`, `metadata` JSON) → Basis für Sync-Lauf-Historie + M33a-Sync-Verlauf
- `AppSyncRunLog` (`type` info/success/warning/error, `message`, `metadata`) → Klartext-Log pro Sync-Vorgang (M33a.4.2)
- `AppSyncItem` (`objectType`, `sourceId`, `destinationId`, `action`, `sourceData`/`destinationData` JSON, `status`, **`rolledBackData`/`rolledBackAt`**, `@@index([objectType, sourceId])`) → Basis für `KwpRegistry`-Mapping + Konflikt-Restore (M093)
- `prisma/schema/core.prisma`
- `ApiKey` + `ApiKeyLog` → `KwpApiKey` (gehasht, Rotation, Request-Logging)
- `IdempotencyKey` → Idempotenz-Schlüssel der Push-Operationen
- `FileObject` / `FileUploadProgress` / `FileChunk` → KwpDocument-Storage-Metadaten
- `Credential` (globaler Secret-Store, `value Json`) → falls Cloud-seitige Secrets nötig
- `prisma/schema/featureFlags.prisma` → `FeatureFlag`/`FeatureFlagFilter`/`FeatureFlagAudit` (DoD-Pflicht „Modul ohne Deploy abschaltbar")
- `prisma/schema/permissions.prisma` → `Role`/`Permission`/`RolePermission`/`UserRole` (generisches RBAC; die 6 `kwp.*`-Permissions sind hier zu **registrieren**, nicht neu zu erfinden)
- `prisma/schema/notifications.prisma` → `NotificationDelivery`/`UserNotificationPreference`/`DeviceToken` (Push/Email-Alerts bei broken/drift)
- `prisma/schema/outbox.prisma` → `OutboxEvent` (Transactional-Push-Queue)
- `prisma/schema/workflows.prisma` → `Workflow`/`WorkflowExecution`/… (M17-Engine; KWP-Trigger `kwp.auftrag_pulled`/`kwp.push_auftrag` und KWP-Actions docken hier an, **nicht** in M19 selbst neu bauen)
---
## Lücken / komplett neu zu bauen
**Es existiert KEIN KWP-spezifischer Code.** grep über das gesamte Boilerplate-Repo (`kwp|KWP|mssql|MSSQL|heartbeat|schema.drift|nssm|sqlserver`) liefert nur einen einzigen Treffer in `packages/cli/.../doctor.test.ts` (unbezogen). Konkret fehlt:
1. **Der komplette `kwpsync.exe` Windows-Client** (Block F). Nichts davon ist in der Boilerplate — kein .NET-/Go-Projekt, kein MSSQL-Treiber, kein NSSM-Wrapper, keine lokale SQLite-Queue, kein DPAPI-Verschlüsselungs-Code, kein Auto-Update. Das ist ein **eigenständiges externes Programm außerhalb des Monorepos** (M089). **(Bei API-Variante entfällt das ganz — siehe KWP-API-Auswirkung.)**
2. **`defineAdapter` ist NICHT das Apps-Engine-Connector-Pattern.** Die AGENTS.md-Tabelle (Zeile 134: „Apps-Engine-Adapter | M008-M010 | `defineAdapter()`") suggeriert ein fertiges Adapter-Framework. Realität: `packages/foundation/src/adapters.ts` ist eine **18-Zeilen-Status-Registry** (`Map` mit key→status). Das in Spec Block I geforderte Adapter-Interface (`connect`/`disconnect`/`validateConnection`/`syncPull`/`syncPush`/`handleWebhook` + Capabilities) **existiert nicht** und muss als Teil von M008-M010 (M33a) erst gebaut werden. M19 setzt darauf auf — ist also von M008-M010 blockiert (Vorbedingung steht korrekt in M089).
3. **KWP-spezifische Datenmodelle**, die `appsEngine.prisma` nicht abdeckt:
- `KwpProjectNumberSequence` (pro Jahr fortlaufende Auftragsnummern „2026-0042") — **komplett neu**, kein Sequenz-Modell vorhanden
- `KwpProjectNumberReservation` (24h-Reservierung mit `expires_at`/`consumed_at`) — **komplett neu**
- `KwpSchemaSnapshot` (`schema_hash`, `tables_metadata` JSON für Diff) — **komplett neu**
- `KwpErrorReport` (`error_kind` schema_drift/connection_lost/sync_conflict/…, `severity`, `summary_text`/`tech_details`, `acknowledged_by`) — teilweise via `AppSyncRunLog`, aber Tenant-Acknowledge-Flow + Severity-Filter fehlen → **neu**
- `KwpSettings`-Spezialfelder: `kwpsync_token`, `kwpsync_version`, `kwpsync_last_heartbeat_at`, `polling_interval_seconds` — als `App.config`/`DataConnection.config`-JSON ablegbar oder eigene Felder → **anpassen/ergänzen**
4. **Sync-Mechanik-Fachlogik** (Block D/G/H): Cursor-basierter Delta-Pull, Rate-Limited-Replay (max 100 Ops/Min mit Fortschritt), **Konflikt-Default „KWP gewinnt immer"** (M093, [N14]) inkl. HiveGrid-eigene-Felder-Schutz, Optimistic-Locking mit `version_hash`, Last-Known-Stand-Restore. `AppSyncRun`/`AppSyncItem` liefern die **Tabellen-Hülle**, aber keinerlei dieser Algorithmen.
5. **Schema-Drift-Sentinel-Fachlogik** (M092): Snapshot-Algorithmus, Hash-Vergleich, Auto-Pause, Plattform-Admin-zuerst-Benachrichtigung, Diff-Tool im Plattform-Admin (M37). Cron-Hülle vorhanden, Logik neu.
6. **Setup-Wizard-UI + alle Routes/View-Models/i18n** (Block C). Keine `app/routes`-Integration, keine `ui-hivegrid`-Components, kein Sidebar-Eintrag „KWP-Verbindung" (in INTEGRATION-CONTRACTS §1 unter Einstellungen vorgesehen → M089-M095, aber Code existiert nicht). Subframe-Design + Adapter-Layer komplett neu.
7. **Permissions registrieren:** `kwp.read/connect/manage/manual_sync/disconnect/admin` sind in `packages/entitlements/src/permissions.ts` (HiveGrid-Seite) noch zu definieren; RBAC-Tabellen existieren generisch.
8. **Plantafel-Sync (M094)** und **Dokumenten-Archiv-Sync (M095)** Fachlogik — Storage-/File-Modelle vorhanden, Spiegelungs-Logik (Kunden-Archiv vs. Projekt-Archiv, einseitig, „in KWP gelöscht"-Badge) neu.
---
## Empfohlenes Vorgehen (Reihenfolge)
**Phase 0 — Voraussetzung außerhalb M19:** Erst M008-M010 (M33a Apps-Engine) bauen — das echte Adapter-Interface (`connect/syncPull/syncPush/handleWebhook` + Status + Setup-Wizard-Shell). M19 ist davon hart abhängig; `defineAdapter` reicht NICHT.
1. **Daten-Layer aus Boilerplate übernehmen + KWP-Felder ergänzen:** `appsEngine.prisma`-Modelle als Basis nehmen (`App`/`DataConnection`/`AppSyncRun`/`AppSyncItem`), `ApiKey`+`IdempotencyKey` 1:1 nutzen. Neue Modelle in eigenem `prisma/schema/kwp.prisma`: `KwpProjectNumberSequence`, `KwpProjectNumberReservation`, `KwpSchemaSnapshot`, `KwpErrorReport`. Forward-Only-Migration mit Inverse-SQL-Kommentar (DoD F.28).
2. **Querschnitt 1:1 verdrahten, nicht nachbauen:** Token-Auth via `ApiKey`, Idempotenz via `IdempotencyKey`+`buildIdempotentJobId`, Push-Queue via Outbox (`kwp.push.*`-Prefix in `event-router.ts`), Notifications via `notify()` (M011), Storage via `@toolctrl/storage`, Status-5-Werte via `foundation/adapters` `AdapterStatus`, Feature-Flag via `FeatureFlag`.
3. **Ingress-Schicht aus `apps/api` ableiten:** KWP-Queue/Heartbeat/Pull-Endpoints unter `/api/v1/integrations/kwp/...`, Auth-Klasse `tenant-api-key` aus `route-access-inventory.ts`, Webhook-Handler nach Vorbild `mailersend-signature.ts`.
4. **Worker-Crons aus `apps/worker` ableiten:** Heartbeat-Watchdog (M090), Daily-Schema-Drift (M092), Rate-Limited-Replay (M091) — nach Muster `dsgvo-grace-cron.ts`; neuen Queue-Namen in `packages/queue/src/names.ts` registrieren.
5. **KWP-Fachlogik neu bauen (Kern-Aufwand):** Mapping-Registry-Semantik auf `AppSyncItem`, Konflikt-Default „KWP gewinnt" (M093) inkl. Restore aus `rolledBackData`, Projektnummern-Sequenz/Reservierung, Schema-Snapshot-Diff. Reine Domain-Funktionen in `app/domain/**` (AGENTS.md Layer-Trennung).
6. **UI komplett neu (Subframe → Adapter → Routes):** Setup-Wizard, Status-Dashboard, Sync-Verlauf, Fehlerbericht-Liste, Konflikt-Diff-View. Sidebar-Eintrag „KWP-Verbindung" (Einstellungen-Gruppe), Breadcrumbs, i18n, Smoke-Tests, Visual-Regression — alle DoD-Punkte.
7. **kwpsync.exe als separates Repo/Artefakt** (M089) — nur in der **Sync-Programm-Variante** nötig (siehe nächster Abschnitt).
---
## KWP-API-Auswirkung (Pflicht-Abschnitt — Stand 2026-06-04, NEU/WICHTIG)
> **Kontext:** KWP bekommt jetzt eine **offizielle API-Anbindung**. Der bisherige Plan (lokaler Windows-Service + NSSM + persistente lokale SQLite-Queue + Schema-Drift-Sentinel, M089-M095) war für **KWP OHNE API** konzipiert. Die Spec selbst hat das antizipiert (19.11.2: „Native KWP-API … würde die Architektur stark vereinfachen"). Mit API wird aus der Drei-Komponenten-Architektur eine **Zwei-Komponenten-Architektur** (HiveGrid Cloud ↔ KWP-API direkt), der „Kunde-vor-Ort"-Mittelteil entfällt.
### (1) Was die API-Anbindung am Architektur-Plan ändert
- **`kwpsync.exe` entfällt als Mittelschicht.** Statt Cloud → kwpsync.exe (Poll) → MSSQL wird es Cloud → HTTPS → KWP-API. Damit fallen weg: MSSQL-Direktzugriff, DPAPI-Config, lokale SQLite-Queue, NSSM-Service-Wrapper, Auto-Update-Mechanismus, Code-Signing.
- **Aus „windows_client" wird ein Standard-Connection-Type.** Die Spec Block I nennt `connection_type = windows_client` als KWP-Spezialfall. Mit API wird daraus ein **regulärer `rest-api`/`oauth2`-Adapter** — exakt das, was `DataConnection.type` in `appsEngine.prisma` ohnehin als Werte vorsieht ("rest-api", "oauth2", "webhook"). **Damit passt KWP plötzlich nahtlos ins generische Apps-Engine-Muster** statt Sonder-Pattern zu sein.
- **Egress statt Ingress:** Statt dass kwpsync.exe in die Cloud pollt, ruft die HiveGrid Cloud die KWP-API aktiv auf. Dafür ist `packages/marketing-outbound/src/types.ts` → `adapterFetch()` (HTTPS-only, SSRF-Allowlist, Timeout) die passende sichere HTTP-Egress-Vorlage. Pull wird zu einem normalen Cron-Job (`apps/worker`), Push zu einem Outbox-getriebenen API-Call.
- **DSGVO-Punkt verschiebt sich:** Spec 19.1.2 („KWP-Zugangsdaten liegen lokal beim Kunden, niemals in der Cloud") gilt bei API-Anbindung nicht mehr 1:1 — API-Credentials liegen verschlüsselt in `DataConnection.config` in der Cloud. Das ist DSGVO-konform (Plattform-Schlüssel, Pflichtenheft 33a.9.3), **erfordert aber eine bewusste Neubewertung** des „rohe DB-Zugriffe verlassen das Netzwerk nie"-Versprechens.
### (2) Welche Milestones M089-M095 sich vereinfachen oder entfallen
| Milestone | Mit Windows-Sync (alt) | Mit KWP-API (neu) | Verdict |
|---|---|---|---|
| **M089** kwpsync.exe (Win-Service+NSSM) | komplettes externes .NET/Go-Programm | — | **ENTFÄLLT** (ggf. Restaufwand: API-Connector-Adapter statt Programm, aber innerhalb des Monorepos) |
| **M090** Heartbeat + Auto-Recovery | 5-Min-Heartbeat des Clients, 15-Min-Watchdog, Auto-Restart | API-`validateConnection` = einfacher Health-Probe-Call (Spec/M33a haben das ohnehin: aktiv=5min/eingeschränkt=1min/defekt=15min) | **STARK VEREINFACHT** → reduziert auf den generischen M33a-Connection-Health-Check; kein Client-Recovery mehr |
| **M091** Persistente Warteschlange + Replay | **zwei** Queues (Cloud + lokale SQLite), Rate-Limited-Replay nach Outage | nur **eine** Cloud-Queue (Outbox) + Rate-Limit-Replay bei KWP-API-Down | **VEREINFACHT** → lokale SQLite-Queue entfällt komplett; Outbox + `packages/queue` decken den Rest ab |
| **M092** Schema-Drift-Sentinel | Daily-MSSQL-Schema-Snapshot via Client | **abhängig von API-Vertrag:** versionierte API → Drift wird zu **API-Versions-/Contract-Check** (deutlich leichter); KWP ändert MSSQL nicht mehr „still" hinter dem Rücken | **VEREINFACHT bis ggf. ENTFÄLLT** (bei stabiler versionierter API). Bleibt sinnvoll als API-Response-Schema-Validierung (zod) |
| **M093** Konflikt „KWP gewinnt" | unverändert relevant (fachliche Regel, nicht transport-abhängig) | unverändert relevant | **BLEIBT** (Domain-Logik, transport-unabhängig) |
| **M094** Plantafel-Sync | bidirektional via kwpsync.exe (Poll/Write MSSQL) | bidirektional via API (Webhook KWP→HiveGrid + API-Call HiveGrid→KWP) | **VEREINFACHT** falls KWP-API Webhooks bietet (Spec 19.11.3 „Trigger-basierter Pull" wird dann Phase-1-fähig statt Phase-2) |
| **M095** Dokumenten-Archiv-Sync | KWP→HiveGrid via Client-Filezugriff | KWP→HiveGrid via API-Dokumenten-Endpoint | **VEREINFACHT** (kein lokaler Dateizugriff; `@toolctrl/storage` + API-Download) |
**Netto:** M089 entfällt, M090/M091/M092/M094/M095 vereinfachen sich teils erheblich, nur M093 (Konflikt-Regel) bleibt unverändert. Der Gesamt-Aufwand des Moduls **sinkt deutlich**, und das Reuse-Level steigt faktisch von medium Richtung **medium-high**, weil die generische `appsEngine.prisma` + `apps/api`/`apps/worker`-Fundamente bei einer API-Anbindung **direkter** passen als beim Windows-Client-Sonderweg.
### (3) Wie Apps-Engine/Adapter (defineAdapter, plugins, plugin-sdk) statt des Sync-Programms genutzt werden
- **`appsEngine.prisma` ist der richtige Boden — nicht das WASM-Plugin-System.** Wichtige Abgrenzung (auch in der Inventur betont, `integrations-ki.md` §4 + „Wichtige Abgrenzungen"): Es gibt **zwei** „Apps-Engine"-Bedeutungen.
- **`packages/plugin-runtime` + `packages/plugin-sdk`** = WASM-Sandbox für **Drittanbieter-Plugins** (Capability-Consent, Ed25519-Signatur, Hook-Lifecycle). In Prod sind **nur `log`+`audit`-Capabilities** freigeschaltet (`PLUGIN_AVAILABLE_HOST_CAPABILITY_NAMES`), `http.outbound`/`db`/`storage` sind `productionAvailable: false`. → **Für KWP NICHT geeignet**: KWP ist ein First-Party-Connector mit DB/HTTP-Bedarf, kein sandboxed Drittanbieter-WASM-Modul. Marketplace ist ohnehin Phase 2 (33a.9.1).
- **`prisma/schema/appsEngine.prisma`** (`App`/`DataConnection`/`AppSyncRun`/`AppSyncItem`) = die **First-Party-Data-Sync-Engine**. → **Das ist das richtige Fundament für eine KWP-API-App.**
- **`defineAdapter()` (`packages/foundation/src/adapters.ts`)** liefert nur Status-Registry (`not_configured/configured/healthy/degraded/unavailable`). Diese 5 Werte mappen sauber auf die M33a-Connection-Status. Die eigentliche Connector-Verhaltens-Schnittstelle (`connect/syncPull/syncPush/validateConnection/handleWebhook` aus Spec Block I) muss in M008-M010 (M33a) gebaut werden; KWP-API ist dann der **erste konkrete Adapter** dahinter — als `rest-api`/`oauth2`-`DataConnection`.
- **Empfohlene Mapping-Strategie bei API-Variante:**
- KWP-App = `App{ type: "kwp" }`, eine `DataConnection{ type: "rest-api" | "oauth2", config: <verschlüsselte API-Creds> }` pro KWP-Server.
- Token/Key der KWP-API = `ApiKey` (gehasht) bzw. OAuth-Token in `DataConnection.config`.
- Pull-Lauf = `AppSyncRun{ executionMode: "scheduled" }`; Push-Lauf = `AppSyncRun{ executionMode: "webhook"/"manual" }`; Mapping HiveGrid↔KWP-Entität = `AppSyncItem` (`sourceId`/`destinationId`); Konflikt-Restore = `AppSyncItem.rolledBackData`.
- Connector-Adapter als reines Domain-Modul in `app/domain/integrations/kwp/**`, das `adapterFetch()` für Egress nutzt; Cron/Outbox aus `apps/worker`.
- **Konsequenz für die Planung:** Vor Umsetzung von M19 muss der Plan M089-M095 gegen die API neu geschnitten werden (M089 streichen, M090-M092/M094-M095 als API-Adapter-Tasks neu fassen, M093 unverändert lassen). Die Boilerplate **unterstützt die API-Variante besser** als die ursprünglich geplante Windows-Client-Variante.
---
## Reuse-Level-Begründung: **medium**
- **Pro (warum nicht low):** Komplettes Querschnitt-Fundament vorhanden — Data-Sync-Datenmodell (`appsEngine.prisma`), gehashte Token (`ApiKey`), Idempotenz, Outbox-Push-Queue, Cron+Queue-Worker, Public-API-Auth-Schicht, Storage, Notifications, Status-Registry, Feature-Flags, RBAC. Bei API-Anbindung passt das Apps-Engine-Datenmodell sogar direkt (`type: "rest-api"`).
- **Contra (warum nicht high):** Null KWP-/ERP-Fachlogik in der Boilerplate. Das in AGENTS.md angedeutete Adapter-Framework (`defineAdapter`) ist nur eine Status-Map; das echte Connector-Interface (M33a Block I) existiert nicht. Alle KWP-spezifischen Modelle (Projektnummern-Sequenz/Reservierung, Schema-Snapshot, Error-Report), die gesamte Sync-/Konflikt-/Drift-Algorithmik und die komplette UI sind neu. In der **Windows-Sync-Variante** kommt ein komplettes externes Programm dazu (low-touch zur Boilerplate). In der **API-Variante** entfällt das und das Modell rutscht praktisch Richtung medium-high.
@@ -0,0 +1,119 @@
# Boilerplate-Abgleich — M20 Dashboard / Reporting
| Feld | Wert |
|---|---|
| **Modul-ID** | M20 |
| **Name** | Dashboard / Reporting (Geschäftsführer-Dashboard) |
| **Milestones** | M147 (Layout + Widget-Engine), M148 (7 Rollen-Standard-Layouts), M149 (Drill-Down + Custom-Widget-Konfig), M150 (Export + Report-Versand) |
| **Pflichtenheft-Block** | Block 6 · Modul 20 (`_bmad-output/pflichtenheft/blocks/block6.md` §§20.1–20.10) |
| **Detail-Spec** | `_bmad-output/specs/M20-dashboard.md` |
| **Reuse-Level** | **medium** |
---
## Kurz-Verdict
Die Boilerplate liefert ein solides **Fundament für ein fixes Stat-Dashboard**, aber **keine konfigurierbare Widget-/Layout-Engine**, wie M20 sie verlangt. Konkret vorhanden und stark nutzbar:
- Ein funktionierendes Tenant-Dashboard-Route-Pattern mit Permission-Gate, Period-Filter, Server-Timing-Metrics und i18n.
- Ein **Dashboard-Modul-Registry-Pattern** (`dashboard-registry`) mit Feature-Flag-/Plan-Gating — ideale Vorlage für den permissions-aware Widget-Katalog (Block C/F).
- Ein **Stat-/Drill-Down-Pattern** (`Stat`-DTO mit `path`/`entity.slug`, `DashboardStats`-Komponente mit `<Link>`) — direkte Vorlage für die Kern-Anforderung Drill-Down (§20.4).
- Eine vollständige **Export-Infrastruktur** (`packages/exports`: PDF via `@react-pdf/renderer`, CSV, XLSX, S3-Sinks) für M150.
- Ein **Realtime-/SSE-Fundament** (`packages/realtime`) mit Reconnect/Backoff für Live-Updates (§20.7).
- Recharts + Chart-UI-Primitive für Visualisierungen.
Was komplett fehlt: das **Datenmodell** (`Dashboard`, `DashboardLayout`, `WidgetInstance`), die **Drag-&-Drop-Grid-Engine** (kein `react-grid-layout`; nur `@dnd-kit` als Baustein vorhanden), die **Per-Widget-Caching-/Aggregat-Schicht**, der **konkrete Widget-Katalog** (Sales/Projekt/HR/… aus den Modulen) und die HiveGrid-**Layer-Split-Packages** (`ui-hivegrid`, gefüllte `view-models`, Subframe-Adapter).
> **Wichtiger Hinweis zur „packages/metrics + Widget-Engine"-Annahme aus dem Auftrag:** `packages/metrics` ist **Prometheus/prom-client** (Ops-Observability: Histogramme, AI-Cost-Counter, Metrics-Token-Auth) — **NICHT** eine Business-KPI- oder Widget-Engine. Das `Widget`-Modell in `core.prisma` ist ein generisches Legacy-UI-Widget (`name`/`appearance`/`metadata` als Strings) und deckt das M20-Widget-Modell **nicht** ab.
---
## Übernehmbar aus Boilerplate
> Pfade beziehen sich auf `/Users/raffaelsheikh/Documents/GitHub/boilerplate`. Der Boilerplate-Code lebt in der Legacy-ToolCTRL-Struktur (`apps/web/app/...`, `~/utils/...`), **nicht** im HiveGrid-Ziel-Layer-Split. Übernahme bedeutet daher fast überall „als-vorlage" + Umbau in `app/routes` + `packages/view-models` + `packages/ui-hivegrid`.
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `apps/web/app/routes/app.$tenant/dashboard.tsx` | Tenant-Dashboard-Route: Permission-Gate (`app.dashboard.view`), Period-Filter (`PeriodHelper`), Server-Timing-Metrics, i18n-Title, Empty/Permission-Handling, Modul-Gating-Verdrahtung | **als-vorlage** (Loader-/VM-Struktur übernehmen, in HiveGrid-Layer-Split überführen) |
| `apps/web/app/services/dashboard-registry/registry.server.ts` | **Dashboard-Modul-Registry** als Code-Konstante (`id`, `titleKey`, `descriptionKey`, `featureFlagKey`, `planFeatureKey`), `Object.freeze`-Pattern | **als-vorlage** — direkte Blaupause für `WidgetCatalog` (Code-Registry, Block A/C) |
| `apps/web/app/services/dashboard-registry/gating.server.ts` | Gating-Engine: pro Modul Feature-Flag + Plan-/Entitlement-Check → `allowed` + Diagnostic + Redirect | **als-vorlage** — Vorlage für permissions-aware Widget-Sichtbarkeit (§20.8 / Block F); Permission-Check ersetzt/ergänzt Plan-Check |
| `apps/web/app/application/dtos/stats/Stat.ts` (+ `StatChange.ts`) | `Stat`-DTO mit `name`, `stat`, `previousStat`, `change`, `changeType`, **`path`**, `entity.slug` | **anpassen** — `path`/`entity.slug` sind die Drill-Down-Hooks (§20.4); ins VM überführen |
| `apps/web/app/components/ui/stats/DashboardStats.tsx` | Stat-Grid-Komponente, rendert klickbare Stat-Kacheln via `<Link to={item.path}>` | **als-vorlage** — Drill-Down-Verlinkung; nach `packages/ui-hivegrid` portieren (VM-only, kein Loader-Zugriff) |
| `apps/web/app/utils/services/appDashboardService.ts` | Aggregat-Service: zählt Entity-Rows seit `gte`, berechnet `change%`/`changeType`, baut `Stat[]` | **als-vorlage** — Muster für serverseitige Aggregat-Berechnung (§20.7.1); pro HiveGrid-Widget eigener Builder |
| `apps/web/app/utils/app/DashboardUtils.ts` (`getStatChangePercentage`, `getStatChangeType`) | Reine Change-Berechnungs-Helfer | **1:1** (pure functions, in VM-Builder nutzbar) |
| `packages/exports/src/pdf/` + `packages/exports/src/index.ts` | PDF-Export (`@react-pdf/renderer`), Streaming, S3-Multipart-Sink; `audit-pdf.ts` als konkretes PDF-Beispiel | **anpassen** — Dashboard-PDF-Snapshot (M150) |
| `packages/exports/src/csv/` + `packages/exports/src/xlsx/` | CSV-/XLSX-Export mit Streaming + Spreadsheet-Safety (`spreadsheet-safety.ts`), `format-caps.ts`, `limits.ts` | **anpassen** — Widget-CSV-Export (M150); ersetzt `json2csv` (nicht in BP) |
| `packages/realtime/src/server.ts` (`serializeRealtimeEvent`, SSE) | SSE-Event-Serialisierung (id/event/data + retry), Sanitizing | **anpassen** — Live-Update-Push-Kanal (§20.7.3); neue Event-Typen für Dashboard nötig |
| `packages/realtime/src/client.ts` | SSE-Subscriber mit Reconnect/Backoff, Status-Diagnostics, EventSource-Abstraktion | **anpassen** — Client-Side Live-Update-Empfang inkl. „Live-Updates pausieren"-Opt-Out (§20.7.4) |
| `packages/realtime/src/events.ts` | Event-Schema-Registry (`REALTIME_EVENT_NAMES`) + Validierung | **anpassen** — derzeit nur Notification-Events; Dashboard-Events (`deal.won`, `lead.received`, `defect.accepted`) ergänzen |
| `apps/web/app/components/ui/chart.tsx` | shadcn-Chart-Wrapper um **Recharts** (ChartConfig, Theming, Locale-Formatters) | **1:1 / anpassen** — Visualisierungs-Primitive für Trend-/Conversion-Widgets (§20.1) |
| `apps/web/app/components/analytics/AnalyticsOverview.tsx` (+ `AnalyticsOverviewChart.tsx`) | Echtes Chart-Dashboard-Pattern: MetricCards + lazy-geladener Chart, klickbare Metriken via `<Link>` | **als-vorlage** — Muster für klickbare KPI-Karten + **Lazy-Loading** (§20.7.5) |
| `packages/view-models/src/index.ts` | VM-Foundation: `VMState`, `ActionCallbacks`, `SidebarRegistryItem`, `RegistryGate` (`permission`/`flag`), `I18nLabel` | **1:1** — Pflicht-Basis für alle M20-VMs (AGENTS.md M013/M014) |
| `packages/feature-flags/` | Feature-Flag-Querschnitt (M006) für Dashboard- und Widget-Toggles | **1:1** |
| `packages/entitlements/` | Plan-/Entitlement-Checks (im `gating.server.ts` bereits verdrahtet) | **1:1** (für `general.tenant_health`-Widget + Gating) |
| `packages/db/src/client.server.ts` (`getReadDbClient`/`getExtendedDbClient`) | Read-/Tenant-scoped Prisma-Clients, Outbox/Audit-Middleware | **1:1** — read-heavy Dashboard-Queries |
| `packages/db/src/kpi.server.ts` | Admin-Plattform-KPIs (Tenant-Counts, Active-User via `log.groupBy`) | **als-vorlage** (nur Plattform-Admin-Scope; Muster für Count-/GroupBy-Aggregate, nicht der Tenant-Widget-Katalog) |
| `infra/grafana/business-kpi.json` | Vorgefertigtes Grafana-Business-KPI-Dashboard | **Referenz** — Ops-Sicht, kein In-App-Widget; als Inspiration für KPI-Auswahl |
---
## Vorhandene Daten-Modelle (Prisma)
> Verifiziert in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`.
| Modell | Pfad | Relevanz für M20 |
|---|---|---|
| `Widget` | `prisma/schema/core.prisma:267` | **Deckt M20 NICHT ab.** Generisches Legacy-UI-Widget: `id`, `tenantId?`, `name`, `appearance` (String), `metadata` (String), `@@unique([tenantId, name])`. Keine Layout-/Instanz-/Config-/Position-Struktur. Reader: `packages/db/src/widgets.server.ts` (`findLegacyWidgetById`). Allenfalls als Namens-Squatter zu beachten (Kollision mit neuem `WidgetInstance`). |
| `MetricLog` | `prisma/schema/core.prisma:180` | Request-Performance-Log (route/duration/userId/tenantId) — **Ops-Metrik**, nicht Business-Aggregat. |
| `AnalyticsSettings`, `AnalyticsUniqueVisitor`, `AnalyticsPageView`, `AnalyticsEvent` | `prisma/schema/analytics.prisma` | First-Party-**Web**-Analytics (Visitor/PageView/Event, UTM, Geo, Device), portal-zentriert. Quelle für ein evtl. „Web-Traffic"-Widget, aber **kein** CRM-/Sales-/HR-Aggregat. |
| `FeatureFlag` (+ Filter) | `prisma/schema/featureFlags.prisma` | Für Dashboard-/Widget-Feature-Gating (M006). |
| `CollabDoc` | `prisma/schema/collabDocs.prisma` | Yjs-Realtime-Docs — nicht Dashboard-relevant (nur falls geteilte Layout-Editing-Sessions gewünscht; Phase-1 nicht gefordert). |
**Fazit Daten-Layer:** Die vier M20-Kernmodelle (`Dashboard`, `DashboardLayout`, `WidgetInstance`; `WidgetCatalog` als Code-Registry) existieren **nicht** und müssen neu gebaut werden. `Tenant`-/`User`-Relationen und das Tenant-Scoping-Muster sind vorhanden und wiederverwendbar.
---
## Lücken / komplett neu zu bauen
1. **Datenmodell M20** — neue Prisma-Modelle `Dashboard`, `DashboardLayout`, `WidgetInstance` (Spec Block A). Forward-Only-Migration mit Inverse-SQL (DoD). Achtung: `Widget` ist als Modellname belegt → `WidgetInstance` + `WidgetCatalogKey` verwenden.
2. **WidgetCatalog (Code-Registry)** — die konkrete Liste aus Block C (`sales.*`, `projects.*`, `tasks.*`, `activity.*`, `comms.*`, `hr.*`, `ai.*`, `general.*`) mit Default-Config-Schema, Render-Komponente, Datenquelle + Quellen-Modul-Permission. Das `dashboard-registry`-Pattern ist die Vorlage, der **Inhalt** ist komplett neu und modul-abhängig (M3/M4/M26/M1/M9/M12/M14/M18).
3. **Drag-&-Drop-Grid-Engine** — `react-grid-layout` ist **NICHT** in den Deps (entgegen PLAN-Hinweis); nur `@dnd-kit/core` + `@dnd-kit/sortable` vorhanden. Entweder Grid-Library hinzufügen oder Grid auf `@dnd-kit` selbst bauen (Resize ist mit dnd-kit nicht out-of-the-box). Layout-Persistenz (`layout_data` JSON) neu.
4. **Per-Widget-Aggregat-/Caching-Schicht** — Cache-Key `tenant_id + widget_key + config_hash + user_id`, TTL 60 s, Invalidierung (§20.7). Kein generischer Widget-Cache vorhanden; `appDashboardService` zählt direkt ohne Cache. Pre-Calc-Aggregat-Tables + Cron (§20.7.2) neu — `apps/worker` + BullMQ als Fundament vorhanden.
5. **7 Rollen-Default-Layouts als deklaratives Branchen-Template** (M148, §20.5, LP-3/AV#3) — Seed-Daten beim Tenant-Anlegen. Seed-Infra (`packages/db/src/seed.server.ts`, `entitlements-seed`, `permissions-seed`) vorhanden, die **Layout-Templates** selbst neu.
6. **Widget-Datenquellen** — jeder Widget-Builder hängt an einem anderen Modul (Sales-Pipeline-KPIs, Forecast, Vertriebler-Performance, Projekt-Auslastung/Plantafel, offene Mängel, HR „wer ist heute da", ungelesene Mails, Anrufe heute, KI-Vorschläge). Diese Quell-Module sind im Boilerplate noch nicht als HiveGrid-Module gebaut → harte Abhängigkeit zu M01/M03/M04/M09/M12/M14/M18/M26.
7. **Drill-Down-Filter-Ableitung** (§20.4, Kern-Anforderung) — aus Widget-Config den vorgefilterten Listen-Link bauen, permissions-aware. `Stat.path`/`entity.slug` ist der Hook, die **Filter-Serialisierung** je Widget neu.
8. **Dashboard-Permissions** (`dashboard.read/customize/create/share/admin` + Bundles, Block F) — neue Permission-Keys. Boilerplate hat nur `app.dashboard.view` / `admin.dashboard.view` (`packages/db/src/permissions-seed.server.ts`). Permission-Definition gehört nach `packages/entitlements/src/permissions.ts` (Integration-Contract §10).
9. **Dashboard-Live-Events** — `packages/realtime` kennt nur Notification-Events; Dashboard-Events (`deal.won`, `lead.received`, `defect.accepted`) + Server-Push-Verdrahtung + „Live-Updates pausieren"-Opt-Out neu.
10. **HiveGrid-Layer-Split** — `packages/ui-hivegrid`, gefüllte `packages/view-models/src/dashboard/*.vm.ts`, Subframe-Adapter existieren noch nicht. Der Boilerplate-Dashboard-Code liegt monolithisch in `apps/web` mit `useLoaderData`/`~/db`-Nähe und verletzt die HiveGrid-Anti-Patterns → bei Übernahme zwingend umbauen.
11. **Sidebar-/Shell-Wiring** — `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` (Integration-Contract §2) existiert nicht; Boilerplate nutzt eine andere Shell. Sidebar-Eintrag „Dashboard" (Gruppe ARBEITEN) neu zu registrieren. `SidebarRegistryItem`-Typ aus `packages/view-models` ist die Basis.
12. **Mobile-First Monteur-Dashboard** (LP-2, §20.5.7) — Mobile-Tab-Bar-Eintrag (M169) + reduziertes Layout neu.
13. **Klartext-States** — Empty/Loading/Error pro Widget aus M002-Catalog + M003-i18n. Boilerplate-Dashboard hat i18n-Keys (`app.dashboard.*`), die konkreten Widget-Texte (§20.10) neu.
14. **Report-Versand-Trigger** (M150) — manuelle Generierung via vorhandene `exports`-Infra; geplante Email-Reports sind explizit Phase 2 (§20.9.2, Out-of-Scope).
---
## Empfohlenes Vorgehen
**Phase 0 — Layer-Fundament & Übernahme der Patterns (vor M147):**
1. `packages/view-models`-Foundation (`VMState`, `ActionCallbacks`, `SidebarRegistryItem`) ist bereits da → 1:1 nutzen, `dashboard`-VM-Ordner anlegen.
2. `Stat`-DTO + `getStatChangePercentage`/`getStatChangeType` (pure) nach `packages/view-models`/`app/domain` portieren (1:1).
3. `DashboardStats`- und `AnalyticsOverview`-Komponente als Vorlage nach `packages/ui-hivegrid` übernehmen (VM-only, Drill-Down via `<Link>` beibehalten).
4. `dashboard-registry`-Pattern (`registry.server.ts` + `gating.server.ts`) als Blaupause kopieren → daraus den `WidgetCatalog` + permissions-aware Gating formen (Permission-Check statt Plan-Check).
**M147 — Layout + Widget-Engine:**
5. Neue Prisma-Modelle `Dashboard`/`DashboardLayout`/`WidgetInstance` + Migration. `Tenant`-Scoping-Muster aus `packages/db` nutzen.
6. Grid-Engine: Entscheidung `react-grid-layout` hinzufügen **oder** auf vorhandenem `@dnd-kit` bauen (Resize-Aufwand einplanen).
7. Per-Widget-Aggregat-Builder nach Muster `appDashboardService` (serverseitig, im Loader → VM). Caching-Schicht (TTL 60 s, Cache-Key per Spec) neu; `getReadDbClient()` für read-heavy Queries.
8. Recharts/`chart.tsx`-Primitive für Trend-/Conversion-Widgets 1:1 übernehmen.
9. Realtime: `packages/realtime` Server/Client übernehmen, Dashboard-Event-Typen ergänzen, Lazy-Loading (lazy-Chart wie `AnalyticsOverviewChart`).
**M148 — 7 Rollen-Default-Layouts:**
10. Deklarative Layout-Templates als Seed-Daten (Seed-Infra aus `packages/db/src/seed.server.ts` vorhanden); permissions-aware Widget-Filter aus dem Gating-Pattern.
**M149 — Drill-Down + Custom-Konfig:**
11. Drill-Down-Filter-Ableitung je Widget (`Stat.path`/`entity.slug` als Hook), permissions-aware. Widget-Picker mit Catalog-Gating (ausgegraut bei fehlender Permission).
**M150 — Export:**
12. `packages/exports` (PDF `@react-pdf/renderer` + CSV/XLSX) direkt anbinden: Dashboard-PDF-Snapshot + Widget-CSV. Geplante Email-Reports bleiben Phase 2.
**Querschnitt durchgehend:** Permission-Keys in `packages/entitlements`, Feature-Flags (M006), i18n (M003), Audit (M001 auto) für Layout-Änderungen an shared Dashboards (AV#6), Sidebar-Eintrag (§2), Smoke-Test + Hot-Swap-Drill + Visual-Regression (DoD D).
@@ -0,0 +1,119 @@
# Boilerplate-Abgleich — Modul 21: Audit-Log (Anzeige)
- **Modul-ID:** M21 (Detail-Spec `_bmad-output/specs/M21-audit-log.md`)
- **Name:** Audit-Log — tenant-weiter Trail aller schreibenden Aktionen mit Vorher/Nachher-Diff
- **Milestones:** M151 (Anzeige + Filter + Volltext-Suche), M152 (Compliance-Export + Aufbewahrung + Legal Hold); baut auf M001 (Audit-Default) auf
- **Pflichtenheft-Block:** Block 7, Abschnitte 21.1–21.8
- **Reuse-Level:** **medium**
> Die Boilerplate liefert ein produktionsreifes Audit-**Fundament** (Tamper-evidente `AuditEvent`-Tabelle mit Hash-Chain, Write-Path, Retention-Worker, Compliance-Export inkl. PDF/CSV, Permission-gegatete Admin- + Tenant-Routen). Aber: Das Boilerplate-Audit-Modell ist auf **Compliance-Evidence** (SOC2/HIPAA/GDPR) ausgelegt, **nicht** auf den HiveGrid-Detail-Spec mit Vorher/Nachher-**Diff**, Klartext-`summary_text`, Subject-/Actor-**Snapshot-Labels**, **Saved Views**, tenant-Admin-konfigurierbarer **Retention in Monaten** und **Legal Hold**. Diese fachlichen Kern-Features fehlen komplett. Die Listen-/Detail-Anzeige-UI für Endnutzer (Tabelle, Diff-View, Per-Subject-Drawer) ist ebenfalls neu zu bauen — Boilerplate hat nur Export-Forms und eine Admin-`Log`-Tabelle.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `prisma/schema/core.prisma:55` (`model AuditEvent`) | Audit-Tabelle: `tenantId`, `actorId/Type/Email/Ip/UserAgent`, `action`, `targetType/Id`, `requestId`, `sourceModule`, `outcome`, `dataSensitivity`, `regulatoryScope[]`, `metadata` (JSON), `previousHash`/`eventHash`. Indizes auf `(tenantId,createdAt)`, `(actorId,createdAt)`, `(action,createdAt)`, `requestId`, `(tenantId,action,createdAt)` | **anpassen** — Felder weitgehend deckungsgleich mit Spec Block A; ABER es fehlen `before_diff`/`after_diff`/`summary_text`/`actor_label`/`subject_label`. Spec-`subject_type`/`subject_id` = boilerplate `targetType`/`targetId`. `actor_kind`-Werte (`ki_agent`, `automation`, `platform_admin`) müssen ergänzt werden. Append-Only-DB-Trigger (Spec) fehlt. |
| `packages/db/src/audit-events.server.ts` | Single-Write-Path `createAuditEvent()` / `publishAuditEventViaOutbox()` (Transaction-aware via Outbox), Metadata-Sanitizing (Secret-Redaction), Per-Tenant-Hash-Chain-Link, `buildAuditEventCreateData()`, `toAuditHashPayloadFromRow()` | **1:1** für M001-Fundament; **anpassen** für M21 (neue Diff-/Summary-Felder ins Write-Schema aufnehmen) |
| `packages/db/src/audit-hash.ts` | Tamper-Evidenz: `computeAuditHash()` (HMAC-SHA256 via `AUDIT_CHAIN_SECRET`, Fallback SHA-256), `canonicalizeAuditPayload()`, `verifyAuditChain()` mit Break-Reporting | **1:1** — deckt Spec Block I (Hash-Chain Phase 2) bereits vor; Verifikations-Tool-Backend ist da, nur UI fehlt |
| `packages/foundation/src/audit.ts` | DB-agnostische `auditEvent(input, client)`, `withAuditMiddleware(handler, buildEvent, client)`, Typen `AuditEventInput`/`AuditEventClient` | **als-vorlage** für M001-Querschnitt-Aufruf; M21 selbst liest nur |
| `packages/db/src/logs.server.ts` | Admin-/Tenant-Audit-**Listen-Query**: `getAdminAuditLogList()`, `getTenantAuditLogList(tenantId, …)`, `buildAuditLogWhereFromFilters()` (Filter q/action/url/userId/createdAtFrom/To, Volltext-OR), `serializeAdminAuditLogsCsv()`, Pagination (take/skip, max 500) — operiert allerdings auf dem **`Log`**-Modell, nicht `AuditEvent` | **als-vorlage** — Filter-/Pagination-/Where-Builder-Muster direkt übertragbar; muss auf `AuditEvent`-Modell + Spec-Filter (Actor-Kind, Subject-Type, Bulk-Op-ID, IP) umgeschrieben werden |
| `packages/db/src/export-jobs.server.ts` + `prisma/schema/queue.prisma:27` (`model ExportJob`) | Async-Export-Job-Lifecycle: `ExportJobCreateInput`, RUNNING/SUCCEEDED/FAILED/PAUSED-Transitions, `artifactBucket/Key`, `rowCount`, `expiresAt`/`deletedAt`/`hardDeletedAt` (TTL + Cleanup), `requestId`, Retention-Records | **anpassen** — exakt das Job-Modell für M152-Compliance-Export. Es fehlt im Modell `filter_criteria`/`format`-Enum-Detaillierung + `purpose/reason`-Pflichtfeld (Spec Block G) + Self-Audit `audit.exported` |
| `packages/exports/src/audit-pdf.ts` (`./audit-pdf`) | `serializeAuditEventsToPdf` / `…Stream`, `AuditPdfEvent`, Cover-Page (Tenant/Range/Filter/Integrity) + tabellarischer Event-Log, Row-Cap `PDF_STREAM_HARD_ROW_LIMIT=5000` | **1:1** für M152-PDF-Export (Spec Block G PDF-Format); HiveGrid muss DE-Labels + Zweck-Begründung in Header ergänzen |
| `packages/exports/src/{csv,json}` + `src/sinks/s3-multipart.ts` + `src/limits.ts` | CSV/JSON-Serializer (streaming), S3-Multipart-Sink (8 MB Parts), Plan-Row-Caps, Spreadsheet-Injection-Safety | **1:1** für M152-Export CSV/JSON nach Scaleway-S3 |
| `apps/web/app/routes/admin/audit-export.helpers.server.ts` | `parseAuditExportFilters()`, `buildAuditExportWhere()` (tenantId/actorType/outcome/regulatoryScope/Date-Range), `computeAuditIntegritySummary()`, `renderAuditExportCsv()`, `AUDIT_EXPORT_MAX_ROWS=50000`, `csvCell()` (injection-safe) | **als-vorlage** — Filter-→-Where-Übersetzung + Integritäts-Summary direkt adaptierbar; Filter-Set muss auf Spec Block F erweitert werden |
| `apps/web/app/routes/app.$tenant/settings/audit-export.tsx` | **Tenant-scoped** Export-Route (Permission `app.settings.auditTrails.view`, tenantId-locked gegen Query-Smuggling), CSV+PDF-Download-Form mit Filtern, i18n-Keys | **als-vorlage** — zeigt das korrekte Tenant-Scope-Pattern; ist aber synchroner Direkt-Download, **kein** Async-Job-mit-Notification (Spec verlangt Job + Vorschau-Count + Begründung) |
| `apps/web/app/routes/admin/audit-trails.tsx` + `apps/web/app/components/audit/AdminAuditLogToolbar.tsx` | Admin-Listen-View: `IndexPageLayout` + `LogsTable` + Filter-Toolbar + CSV-Export-Button + Pagination, `verifyUserHasPermission("admin.auditTrails.view")` | **als-vorlage** — Layout-/Toolbar-/Tabellen-Muster; läuft auf `Log`-Modell + Plattform-Admin (M041), nicht auf der Tenant-`AuditEvent`-Anzeige aus M151 |
| `apps/web/app/utils/audit/pdfRender.ts` (`renderAuditExportPdfResponse`, `buildAuditPdfLabels`) + `apps/web/app/utils/audit/withAuditContext.ts` | PDF-Response-Wrapper + Audit-Kontext-Middleware (Request-ID, Actor-Resolution) | **anpassen** — DE-Labels für HiveGrid |
| `apps/worker/src/jobs/audit-retention.ts` | Täglicher Retention-Cron: `runAuditRetentionJob()` mit Seams (tenantLister/eventDeleter/auditWriter), `resolveAuditRetentionPolicy()`, Meta-Audit der Löschung selbst | **anpassen** — Mechanik passt (Spec Block H: täglicher Lösch-Job mit Self-Audit). ABER: Boilerplate-Policy ist **regulatory-scope-basiert in Tagen** (365/6y/7y), Spec verlangt **tenant-Admin-konfigurierbar 6–120 Monate** + **Legal-Hold-Pause** — beide fehlen und sind nachzurüsten |
| `apps/worker/src/jobs/audit-event-persist.ts` (+ `-bootstrap`) | Outbox-Consumer der Audit-Inserts (`outbox.audit`-Queue), führt prevHash-Lookup + Chain-Link in kurzer Transaktion aus | **1:1** für M001-Async-Audit-Schreibpfad |
| `apps/worker/src/jobs/export.ts` + `dsgvo-export.ts` | Generischer Export-Job + DSGVO-Export-Muster (Aggregation, S3-Stream, Signed-URL, Self-Audit `EXPORT_PERSONAL_DATA`) | **als-vorlage** für M152-Audit-Export-Worker (gleiches Muster: Filter → Stream → S3 → Signed-URL → `audit.exported`) |
| `packages/db/src/permissions-seed.server.ts:102/177/202/237` | Permissions `admin.auditTrails.view` (SuperAdmin) + `app.settings.auditTrails.view` (SuperUser/Admin) bereits geseedet + Rollen-Mapping | **anpassen** — entspricht grob Spec `audit.read`; Spec verlangt zusätzlich getrenntes `audit.admin` (Export/Retention/Legal-Hold) und `audit.read` als atomares Bundle (M27) |
| `prisma/schema/featureFlags.prisma` + `packages/feature-flags` | Feature-Flag-Querschnitt (`FeatureFlag`, append-only `FeatureFlagAudit`) | **1:1** für DoD-Punkt „Feature-Flag-fähig" |
| `packages/i18n` + `apps/web/.../audit-export-i18n.test.ts` | i18n-Querschnitt + bestehende Audit-Export-i18n-Keys | **anpassen** — Keys-Schema nutzen, HiveGrid-Audit-Action-Labels + Klartext-Hinweise (Block L) neu |
| `packages/view-models` | View-Model-Layer-Paket (M013-Konvention) | **als-vorlage** — pure VM-Builder für Audit-Listen-/Detail-VM neu darin anlegen |
| `apps/api/.../middleware/audit-context.ts` + `request-id.ts` | Request-ID + Audit-Kontext-Middleware in der API | **1:1** — liefert `requestId`/Actor-Kontext für Audit-Einträge |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
| Modell | Pfad | Bezug zu M21 |
|---|---|---|
| `AuditEvent` | `prisma/schema/core.prisma:55` | **Kern-Modell.** Deckt Spec Block A zu ~70% (Actor-Felder, Action, Target, requestId, metadata, IP/UA, Hash-Chain, Indizes). **Fehlend:** `before_diff`, `after_diff`, `summary_text`, `actor_label`, `subject_label` |
| `Log` | `prisma/schema/core.prisma:36` | Älteres, schlankeres Aktivitäts-Log (url/action/details, Tenant/User/ApiKey/Row/Comment-Relationen). Wird von `logs.server.ts`-Listen-Query genutzt — **NICHT** das Spec-Audit-Modell, eher Aktivitäts-/Row-Log |
| `ExportJob` + Enum `ExportJobStatus` | `prisma/schema/queue.prisma:20/27` | **Direkt für M152-Export-Job.** Hat TTL (`expiresAt`/`deletedAt`/`hardDeletedAt`), `artifactBucket/Key`, `rowCount`, Status-Lifecycle. Fehlend: `filterCriteria`, `purpose/reason` |
| `ImpersonationAudit` | `prisma/schema/accounts.prisma:266` | Spezial-Audit für Admin-Impersonation (SetNull-Tombstone) — Referenz-Muster, nicht direkt M21 |
| `FeatureFlagAudit` | `prisma/schema/featureFlags.prisma:44` | Append-only Flag-Audit — Beispiel für append-only-Pattern; Spec `feature_flag.changed`-Marker |
| `PluginCapabilityUse` / `PluginInvocation` | `prisma/schema/plugins.prisma` | Per-Capability-Audit mit eigener Hash-Chain (`prevHash`/`rowHash`) — Referenz-Muster für Tamper-Evidenz |
| `QueueFailedJob` | `prisma/schema/queue.prisma:1` | Export-Job-Failure-Tracking |
**Komplett fehlende Modelle (Spec Block A Sub-Entitäten):**
- `AuditRetentionPolicy` (tenant-Admin Retention in Monaten + `legal_hold_active`/`legal_hold_reason`/`legal_hold_set_by/at`) — **existiert NICHT**
- Saved-View / `SavedAuditView` (max 5/User, Spec Block F / 21.3.4) — **existiert NICHT**
- `AuditExportJob` als dedizierte Audit-Variante mit `filter_criteria` + Zweck-Begründung — **existiert NICHT** (nur generisches `ExportJob`)
---
## Lücken / komplett neu zu bauen
### Daten-Layer
1. **Diff-Felder am `AuditEvent`** — `before_diff` (JSONB), `after_diff` (JSONB), `summary_text` (DE-Klartext, Pflicht), `actor_label`, `subject_label` (Snapshot-Namen für Hard-Delete-Resistenz). Spec-zentral (Block D/E), in Boilerplate **nicht vorhanden**.
2. **`actor_kind`-Erweiterung** — Boilerplate kennt `user/admin/api_key/system/plugin`; Spec braucht zusätzlich `automation`, `ki_agent`, `platform_admin`.
3. **`AuditRetentionPolicy`-Modell** + tenant-Admin-Konfig 6–120 Monate (Default 24) + Legal-Hold-Felder (Block H) — **neu**.
4. **Saved-Views-Modell** (max 5/User, geseedete Default-Ansichten) (Block F) — **neu**.
5. **GIN-Index auf `metadata`** + **Trigram-Index auf `summary_text`** (Volltext) — Spec Block A; Boilerplate hat weder `summary_text` noch diese Indizes.
6. **Append-Only-DB-Trigger** (UPDATE/DELETE-Block auf App-Role) — Spec verlangt DB-Trigger; Boilerplate setzt Append-Only nur auf App-Ebene (kein DELETE/UPDATE-Pfad im Code), kein DB-Trigger.
### Domänen-Logik
7. **Cursor-Pagination** (Spec: cursor-basiert, 100/Page, „Mehr laden") — Boilerplate `logs.server.ts` ist **offset-basiert** (take/skip).
8. **Erweiterte Filter** — Actor-Kind, Subject-Type-Multi-Select, Subject-ID-Autocomplete, Bulk-Op-ID, IP-Filter (Forensik). Boilerplate-Where-Builder kennt nur action/url/userId/tenantId/Date.
9. **Sensible-Diff-Maskierung** (HR/Portal/Secrets → nur „Wert geändert", permission-abhängig `hr.read_all`/`audit.admin`) — **neu** (Boilerplate redacted nur metadata-Keys generisch, kein permission-abhängiges Diff-Gating).
10. **Bulk-Op-Aggregat + Aufklapp-Logik** (Geschwister-Entries derselben `bulk_op_id`) — **neu**.
11. **Vorschau-Count vor Export-Submit** + **Pflicht-Zweck-Begründung** + **Async-Job statt Direkt-Download** (Block G) — Boilerplate-Tenant-Export ist synchroner Direkt-Stream.
12. **Legal-Hold-Logik** im Retention-Worker (Pause für Tenant bei `legal_hold_active`) + Retention-Reduzierungs-Vorschau („ca. X Einträge betroffen") — **neu**.
13. **Permissions-Split** — separates `audit.admin` (Export/Retention/Legal-Hold) vs. `audit.read`; Boilerplate hat nur ein `…auditTrails.view`. Legal Hold braucht zusätzlich `tenant.admin` UND `audit.admin`.
### UI (größte Lücke)
14. **Tenant-Audit-Listen-View für Endnutzer** (M151) — Tabelle/Stream mit Actor-Avatar, Action-Label (i18n), Subject klickbar, Summary-Text, relative/absolute Zeit, Quick-Filter, Date-Range, Saved-Views-UI. Boilerplate hat nur **Admin**-`Log`-Tabelle + **Export-Form**, keine Tenant-`AuditEvent`-Anzeige.
15. **Detail-View mit Diff-Visualisierung** (Block E) — Skalar nebeneinander, JSON-Struktur-Diff, Sensible-Marker, Copy-`request_id`, „Ähnliche/Verlauf"-Links. **Komplett neu.**
16. **Per-Subject-Audit-Drawer** in Detail-Views (Kontakt/Deal/Projekt) + **Per-User-Audit-Drawer** in Admin/users/:id (Block C) — **neu**, gegated `audit.read`.
17. **Settings-View** (`/admin/audit/settings`) — Retention-Slider, Legal-Hold-Toggle mit Pflicht-Begründung, Hash-Integritäts-Prüf-Button (Phase 2-Backend existiert via `verifyAuditChain`). **Neu.**
18. **Export-Job-Übersicht + Anlage + Status/Download-Routen** (`/admin/audit/exports*`) — **neu** (Boilerplate hat Form, keine Job-Übersicht).
19. **Klartext-Hinweise** (Block L: leerer Trail, Subject gelöscht, Bulk-Op, Automation/KI-Bezug, Legal-Hold-Banner) — i18n-Keys neu.
20. **Sidebar-Eintrag** (Gruppe EINSTELLUNGEN → Audit-Log, INTEGRATION-CONTRACTS §1) + Breadcrumbs — in HiveGrid-App-Shell neu zu registrieren.
### Querschnitt
21. **Activity-Stream-Brücke** (Block K: `audit_event`-Activity-Typ) — abhängig von M045; **neu**.
22. **Notifications** bei Export-Fertigstellung (`notify()`, M011) — Wiring neu.
---
## Empfohlenes Vorgehen
**Phase 0 — M001-Fundament prüfen/erweitern (Voraussetzung):**
1. `AuditEvent`-Modell (`core.prisma:55`) **übernehmen** und um Spec-Felder erweitern: `before_diff`, `after_diff`, `summary_text`, `actor_label`, `subject_label`; `actor_kind`-Werte ergänzen. Forward-only Migration mit GIN-(`metadata`)- + Trigram-(`summary_text`)-Index. Append-Only-DB-Trigger ergänzen.
2. Write-Path `createAuditEvent()`/`publishAuditEventViaOutbox()` (`packages/db/src/audit-events.server.ts`) + `audit-hash.ts` + Outbox-Consumer (`audit-event-persist.ts`) **1:1 übernehmen**, Diff-/Summary-Felder ins Schema einziehen.
**Phase 1 — M151 Anzeige (Wiederverwendung mittel, UI neu):**
3. Listen-Query: `logs.server.ts`-Filter-/Where-/Pagination-Muster **als Vorlage**, neu gegen `AuditEvent` schreiben (Cursor-Pagination, erweiterte Filter Block F, `withTenantScope()`+`withPermission()` aus M004).
4. View-Model-Builder in `packages/view-models` (M013) — Listen-VM + Detail-VM (Diff-Strukturierung, Sensible-Maskierung, Snapshot-Labels) — **neu**, pure Functions + Zod.
5. UI komplett neu via Subframe-Adapter (M015): Listen-View, Detail-/Diff-View, Per-Subject/-User-Drawer. Sidebar + Breadcrumbs + i18n-Keys.
6. Saved-Views-Modell + Default-Ansichten-Seed — **neu**.
**Phase 2 — M152 Export + Retention + Legal Hold:**
7. Export: `ExportJob`-Modell (`queue.prisma:27`) + `export-jobs.server.ts` + `dsgvo-export.ts`-Muster **übernehmen/anpassen** (filter_criteria + Zweck-Begründung + Vorschau-Count). Serializer `audit-pdf.ts`/`csv`/`json` + S3-Multipart-Sink **1:1**. Self-Audit `audit.exported` + `notify()` bei Fertigstellung.
8. Retention: `audit-retention.ts`-Worker **anpassen** — Policy von regulatory-scope-Tagen auf `AuditRetentionPolicy` (Monate, tenant-konfigurierbar) + Legal-Hold-Pause umstellen. Reduzierungs-Vorschau-Count.
9. Settings-UI (Retention/Legal-Hold/Integritäts-Prüfung) — **neu**; Hash-Verifikations-Backend (`verifyAuditChain`) ist bereits da.
**Permissions (parallel, M27-abhängig):** `audit.read` / `audit.admin` als atomare Bundle-Permissions definieren; Boilerplate-`…auditTrails.view` als Ausgangspunkt, aber splitten.
---
## Notizen / Realitäts-Check
- Das Boilerplate-Audit ist **Compliance-Evidence-orientiert** (SOC2/HIPAA/GDPR, Hash-Chain, regulatory-scope-Retention), nicht **operativ-nachvollziehbar** (Diff/Summary/Snapshot). Das ist die fundamentale Differenz: Tamper-Evidenz + Schreibpfad + Export-Pipeline sind **vorbildlich vorhanden**, aber der für HiveGrid-Endnutzer zentrale **Diff-/Klartext-/Anzeige-Layer** ist neu.
- Der Boilerplate hat **zwei** Audit-artige Tabellen: `AuditEvent` (Compliance, `core.prisma:55`) und `Log` (Aktivitäts-/Row-Log, `core.prisma:36`). Die bestehende Listen-UI (`audit-trails.tsx` + `logs.server.ts`) läuft auf `Log`, nicht auf `AuditEvent`. Für M21 ist `AuditEvent` das richtige Modell — die Listen-Query muss umgehängt werden.
- Phase-2-Features der Spec (Hash-Chain Block I) sind im Backend **bereits implementiert** (`audit-hash.ts`, `verifyAuditChain`) — nur das Verifikations-Tool-UI in den Settings fehlt. Das ist ein unerwarteter Vorsprung.
- Kein `before/after`-Snapshot in M001-Prisma-Middleware-Default: AGENTS.md/INTEGRATION-CONTRACTS §6 sagen „Audit ist automatisch via Prisma-Middleware" — diese Middleware ist im Boilerplate **nicht** vorhanden (es gibt `withAuditMiddleware` für Service-Handler + `createAuditEvent`, aber keine generische Prisma-`$extends`-Middleware, die before/after automatisch erfasst). Das ist eine M001-Lücke, die M21 voraussetzt.
@@ -0,0 +1,138 @@
# Boilerplate-Abgleich — M22 Notifications
| Feld | Wert |
|---|---|
| **Modul-ID** | M22 (Notifications) |
| **Modul-Name** | Drei-Kanal-Notifications (In-App / Email / Mobile-Push) |
| **Milestones** | M115 (Notification-Center + Lese-Status), M116 (User-Settings: Kanal + Stille-Stunden + Geräte), M117 (Kategorien + Bündelung + Anti-Spam + Versand-Pipeline) |
| **Block** | Block 5 (Kommunikation), §22 |
| **Service-Layer-Querschnitt** | M011 (`notify()`) — Boilerplate-Foundation deckt das Fundament |
| **Reuse-Level** | **MEDIUM** |
> **Quellen verifiziert (eigene Reads/Greps, nicht nur Inventur):**
> Detail-Spec `_bmad-output/specs/M22-notifications.md`, Pflichtenheft `block5.md §22`, die 3 PLAN.md (M115-M117), AGENTS.md (DoD A-F), INTEGRATION-CONTRACTS.md (§1 Sidebar, §7 Notification-Wiring), Boilerplate-Inventur (foundation/data-layer/apps-infra/ui-shell) UND direkte Reads der Boilerplate-Dateien (Prisma-Schema, db/foundation/realtime/mail-Pakete, API-Routes, Worker-Job, Web-Routes/Components).
---
## Kern-Befund (eine Zeile)
Die Boilerplate hat einen **fast vollständigen vertikalen In-App-Notification-Slice** (DB-CRUD → SSE-Stream-API → SSE-Client mit Reconnect → Notification-Center-Component → Tenant-Route + minimale Settings-Route) **plus** ein **Mobile-Push-Fundament** (DeviceToken-Modell, Register/Revoke-Routes, provider-pluggable Push-Fan-out-Worker, Capacitor-Push-Registration) **plus** BullMQ-`notifications`-Queue, `@toolctrl/mail` + React-Email-Templates und sogar eine Novu-Anbindung. **Aber:** Das Datenmodell ist eine schlanke *Delivery/Diagnostics*-Sicht (`NotificationDelivery`) — es fehlt das reiche M22-`Notification`-Aggregat (title/body/deeplink/actor/priority/bundle), die **PUSH-Channel-Enum-Werte**, **Quiet-Hours**, **Bundle-Window/Sliding-Window**, **Priority** und die ganzen **Settings-/Devices-/Bündel-UI-Flächen**.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/foundation/src/notifications.ts` | `notify()`, `notifyMany()`, `bundleNotifications()`, `isNotificationEnabled()`, `assertI18nKey()`, Typen `NotificationInput/Preference/Bundle` — DB-/framework-agnostisches Querschnitt-Primitiv mit Preference-Check, sourceKey-Upsert (Idempotenz), Metadata-Sanitizing/Redaction | **als-vorlage** — exakt der M011-`notify()`-Einstiegspunkt aus INTEGRATION-CONTRACTS §7. Muss aber um `PUSH`-Channel, `priority`, Quiet-Hours-Resolver und echte Bundle-**Window**-Logik erweitert werden (heute nur statisches Gruppieren ohne Zeitfenster). |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/notifications.server.ts` | Vollständige Server-Helper: `createNotificationDelivery` (upsert/idempotent per sourceKey), `markInAppNotificationDeliveryRead`, `markNotificationDeliveryStatus`, `getNotificationDeliveryDiagnostics`, `getNotificationStreamSnapshot` (Cursor-Stream, 50/Seite), `upsert/getUserNotificationPreferences`, Redaction (`redactNotificationDiagnosticText`) | **anpassen** — Lese-Pfade (Stream-Snapshot, mark-read, Preferences) sind 1:1 nutzbar. Schreib-Pfad braucht die neuen M22-Felder (title/body/deeplink/actor/priority/bundle_id). PUSH-Channel und Priority-Threshold fehlen. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/notifications.prisma` | Modelle `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` + Enums `NotificationDeliveryStatus`, `NotificationPreferenceChannel` | **anpassen** — siehe „Vorhandene Daten-Modelle". DeviceToken ist quasi 1:1 die M22-`NotificationDevice`. Die anderen beiden sind Fundament, brauchen aber Felderweiterung + neue Modelle (Bundle, QuietHours, reiches Notification-Aggregat). |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/db/src/device-tokens.server.ts` | `registerDeviceToken` (idempotent per token), `revokeDeviceToken`, `revokeDeviceTokensForUser` (Logout-Hook), `listActiveDeviceTokens`, `findDeviceTokenByToken`, `parseDevicePlatform`/`parsePushProvider`/`defaultPushProviderFor` | **1:1** — deckt Block H Token-Registrierung + Auto-Revoke (Logout/Token-ungültig) komplett ab. Nur Bulk-Revoke-UI und `last_seen_at`-Bump bei Push-Erfolg ergänzen. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/api/src/routes/devices-routes.ts` | `POST /v1/devices/register` + `POST /v1/devices/revoke` (Validierung, Längen-Limits, Klartext-Fehler) | **anpassen** — entspricht Spec-Endpoint `/api/v1/notifications/mobile-device`. Auth ist heute service-credential mit userId/tenantId aus dem Body; für M22-Self-Scope (eingeloggter User) muss gegen den authentifizierten Principal verifiziert werden. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/worker/src/jobs/push-notification.ts` | `dispatchPushNotification()` — provider-pluggabler Fan-out (APNs/FCM/WebPush via Seam), Auto-Revoke bei `Unregistered`/`InvalidRegistration`/`BadDeviceToken`, Concurrency-Limit, Token-Redaction in Logs | **1:1** (Orchestrierung) — pure, testbar. Block J „Push-Sender" + Token-Invalidierungs-Handling (Block H). Provider-SDK-Adapter (firebase-admin/node-apn) werden in `apps/worker/src/index.ts` injiziert (Operator-Setup). |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/realtime/src/events.ts` + `client.ts` + `server.ts` + `guards.ts` | Realtime-SSE-Schicht: `REALTIME_EVENT_NAMES` (`notification.snapshot`/`notification.update`/`keepalive`/`error`), `validateRealtimeEvent` (Secret-Key-Reject, Allow-list, Cursor), Subscriber-Client mit Exponential-Backoff-Reconnect, Scope-Guards | **1:1** — deckt Block B.1 „Real-time-Update über SSE" + Polling-Fallback (im Client). Nur `notification.update`-Push (Live-Toast) muss vom Sender aus emittiert werden. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/api/src/routes/notifications-routes.ts` | `GET /v1/notifications/stream` — SSE-Endpoint: Scope-Normalisierung, Workspace-Revalidation, Snapshot-Push, Keepalive, harte Security-Header | **anpassen** — Stream-Fundament steht. Heute reiner Snapshot+Keepalive (kein dauerhafter Live-Push pro neuer Notification); für echtes Live-Center muss ein Pub/Sub-Fan-out (shared-store/Redis) ergänzt werden. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/components/notifications/NotificationCenter.tsx` | Fertige In-App-Center-Component: SSE-Subscribe + Reconnect, Merge/Dedupe/Sort, Mark-as-read-Form, Empty-State, i18n, Status-Badges, Last-Error-Redaction | **als-vorlage** — gute Basis für M115, aber: zeigt heute *Delivery-Diagnostik* (Status/Attempts/Channel) statt der M22-Center-Felder (title/body/Actor-Avatar/Deeplink/Priority/Bundle-Count, Tabs Alle/Ungelesen/Markiert, Filter, Suche, Bulk). Darf NICHT direkt aus Routes importiert werden — über Adapter (`packages/ui-hivegrid/src/adapters/`, AGENTS.md A.4). |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/routes/app.$tenant/notifications.tsx` (+ `notifications/stream.tsx`) | Tenant-Route mit Loader (Diagnostics→VM), Action (mark-read + CSRF), Stream-URL-Bridge | **als-vorlage** — exaktes RR7-Loader/Action-Muster für M115. Felder/VM müssen aufs reiche M22-Modell umgebaut werden. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/routes/app.$tenant/settings/notifications.tsx` | Settings-Route: Preference-Toggle pro Kanal/Kategorie (IN_APP/EMAIL), CSRF-Action, Loader | **als-vorlage** — Grundgerüst für M116-Präferenzen. Sehr schmal (3 Kategorien hartkodiert, kein PUSH, kein Priority-Threshold, kein Master-Toggle, keine Modul-Gruppierung, keine Quiet-Hours, keine Geräte). |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/components/layouts/buttons/NotificationsButton.tsx` + `.../sidebars/shadcn/nav-actions.tsx` + `.../icons/IconNotifications.tsx` | Glocken-Button in der Header-Action-Leiste (feature-gated `appConfiguration.notifications.enabled`), verlinkt auf Tenant-/Admin-Notifications | **anpassen** — Block B.1 „Glocke in Top-Bar". Unread-Count-Badge + Tab-Title-Update fehlen und müssen ergänzt werden. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/mail/src/index.ts` (+ `templates-react/`, `mailersend.ts`, `queue.ts`) | `@toolctrl/mail`: MailerSend-Provider (EU-Region nutzbar), `QueuedMailProvider`, React-Email-Render (`renderTransactionalEmail`), Brand-Defaults (`buildBrand`/`withBrandDefaults`) | **anpassen** — Block B.2 Email-Versand + Branding. EU-Stack (LP-13). Tenant-Sending-Domain-Anbindung (Modul 9) + Bounce-Pause-Logik + Notification-spezifische Templates fehlen. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/email-templates/src/index.ts` | React-Email-Template-Engine (`renderEmail` → `{html,text}`), Layout-Primitives, Theme-Tokens | **als-vorlage** — Muster für die Notification-Email-Templates (Lead/Task/Termin/Auth/Subscription) aus Locale-Files (LP-12). |
| Worker-Infra: `apps/worker/src/queues.ts` (`defaultWorkerQueueNames` enthält `notifications`), `handlers.ts` (`HandlerDependencies` DI-Seam für notification), `outbox/event-router.ts` (`notification.`-Prefix → Queue), `@toolctrl/queue` `buildNotificationJob`/`enqueueJob`/`queueNames.notifications` | BullMQ-`notifications`-Queue + Transactional-Outbox-Routing + Retry/Backoff-Fundament | **1:1 / anpassen** — Block J Versand-Pipeline (Producer→Queue→Worker→Status-Tracker), Retry-Logik (3x Backoff) ist BullMQ-Standard. Bundle-Manager + Quiet-Hours-Resolver + Pro-User-Push-Limit (10/h) sind neu zu bauen. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/mobile/src/pushRegistration.ts` + `capacitor.config.ts` (Plugin `push-notifications`, `local-notifications`) | Capacitor-Push-Token-Registration gegen `/v1/devices/register`, idempotenter Boot | **1:1** — Block H Registrierungs-Flow (Modul 36). Apple-Compliance-Pre-Prompt-Teaser-Screen (LP-8) fehlt und ist im Mobile-Modul zu bauen. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/modules/notifications/services/.server/NotificationService.ts` | Modul-Service mit BullMQ-`notifications`-Queue-Enqueue + **Novu-API-Anbindung** (`novuApiFetch`, `NOTIFICATIONS_NOVU_API_KEY`), Subscriber/Message/Integration-DTOs | **als-vorlage / anpassen** — bestätigt Spec-Wahl „Novu als Abstraktions-Schicht" (Block B.3). ACHTUNG: `.server.ts`-Re-Export ist projektverboten (User-Memory) → Component-Extraktion statt Re-Export. |
---
## Vorhandene Daten-Modelle
Alle in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/notifications.prisma`:
### `NotificationDelivery` (`:14`)
- Felder: `id`, `createdAt`, `updatedAt`, `tenantId?`, `userId?`, `channel` (Enum), `category` (String), `messageKey`, `sourceKey?`, `status`, `attemptsMade`, `providerMessageId?`, `lastError?`, `payloadMetadata Json?`, `sentAt?`, `readAt?`
- Indizes: `(tenantId,userId,status)`, `(tenantId,channel,category)`, `(status,updatedAt)`, `(providerMessageId)`; **`@@unique([tenantId,userId,channel,sourceKey])`** (Idempotenz-Upsert)
- **Lücke vs. Spec-`Notification`:** kein `title`, `body`, `deeplink`, `event_type`, `event_subject_type/id`, `actor_user_id`, `actor_kind`, `priority`, `dismissed_at`, `bundle_id`, `delivery_status pro Kanal` (hier 1 Row pro Channel statt 1 JSONB). Ist eher eine *pro-Kanal-Delivery-Zeile* als das reiche *Notification-Aggregat* der Spec.
### `UserNotificationPreference` (`:41`)
- Felder: `id`, `tenantId`, `userId`, `channel` (Enum), `category` (String), `enabled` (Bool); `@@unique([tenantId,userId,channel,category])`
- **Lücke vs. Spec-`NotificationPreference`:** kein `priority_threshold`, kein PUSH-Channel (Enum nur IN_APP/EMAIL), `category` ≠ `event_type`-Katalog, kein Rolle-Default-Seeding.
### `DeviceToken` (`:66`)
- Felder: `id`, `createdAt`, `updatedAt`, `userId`, `tenantId?`, `platform` (`ios|android|web`), `token @unique`, `pushProvider` (`fcm|apns|webpush`), `appVersion?`, `lastSeenAt`, `revokedAt?`
- Indizes: `(userId,tenantId)`, `(tenantId,platform)`, `(revokedAt)`
- **Sehr nah an Spec-`NotificationDevice`** — fehlt nur `device_label`, `os_version` (separat von appVersion). `device_kind` = `platform`, `revoked_at` = `revokedAt`, `last_seen_at` = `lastSeenAt`. Quasi 1:1.
### Enums
- `NotificationDeliveryStatus`: `PENDING | SENT | FAILED | SKIPPED | READ` (passt grob, Spec hat granularere `delivery_status` pro Kanal als JSONB — z.B. `bounced`, `skipped:user_preference`, `failed:smtp_timeout`)
- `NotificationPreferenceChannel`: `IN_APP | EMAIL` — **`PUSH` fehlt** (obwohl DeviceToken + Push-Worker existieren). Konzeptioneller Bruch zwischen Preference-Enum und Push-Fähigkeit.
**FEHLENDE Modelle komplett (Spec Block A):** `Notification` (reiches Aggregat), `NotificationBundle` (Anti-Spam), `NotificationQuietHours` (Ruhezeiten). Diese existieren in der Boilerplate **nicht**.
---
## Lücken / komplett neu zu bauen
**Daten-Layer (neu):**
- `Notification`-Aggregat mit title/body/deeplink/event_type(Enum)/event_subject/actor/actor_kind/priority/dismissed_at/bundle_id/metadata + `delivery_status` JSONB pro Kanal (Spec Block A)
- `NotificationBundle`-Modell (event_type_group, count, subject_summary, bundle_window_seconds, closed_at) — **kein Pendant** in BP
- `NotificationQuietHours`-Modell (weekday, start/end, urgent_override, enabled) — **kein Pendant** in BP
- `PUSH` als dritter `NotificationPreferenceChannel`-Enum-Wert + `priority` + `priority_threshold`
- M22-Index `(tenant_id, recipient_user_id, created_at DESC)` und Unread-Count-Index — die BP-Indizes sind delivery-zentriert (status/channel/category), nicht ideal für das Center-Default-Query
**Domänen-Logik (neu / erweitern):**
- **Bundle-Manager mit Sliding-5-Min-Window** (BP `bundleNotifications` gruppiert nur statisch, kein Zeitfenster, kein Counter, kein 100-Max, kein Schließen-nach-5-Min)
- **Quiet-Hours-Resolver** (Send/Queue/Hold-Entscheidung pro Kanal; Urgent-Override; Queue-bis-Ruhezeit-Ende) — komplett neu
- **Priority-Pipeline** (urgent nie gebündelt; priority_threshold pro Preference) — komplett neu
- **Pro-User-Push-Limit 10/h** (Anti-Storm, M117) — neu
- **Email-Bounce-Pause-Logik** (7-Tage-Pause + Klartext-Hinweis im Center) — neu
- **Override-Hierarchie** (Channel-Stumm > Ruhezeit > User-Default > Channel-Default, §22.3.6) — neu
- **Default-Preference-Sets pro Rolle** als deklarative Templates (Vertriebler/PL/Monteur/Büro/GF/Marketing, LP-3) + Onboarding-Seeding — neu
- **Event-Typen-Katalog** (Enum/Registry über alle Producer-Module, Block I) — neu, wächst modulweise
- **Anomalie-/Rate-Limit-Detection** für Notification-Storms (AV #16) — neu
**UI (neu / stark erweitern):**
- **M115:** Reiches Notification-Center (Tabs Alle/Ungelesen/Markiert, Filter Event-Typ/Actor/Zeitraum/Priority, Suche, Bulk-Auswahl + Bulk-mark-read/clear mit >50-Bestätigung, Bundle-Expand „Alle anzeigen", Deeplink-Klick→mark-read, Actor-Avatar, Priority-Marker, Unread-Badge in Glocke, Tab-Title-Count). BP-Center ist Diagnostik-Liste — Umbau nötig.
- **M116:** Präferenz-Editor nach Modul gruppiert (3 Toggles + Priority-Threshold pro Event-Typ, Master „Alle Email aus", Reset-auf-Default, Default-Set-anwenden, Search). Quiet-Hours-Editor (Wochentag-Picker, Time-Picker, Urgent-Override, Master-Toggle, Konflikt-Hinweis). Geräte-Liste (Test-Push, Umbenennen, Revoke, Bulk-Revoke, Status-Marker, Empty-State App-Store-Link). **Alle drei UI-Flächen fehlen** (BP hat nur den minimalen Channel/Category-Toggle).
- **`/admin/notifications/templates`** read-only Email-Template-Anzeige + **`/admin/notifications/test`** Test-Notification-Werkzeug (Permission `notifications.admin`). BP-`admin/notifications/*` ist ein anderes Konzept (Novu-Operator-CRUD: channels/messages/subscribers) — als Inspiration, nicht direkt.
**Querschnitt-Wiring (HiveGrid-spezifisch, neu):**
- Sidebar-Eintrag in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` (Gruppe gemäß INTEGRATION-CONTRACTS §1: „Notification-Center (Glocke) → M115-M117") — die BP-Sidebar/`sidebar.config.ts` existiert in HiveGrid noch nicht (neue Struktur lt. AGENTS.md)
- `@hivegrid/notifications` `notify()`-Fassade gemäß INTEGRATION-CONTRACTS §7 (mit `priority`, `category`, `template`, `payload`) — BP-`notify()` ist die Vorlage, aber die §7-Signatur (recipientId/template/priority) weicht ab
- View-Model-Layer in `packages/view-models/**` (pure) + Adapter `packages/ui-hivegrid/src/adapters/` für die Subframe/BP-Components (AGENTS.md A-Disziplin)
- Permission `notifications.admin` + Self-Scope-404-statt-403 (Spec Block K)
- Feature-Flag (M006) + Smoke/Visual/Hot-Swap-Tests (M016/M017) + i18n-Keys (M003)
- Sicherheits-Versand-Failures → Modul-21-Audit (AV #6)
**EU-Stack / Provider-Setup (Operator):**
- Novu Self-Hosted auf Scaleway (BP nutzt `api.novu.co` Cloud — für LP-13 auf EU-Self-Hosted umstellen)
- EU-SMTP-Provider-Config (MailerSend EU / Resend EU / Scaleway) — BP-MailerSend ist Region-konfigurierbar
- APNs/FCM-Credentials (`FCM_SERVICE_ACCOUNT_JSON`, `APNS_KEY/KEY_ID/TEAM_ID/BUNDLE_ID`) — Worker-Boot liest sie bereits
---
## Empfohlenes Vorgehen
**Phase 0 — Datenmodell-Fundament (vor M115):**
1. `DeviceToken` quasi 1:1 übernehmen, nur `device_label` + `os_version` ergänzen.
2. `NotificationPreferenceChannel` um `PUSH` erweitern; `UserNotificationPreference` um `priority_threshold` ergänzen.
3. Neues reiches `Notification`-Aggregat + `NotificationBundle` + `NotificationQuietHours` als Forward-Only-Migration anlegen (Inverse-SQL als Kommentar, AGENTS.md F.28). Entscheiden: `Notification`-Aggregat zusätzlich zu/ersetzend `NotificationDelivery` — Empfehlung: reiches `Notification` als SSOT, `delivery_status` als JSONB pro Kanal (Spec Block A), `NotificationDelivery`-Diagnostik-Pattern für den Versand-Status weiterverwenden.
**Phase 1 — M115 (In-App-Center):**
4. `@toolctrl/db/src/notifications.server.ts` Lese-Pfade (`getNotificationStreamSnapshot`, `markInAppNotificationDeliveryRead`, Preferences) übernehmen, Schreib-Pfad aufs neue Modell anpassen.
5. SSE-Stack (`packages/realtime` + `notifications-routes.ts` `GET /v1/notifications/stream`) 1:1 übernehmen; Live-`notification.update`-Fan-out über shared-store/Redis-Pub/Sub ergänzen.
6. `NotificationCenter.tsx` + `app.$tenant/notifications.tsx` als Vorlage; aufs reiche M22-Center (Tabs/Filter/Suche/Bulk/Bundle/Actor/Priority) umbauen; via Adapter aus `packages/ui-hivegrid` einbinden (kein Direkt-Import, kein `.server`-Re-Export).
7. Glocke (`NotificationsButton`) übernehmen + Unread-Count-Badge + Tab-Title-Count ergänzen. Sidebar-Eintrag in `sidebar.config.ts` (§1).
**Phase 2 — M116 (Settings + Quiet-Hours + Devices):**
8. `device-tokens.server.ts` + `devices-routes.ts` (auf Self-Scope-Auth umstellen) + Push-Fan-out-Worker übernehmen; Geräte-Verwaltungs-UI neu bauen (Test-Push/Umbenennen/Revoke/Bulk).
9. Präferenz-Editor (Modul-gruppiert, 3 Kanäle + Priority-Threshold + Master-Toggle + Reset) — `settings/notifications.tsx` nur als Action/Loader-Muster.
10. Quiet-Hours-Modell + Resolver + Editor komplett neu; Default-Set-Seeding pro Rolle (deklarative Templates).
**Phase 3 — M117 (Bündelung + Pipeline + Anti-Spam):**
11. BullMQ-`notifications`-Queue + Outbox-Routing + Retry 1:1 nutzen.
12. Bundle-Manager mit Sliding-Window (5 Min, Counter, 100-Max, Close-after-idle) neu — `foundation.bundleNotifications` nur als Daten-Shape-Vorlage.
13. Pro-User-Push-Limit (10/h), Urgent-Bypass, Bounce-7-Tage-Pause, Override-Hierarchie neu.
14. Push-Sender (Novu EU) + Email-Sender (`@toolctrl/mail` + `email-templates`, Tenant-Branding via Modul 31) + In-App-Sender (write + SSE-update) verdrahten; `delivery_status` zurückschreiben.
15. Sicherheits-Failures → Modul-21-Audit; Anomalie-Detection (AV #16).
**Querschnitt durchgehend:** `@hivegrid/notifications`-`notify()`-Fassade als einziger Producer-Einstieg (§7), pure View-Models, i18n-Keys, Feature-Flag, Smoke/Visual/Hot-Swap-Tests, 404-statt-403, Klartext-Empty/Loading/Error-States (AGENTS.md A-F).
@@ -0,0 +1,115 @@
# Boilerplate-Abgleich: M23 — Files / Dokumentenablage
| Feld | Wert |
| --- | --- |
| **Modul-ID** | M23 |
| **Name** | Files / Dokumentenablage |
| **Milestones** | M052 (Datei-Upload), M053 (Speicher-Backend Scaleway), M054 (Permissions/Sharing/Versionierung), M055 (Suche/Tagging/Archivierung) |
| **Block** | Block 2 (CRM-Kern), Pflichtenheft Modul 23 |
| **Reuse-Level** | **medium** |
| **Spec** | `/Users/raffaelsheikh/Documents/GitHub/HiveGrid-Cloud/_bmad-output/specs/M23-files.md` |
## Kurzfazit
Die Boilerplate hat ein **reifes Storage-Infrastruktur-Fundament** (`packages/storage`): S3-Adapter (Scaleway-tauglich), Signed-URLs mit SigV4, Tenant-Pfad-Isolation, MIME-Magic-Bytes-Validierung, ClamAV-Anti-Virus-Pipeline (web → queue → worker → quarantine), MIME-Allow-List und Pre-Upload-Scan. Diese decken **Block E (teilweise), J (Storage-Tenant-Check), K (Storage-Architektur) und M052/M053 großflächig** ab.
**Aber:** Das HiveGrid-Fachdomänen-Modell (`File`, `FileVersion`, `FileEntityLink`, das fachliche `FileUploadProgress`/`FileChunk` mit Tenant/Resume/Checksum) fehlt komplett. Die Boilerplate-Modelle `FileObject`/`FileUploadProgress`/`FileChunk` (`core.prisma`) sind **nicht tenant-scoped, nicht versioniert, kein Entity-Link, kein Resume-State** — nur generische Storage-Metadaten. Der komplette **Files-Browser (UI), Versionierung, Entity-Verknüpfung, Preview-/Thumbnail-Generierung und Volltextsuche/OCR** ist neu zu bauen.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/index.ts` | `createS3StorageAdapter` / `createLocalStorageAdapter`, `putObject`/`deleteObject`, `StorageAdapterError` (mit Secret-Redaction), `UploadBody`-Typen | **1:1** — S3-PUT/DELETE auf Scaleway. Bucket-pro-Region passt zur Spec K. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/signed/index.ts` | `createS3SignedUrlAdapter`, `createSignedUploadUrl`/`createSignedDownloadUrl`, Expiry-Clamping (`DEFAULT_MAX_SIGNED_URL_EXPIRY_SECONDS=3600`), `checksumSha256`/`contentLength`-Header | **1:1** — kurzlebige Signed-URLs für Download/Preview (Spec K: keine Public-URLs). |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/access/index.ts` | `authorizeStorageObjectAccess` (Tenant-Prefix `tenants/{tenantId}/` Mismatch-Check, blocked/quarantined-Gate), `authorizeStorageUploadConstraints` (MIME-Allow + maxBytes) | **1:1** — Spec K „App-Code prüft storage_key ↔ tenant_id" + Spec E Max-Size + Mime-Block. Pfad-Konvention `tenants/{tenantId}/...` deckt sich exakt mit Spec A/F. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/scan/magic-bytes-adapter.ts` | `createMagicBytesScanAdapter` — synchroner Pre-Upload-Scan: Magic-Bytes vs. declared MIME, Allow-List, Filename-Path-Traversal-/Control-Char-Schutz, SVG-Sonderfall | **anpassen** — Allow-List-Kontexte erweitern (siehe unten). Deckt Spec E „Mime-Typ blockiert" + M052 Format-Whitelist. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/scan/allow-list.ts` | `DEFAULT_ALLOW_LIST` + `resolveAllowList(env)` (env-overridable `STORAGE_SCAN_ALLOWLIST_<CONTEXT>`) | **anpassen** — Kontexte sind aktuell `avatar/mediaLibrary/emailAttachment/formUpload/kbAsset`; ein `files`-Kontext mit der breiteren HiveGrid-Liste (Video/Audio/HEIC/ODF) fehlt. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/scan/clamav-scanner.ts` | `createClamAVStorageScanner` (clamd TCP, `loadObject`-Seam, Reachability-Check, `ClamAVUnreachableError`) | **1:1** — M052 Anti-Virus (ClamAV-EU/self-hosted). |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/scan/s3-quarantine.ts` | `createS3StorageQuarantineAdapter` — infizierte Objekte nach `quarantine/`-Prefix kopieren + Original löschen | **1:1** — Sicherheits-Quarantäne für infizierte Uploads. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/storage/src/scan/index.ts` | `createStorageScanService` (Scanner+Quarantine-Orchestrator), `ScanVerdict`, `StorageScanResult` (clean/infected/error, mit Klartext-Diagnostik) | **1:1** — Worker-Orchestrierung des Post-Upload-Scans. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/utils/integrations/storageService.ts` | `storeFile` (Pre-Scan → PUT → `enqueuePostScan`), `putBufferAsFile`, `deleteFile`, SSRF-sichere `fetchUploadContent`, Provider-Fallback (s3↔local), `StorageScanRejectedError` | **als-vorlage** — fertiges Web-Wiring von Scan+Upload+Fallback; HiveGrid braucht tenant-scoped Variante (Bucket/Key aus `tenant_id`) und Chunked-Upload statt Single-Shot. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/utils/integrations/storageAudit.server.ts` | `auditRejectedUpload(request, context)` — onReject-Callback schreibt `upload.rejected.scan` Audit-Event (PII-frei) | **als-vorlage** — Audit-Pattern für abgelehnte Uploads (Spec AV #6, M002 Klartext). |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/worker/src/scanService.ts` | `createWorkerScanService(env)` — verdrahtet ClamAV-Scanner + S3-Quarantine aus Env, undefined-safe Boot | **1:1** — Worker-Bootstrap des Scan-Service. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/worker/src/handlers.ts` (queue `fileScan`, ab Zeile 753) | `toolctrl.file-scan`-Handler: ruft `scanService.scanObject`, liefert `scan.clean`/`scan.infected`, Klartext-Diagnostik bei `error` | **als-vorlage** — Vorlage für HiveGrid Scan-Job; muss um Status-Rückschreibung in `File.preview_status`/Scan-Status erweitert werden. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/api/src/storage.ts` | Vollständige **SigV4-Signierung** für S3-Signed-URLs inkl. Adapter-/Signing-Key-Cache (Secret nie roh gecached) | **1:1** — die konkrete Signer-Implementierung für `createS3SignedUrlAdapter`. Direkt für Scaleway nutzbar. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/utils/integrations/fileScanQueue.server.ts` | `enqueueFileScan` (BullMQ-Enqueue für Post-Upload-Scan, no-op ohne `SCAN_QUEUE_ENABLED`) | **1:1** — Enqueue-Helper für den Scan-Job. |
| `prisma/schema/core.prisma` → `FileObject` (Zeile 213) | Storage-Metadaten (`provider/bucket/key` unique, `contentType`, `byteSize`, `checksumSha256`, `publicUrl`, `uploadedAt`, `lastSignedUpload/DownloadAt`, `metadata`) | **als-vorlage** — Feld-Set für das HiveGrid-`File`-Modell; aber **nicht tenant-scoped, keine Versionen, keine Soft-Delete/Tags/Preview** — als Spaltenvorlage, nicht 1:1. |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/apps/web/app/components/ui/uploaders/` (`UploadDocument`, `PreviewUploadersDocument`, `MediaItem`), `input/InputMedia.tsx`, `input/InputImage.tsx` | Drag-and-Drop-Dropzone + Base64-Multi-File-Upload + Media-Preview-Modal (i18n-fähig) | **als-vorlage** — UI-Bausteine für Drag-and-Drop. **Achtung:** Base64-Single-Shot, kein Chunked/Resume; in HiveGrid via `packages/ui-hivegrid`-Adapter + Subframe neu zu bauen (Layer-Disziplin AGENTS A). |
| `packages/exports` + Worker `dsgvo-export.ts` | `createS3MultipartUploadSink` (8 MB Multipart, Concurrency 4, Abort-on-fail), DSGVO-Export-Bundle als ZIP/JSON → S3 + Signed-URL + AuditEvent | **als-vorlage** — Spec D „DSGVO-Export (Datei + Metadaten als ZIP)" + Spec C „Download als ZIP" (Bulk). Multipart-Sink hilft bei Chunk-Concat (Spec E Schritt 5). |
| `prisma/schema/core.prisma` → `AuditEvent` (Zeile 55) + Audit-Worker (`audit-event-persist.ts`, `audit-retention.ts`) | Compliance-Audit-Trail-Modell + Persist-/Retention-Jobs | **1:1 (Querschnitt)** — Spec AV #6 / Block I Audit-Default. |
---
## Vorhandene Daten-Modelle (Boilerplate)
Quelle: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/core.prisma`
- **`FileObject`** (Zeile 213) — generische Storage-Objekt-Metadaten. **Nicht** tenant-scoped, **keine** Soft-Delete/Versionen/Tags/Preview/Entity-Links. → Feldvorlage für HiveGrid-`File`, **kein** Ersatz.
- **`FileUploadProgress`** (Zeile 195) — `id, fileName, progressServer, progressStorage, url, error, chunks[]`. **Deutlich dünner** als Spec A: **kein** `tenant_id`, `created_by_user_id`, `total_size_bytes`, `total_chunks`, `received_chunks`, `expires_at`, `status`-Enum, `target_file_id`, `target_version_intent`. → Konzept gleich, Felder fast komplett neu.
- **`FileChunk`** (Zeile 205) — `id (Int autoincrement), fileUploadId, index, data Bytes`. Speichert Chunk-Bytes **in der DB** (`data Bytes`), nicht im temporären Storage-Bucket wie Spec A (`storage_key`). **Kein** `tenant_id`, `checksum_sha256`, `size_bytes`, `received_at`, **kein** `(upload_id, chunk_index)`-Unique. → Architektur-Abweichung (DB-Bytes vs. Storage-Temp); neu zu modellieren.
- **`AuditEvent`** (Zeile 55) — Compliance-Audit-Trail (Querschnitt, 1:1 nutzbar).
**Nicht vorhanden** (komplett neu): `File` (fachlich, tenant-scoped, soft-delete, RLS), `FileVersion`, `FileEntityLink`.
**Kein passendes Tag/Activity-Modell auf Datei-Ebene:** Tags existieren nur row-scoped im Entity-Builder (`EntityTag`/`RowTag` in `entityBuilder.prisma`) — nicht das HiveGrid-Tag-System (M044) und nicht der zentrale Activity-Stream (M045). Beides ist HiveGrid-Eigenbau, auf den M23 nur referenziert.
---
## Lücken / komplett neu zu bauen
### Daten-Layer
- **`File`-Modell** (tenant-scoped, soft-delete, RLS) mit `display_name`, `mime_type`, `size_bytes`, `storage_key`, `current_version`, `checksum_sha256`, `preview_status`-Enum, `preview_storage_key`, `assigned_to_user_id`, `tags[]`, `notes`, `archived_at`, `source`-Enum, `created_by_portal_user_id` (Block H). Inkl. Indizes (Default-Sort, Owner, Checksum-Dedupe, **Trigram-Index `display_name`**).
- **`FileVersion`** (append-only, `(file_id, version_number)` unique).
- **`FileEntityLink`** (M:N, `(file_id, entity_type, entity_id)` unique, Index `(tenant_id, entity_type, entity_id)`, `link_role`).
- **Fachliches `FileUploadProgress`/`FileChunk`** mit Tenant, Resume-State, Checksum, Storage-Temp-Pfad (Boilerplate-Modelle reichen nicht).
### Domänen-/Worker-Logik
- **Chunked Upload mit Resume** (5 MB Chunks, max 3 parallel, Init→Stream→Concat→Finalize, Resume nach Reconnect) — Boilerplate kann nur Single-Shot `storeFile`. Multipart-Sink aus `packages/exports` als Concat-Baustein nutzbar.
- **Versionierung** (gleicher Display-Name → V2/V3, Rollback legt neue Version an, Storage-Pfad `…/v{n}/original`) — nicht vorhanden.
- **Preview-Generation** (PDF direkt, **HEIC→JPEG**, **Office→PDF via LibreOffice-Headless**, Video-Thumbnail, Thumbnail für Grid) — **kein** Worker-Job, **keine** Image/PDF/Office-Libs in der Boilerplate (`sharp` nur in pnpm `onlyBuiltDependencies`, **nicht** als Dependency; **kein** `browser-image-compression` trotz M052-Plan-Hinweis; kein libheif/imagemagick/libreoffice/pdf-lib). Komplett neu inkl. Library-Auswahl.
- **Duplikat-Erkennung per Checksum** (Spec E: „Datei existiert bereits…") — `checksumSha256`-Feld + Index vorhanden, Match-Logik + UI-Dialog neu.
- **Transitive Sichtbarkeit / `files.read_own`** (Owner ∨ Assignee ∨ Entity-Link mit Lese-Recht; Materialized-View/Cache-Erwägung) — Permission-Logik komplett neu; `withTenantScope()`/`withPermission()` (M004) als Wrapper.
- **Bulk-Actions** (Tag/Assign/Link/Archive/ZIP-Download/Hard-Delete) — neu.
- **Cleanup-Jobs** (expired Chunks täglich, Hard-Delete-Cascade Storage, Orphan-Reconciliation wöchentlich) — neu (Cron-Pattern wie `audit-retention.ts`/`dsgvo-grace-cron.ts` als Vorlage).
- **OCR + Volltextsuche** (M055; Tesseract Phase 1) — neu; Such-Index-Registrierung via M056/`indexed-entities`.
### Permissions
- **Bundle `files.none/read_own/read_all/write/delete/admin`** (Spec J Variante 2) — neu; HR-Sonderfall (`user`-Entity folgt HR-Hub M14).
### UI (komplett neu in `packages/ui-hivegrid` + Subframe via Adapter)
- Files-Browser (Tabelle/Grid umschaltbar, Filter, Bulk, Cursor-Pagination, Spalten-Config, Thumbnail-Lazy-Load).
- File-Detail (Preview-Viewer PDF/Bild-Zoom/Office/Video/Audio, Inline-Edit, Verknüpfungs-Verwaltung mit Auto-Complete, Versions-Sektion).
- Upload-View (Drag-and-Drop, Multi-File-Queue, Chunked-Progress mit Klartext + ETA, Resume) — Boilerplate-Dropzone nur als Vorlage.
- Embedded Files-Browser (vorgefiltert) in Detail-Views von Kontakt/Firma/Deal/Projekt/Lead/Auftrag (Spec B / Pflichtenheft 23.7.4).
- Versionsverlauf-Sub-View.
### Querschnitt-Wiring (HiveGrid-Eigenbau, M23 referenziert nur)
- Activity-Stream-Integration (`logActivity`, Activity-Typen `file_uploaded`/`file_version_added`/`file_link_*`/`file_archived`/`file_renamed`/`file_deleted`) — M045.
- Tags-Wiederverwendung — M044.
- Notifications via `notify()` — M011.
- Feature-Flag-Schaltbarkeit — M006.
- Sidebar-Eintrag „Dateien" — `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` (INTEGRATION-CONTRACTS §1, Gruppe gem. Sidebar-Baum, Zeile 25).
- Mobile Offline-Foto-Upload (IndexedDB-Queue, LP-8) — Modul 36 / M170, nicht in Boilerplate.
---
## Empfohlenes Vorgehen
**Reihenfolge — zuerst BP-Infrastruktur verdrahten, dann Fachdomäne, dann UI:**
1. **M053 (Storage-Backend) zuerst-nah verdrahten:** `@toolctrl/storage` als Workspace-Dep einziehen. S3-Adapter + Signed-URL-Adapter (`apps/api/src/storage.ts`-SigV4 als Signer) auf Scaleway konfigurieren. `authorizeStorageObjectAccess` als verbindlichen Tenant-Pfad-Gate (`tenants/{tenantId}/files/{fileId}/v{n}/original`) übernehmen — **1:1**.
2. **M052 Anti-Virus + MIME-Whitelist:** ClamAV-Scanner + Quarantine + `createStorageScanService` + `file-scan`-Worker-Handler **1:1/als-vorlage** übernehmen. **Allow-List um `files`-Kontext erweitern** (Video/Audio/HEIC/ODF/Text/Markdown). `storageService.ts`/`storageAudit.server.ts` als Vorlage für das HiveGrid-Upload-Wiring (tenant-scoped, Audit bei Reject).
3. **Daten-Modell neu bauen:** `File`/`FileVersion`/`FileEntityLink` + fachliches `FileUploadProgress`/`FileChunk` (tenant-scoped, Resume, Checksum). `FileObject`/`AuditEvent` als Feld-/Querschnitt-Vorlage. Forward-Only-Migration mit Inverse-SQL-Kommentar (DoD).
4. **Chunked Upload + Versionierung** (M052/M054): Init/Stream/Concat-Endpoints; Multipart-Sink aus `packages/exports` für Concat; Resume-Logik gegen `FileUploadProgress`. Versionierung + Rollback (neue Version statt In-Place).
5. **M054 Permissions:** Bundle-Engine `files.*` + transitive Sichtbarkeit über `FileEntityLink` (Index nutzen), via `withTenantScope()`/`withPermission()`. HR-Sonderfall an M14 anbinden.
6. **Preview-Generation neu** (eigener Worker-Job, Pattern aus `handlers.ts`): Library-Auswahl finalisieren (sharp/libheif für HEIC, LibreOffice-Headless für Office, pdf/Frame für Video). Status-Rückschreibung in `File.preview_status`. **Höchster Eigenbau-Aufwand, keine BP-Vorlage.**
7. **M055 Suche/Tagging/Archivierung:** Tags via M044, Archivierung (`archived_at`-Soft-Delete + „Archivierte anzeigen"-Toggle), OCR-Pipeline + Such-Index-Registrierung (M056). Cleanup-Jobs (Cron-Pattern aus `audit-retention.ts`).
8. **UI in `packages/ui-hivegrid` + Subframe** (Layer-Disziplin AGENTS A, mandatory Subframe-Design): Files-Browser, Detail, Upload (Chunked-Progress), Versions-Sub-View, embedded vorgefilterter Browser. Boilerplate-Dropzone nur als Funktions-Vorlage. Sidebar-Wiring + Breadcrumb + i18n-Keys.
9. **Querschnitt schließen:** Activity-Stream (M045), Notifications (M011), Feature-Flag (M006), Audit (M001), DSGVO-Export (`packages/exports`-Vorlage). Smoke-Test + Hot-Swap-Drill + Visual-Regression-Snapshot (DoD).
**Achtung-Punkte:**
- BP-`FileChunk` speichert Bytes in der DB (`data Bytes`) — Spec will Chunks im temporären **Storage-Bucket** (`storage_key`). Architektur-Entscheidung früh treffen, BP-Modell nicht blind übernehmen.
- `browser-image-compression` ist **nicht** in der Boilerplate (entgegen M052-Plan-Hinweis) — als Dependency neu hinzufügen oder Server-side-Compression.
- Preview/HEIC/Office-Konvertierung ist der größte Greenfield-Block — frühzeitig spiken (Library + Worker-Timeout).
@@ -0,0 +1,133 @@
# Boilerplate-Abgleich: M24 — Globale Suche
> Faktischer Abgleich der HiveGrid-Detail-Spec (M24) gegen die Boilerplate.
> Boilerplate-Wurzel (NUR LESEN): `/Users/raffaelsheikh/Documents/GitHub/boilerplate`
> Spec-Quelle: `/Users/raffaelsheikh/Documents/GitHub/HiveGrid-Cloud/_bmad-output/specs/M24-globale-suche.md`
> Alle Pfade absolut. Nichts erfunden — jeder Eintrag verweist auf konkrete Datei(en); fehlt etwas, ist es klar als Lücke markiert.
## Kopf
| Feld | Wert |
| --- | --- |
| Modul-ID | M24 |
| Name | Globale Suche (Cmd+K, hybrid FTS + Embeddings, permission-aware, Adapter-Pattern) |
| Milestones | M056 (Such-Index), M057 (Permission-aware-Search), M058 (Such-UI + Saved-Searches) |
| Block | Block 2 (`_bmad-output/pflichtenheft/blocks/block2.md`, Modul 24) |
| Reuse-Level | **MEDIUM** |
**Kernaussage:** Die Boilerplate hat ein erstaunlich vollständiges **Such-/Embedding-Such-Fundament**: ein tenant-scoped `@toolctrl/search`-Paket (Index-Schemas, Outbox-Publisher, Query-Layer, Sanitize, Fallback-Pfad), den dazu passenden **Search-Indexer-Worker** (Upsert/Delete/Bulk-Reindex über Outbox→BullMQ), die komplette **Embeddings-/RAG-Pipeline** (`@toolctrl/ai/embeddings`) mit EU-Providern (Scaleway 1024-dim — exakter Match zur Spec) und den **pgvector-Store** mit tenant-scoped Wrapper. Außerdem existiert eine funktionierende **Cmd+K-Command-Palette** + `SearchButton` (in der Boilerplate-Web-App). Das Adapter-/Schema-Pattern (`SearchIndexSchema`, `allowedDocumentFields`-Whitelist) ist konzeptuell genau das von der Spec geforderte SearchAdapter-Modell.
**Die zwei großen Architektur-Divergenzen, die das Reuse-Level auf MEDIUM drücken:**
1. **Such-Engine:** Die Boilerplate sucht via **Meilisearch** (`packages/search` → `meilisearch`-SDK). Die HiveGrid-Spec (Block C/H) und der M056-Plan verlangen **Postgres-FTS** (`tsvector`/`tsquery`, deutsche Konfiguration, „kein Extra-Service") + **pgvector**-Hybrid mit **Reciprocal-Rank-Fusion**. → Der FTS-Pfad und die Fusion sind in der Boilerplate **nicht** vorhanden. Es gibt einen SQL-ILIKE-Fallback (`crmContactSearch.server.ts`), aber kein `tsvector`-FTS und keine Hybrid-Fusion.
2. **Daten-Modell:** Die Spec verlangt eine **zentrale `SearchIndexEntry`-Tabelle** (polymorpher Cross-Entity-Index mit `tsvector_de`, `embedding vector(1024)`, `permission_subject` JSONB), plus `RecentSearch` und `EmbeddingProviderJob`. **Keines** dieser Prisma-Modelle existiert in der Boilerplate (verifiziert via Grep über `prisma/schema/*.prisma`). Die Boilerplate hält den Such-Index extern in Meilisearch + die Vektoren in `vec_*`-Tabellen (von pgvector via Raw-SQL angelegt) — kein zentrales Postgres-Index-Modell.
**Wo HiveGrid sich entscheiden muss:** Entweder (A) die BP-Meilisearch-Architektur übernehmen (entgegen Spec/Plan, dafür mehr 1:1-Reuse) oder (B) bei Spec/Plan bleiben (Postgres-FTS) und nur die **Embedding-/Vektor-Hälfte** + Outbox-/Worker-/Adapter-Muster aus der BP übernehmen, den FTS-Pfad + `SearchIndexEntry`-Modell + Fusion neu bauen. Dieser Report bewertet gegen die **HiveGrid-Spec (Variante B)**, weil Memory/Plan-Hinweise (M056: „Postgres FTS bevorzugen — kein Extra-Service") das so vorgeben.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `packages/search/src/schemas/types.ts` | `SearchIndexSchema`-Interface: `name`, `primaryKey`, `searchableAttributes`, `filterableAttributes`, `sortableAttributes`, `rankingRules`, `allowedDocumentFields` (Whitelist gegen PII-Leak) | **als-vorlage** — konzeptuell exakt das **SearchAdapter**-Modell der Spec Block D (welche Felder fließen in den Index). Whitelist-Pattern (`allowedDocumentFields`) = direkte Vorlage für `buildSearchableContent()`. Muss um `buildPermissionSubject()`, `getRouteForEntity()`, `getDisplayMetadata()`, `filterVisibleForUser()` erweitert werden |
| `packages/search/src/schemas/{crm-contacts,users,rows,entities,kb-articles}.ts` + `schemas/index.ts` | Konkrete Index-Schema-Definitionen pro Entity-Typ; `ALL_SEARCH_INDEXES`, `BULK_REINDEX_SUPPORTED_INDEXES`, `getSearchIndexSchema(name)` (zentrale Schema-Registry) | **als-vorlage** — Muster für HiveGrid `indexedEntities`-Map (INTEGRATION-CONTRACTS §8 `packages/db/src/search/indexed-entities.ts`). `crm-contacts`/`users` decken Adapter-Typen `contact`/`user`. Felder/Permission-Keys auf HiveGrid-Schema umsetzen |
| `packages/search/src/outbox-publisher.ts` | `publishSearchUpsertEvent`/`publishSearchDeleteEvent`/`publishSearchBulkReindexEvent`, `searchEventType(entity, verb)` → `search.index.<entity>.<verb>`; `SearchOutboxPublisher`-Interface (dependency-frei, in DB-Transaktion committbar) | **1:1 / anpassen** — exakt das **Outbox-Pattern** der Spec Block H (schreibende Op → Outbox-Eintrag → Worker). Re-Index-Trigger (Insert/Update/Delete/Archiv) der Spec Block D docken hier an. `sanitizeDocument`-Aufruf bleibt; Event-Type-Konvention 1:1 übernehmbar |
| `packages/search/src/sanitize.ts` (`sanitizeDocument`) | Projiziert ein Dokument auf `allowedDocumentFields` (droppt nicht-gelistete Felder vor Index) | **1:1** — verhindert PII-Leak in den Index (Spec AV#5, M057 Field-Level-Redact-Fundament). Direkt für `body_text`-Aufbau wiederverwendbar |
| `packages/search/src/query.ts` | Tenant-scoped Query-Layer: `searchInIndex`, `searchWithFallback`, `bulkIndex`, `deleteFromIndex`, `health`, `TenantScopeError`; `assertTenantId()` erzwingt non-empty Tenant; `buildFilter()` AND-merged tenantId + extraFilters; Filter-Value-Escaping gegen Injection | **als-vorlage** — Tenant-Isolation-Pattern (Spec LP-1, RLS-Äquivalent) + Fallback-Architektur (Spec Block I: „Embedding-Provider-Down → nur FTS"). **Bei Variante B Meilisearch-spezifisch** → Pattern (Tenant-Pflicht, Filter-Escaping, Fallback-Hook) übernehmen, SQL-FTS-Implementation neu schreiben |
| `apps/worker/src/jobs/search-indexer.ts` | Search-Indexer-Worker: konsumiert `outbox.search-index`-Queue, projiziert `upsert`/`delete`/`bulk-reindex` auf den Index; Cursor-Pager für Bulk-Reindex (1000er-Batches), `BULK_PAGER_STALLED`/`BULK_REINDEX_INCOMPLETE`-Guards, Tenant-Mismatch-Check Envelope↔Payload | **als-vorlage** — exakt die **Indexierungs-Architektur** der Spec Block H (Outbox→Worker→Index, 5-10s Latenz). Bulk-Reindex = Initial-Backfill der Spec Block C + Admin-Re-Index (M_search.admin Phase 2). Bei Variante B: Index-Ziel von Meilisearch auf `SearchIndexEntry`-Upsert (FTS) + Embedding-Job-Enqueue umbauen |
| `apps/worker/src/jobs/search-indexer-bootstrap.ts` + `search-indexer-bulk-pagers.ts` | Worker-Bootstrap (Queue-Worker verdrahten) + Bulk-Pager-Registry pro Index | **als-vorlage** — Worker-Wiring-Muster |
| `apps/worker/src/outbox/{event-router,dispatcher,bootstrap}.ts` | Outbox-Event-Router (mappt `search.index.*` auf BullMQ-Queue `outbox.search-index`) + Dispatcher (liest Outbox-Tabelle, fan-out) | **als-vorlage / anpassen** — generisches Outbox→Queue-Fan-out. HiveGrid braucht eigenes Outbox-Modell (Spec Block H `search_outbox`); das Router-/Dispatcher-Muster ist direkt übernehmbar |
| `packages/ai/src/embeddings/factory.ts` + `embeddings/types.ts` | `createEmbeddingProvider()`, `registerEmbeddingProviderAdapter()`, `resolveEmbeddingProviderName(env)` (Default-Reihenfolge), `resolveEmbeddingDimensions(env)`; `EmbeddingProvider`-Interface (`embed`/`embedSingle`/`dimensions`/`maxBatchSize`), typisierte `EmbeddingProviderError` | **1:1** — Spec Block C Embedding-Generation-Fundament. Multi-Provider-Fallback-Architektur (Spec AV#13) |
| `packages/ai/src/embeddings/providers/scaleway.ts` | Scaleway-Embeddings-Adapter, Default-Modell `bge-multilingual-gemma2` (**1024 dim**), Batch 64, max 4096 Tokens | **1:1** — Spec Block C: „Embedding-Dimension einheitlich 1024" + EU-Provider (LP-13). Exakter Match. (Spec nennt primär Mistral-embed; Scaleway-Adapter liefert ebenfalls 1024-dim → der von der Spec offen gelassene Dimensions-Punkt ist faktisch auf 1024 entschieden) |
| `packages/ai/src/embeddings/providers/aleph-alpha.ts` | Aleph-Alpha-Embeddings-Adapter (EU, Germany) | **1:1** — Tertiär-Provider der Spec Block C (`alephalpha`) |
| `packages/ai/src/embeddings/embed-and-upsert.ts` | `embedAndUpsert()` — chunked Embed + Upsert in `VectorUpsertSink`; erzwingt `tenantId` im Payload; deterministische `buildChunkId()`; optionale Kostenverfolgung via `executeLLMCall` (capability `embedding`) | **1:1 / anpassen** — Spec Block C Schreibseite (bei Insert/Update → Embedding generieren). Re-Embed-bei-Text-Änderung (Spec Block D Trigger) + `embedding_status`-Pflege (`pending`/`generating`/`ready`/`failed`/`skipped`) andocken |
| `packages/ai/src/embeddings/retrieve.ts` | `retrieveRelevantContext()` — Query embedden, Fan-out über Collections, Rank-Merge (Score-sort + min-Score 0.7, top-K 8); tenant-scoped via `RagRetrievalStore.searchForTenant` | **1:1 / anpassen** — Spec Block C semantischer Pfad. Rank-Merge ist die **halbe Fusion** — der FTS-Pfad + RRF-Gewichtung (70/30) muss ergänzt werden |
| `packages/ai/src/embeddings/chunking.ts` | `splitForEmbedding()`, `countTokens()`, `DEFAULT_CHUNK_TOKENS`/`OVERLAP_TOKENS` | **1:1** — Text-Chunking für `body_text` (lange Inhalte) |
| `packages/ai/src/embeddings/outbox-publisher.ts` | `publishRagUpsertEvent`/`publishRagDeleteEvent`, `ragEventType()` → `rag.index.<entity>.<verb>` (Queue `outbox.rag-index`), Idempotenz via `dispatchKey` | **als-vorlage** — exakt das von der Spec Block C/D verlangte async Re-Embedding-Event-Muster (parallel zum FTS-Outbox). Kann für den Embedding-Teil des Such-Index 1:1 als Vorlage dienen |
| `apps/worker/src/jobs/rag-indexer.ts` + `rag-indexer-bootstrap.ts` | RAG-Indexer-Worker: konsumiert `outbox.rag-index`, chunk+embed+upsert / delete-by-source; tenant-scoped, provider-agnostisch | **als-vorlage** — exakt die **asynchrone Embedding-Generation** der Spec Block C (Worker holt Eintrag, baut Text, ruft Provider, persistiert). Ziel-Sink von `vec_*`-Tabelle auf `SearchIndexEntry.embedding` umlenken; Retry-Backoff (Spec: 3 Versuche → `failed` mit `last_error_text`) ergänzen (`EmbeddingProviderJob`) |
| `packages/vector-store/src/factory.ts` + `adapters/pgvector.ts` | `createVectorStore()` (pgvector Default / Qdrant Upgrade), `PgvectorAdapter` über `PrismaLike` (`$queryRawUnsafe`), legt `vec_<ns>_<collection>`-Tabellen + **HNSW-Index** an, Payload-Filter via `payload ->> 'key'` | **1:1 / als-vorlage** — Spec Block C/H: pgvector + Cosine-Similarity-Index. Spec lässt IVFFlat-vs-HNSW offen, nennt initial IVFFlat; BP-Adapter nutzt **HNSW**. Wenn HiveGrid das `embedding`-Feld direkt auf `SearchIndexEntry` legt (statt separate `vec_*`-Tabelle), ist der Adapter eher **Vorlage** für die SQL-Cosine-Query als 1:1-Drop-in |
| `packages/vector-store/src/wrappers/tenant-scoped.ts` | `withTenantScope()` → `TenantScopedVectorStore`; `searchForTenant()` injiziert `tenantId`-must-Filter zwingend; bare `searchVectors()` wirft | **1:1** — Spec LP-1 Tenant-Isolation für den Vektor-Pfad. „kein Cross-Tenant" built-in |
| `prisma/migrations/20260517085146_04_enable_pgvector/migration.sql` | Aktiviert `pgvector`-Extension in Postgres | **1:1** — Voraussetzung für `SearchIndexEntry.embedding vector(1024)` |
| `apps/web/app/components/ui/commandPalettes/CommandPalette.tsx` + `CommandPaletteDialog.tsx` + `command.tsx` | Cmd+K/Ctrl+K-Command-Palette: Keyboard-Listener (`metaKey\|ctrlKey + k`), Lazy-Loaded Dialog, Context-Provider, `ScopedCommandAction`-Actions | **als-vorlage** — Spec Block E: Cmd+K-Modal mit Auto-Fokus, Keyboard-Navigation, Esc-Schließen. **Achtung:** liegt in der **Boilerplate-Web-App** (shadcn-basiert), NICHT in HiveGrids `packages/ui-hivegrid`. M058-Plan-Hinweis „kbar in Boilerplate-Deps" ist **faktisch falsch** — `kbar` ist nirgends in der BP (verifiziert via Grep über alle `package.json`). Das vorhandene CommandPalette ist die nutzbare Vorlage, NICHT kbar |
| `apps/web/app/services/command-registry/commandRegistry.ts` | `ScopedCommandAction`-Registry (scoped Commands für die Palette) | **als-vorlage** — Muster für Action-Registrierung im Such-Modal (Treffer-Klick → Route via `getRouteForEntity`) |
| `apps/web/app/components/layouts/buttons/SearchButton.tsx` | Such-Anker im Header (`UI_ACTION_IDS.search`, i18n `shared.search`, `onClick`-Trigger) | **als-vorlage** — Spec Block B: „Such-Anker im Header", Mobile-Trigger. UI-Stil ist BP-spezifisch → in HiveGrid neu via Subframe/`ui-hivegrid` |
| `apps/web/app/modules/crm/services/crmContactSearch.server.ts` | Permission-/Tenant-scoped Such-Service mit **Fallback Meilisearch→SQL-ILIKE** (`SEARCH_BACKEND=sql` erzwingbar, transparenter Connectivity-Fallback) | **als-vorlage** — Muster für Service-Layer (View-Model-Aufbau, Fallback-Architektur Spec Block I). Der **SQL-ILIKE-Fallback** ist KEIN `tsvector`-FTS, dient aber als Skelett für den HiveGrid-FTS-Service |
| `apps/web/app/modules/rows/services/rowSearch.server.ts` + `rowSearchOutbox.server.ts` | Generischer Row-Such-Service + Outbox-Anbindung (Domain-Write → Such-Event) | **als-vorlage** — Muster wie ein Domain-Modul seinen Adapter+Outbox verdrahtet (Spec Block D Modul-Registrierung) |
| `packages/queue/src/{names,payloads,producers}.ts` | BullMQ-Queue-Namen, typisierte Job-Payloads (inkl. `ai.embedding`), Producer mit Idempotenz (`buildIdempotentJobId`) | **1:1 / anpassen** — async Embedding-Jobs (Spec Block C `EmbeddingProviderJob`). Eigener Queue-Eintrag `outbox.search-index` ist bereits konventioniert |
| `packages/ai/src/cost/executeLLMCall.ts` | Wrappt Provider-Calls, Token/Kosten-Tracking, Quota-Pre-Check, Outbox-Event `ai.cost.event.persist` | **1:1** — Query-Embedding-Calls (Spec Block C) sollen durch denselben Cost-Wrapper (capability `embedding`), damit Such-Embedding-Kosten ins Pay-as-you-go (M088) fließen |
| `packages/ai/src/cost/spend-cache.ts` / `createSpendCache` + `@toolctrl/shared-store` `createCacheStore` | Cache-Primitive (TTL) | **als-vorlage** — Spec Block C **Query-Embedding-Caching** (24h TTL, Cache-Key SHA256(query+provider_version)). `createCacheStore` (Redis-KV, Cross-Instance-Invalidation) ist das passende Primitive |
---
## Vorhandene Daten-Modelle
**Wichtig:** Für die globale Suche selbst existiert in der Boilerplate **kein einziges Prisma-Modell** (kein `SearchIndexEntry`, kein `RecentSearch`, kein `SavedSearch`, kein `EmbeddingProviderJob` — verifiziert via `grep -rilE "RecentSearch|SavedSearch|SearchIndex|SearchHistory" prisma/schema/`). Die BP hält Such-Daten **außerhalb** von Prisma (Meilisearch-Indexe + pgvector-`vec_*`-Tabellen via Raw-SQL).
Mittelbar relevante / als-Index-Quelle dienende Modelle (Stand BP, ToolCTRL-Schema — NICHT HiveGrid-Entitäten):
| Modell | Pfad | Bezug zur Spec |
| --- | --- | --- |
| `Row` / `RowValue` (Entity-Builder) | `prisma/schema/entityBuilder.prisma` | BP indexiert CRM-Contacts als `Row`s mit `entityName="contact"`; SQL-Fallback scannt `RowValue.textValue`. HiveGrid hat dagegen **dedizierte** Tabellen (Kontakt, Verkaufsfall, …) → BP-Row-Modell ist NICHT 1:1, nur Pattern-Vorlage |
| `AICostEvent` | `prisma/schema/ai.prisma` | Append-only Cost-Log für Embedding-Calls (capability `embedding`) — Such-Embedding-Kosten landen hier. **1:1 nutzbar** als Querschnitt |
| `Outbox`-Modell | `prisma/schema/outbox.prisma` | Generische Outbox-Tabelle, von Dispatcher gelesen. **Als-Vorlage** für `search_outbox` (Spec Block H) |
| `QueueFailedJob` | `prisma/schema/queue.prisma` | DLQ-Spiegel für fehlgeschlagene Jobs — deckt Spec Block C „Provider-Failure → retrying/failed" indirekt (BullMQ-Retry + DLQ) |
| pgvector `vec_*`-Tabellen | via `PgvectorAdapter` Raw-SQL (kein Prisma-Modell) | Vektor-Speicher. HiveGrid-Spec will `embedding` **direkt auf `SearchIndexEntry`** → hier weicht HiveGrid vom BP-Layout ab |
**Fazit Daten-Layer:** Die drei Kern-Modelle der Spec (`SearchIndexEntry`, `RecentSearch`, `EmbeddingProviderJob`) sind **komplett neu** zu bauen. Die `pgvector`-Extension (Migration vorhanden) und das Cost-Event-Modell sind die einzigen direkt nutzbaren DB-Bausteine.
---
## Lücken / komplett neu zu bauen
1. **`SearchIndexEntry` Prisma-Modell + Migration** (Spec Block A) — zentrale polymorphe Cross-Entity-Index-Tabelle mit `tsvector_de` (Generated Column, deutsche FTS-Config), `embedding vector(1024)`, `permission_subject` JSONB, `embedding_status`-Enum, `entity_type`-Enum, GIN-Index (FTS), HNSW/IVFFlat-Index (Cosine), Unique `(tenant_id, entity_type, entity_id)`, RLS. **Existiert nicht in BP.**
2. **Postgres-FTS-Pfad** (Spec Block C/M056) — `tsvector`/`tsquery` mit deutscher Konfiguration, Stemming, Umlaut-Normalisierung, **Trigram-Index** für Fuzzy-Name-Match. BP nutzt Meilisearch + SQL-ILIKE — **kein `tsvector`-FTS vorhanden**.
3. **Hybrid-Fusion (RRF)** (Spec Block C) — Reciprocal-Rank-Fusion / gewichtetes Score-Merging FTS↔Semantik (Default 70/30), plus Boosts (`last_activity_at`, exakter Title-Match, User-eigene Entitäten). BP hat nur einen **einseitigen** semantischen Rank-Merge (`retrieve.ts`), keine FTS↔Vektor-Fusion. **Neu.**
4. **`RecentSearch` Prisma-Modell + Service** (Spec Block G) — pro User letzte 50 Queries, `applied_filters` JSONB, `clicked_entity_*`, Retention (50 / 90 Tage), Privacy (privat pro User, DSGVO-Export, Hard-Delete bei Account-Löschung). **Existiert nicht.**
5. **Saved-Searches** (M058-Plan: bis 5 pro User; Spec: Phase 2) — User speichert Query+Filter unter Namen. **Modell + UI neu.** (Spec markiert als Phase 2, M058-Plan zieht es nach Phase 1 — Klärungs-/Scope-Punkt.)
6. **`EmbeddingProviderJob` Prisma-Modell** (Spec Block A) — Worker-Queue-State (provider, status, attempts, `last_error_text`, completed_at). BP nutzt stattdessen BullMQ-Job-State + DLQ; ein **persistiertes Job-Modell mit Klartext-Error** fehlt → neu (oder bewusst durch BullMQ+`QueueFailedJob` ersetzen).
7. **SearchAdapter-Voll-Interface** (Spec Block D) — BP-`SearchIndexSchema` deckt nur `buildSearchableContent`-Äquivalent (Feld-Whitelist). Es fehlen: `buildPermissionSubject(entity) → JSONB`, `getRouteForEntity(entity) → string`, `getDisplayMetadata(entity) → {icon, badge}`, `filterVisibleForUser(entries, perms) → entries[]`, `triggers`-Liste. **Erweiterung/Neu.**
8. **Permission-aware Server-Side-Filter** (Spec Block F / M057) — `filterVisibleForUser` pro Adapter, server-side **vor** Ausgabe (kein Client-Hiding), Trefferanzahl-Konsistenz (nur erlaubte Treffer), **Field-Level-Redact** für Snippets (M057-Erfolgskriterium: Volume-Wert im Snippet redacten). BP hat nur tenant-scoped Filter + `sanitizeDocument` (Index-Zeit-Whitelist), **kein per-User-Permission-Filter zur Query-Zeit** und **kein Snippet-Field-Redact**. **Neu** (Kern von M057).
9. **`indexedEntities`-Registry für HiveGrid** (INTEGRATION-CONTRACTS §8, `packages/db/src/search/indexed-entities.ts`) — Map `entity → {table, searchable[], permission}`. BP-`ALL_SEARCH_INDEXES` ist die Vorlage, aber auf ToolCTRL-Entitäten gemünzt. **Neu mit HiveGrid-Entitäten** (contact, company, lead, deal, project, order, form_submission, user, file, task).
10. **Domain-Adapter pro HiveGrid-Modul** (Spec Block D, M001/M042-M046 abhängig) — je Adapter für Kontakt/Firma/Lead (M01), Verkaufsfall (M03), Projekt/Auftrag (M04), Form-Submission (M05), User/Mitarbeiter (M14), File (M23), Task (M26). **Komplett neu** (jeweils im Ziel-Modul zu bauen).
11. **Such-UI in `packages/ui-hivegrid`** (Spec Block E, M058 + M015) — Cmd+K-Modal + Vollbild-Such-View (`/{tenant}/search`) als HiveGrid-VM-Component (View-Model vom Loader, keine Server-Imports im UI-Paket, i18n-Keys, Subframe-Adapter). Das BP-CommandPalette ist nur **Vorlage** (anderer UI-Stack/-Paket). **Neu** inkl. Filter-Chips, Tastatur-Navigation, Snippet-Highlight, Recent-Searches-Anzeige, Sortier-Optionen, Cursor-Pagination, Mobile-Vollbild.
12. **Routen + Wiring** — `/{tenant}/search` Route, globaler Cmd+K-Layout-Mount, Header-Such-Anker, Mobile-Tab/Header (INTEGRATION-CONTRACTS §3/§13/§14), Feature-Flag (M006), Audit (M001/M021: Suchanfrage + Trefferanzahl loggen — Spec Block F). **Neu.**
13. **`search_outbox` + Trigger/Outbox-Wiring** (Spec Block H) — HiveGrid-eigenes Outbox-Modell + Re-Index-Trigger auf den Quell-Tabellen. BP-Outbox-Router/Dispatcher ist Vorlage; das HiveGrid-Modell + Verdrahtung an die dedizierten Tabellen ist neu.
14. **Query-Embedding-Cache** (Spec Block C) — SHA256-Key, 24h-TTL. Primitive (`createCacheStore`) vorhanden, die Such-spezifische Cache-Schicht ist neu.
15. **Klartext-Hinweise** (Spec Block I / M002) — alle 7 Fehler-/Hinweis-Texte (keine Treffer, Permission-Reduktion, Embedding-Down-Banner, <2 Zeichen, Indexierungs-Verzögerung, Re-Index-Lauf, Server-Fehler) als i18n-Keys im M002-Catalog. **Neu.**
---
## Empfohlenes Vorgehen
**Phase 0 — Architektur-Entscheid (blockierend, vor M056-Bau):**
- Klären: **Postgres-FTS (Spec/Plan)** vs. **Meilisearch (BP)**. Memory + M056-Plan sagen FTS („kein Extra-Service"). Empfehlung: **bei FTS bleiben**, BP-Meilisearch-Pakete als **Pattern-Vorlage** (Outbox, Schema-Registry, Tenant-Scope, Fallback) statt als Engine nutzen.
- Klären: `embedding` **direkt auf `SearchIndexEntry`** (Spec) vs. separate pgvector-`vec_*`-Tabelle (BP). Spec-Variante reduziert Joins, weicht aber von `PgvectorAdapter` ab → Adapter wird Vorlage für die Cosine-Query.
- Klären: Saved-Searches Phase 1 (M058-Plan) vs. Phase 2 (Spec).
- Klären: `EmbeddingProviderJob`-Tabelle (Spec) vs. BullMQ-Job-State + `QueueFailedJob` (BP-Konvention).
**M056 — Such-Index (zuerst BP-Bausteine übernehmen):**
1. `pgvector`-Migration aus BP übernehmen (1:1).
2. `SearchIndexEntry`-Modell + Migration **neu** bauen (`tsvector_de` Generated Column, GIN-Index, Cosine-Index, RLS).
3. `EmbeddingProvider`-Pakete (`@toolctrl/ai/embeddings` factory/scaleway/aleph-alpha/embed-and-upsert/chunking/retrieve) **1:1** übernehmen (EU-1024-dim ist exakter Match).
4. Outbox-Publisher + Search-Indexer-Worker + RAG-Indexer-Worker als **Vorlage** übernehmen, Ziel-Sink auf `SearchIndexEntry` (FTS-Felder synchron, Embedding async) umbauen; `EmbeddingProviderJob`-State + Retry-Backoff ergänzen.
5. `indexedEntities`-Registry (HiveGrid) + erste Adapter (Kontakt/Firma/Lead aus M042-M046, da Vorbedingung) bauen.
6. FTS-Pfad (`tsquery` deutsch + Trigram) + Hybrid-Fusion (RRF + Boosts) **neu** — kein BP-Äquivalent.
**M057 — Permission-aware (auf M056 + M004):**
7. `permission_subject`-Befüllung pro Adapter (`buildPermissionSubject`).
8. `filterVisibleForUser` server-side im Query-Pfad (vor Ausgabe), `withTenantScope`-Pattern aus BP als Tenant-Basis, Permission-Filter darauf neu.
9. Field-Level-Snippet-Redact (M057-Erfolgskriterium Volume-Feld) — neu; BP `sanitizeDocument` deckt nur Index-Zeit, nicht Query-Zeit-Redact.
10. Trefferanzahl-Konsistenz + Klartext-Hinweis „weitere Treffer nicht sichtbar" (Schwellwert klären — offener Spec-Punkt).
11. Audit-Wiring (Suchanfrage + Trefferanzahl, M001/M021).
**M058 — UI + Saved-Searches (auf M056/M057 + M015):**
12. Cmd+K-Modal in `packages/ui-hivegrid` **neu** (BP-`CommandPalette` als Interaktions-Vorlage: Keyboard-Listener, Lazy-Dialog, Esc/Focus). **Nicht** kbar (existiert nicht in BP).
13. Vollbild-Such-View `/{tenant}/search` (Filter-Sidebar, Sortier, Cursor-Pagination, URL-State) — neu.
14. `RecentSearch`-Modell + Service (Retention, Privacy, DSGVO) — neu.
15. Saved-Searches (falls Phase 1) — neu.
16. Query-Embedding-Cache (`createCacheStore`, 24h-TTL) — Primitive aus BP, Schicht neu.
17. Klartext-Catalog (Spec Block I) + i18n-Keys; Feature-Flag (M006); Header-Anker + Mobile (BP-`SearchButton` als Stil-Vorlage, neu via Subframe).
**Reihenfolge-Prinzip:** Erst die **EU-Embedding-Pipeline + pgvector + Outbox/Worker-Muster aus der BP** übernehmen (höchster 1:1-Anteil), dann den **FTS-Pfad, das `SearchIndexEntry`-Modell, die Fusion und den Permission-Query-Filter** neu bauen (kein BP-Äquivalent), zuletzt die **HiveGrid-UI** (BP-CommandPalette nur als Vorlage).
@@ -0,0 +1,116 @@
# Boilerplate-Abgleich — Modul M25 (Calendar-Sync)
**Modul-ID:** M25 (Block 5 / Pflichtenheft 25.x)
**Name:** Calendar-Sync (Google + Microsoft, bidirektional)
**Milestones:** M118 (Google+MS via Apps-Engine), M119 (Termin-Sync + Konflikt), M120 (Display + Modul-Verknüpfung + Activity-Stream)
**Block:** block5.md §§ 25.1–25.7 (+ Phase-2 25.8)
**Reuse-Level:** **low**
> Begründung Reuse-Level: Es gibt in der Boilerplate **keinen** Calendar-, OAuth2-Connect-, Google-Calendar- oder Microsoft-Graph-Code und **kein** zugehöriges Datenmodell. Die fünf fachlichen Kern-Entitäten (`CalendarConnection`, `CalendarEvent`, `CalendarEventMapping`, `CalendarSyncCursor`, `CalendarWebhookEvent`), das `CalendarAdapter`-Interface, der bidirektionale Sync, das Webhook-Push-Subsystem und die Verfügbarkeits-Berechnung müssen komplett neu gebaut werden. Was vorhanden ist, ist **Querschnitt-Fundament** (Token-Verschlüsselung, SSRF-Schutz, Outbox/Queue/Cron, Notifications, Audit, Adapter-Registry, Webhook-HMAC-Verify, EU-Provider-Pattern) — solide, aber nur Bausteine, kein fertiges Modul. Damit klar über „none", aber nicht „medium".
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `packages/foundation/src/adapters.ts` | `defineAdapter()` / `getAdapter()` + `AdapterStatus` (`not_configured`/`configured`/`healthy`/`degraded`/`unavailable`) | als-vorlage — Registry-Muster für das `CalendarAdapter`-Interface (Block G). Das vorhandene `AdapterDefinition` ist generisch (`key`/`status`/`adapter`); die calendar-spezifische Methoden-Signatur (`initiateOAuth`/`fetchEventsDelta`/`createEvent`…) ist neu zu definieren. Status-Enum 1:1 nutzbar für Connection-Health. |
| `apps/web/app/utils/api/.server/CryptoApi.ts` | AES-256-GCM `encrypt`/`decrypt` (versionierter Payload `version:iv:tag:ciphertext`), Key via Hash | anpassen — direktes Vorbild für `oauth_refresh_token`/`oauth_access_token` at-rest (Block C, Token-Storage). Spec verlangt envelope-encryption mit tenant-spezifischem KMS-Schlüssel — CryptoApi ist single-key; für Tenant-Key/KMS erweitern. |
| `apps/web/app/utils/leads/marketingIntegrations.db.server.ts` | `encryptSensitive`/`decryptSensitive` über `SENSITIVE_KEYS` + `CRYPTO_TAG`-Präfix; „Admin-UI zeigt entschlüsselte Secrets nie erneut" | als-vorlage — exakt das Pattern für selektiv verschlüsselte Token-Felder in `CalendarConnection` + Never-Redisplay-Regel (Block C: „Logging niemals Token-Klartext"). |
| `packages/foundation/src/outbound-http.ts` (Subpath `@toolctrl/foundation/outbound-http`) | `fetchOutboundHttpWithRedirects`, `validateOutboundHttpUrl`, SSRF-Block (private/internal IPv4+IPv6, DNS-Resolve, Redirect-Limit, Timeout, Response-Size-Limit) | 1:1 — für jeden ausgehenden Provider-Call (Google/MS Token-Endpoint, Calendar-API, Webhook-Subscribe). Pflicht-Querschnitt. |
| `packages/api-client-ts/src/webhooks.ts` + `apps/api/src/webhooks/mailersend-signature.ts` | HMAC-SHA256-Verify mit `timingSafeEqual`, Timestamp-Toleranz, Header-Parsing (`webhook-signature`), typisierte Failure-Codes | als-vorlage — Vorbild für `verifyWebhookSignature(headers, body)` (Block G/E). Provider-spezifisch anzupassen (Google `X-Goog-Channel-Token`, MS `clientState`) — nicht HMAC, aber Verify-Struktur (constant-time, fail-closed, Failure-Codes) übernehmbar. |
| `packages/db/src/outbox.ts` + `prisma/schema/outbox.prisma` (`OutboxEvent`) | `publishOutboxEvent(tx, args)` in `$transaction`, `dispatchKey @unique` (Idempotenz), `eventType`-Pattern `<domain>.<entity>.<verb>` | 1:1 — Webhook-Empfang → DB-Write → async Verarbeitung (Block E, Schritt 3-5). `CalendarWebhookEvent` kann eigenständig sein ODER über Outbox laufen; Outbox-Idempotenz (`dispatchKey`) deckt „Provider duplizieren Notifications" (Block E Idempotenz) ab. |
| `apps/worker/src/outbox/event-router.ts` (`DEFAULT_OUTBOX_ROUTING`) | Prefix→Queue-Routing (longest-prefix-wins) | anpassen — neuen Prefix `calendar.` (z.B. `calendar.sync.*`) ergänzen, auf eine Worker-Queue routen. |
| `packages/queue` (`enqueueJob`, `buildIdempotentJobId`, Prioritäten) + `apps/worker` (Handler-Registry, Cron-Scheduler `*-cron.ts`, Gruppen/Concurrency) | BullMQ-Wrapper + single-instance Cron-Scheduler-Muster (`dsgvo-grace-cron.ts`, `lead-stale-trigger.ts`, `ai-cost-*-cron.ts`) | als-vorlage — (a) Sync-Worker (Webhook-Delta-Pull), (b) **Webhook-Subscription-Renewal-Cron** (Block E: alle 6h, Subscriptions die in 24h ablaufen). Cron-Job-Skelett + Idempotenz direkt übernehmbar. |
| `packages/foundation/src/notifications.ts` (`notify`, `bundleNotifications`, `assertI18nKey`, `NotificationPreferenceChannel` IN_APP/EMAIL) + `prisma/schema/notifications.prisma` | Tenant-pflichtige Notifications mit Preference-Check, Upsert per `sourceKey`, Bündelung | 1:1 — Konflikt-Notifications (Block F), Verbindungsfehler-Mails (Block C Re-Connect), „externe Version gewinnt"-Hinweis. INTEGRATION-CONTRACTS §7 verlangt ohnehin `notify()` statt Direkt-Mail. |
| `packages/foundation/src/audit.ts` (`auditEvent`, `withAuditMiddleware`) + `prisma/schema/core.prisma` (`AuditEvent`, Hash-Chain) | Audit-Trail mit Tamper-Evidence | 1:1 — Connect/Disconnect/Konflikte (AV #6, Spec „Audit-Trail-Default"). Da OAuth-Connect/Webhook-Calls außerhalb Prisma laufen, **manuelles** `auditEvent` nötig (INTEGRATION-CONTRACTS §6 Ausnahme). |
| `packages/foundation/src/permissions.ts` (`withTenantScope`, `withPermission`, `withOwnerCheck`, `requirePermission`; 404-statt-403) | Tenant-Scope + Permission- + Owner-Gating | 1:1 — `calendar.connect_own` (Owner-Check `connection.user_id = self`) + `calendar.admin` (Block I). Permission-Denied → 404 deckt Bauprinzip A.5.3 / DoD ab. |
| `prisma/schema/permissions.prisma` (`Role`/`Permission`/`RolePermission`/`UserRole`) | RBAC-Fundament | als-vorlage — die zwei atomaren Permissions + Bundles (Block I) sind neu zu seeden; das Modell trägt sie. HiveGrid-seitig `packages/entitlements/src/permissions.ts` (INTEGRATION-CONTRACTS §10). |
| `packages/foundation/src/idempotency.ts` (`withIdempotency`) + `prisma/schema/core.prisma` (`IdempotencyKey`) | Replay-sichere Mutationen, In-Process-Lock, Fingerprint-Hash | anpassen — für Webhook-Verarbeitung + Create/Update-Push (Optimistic-Locking-Ergänzung, Block F). |
| `packages/feature-flags` (`evaluateFlag`, Targeting, React-Hooks) + `prisma/schema/featureFlags.prisma` | Feature-Flag-Engine | 1:1 — DoD verlangt „Modul ohne Deploy abschaltbar". Connect-Button + Routes hinter `calendar`-Flag. |
| `packages/i18n` (Namespaces/Locales, `formatDate`/`formatRelativeTime`, `assertI18nKey`) | i18n + locale-explizite Datums-Formatierung | 1:1 — alle Klartext-Hinweise (Block J), Termin-Zeiten in Detail-View. DoD: alle UI-Texte als i18n-Keys. |
| `packages/view-models/src/index.ts` (`VMState`, `ActionCallbacks`, `RegistryGate`, `SidebarRegistryItem`, `ready`/`empty`) | VM-State-Maschine + Shell-Registry-Typen | 1:1 — Loader baut VM, UI empfängt nur VM+Actions (DoD Code-Layer + INTEGRATION-CONTRACTS §11). Loading-/Empty-/Error-State (Klartext-DoD). |
| `packages/foundation/src/errors.ts` (`CatalogError`, `foundationErrors`, `toPinoLogObject`) | Strukturierte Klartext-Fehler (`code`/`status`/`i18nKey`) | als-vorlage — Adapter-Klartext-Codes (Block G: `auth_token_expired`, `rate_limit_exceeded`, `provider_outage` …). Catalog-Einträge sind HiveGrid-spezifisch neu zu registrieren. |
| `packages/logger` (`createLogger` mit Redaction von `token`/`secret`/`apiKey`/`authorization`) | Pino-Logger mit Secret-Redaction | 1:1 — Block C „Logging niemals Token-Klartext" wird durch die eingebauten Redact-Paths abgedeckt. |
| `prisma/schema/notifications.prisma` (`DeviceToken`) + `apps/worker/src/jobs/push-notification.ts` | Push-Token + Push-Versand | 1:1 — Termin-Erinnerungen / Konflikt-Push (Pflichtenheft 22.7.4). Über `notify()` (Kanal-Entscheidung im Notification-Service). |
| `prisma/schema/entityBuilder.prisma` (`EntityView.layout` kennt `"calendar"`) | EntityView-Calendar-Layout-Enum (No-Code-Builder) | als-vorlage (schwach) — nur ein Layout-String, **keine** fertige Calendar-View-Component. Für M120 (Tag/Woche/Monat-Display) liefert die BP keinen Renderer; Component ist neu (Subframe-Design + ui-hivegrid). |
| `apps/api` (Hono-App, `route-access-inventory.ts`, `webhook-signature`-Route-Klasse, `error-envelope`, `request-id`, `body-limit`) | Public-API-Runtime mit Auth-Klassen-Matrix inkl. `webhook-signature` | als-vorlage — die öffentlichen Webhook-Endpoints `/webhooks/calendar/{provider}` (Block E, keine User-Auth, Provider-Signature) passen in die bestehende `webhook-signature`-Route-Klasse + Middleware-Kette. |
| `prisma/schema/appsEngine.prisma` (`App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem`) | Data-Sync-„Apps-Engine": App-Installation pro Tenant, **`DataConnection.config` = „Encrypted JSON credentials"**, Sync-Run mit `executionMode` manual/scheduled/webhook + Metriken (`itemsProcessed/Created/Updated/Failed`) + Rollback (`AppSyncItem.rolledBackData`) | als-vorlage — **wichtigstes Boilerplate-Modell für M118** (Milestone-Titel: „via Apps-Engine"). `DataConnection` kann die OAuth2-Verbindung tragen ODER als Muster für `CalendarConnection` dienen. `AppSyncRun`/`AppSyncRunLog` ist ein fertiges Sync-Lauf-/Log-Modell (Sync-Status, Konflikt-Logs Block F „letzte 50 Konflikte"). **Aber:** keine Provider-Logik, kein Cursor/Webhook-Modell, kein Event-Cache — nur Hülle. Entscheidung nötig: `appsEngine`-Modelle wiederverwenden vs. dedizierte `Calendar*`-Modelle in `app.prisma`. |
---
## Vorhandene Daten-Modelle (konkrete Prisma-Modelle mit Pfad)
Es gibt **kein** Calendar-spezifisches Prisma-Modell. Nutzbare/angrenzende Modelle:
- `prisma/schema/appsEngine.prisma` — `App`, `DataConnection` (verschlüsselte Credentials), `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem`. **Am nächsten** an „Connection + Sync-Lauf + Konflikt-Log". Deckt aber `CalendarEvent`-Cache, `CalendarEventMapping`, `CalendarSyncCursor`, `CalendarWebhookEvent` **nicht** ab.
- `prisma/schema/outbox.prisma` — `OutboxEvent` (`dispatchKey @unique`, Idempotenz, `priority`, `lockedUntil`). Für Webhook→Worker-Entkopplung + Dedup.
- `prisma/schema/notifications.prisma` — `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken`. Konflikt-/Fehler-Notifications + Push.
- `prisma/schema/core.prisma` — `AuditEvent` (Connect/Disconnect/Konflikt-Audit), `IdempotencyKey`, `Credential` (globaler Secret-Store, `value Json`).
- `prisma/schema/accounts.prisma` — `User`, `Tenant`, `TenantUser` (User-/Tenant-Anker; pro-User-Connection-Scoping). `WorkspaceAuthProvider` (inbound SSO/OIDC — **nicht** outbound OAuth-Connect, nur als Feld-Vorbild `authorizationUrl`/`tokenUrl`/`clientId`).
- `prisma/schema/permissions.prisma` — `Permission`/`Role`/`UserRole` für `calendar.connect_own` / `calendar.admin`.
- `prisma/schema/featureFlags.prisma` — `FeatureFlag` (Modul-Abschaltung).
- `prisma/schema/app.prisma` — **leerer Platzhalter** (`// custom database models`): vorgesehener Ort für die neuen `Calendar*`-Modelle.
**Neu zu erstellende Modelle (alle, gemäß Spec Block A):** `CalendarConnection`, `CalendarEvent`, `CalendarEventMapping`, `CalendarSyncCursor`, `CalendarWebhookEvent` — in `prisma/schema/app.prisma` (oder eigene `calendar.prisma`). Pflicht: tenant-scoped + RLS, FK auf `User`/`Tenant`, Eintrag in `packages/db/src/dsgvo/user-fk-map.ts` (CI-Gate bricht sonst — DSGVO Block, Spec „Account-Löschung → harte Löschung der Event-Caches").
---
## Lücken / komplett neu zu bauen
**Datenmodell**
- Alle fünf `Calendar*`-Modelle inkl. Indizes/Constraints (Block A) — nicht vorhanden.
- `user-fk-map.ts`-Einträge für die neuen User-FK-Modelle (DSGVO, Pflicht-Gate).
**Provider-Integration (Kern, fehlt vollständig)**
- `CalendarAdapter`-Interface mit allen Methoden (Block G): `initiateOAuth`, `handleOAuthCallback`, `refreshAccessToken`, `listCalendars`, `subscribeWebhook`/`unsubscribeWebhook`/`renewWebhook`, `fetchEventsInitial`/`fetchEventsDelta`, `createEvent`/`updateEvent`/`deleteEvent`, `verifyWebhookSignature`.
- **Google-Adapter** (Calendar API v3, Push-Notification-Channels, `syncToken`, `If-Match`/etag) — neu.
- **Microsoft-Adapter** (Graph Calendar, Subscriptions/`deltaLink`, `clientState`, Azure-AD-Multi-Tenant-App + Admin-Consent-Flow) — neu.
- OAuth2-Connect-Flow Ende-zu-Ende (state-Token signiert mit `tenant_id+user_id`, code↔token-Exchange, User-Info-Fetch) — **kein** outbound-OAuth-Connect existiert (nur inbound SSO/OIDC + ein GitHub-Service `apps/web/app/utils/integrations/githubService.ts`, das als loses Vorbild taugt).
**Sync-Engine (fehlt)**
- Bidirektionaler Sync (Block D): Schreib-Pfad HiveGrid→extern (Booking/Projekt/Plantafel/Tasks-Spiegelung, Mehrfach-Provider-Mappings), Lese-Pfad extern→HiveGrid (Initial-Full-Pull -7/+60, inkrementell via Cursor, „Free"-Filter, Private→„Privat"-Titel-Reduktion).
- Konflikt-Handling Last-Write-Wins-mit-Sichtbarkeit (Block F): 412-Precondition-Failed-Behandlung, „externe Version gewinnt" + Restore-Option, Mapping-Cleanup bei extern gelöschten Events, `last_known_internal_version_token`-Check.
**Webhook-Subsystem (fehlt fachlich)**
- Provider-Webhook-Empfangs-Endpoints `/webhooks/calendar/{provider}` mit Provider-spezifischer Signature-Verifikation (Google `X-Goog-Channel-Token`, MS `clientState`) — generisches HMAC-Verify-Muster vorhanden, aber Provider-Logik neu.
- **Subscription-Renewal-Cron** (Google ~1 Woche, MS ~3 Tage; alle 6h, Renewal in 24h-Fenster) — Cron-Skelett vorhanden, Logik neu.
- 410-Gone → Full-Resync.
**Verfügbarkeits-Berechnung (fehlt, Block H)**
- Slot-Engine: Working-Hours + interne Events + externe Busy-Events + Plantafel → freie Slots; 30-Tage-Slot-Cache mit Invalidierung bei `CalendarEvent`-Update; DSGVO-Filterung (Booking erhält nur `is_available`-Boolean, nie Event-Inhalt). Hängt an Modul 6/M126.
**Querschnitt-Wiring (HiveGrid-seitig neu zu verdrahten)**
- Sidebar-Eintrag „Termine" (Gruppe ARBEITEN, M120) + Settings-Eintrag „Integrationen → Kalender" in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts`.
- Routes (Block B / INTEGRATION-CONTRACTS §3): `settings/integrationen/calendar/...` + Connect/Callback + `:connection_id`-Detail.
- Calendar-Display-Component (Tag/Woche/Monat) für M120 — kein BP-Renderer vorhanden (Subframe-Design Pflicht, siehe Memory `feedback_subframe_mandatory`).
- Activity-Stream-Hooks (`logActivity`): `calendar_connected`/`_disconnected`/`_sync_error`/`appointment_synced_external` — bewusst NICHT pro Event-Update (M120 / INTEGRATION-CONTRACTS §5).
- Off-Boarding-Hook (Block I / Modul 27): User-Deaktivierung → Connections `paused`, Webhooks abmelden; Hard-Delete → Caches hart löschen.
- Tenant-spezifische KMS/Envelope-Encryption für Refresh-Tokens (Spec verlangt Tenant-Key; CryptoApi ist single-key — Erweiterung nötig).
**Anti-Liste (Phase 2, NICHT bauen):** iCal-Feed, Resource-Calendar, Tenant-Zentral-Connection, Apple/iCloud-CalDAV, FastMail, konfigurierbares Sync-Fenster, Multi-Calendar-Settings-UI, Color-Sync, KI-Konflikt-Resolution, erweiterte Zeitzonen, Recurring-Event-Single-Instance-Edits.
---
## Empfohlenes Vorgehen
1. **Querschnitt zuerst aus BP verdrahten (1:1):** `withTenantScope`/`withPermission`/`withOwnerCheck` (Permissions), `notify`/`auditEvent` (Konflikt/Connect-Audit), `VMState`/`ActionCallbacks` (VM-Layer), `i18n`-Keys + `formatDate`, `feature-flags` (Modul-Abschaltung), `outbound-http` (SSRF für alle Provider-Calls), `logger` (Token-Redaction). Permissions `calendar.connect_own`/`calendar.admin` in `packages/entitlements/src/permissions.ts` + seed.
2. **Datenmodell (M118):** Die fünf `Calendar*`-Modelle in `app.prisma`/`calendar.prisma` anlegen (Block A, inkl. Indizes/Unique-Constraints), `user-fk-map.ts` ergänzen, Forward-Only-Migration mit Inverse-SQL-Kommentar (DoD). **Vorab entscheiden:** `appsEngine`-Modelle (`DataConnection`/`AppSyncRun`/`AppSyncRunLog`) wiederverwenden für Connection+Sync-Lauf+Konflikt-Log, oder dedizierte Modelle — `appsEngine` ist die nächste Vorlage, deckt aber Cache/Cursor/Webhook nicht und vermischt zwei „Apps-Engine"-Bedeutungen (siehe Inventur integrations-ki §4.4).
3. **Token-Storage (M118):** `CryptoApi`/`marketingIntegrations`-Verschlüsselungs-Pattern als Vorlage; auf Tenant-Key/KMS-Envelope erweitern (Spec/AV #13). Never-Redisplay-Regel übernehmen.
4. **Adapter-Pattern (M118):** `CalendarAdapter`-Interface (Block G) definieren; Google- und MS-Adapter neu implementieren; Klartext-Fehler-Codes als `CatalogError`-Katalog (Block G/J) registrieren. OAuth-Connect/Callback-Routes über `apps/api`-Webhook/Auth-Klassen-Muster + `outbound-http`.
5. **Webhook + Sync-Worker (M118→M119):** Public Webhook-Endpoints (Provider-Signature-Verify nach `mailersend-signature`-Muster) → `CalendarWebhookEvent`/Outbox (`dispatchKey`-Idempotenz) → BullMQ-Sync-Worker (Cursor-Delta-Pull). `calendar.`-Prefix in `event-router.ts`. Renewal-Cron nach `*-cron.ts`-Muster.
6. **Bidirektionaler Sync + Konflikt (M119):** Schreib-Pfad (Spiegelung Booking/Projekt/Plantafel, Mehrfach-Provider-Mappings), Lese-Pfad (Privacy-Reduktion), Last-Write-Wins-mit-Restore-Option via `notify`.
7. **Verfügbarkeit + Display (M119/M120):** Slot-Engine + Cache (an Modul 6/M126 koppeln); Calendar-View-Component via Subframe neu (kein BP-Renderer); Activity-Stream-Hooks; Sidebar/Route/Breadcrumb-Wiring; Off-Boarding-Hook.
8. **DoD-Schluss je Milestone:** Smoke-Test je Route, Hot-Swap-Drill gegen `ui-mock`, Visual-Snapshot, Permission-Denied→404, Feature-Flag, Customer-Portal-Sichtbarkeit explizit (default NICHT sichtbar).
---
### Quellen-Hinweis
Alle Aussagen direkt in `/Users/raffaelsheikh/Documents/GitHub/boilerplate` verifiziert (Greps auf `calendar|ical|google.?calendar|microsoft.?graph|oauth2|encrypt`, gelesene Dateien: `foundation/src/adapters.ts`, `CryptoApi.ts`, `marketingIntegrations.db.server.ts`, `api-client-ts/src/webhooks.ts`, `mailersend-signature.ts`, `appsEngine.prisma`, `entityBuilder.prisma`, `outbox/event-router.ts`). **Kein** Calendar-/OAuth-Connect-/Provider-Code gefunden.
@@ -0,0 +1,138 @@
# Boilerplate-Abgleich: M26 — Tasks / Aufgaben
| Feld | Wert |
| --- | --- |
| Modul-ID | M26 |
| Name | Tasks / Aufgaben |
| Milestones | M071 (CRUD + Zuweisung), M072 (Listen pro Modul / Inline), M073 (Erinnerungen + Push + Email), M074 (Aufgaben-Sync ERP / KWP) |
| Block | Block 3 · Pflichtenheft §26 (26.1–26.10) |
| Spec | `/Users/raffaelsheikh/Documents/GitHub/HiveGrid-Cloud/_bmad-output/specs/M26-tasks.md` |
| **Reuse-Level** | **low–medium** (Querschnitt-Infrastruktur reif; das eigentliche Task-Domänen-Modell muss neu gebaut werden — `RowTask` deckt es NICHT ab) |
> Boilerplate-Wurzel: `/Users/raffaelsheikh/Documents/GitHub/boilerplate` (nur gelesen).
---
## Kern-Befund (Anti-Erfindung)
In der Boilerplate existiert **`RowTask`** (`prisma/schema/entityBuilder.prisma:434`) — aber das ist **NICHT** das HiveGrid-Task-Modul. `RowTask` ist eine **leichtgewichtige Checklisten-Sub-Entität, die fest an eine Entity-Builder-`Row` hängt** (Mini-To-do im Row-Overview), vergleichbar mit `RowComment`/`RowTag`. Es fehlen die zentralen Eigenschaften aus der M26-Spec:
| M26-Anforderung | `RowTask` (Boilerplate) | Lücke |
| --- | --- | --- |
| `tenantId` (direkt am Task, für Tenant-Scope-Filter + Indizes) | fehlt (nur indirekt über `row.tenantId`) | **ja** |
| `status` Enum `open\|in_progress\|done` | nur `completed Boolean` | **ja** (kein In-Arbeit-Status) |
| `priority` Enum `low\|normal\|high\|urgent` | fehlt | **ja** |
| `assigned_to_group_id` (Gruppen-Zuweisung) | nur `assignedToUserId` (Person) | **ja** |
| `source` Enum (`manual\|chat_message\|form_submission\|automation`) + `source_ref` JSON | fehlt | **ja** |
| `linked_entity_type` / `linked_entity_id` (polymorphe Verknüpfung zu Contact/Company/Lead/Deal/Project) | fehlt — Bindung ist hart an `rowId` einer Entity-Builder-Row | **ja** (Architektur-Unterschied) |
| `archived_at` (Auto-Archivierung) | fehlt | **ja** |
| `deadline_at` | vorhanden (`deadline DateTime?`) | nein |
| `completed_at` / `completed_by_user_id` | vorhanden (`completedAt`, `completedByUserId`) | nein |
| `created_by_user_id` | vorhanden (`createdByUserId`) | nein |
| `TaskWatcher` (M:N) | fehlt | **ja** |
| Reminder-Notifications / Auto-Archivierungs-Cron | fehlt | **ja** |
| `tasks.*`-Permissions (read_own/read_all/write/admin) + Owner-Check + 404-statt-403 | fehlt | **ja** |
**Konsequenz:** `RowTask` (Modell + UI `RowTasks.tsx` + Service `rowTasks.db.server.ts`) ist als **Vorlage/Muster** wertvoll (Felder, Service-Layer-Struktur, Event-DTOs, i18n-Keys, Toggle-UI), aber nicht 1:1 übernehmbar. Das HiveGrid-`Task`-Modell ist eine **eigenständige Entität** in `prisma/schema/app.prisma`, kein Anhängsel an Entity-Builder-Rows.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `prisma/schema/entityBuilder.prisma:434` (`model RowTask`) | Felder-Inspiration für das neue `Task`-Modell (title, description, completed/completedAt/completedByUserId, assignedToUserId, deadline, createdByUserId) | **als-vorlage** — Feld-Set übernehmen, aber um tenantId/status/priority/group/source/linked_entity/archived erweitern |
| `apps/web/app/utils/db/entities/rowTasks.db.server.ts` | Service-Layer-Muster: `get*/create/update*/delete*/getAll*` mit `include`-Pattern, `getAllRowTasks({ completed, assignedOrCreatedUserId, tenantId })` (Owner-/Filter-Logik) | **als-vorlage** — Repository-Struktur + „assigned OR created"-Owner-Filter direkt übertragbar (entspricht Owner-Check der M26-Spec) |
| `apps/web/app/components/entities/rows/RowTasks.tsx` | UI-Muster: Toggle-Erledigen, Quick-Add (nur Titel), Inline-Delete mit `canDelete`-Permission-Gate, Empty-State, i18n-Keys (`shared.addTask`, `shared.noTasks`, `models.rowTask.plural`) | **als-vorlage** — passt zum „Inline-Aufgaben-Bereich" (M072) + Quick-Add (Block E.1); in HiveGrid via Subframe-Adapter neu zu bauen, Logik-Muster übernehmbar |
| `apps/web/app/modules/events/dtos/RowTasks{Created,Updated,Deleted}Dto.ts` + `apps/web/app/modules/events/utils/EventUtils.ts` (Cases `row.tasks.created/updated/deleted`) | Event-/Activity-DTO-Muster + Klartext-Übersetzung pro Event-Typ | **als-vorlage** — Vorlage für M26-Activity-Typen (`task_created/assigned/completed/reopened/deadline_changed/priority_changed/archived`, Block G) |
| `prisma/schema/notifications.prisma` (`NotificationDelivery`, `UserNotificationPreference`, `DeviceToken`) | Vollständiges Notification-Datenmodell: In-App + Email + Push (APNs/FCM/WebPush), Per-Channel/Category-Präferenzen, `@@unique([tenantId, userId, channel, sourceKey])` (Dedup) | **1:1** — direkt für M073 Reminder-Versand nutzbar (Empfänger = Assignee/Group/Watcher) |
| `packages/db/src/notifications.server.ts` | Notifications-Repository: `createNotificationDelivery`, `markNotificationDeliveryStatus`, `upsertUserNotificationPreference`, `getUserNotificationPreferences`, `getNotificationStreamSnapshot`, `redactNotificationDiagnosticText`; Typen `NotificationPreferenceChannel`/`...DeliveryStatus` | **1:1** — Schreib-/Lese-Fläche für Reminder-Deliveries + User-Toggle „Task-Erinnerungen" (M073, Block F.1) |
| `apps/web/app/modules/notifications/services/.server/NotificationService.ts` | Versand-Service (Novu-Integration, `send`, `sendToRoles`, Channel→Category-Mapping, Skipped-Delivery-Tracking, `withNotificationQueue`) | **anpassen** — als `notify()`-Backend (INTEGRATION-CONTRACTS §7) für Task-Reminder; Bündelung (max 1/4h) muss ergänzt werden |
| `apps/worker/src/jobs/dsgvo-grace-cron.ts` | Cron-Job-Vorlage: BullMQ `upsertJobScheduler` (keine Doppel-Schedules bei Re-Deploy), 24h-Kadenz, host-agnostische Dependency-Injection (`listOverdue…` + `enqueue…` + logger), idempotent | **als-vorlage** — Blaupause für (a) Reminder-Cron 24h-vor/bei/+24h und (b) Auto-Archivierungs-Cron `done + completedAt <= now-30d` (M073, Block F.2) |
| `apps/worker/src/jobs/email-sequence-scheduler.ts`, `outbox-done-retention-cron.ts`, `workflow-execution-stalled-reconciler-cron.ts` | Weitere reife Cron-Beispiele (Scheduler-Pattern, Retention-Sweep) | **als-vorlage** — Retention-Sweep = direktes Muster für Auto-Archivierung |
| `packages/db/src/outbox.ts` (`publishOutboxEvent`) + `prisma/schema/outbox.prisma` (`OutboxEvent`) | Transaktionales Outbox: Side-Effects (Reminder-Schedule, Activity, ERP-Push) atomar mit DB-Write; `dispatchKey @unique` (Idempotenz), `scheduledAt`, `priority` | **1:1** — Pflicht-Muster für alle Task-Side-Effects; `scheduledAt` deckt die 24h-Reminder-Zeitpunkte ab |
| `prisma/schema/permissions.prisma` (`Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser`) | RBAC-Fundament inkl. **`Group`/`GroupUser`** | **1:1** — `Group`/`GroupUser` deckt `assigned_to_group_id` + Owner-Check „Mitglied der zugewiesenen Gruppe" (Block H) ab; `tasks.*`-Permissions als neue `Permission`-Einträge |
| `packages/db/src/client.server.ts`, `transactions.server.ts` | DB-Client (`getDbClient`/`getReadDbClient`/`getExtendedDbClient`), `runDbTransaction` | **1:1** — Fundament für alle Task-Repositories; Read-Replica für Listen-Views |
| `packages/db/src/dsgvo/user-fk-map.ts` | Zentrale User-FK-Karte (Test bricht CI bei fehlendem Eintrag) | **anpassen (Pflicht)** — neue `Task`-FKs (`createdByUserId`, `assignedToUserId`, `completedByUserId`) + `TaskWatcher.userId` müssen hier eingetragen werden |
| `prisma/schema/appsEngine.prisma` (`App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem`) | Integrations-/Sync-Engine: pro-Tenant `App` (`@@unique([tenantId, type])`), verschlüsselte `DataConnection`-Credentials, Sync-Run mit Status/Modus/Metriken, `AppSyncItem` mit **`sourceId`/`destinationId`-Mapping + `rolledBackData`/`rolledBackAt`** | **anpassen** — Fundament für M074 KWP-Aufgaben-Sync: Source↔Destination-ID-Brücke (KWP-Email-Identifikation), Sync-Modi (manual/scheduled/webhook), Rollback. Sync-Modi A/B/C („KWP gewinnt") sind als Config-/Konfliktregel zu ergänzen |
| `@toolctrl/view-models` (`packages/view-models/src/index.ts`) | `VMState<T>`, `ActionCallbacks`, `I18nLabel`, `assertI18nLabel`, `SidebarRegistryItem`/`RegistryGate` | **1:1** — View-Model-Vertrag (INTEGRATION-CONTRACTS §11) für Task-Listen/Detail; i18n-Key-Zwang |
| `@toolctrl/ui-app` (`createSidebarRegistry`, `EmptyPage`, `generateA11yTest`) | Sidebar-Registry-Factory + a11y-Test-Template | **1:1** — Sidebar-Eintrag „Aufgaben" (Gruppe `arbeiten`) + a11y-Gate |
| `apps/web/app/components/ui/tables/*` (`TableSimple`, `TablePagination`, `ColumnSelector`, `RowsLoadMoreCard`), `components/ui/multi-select.tsx`, `calendar.tsx`, `command.tsx`, `modals/*`, `emptyState`, `loaders` | shadcn/Radix-Komponentenbibliothek: Tabelle + Cursor-Pagination („Mehr laden"), Multi-Select-Filter, Date-Picker, Modale, Empty/Loading-States | **als-vorlage** — deckt fast alle UI-Elemente aus Block C/D ab (Filter, Bulk-Checkboxes, Pagination, Quick-Actions); in HiveGrid via Subframe-Adapter |
| `apps/web/app/application/sidebar/*` + `apps/web/app/components/layouts/sidebars/shadcn/*` | Datengetriebene Sidebar mit Permission-/Feature-Gating, App-Shell (`AppLayout`/`ShadcnSidebarLayout`), Breadcrumb-Header | **als-vorlage** — Muster für HiveGrid-`sidebar.config.ts` + Breadcrumb (INTEGRATION-CONTRACTS §1/§4) |
| `packages/i18n` + `apps/web/app/locale/*` | i18next + SSR (de/en), `formatRelativeTime`/`formatDate` | **1:1** — Deadline-Anzeige „relativ wenn ≤7 Tage" (Block C) + alle Klartext-Texte (Block I/F) als i18n-Keys |
| `prisma/schema/featureFlags.prisma` + `FeatureFlag`-Layer | Feature-Flags (scope tenant/user, Targeting) | **1:1** — DoD-Pflicht „Modul ohne Deploy abschaltbar"; pro-Tenant ERP-Sync-Aktivierung (M074) |
| `prisma/schema/core.prisma` (`AuditEvent`, Hash-Kette) + `packages/db/src/audit-hash.ts` | Tamper-evidenter Audit-Trail | **1:1** — DoD-Audit-Wiring (INTEGRATION-CONTRACTS §6); ERP-Sync-Konflikt-Markierung im Audit (M074) |
---
## Vorhandene Daten-Modelle (konkret)
**Direkt wiederverwendbar (1:1 / Querschnitt):**
- `prisma/schema/permissions.prisma` — `Group`, `GroupUser` (Gruppen-Zuweisung + Owner-Check), `Permission`/`Role`/`RolePermission`/`UserRole` (RBAC für `tasks.*`).
- `prisma/schema/notifications.prisma` — `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` (M073 Reminder In-App/Email/Push).
- `prisma/schema/outbox.prisma` — `OutboxEvent` (atomare Side-Effects + `scheduledAt` für Reminder-Timing).
- `prisma/schema/core.prisma` — `AuditEvent` (Audit-Querschnitt), `FileObject` (falls Attachments später).
- `prisma/schema/accounts.prisma` — `User`, `Tenant`, `TenantUser` (Identity/Scope-Anker).
- `prisma/schema/featureFlags.prisma` — `FeatureFlag` (Abschaltbarkeit + ERP-Sync-Toggle).
**Vorlage / nahe verwandt, aber nicht das Task-Modell:**
- `prisma/schema/entityBuilder.prisma:434` — `RowTask` (Checklisten-Sub-Entität an `Row`; Feld-Vorlage, nicht die HiveGrid-`Task`).
**Sync-Fundament (M074):**
- `prisma/schema/appsEngine.prisma` — `App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem` (ID-Brücke + Rollback für KWP-Aufgaben-Sync). Konkrete KWP-Sync-Foundation (M089–M095) ist Vorbedingung und liegt außerhalb dieses Moduls.
**Platzhalter für Neu-Modelle:**
- `prisma/schema/app.prisma` — leerer Platzhalter (`// custom database models`), vorgesehener Ort für `Task` + `TaskWatcher` (+ ggf. `TaskSyncLink` für M074).
---
## Lücken / komplett neu zu bauen
1. **`Task`-Prisma-Modell** (neu, in `app.prisma`): alle Felder aus Block A inkl. `tenantId`, `status`-Enum, `priority`-Enum, `source`/`source_ref`, `linked_entity_type`/`linked_entity_id` (polymorph), `archived_at`, `assigned_to_group_id`. Plus die 4 Indizes aus Spec Block A. Plus die Constraints (oneOf Assignee, completed-Felder nur bei `done`, Reset bei `done→open`).
2. **`TaskWatcher`-Modell** (neu, M:N User↔Task) — existiert nirgends.
3. **`tasks.*`-Permissions + Bundle-Mapping** (Block H): `tasks.read_own/read_all/write/admin` als neue `Permission`-Einträge + Owner-Check-Logik (Assignee OR Group-Member OR Creator OR Watcher) + 404-statt-403. Muss neu (RBAC-Fundament vorhanden, Keys nicht).
4. **Listen-View mit Server-Side-Filter** (Block C): Scope-Presets (`mine`/`created_by_me`/`all`), Multi-Select-Filter (Status/Priorität/Quelle/Verknüpfungs-Typ), Date-Range mit Presets, Volltext-Suche (Titel+Beschreibung), Sortierung, Bulk-Actions, Cursor-Pagination. UI-Primitives vorhanden, die Verdrahtung + VM-Builder neu.
5. **Detail-View mit Inline-Edit** (Block D): Markdown-Editor, Assignee-Picker (Person ↔ Group umschaltbar), Entity-Picker (Auto-Complete Contact/Company/Lead/Deal/Project), Status-Wechsler, Duplizieren, Watcher-Verwaltung, Aktivitätsstrom. Neu.
6. **Polymorphe Entity-Verknüpfung + Embed-Anzeige** (Block B/C, M072): Tasks-Tab/Section in Contact/Company/Lead/Deal/Project-Detail-Views mit Pre-Fill. `RowTask`-Modell kann das architektonisch NICHT (hart an Entity-Builder-`Row`) — neu zu bauen.
7. **Erstellungs-Quellen-Integration** (Block E, M072): „Mark as Task" aus Team-Chat (M110–M114), Form-Action „Aufgabe erstellen" (M121–M125), Automation-Action (M075–M082). Abhängig von Modulen, die teils noch nicht existieren. Quell-DTO-Muster aus `events/dtos/RowTasks*` als Vorlage.
8. **Reminder-Logik** (M073, Block F.1): Cron 24h-vor/bei/+24h, Empfänger-Auflösung (Assignee/Group-Members/Watcher), **Anti-Spam-Bündelung max 1/4h pro User** (Bündelung im NotificationService NICHT vorhanden — neu), Email-Eskalation bei +24h. Cron-Pattern + Notification-Layer vorhanden, Domänen-Logik neu.
9. **Auto-Archivierung** (M073, Block F.2): täglicher Cron `status=done AND completed_at<=now-30d AND archived_at IS NULL → archived_at=now()`. Cron-Vorlage vorhanden, Job neu.
10. **Activity-Typen** (Block G, M045): 7 Task-Activity-Typen mit `related_entity_refs` (Task-Activity erscheint im Stream der verknüpften Entität). DTO-/Übersetzungs-Muster aus `RowTasks*Dto` als Vorlage; `@hivegrid/activity`-`logActivity`-Wiring neu.
11. **ERP-Aufgaben-Sync M074** (Pflichtenheft 26.10, [N11]): Per-Mandant/Per-Adapter aktivierbar, Sync-Modi A/B/C („KWP gewinnt"), Identifikations-Brücke (KWP-Box: Email), KWP-Badge, Sync-Status im Detail, Anti-Endlosschleife, Aushebel-Schutz. `appsEngine`-Schema liefert ID-Brücke + Rollback-Run; Konflikt-Auflösung soll M093 wiederverwenden (Vorbedingung). Task-spezifische Mapping-Schicht neu.
12. **i18n-Key-Set** für alle Klartext-Texte (Block I + F + Permission-Tooltips) — neu (Pattern + Infra vorhanden).
13. **Sidebar-Eintrag + Route** in HiveGrid-Targets (`packages/ui-hivegrid/src/app-shell/sidebar.config.ts`, Gruppe `arbeiten`) — die HiveGrid-Pakete (`packages/ui-hivegrid`, `@hivegrid/*`) existieren in der Boilerplate **nicht**; nur das `@toolctrl/*`-Äquivalent als Vorlage.
---
## Empfohlenes Vorgehen
**Phase A — Fundament (M071), zuerst aus BP übernehmen:**
1. `Task` + `TaskWatcher` in `prisma/schema/app.prisma` modellieren (Felder-Vorlage `RowTask`, erweitert um Block-A-Felder; `Group`/`GroupUser` für Gruppen-Zuweisung wiederverwenden). Forward-Only-Migration mit Inverse-SQL.
2. **Pflicht:** neue User-FKs in `packages/db/src/dsgvo/user-fk-map.ts` ergänzen (sonst CI-Block).
3. Repository-Layer nach Muster `rowTasks.db.server.ts` bauen (`get*/create/update/delete/getAll` + Owner-Filter „assigned OR created OR watcher OR group-member"). DB-Client + `runDbTransaction` 1:1 aus `@toolctrl/db`.
4. `tasks.*`-Permissions + Bundle-Mapping (Block H) auf RBAC-Fundament; Owner-Check + 404-statt-403.
5. VM-Builder + Listen-/Detail-View über `@toolctrl/view-models` + shadcn-Tabellen/Modale/Picker (als-vorlage); Sidebar-Eintrag „Aufgaben" (Gruppe `arbeiten`) via Registry-Factory; Quick-Add/Toggle-Logik aus `RowTasks.tsx`.
**Phase B — Embed + Quellen (M072):**
6. Polymorphe Verknüpfung + Tasks-Section in Entity-Detail-Views (neu); Pre-Fill bei „Aufgabe anlegen".
7. Quellen-Integration (Chat/Form/Automation) — DTO-Muster `events/dtos/RowTasks*` übernehmen; abhängig von M110–M114 / M121–M125 / M075–M082.
8. Activity-Typen (Block G) via `logActivity` + Outbox.
**Phase C — Reminder & Archivierung (M073):**
9. Reminder-Cron + Auto-Archivierungs-Cron als BullMQ-`upsertJobScheduler`-Jobs nach Vorlage `dsgvo-grace-cron.ts` / `outbox-done-retention-cron.ts` (host-agnostisch, idempotent).
10. Versand über `notify()`/`NotificationService` + `notifications.server.ts` (1:1); Empfänger-Auflösung + **Anti-Spam-Bündelung (max 1/4h) neu** ergänzen; User-Toggle über `UserNotificationPreference`.
**Phase D — ERP-Sync (M074), zuletzt:**
11. Auf `appsEngine` (`App`/`DataConnection`/`AppSyncRun`/`AppSyncItem`) aufsetzen; KWP-Email-Identifikations-Brücke (`sourceId`/`destinationId`), Sync-Modi A/B/C + Konflikt-Regel (M093 wiederverwenden), KWP-Badge + Sync-Status im Detail, Anti-Endlosschleife (markierte Sync-Herkunft löst keinen Rück-Push aus), Aushebel-Schutz. Vorbedingung: KWP-Sync-Foundation M089–M095.
**Querschnitt durchgängig (DoD):** Outbox für alle Side-Effects, Audit automatisch (M001), Feature-Flag pro Modul, i18n-Keys statt Klartext, View-Model-Trennung (keine Prisma-Imports in UI-Paket), Smoke-Test + Hot-Swap-Drill + Visual-Snapshot.
---
## Hinweise / Vorbehalte
- **`RowTask` ≠ HiveGrid-Task.** Wer `RowTask` 1:1 übernimmt, baut am Datenmodell vorbei (kein tenantId/status/priority/group/source/linked_entity/archived/watcher). Nur als Feld-/Service-/UI-**Vorlage** verwenden.
- **HiveGrid-Target-Pfade existieren nicht in der BP.** `packages/ui-hivegrid`, `@hivegrid/activity`, `@hivegrid/notifications`, `@hivegrid/audit`, `packages/entitlements/src/permissions.ts` (HiveGrid-Variante) sind Soll-Zustand laut INTEGRATION-CONTRACTS; die BP liefert nur die `@toolctrl/*`-Äquivalente als Muster.
- **Bündelung (max 1/4h pro User)** ist im vorhandenen NotificationService nicht implementiert — eigene Window-Logik nötig (M073-Erfolgskriterium).
- **`@toolctrl/ui-subframe` ist leer** (Stub). Der Subframe-Adapter-Layer ist nicht gebaut; HiveGrid muss diese Grenze selbst füllen (MEMORY: „Subframe mandatory for all routes").
- **M074 hängt an externer Foundation** (M089–M095 KWP-Sync, M008–M010 Apps-Engine). `appsEngine`-Schema ist das Fundament, die KWP-spezifischen Adapter/Konfliktregeln liegen außerhalb von M26.
@@ -0,0 +1,142 @@
# Boilerplate-Mapping — M27 Auth / Multi-Tenancy / Rollen / Permissions
**Modul-ID:** M27 (HiveGrid-Modul 27)
**Name:** Anmeldung, Mandanten, Rollen & Permissions
**Milestones:** M018-M027 (M018 Mandanten-Anlage, M019 Mandanten-Trennung-Hardening, M020 Email/PW-Auth, M021 Magic-Link/Sessions, M022 TOTP-2FA, M023 Rollen-Engine/SHK-Rollen/Gruppen, M024 Mitarbeiter-Einladung/Deaktivierung, M025 Konto-Löschung, M026 Mandanten-Lebenszyklus, M027 API-Keys/Tarif-Gates)
**Block:** Block 1 — Plattform-Fundament (Pflichtenheft v1.1, §27)
**Reuse-Level:** **HIGH** — Die Boilerplate liefert ein nahezu vollständiges Auth/Tenancy/RBAC-Fundament inkl. Daten-Modelle, Service-Layer, fertige Settings-UI-Routen und Querschnitt-Helper. HiveGrid muss primär: (1) HiveGrid-spezifische Inhalte (SHK-Rollen-Template, Permission-IDs) einfüllen, (2) drei echte Architektur-Lücken schließen (Server-Side-Sessions in Postgres, Tenant-Lifecycle-Statusmaschine, Reassign-Workflow vor Deaktivierung), (3) das App-Level-Tenant-Scoping ggf. um Postgres-RLS härten (Spec A.4 — bauliche Vorgabe, in BP NICHT vorhanden).
> Verifiziert per direktem Read/Grep in `/Users/raffaelsheikh/Documents/GitHub/boilerplate` (nicht nur Inventur). Stand der Vorphase-Inventur bestätigt; Abweichungen unten markiert.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `prisma/schema/accounts.prisma` | `User`, `Tenant`, `TenantUser`, `TenantUserInvitation`, `MagicLinkToken`, `AdminUser`, `Registration`, `Blacklist`, `ImpersonationAudit`, `TenantIpAddress`, `WorkspaceAuthProvider`, `TenantType` | **anpassen** — Identity/Tenancy-Kern. User hat bereits `passwordHash`, `totpSecretEncrypted`/`totpPendingSecretEncrypted`/`totpEnabledAt`, `mfaRecoveryCodeHashes`, DSGVO-Flags (`pendingDeletion`/`scheduledDeletionAt`/`pseudonymized`/`deletionRequestId`), `defaultTenantId`. Felder zu ergänzen siehe Lücken. |
| `prisma/schema/permissions.prisma` | `Role`, `Permission`, `RolePermission`, `UserRole`, `Group`, `GroupUser` | **1:1** als RBAC-Fundament. `Role` ist tenant-scoped (`tenantId?`), hat `type`/`isDefault`/`assignToNewUsers`/`order`. `UserRole.tenantId?` (Multi-Rolle pro User + tenant-Scope). `Group`/`GroupUser` = exakt die orthogonalen Gruppen aus Spec C.4. Permission `name @unique` (global, atomar) + optional `entityId` (Entity-gebundene Rechte). Deckt C.1 (atomar gespeichert), C.3 (Multi-Rolle additiv), C.4 1:1 ab. |
| `prisma/schema/core.prisma` → `ApiKey`, `ApiKeyLog` | API-Key-Modell mit `keyHash`/`keyPrefix`/`keyLastFour`/`keyVersion` (gehashte Keys, einmalig sichtbar), `alias`, `expires`, `active`, `@@unique([tenantId, alias])`; `ApiKeyLog` für Request-Logging (ip/endpoint/method/status/duration) | **anpassen** — deckt Block E fast komplett (Anlage, Hash, Rotation, Logs). Lücke: Scopes (`read_only`/`webhook_inbound`/`read_full`/`automation_trigger`) + per-Tenant-Rate-Limit-Override + 365-Tage-Default fehlen als Felder. |
| `prisma/schema/core.prisma` → `AuditEvent` | Tamper-evidente Audit-Hash-Kette (`previousHash`/`eventHash`), `actorType`/`actorId`/`actorEmail`/`actorIp`, `dataSensitivity`, `regulatoryScope[]`, `outcome`, reichlich Indizes | **1:1** — Audit-Trail für C.5 (Rollen-/Permission-Änderungen geloggt), D-Lifecycle, E (API-Key-Anlage/Widerruf). Querschnitt M001. |
| `prisma/schema/subscriptions.prisma` (14 Modelle) + `packages/entitlements` | `SubscriptionProduct`/`SubscriptionPrice`/`SubscriptionFeature`/`TenantSubscription`/`TenantFeatureOverride` + Feature-Gate-Engine (`evaluateEntitlement`, `featureGateRegistry`, `featureKeys`, `evaluateProductAccess`, `productBlueprints` mit `crm-saas`) | **anpassen** — deckt Block F.1 (Subscription-Feature-Gates) als Engine ab; Datenmodell SubscriptionProduct→Features→TenantSubscription exakt wie Spec F.1. HiveGrid-Plan-Definitionen (Cloud/Cloud+/Enterprise) sind geparkt (Spec), daher nur Engine übernehmen, `featureKeys` mit HiveGrid-Keys erweitern. |
| `prisma/schema/featureFlags.prisma` | `FeatureFlag` (scope global/tenant/user, v2-Targeting `rules`/`variants`, `enabledFrom/Until`, Flag-Dependencies), `FeatureFlagFilter`, `FeatureFlagAudit` (append-only) | **1:1** — deckt Block F.2 (Feature-Flags, Modul 32) inkl. Filter-Regeln + Audit. |
| `apps/web/app/services/auth/totp.server.ts` (+ `utils/auth/mfa/recovery-codes.server.ts`, `enforcement.server.ts`, `step-up.server.ts`, `challenge-rate-limit.server.ts`) | TOTP-Enrollment (otpauth, SHA1/6-stellig/30s), QR/otpauth-URI, verschlüsseltes Secret (CryptoApi), 10 Recovery-Codes, Pending→Activate-Flow, MFA-Enforcement-Gate, Step-Up, Challenge-Rate-Limit (5/5min konfigurierbar) | **1:1 / anpassen** — deckt Block B.4 (TOTP, Backup-Codes, Setup-Flow) nahezu vollständig. Anpassen: Trust-Device-Cookie + Pflicht-2FA-Toggle pro Tenant (siehe Lücken). |
| `apps/web/app/services/auth/password-policy.server.ts` (+ `password-policy-banner.server.ts`, `password-reset-token.server.ts`) | Zentrale Passwort-Validierung, env-getrieben, HIBP-k-Anonymity-Check (`api.pwnedpasswords.com/range/`, Timeout 3s), Min-Length, bounded i18n-Error-Codes, Soft-Banner bei veraltetem PW, Reset-Token | **1:1** — deckt B.1 (≥12 Zeichen via Config, HIBP-Block mit Klartext, keine erzwungenen Sonderzeichen). Cost-Faktor: siehe `passwordHasher.server.ts`. |
| `apps/web/app/utils/auth/passwordHasher.server.ts` | Passwort-Hashing | **anpassen** — Spec B.1 verlangt bcrypt Cost 12; verifizieren/setzen. |
| `apps/web/app/services/auth/magic-link.server.ts` | Magic-Link: signierte Tokens, `MAGIC_LINK_TOKEN_TTL_MINUTES=15`, einmalig nutzbar (`consumeMagicLinkToken`), Purpose-gebunden, `MagicLinkToken`-Tabelle mit `tokenHash`/`consumedAt`/`expiresAt` | **1:1** — deckt B.2 (15-Min-TTL, einmalig) exakt. Rate-Limit 3/15min via auth-rate-limit ergänzen. |
| `apps/web/app/services/auth/auth-rate-limit.server.ts` | Rate-Limit für `login`/`forgot-password`/`reset-password`/`mfa`, IP+Identifier-gehasht, Window+Limit konfigurierbar, retryAfter | **1:1** — deckt B.1 (5/15min pro Email, IP-Limit) + B.2 + B.4. Captcha (hCaptcha) ab 3. Versuch fehlt (siehe Lücken). |
| `apps/web/app/utils/session.server.ts` | Cookie-Session-Storage (`createCookieSessionStorage`), `setLoggedUser` (löst defaultTenant → Fallback erster Tenant), Impersonation-Session-State + Validierung (TTL, Actor-Check), CSRF | **als-vorlage** — Login-Flow + Tenant-Resolution + Impersonation übernehmbar. ABER: Sessions sind **cookie-basiert, NICHT server-side in Postgres** — Spec B.5 verlangt explizit Postgres-Sessions (siehe Lücken). |
| `apps/web/app/utils/services/tenantService.ts` (+ `utils/app/UrlUtils.ts`, `app/PublicRouteSegments.ts`) | `normalizeTenantSlug`, `UrlUtils.slugify`, `isReservedTenantSlug`, `getAvailableTenantSlug` (Auto-Suffix `-2` bei Konflikt), Reserved-Slug-Set | **1:1 / anpassen** — deckt A.2 (Auto-Generierung, Eindeutigkeit, Reserved-Words, Vorschlag mit Nummer). Reserved-Word-Liste + Umlaut-Mapping (ae/oe/ue) + GmbH/AG-Suffix-Erhalt an HiveGrid-Regeln angleichen. |
| `apps/web/app/utils/services/rolesAndPermissionsService.ts` (+ `@toolctrl/db/permissions-seed.server`) | `seedRolesAndPermissions`, `defaultAppRoleSeeds`/`defaultAdminRoleSeeds`, `defaultPermissionSeedDescriptors`, `syncDefaultRolesAndPermissions`, Seed-Diagnostics | **als-vorlage** — Seeding-Mechanik 1:1 nutzbar. ABER Default-Rollen sind **generisch** (`SuperAdmin`/`SuperUser`/`Admin`/`Billing Admin`/`User`), NICHT die SHK-Rollen. SHK-Branchen-Template (C.2/27.7, 7-10 Rollen) ist als deklaratives Seed neu zu definieren. |
| `apps/web/app/components/layouts/selectors/TenantSelector.tsx` + `NewTenantSelector.tsx` (eingebunden in `routes/app.tsx`) | Tenant-Switcher-Komponente in der App-Shell | **anpassen** — deckt A.3/D.4 (Tenant-Switcher überall verfügbar). Suchfeld bei 5+ Tenants + `last_accessed_at`-Sortierung ergänzen (Feld fehlt, siehe Lücken). |
| `apps/web/app/routes/app.$tenant/settings/members.tsx` (+ `members/groups`) | Mitarbeiter-Verwaltung + Einladungs-Flow (nutzt `TenantUserInvitation`) | **anpassen** — Basis für D.1 (Einladung mit Rolle/Groups/Nachricht, 7-Tage-Token, 3 Pfade). Reassign-Workflow vor Deaktivierung fehlt. |
| `apps/web/app/routes/app.$tenant/settings/roles-and-permissions.tsx` (+ `roles-and-permissions/roles`, `components/core/roles`) | Rollen-Editor-UI (Liste, Anlegen, Permissions, Matrix-Test vorhanden) | **anpassen** — Basis für C.3 (Editor, neue Rolle, „Basieren auf", Duplizieren, Löschen mit Reassign). Bundle/Advanced-Mode-Toggle (C.1) und Permission-Preview-Tool (C.5) ergänzen. |
| `apps/web/app/routes/app.$tenant/settings/groups.tsx` | Gruppen-Editor | **1:1 / anpassen** — deckt C.4 (Anlegen/Umbenennen/Löschen, Mitglieder, Description). |
| `apps/web/app/routes/app.$tenant/settings/api/keys/` (`keys.tsx`, `new.tsx`, `$id.tsx`) + `api/logs.tsx`, `api/docs.tsx` | API-Key-Settings-UI inkl. Anlage, Detail, Logs | **anpassen** — deckt Block E-UI (Liste, einmalig sichtbarer Key, Logs der Anfragen). Scope-Auswahl + Rate-Limit-Override ergänzen. |
| `apps/web/app/routes/app.$tenant/settings/account.tsx` + `privacy.tsx` + `privacy.delete.confirm.$token.tsx` + `privacy.delete-mail` | Account-Löschung (Multi-Step), DSGVO-Privacy, Token-Bestätigung | **anpassen** — deckt D.3 (Multi-Step-Löschung, PII-Überschreibung, 24h-Reverse via Email-Token). Last-Tenant-Admin-Block + 3-Sek-Countdown ergänzen. Cascade-Karte in `packages/db/src/dsgvo/user-fk-map.ts` ist Pflicht-Referenz. |
| `apps/web/app/routes/app.$tenant/settings/profile.tsx` (+ `profile.mfa`, `profile.session`, `profile.locale-picker`, `theme.tsx`) | Persönliche Settings: 2FA-Setup, Session-Übersicht, Sprache, Theme | **anpassen** — deckt B.5-UI (Session-Liste/Revoke) + B.4-UI (2FA-Setup) + Persönliche `/settings/`. Trust-Device-Liste mit Revoke ergänzen. |
| `apps/web/app/routes/app.$tenant/settings/audit-export.tsx` + `logs/` | Audit-Log-UI + Export | **1:1** — Audit-Log-Anzeige für C.5. |
| `packages/foundation/src/permissions.ts` | `requirePermission`, `withTenantScope`, `withPermission`, `withOwnerCheck` | **1:1** — Querschnitt-Helper (Spec AV #5 Permissions-aware-Default, AGENTS.md DoD C.17). Exakt die in INTEGRATION-CONTRACTS §10 referenzierte API. |
| `packages/foundation/src/{audit,notifications,flags,concurrency,idempotency,errors}.ts` | Audit-Helper, Notify, Feature-Flag-Eval, Concurrency (Last-Write-Wins), Idempotenz, Catalog-Errors | **1:1** — Querschnitt für C.5 (Concurrent-Update Last-Write-Wins), D.3-Notify, F.2, Klartext-Errors (M002). |
| `packages/db/src/{client.server,transactions.server,outbox,audit-hash}.ts` + `dsgvo/user-fk-map.ts`, `dsgvo/pseudonymize.ts` | Prisma-Client-Grenze, Transaktionen, Outbox, Audit-Hash, DSGVO-FK-Karte + Pseudonymisierung | **1:1** — Fundament für jede schreibende Aktion (Audit/Outbox), D.3-Pseudonymisierung. |
| `packages/db/src/{users,accounts,api-keys,auth-boundary,auth-providers,device-tokens,impersonation-audit,workspace-invites,permissions-seed,entitlements-seed,subscriptions,settings}.server.ts` | Repository-Befehlsfläche für alle Auth/Tenancy-Domänen | **1:1 / anpassen** — DB-Layer-APIs direkt nutzbar; je Repo-Methode neue HiveGrid-Felder durchreichen. |
| `apps/web/app/utils/services/admin*` + `services/auth/admin-mfa-gate.server.ts`, `admin-fallback.server.ts` | Admin-Panel-Auth (Pflicht-2FA, Fallback-Admin) | **als-vorlage** — relevant für Plattform-Admin (Modul 37, außerhalb M27-Kern, aber `AdminUser`-Modell + Pflicht-2FA-Gate direkt nutzbar). |
| `packages/shared-store` | Redis-KV: `createFixedWindowRateLimiter`, `createSlidingWindowRateLimiter`, `createSessionStore`, `createIdempotencyStore`, Cache mit Tenant-Präfix | **1:1** — Cross-Replica-Rate-Limit (B.1/B.2/B.4, Spec verlangt mehr als der In-Memory-`cache` in auth-rate-limit), Cache-Isolation `tenant:{id}:` (A.4), Session-Store falls hybrid. |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
Alle in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`:
**accounts.prisma:**
- `User` — `email @unique`, `passwordHash?`, `firstName`/`lastName`/`avatar`/`phone`, `defaultTenantId?`, `verifyToken`, TOTP (`totpSecretEncrypted`, `totpPendingSecretEncrypted`, `totpEnabledAt`, `totpPendingCreatedAt`), `mfaRecoveryCodeHashes Json`, `mfaRecoveryCodesGeneratedAt`, `active`, DSGVO (`pendingDeletion`, `scheduledDeletionAt`, `deletionRequestedAt`, `pseudonymized`, `pseudonymizedAt`, `deletionRequestId @unique`), `locale`, `lightOrDarkMode`. **Hat noch `githubId`/`googleId @unique` (Boilerplate-OAuth — EU-Sterilisation pending, für M27-Scope irrelevant/zu entfernen).**
- `Tenant` — `slug @unique`, `name`, `icon`/`logo`, `subscriptionId?`, `active`, `deactivatedReason?`, `theme`/`themeConfig`. **Kein Lifecycle-Status-Enum, kein `lastAccessedAt`.**
- `TenantUser` — Membership (`tenantId`, `userId`, `type Int`, `joined Int`, `status Int` — Int-codiert, kein Enum).
- `TenantUserInvitation` — `email`, `firstName`/`lastName`, `type Int`, `pending`, `status @default("pending")`, `tokenHash?`/`tokenPrefix?`, `expiresAt?`, `acceptedAt?`/`revokedAt?`, `createdUserId?`/`fromUserId?`/`acceptedByUserId?`/`revokedByUserId?`. Indizes auf `(tenantId,status,expiresAt)`, `(tenantId,email,status)`, `(tenantId,tokenPrefix)`.
- `MagicLinkToken` — `tokenHash @unique`, `expiresAt`, `consumedAt?`, FK User Cascade.
- `AdminUser` — Plattform-Admin-Panel-Zugang (+ OIDC-SSO-Felder `ssoProvider`/`ssoSubjectId`/`ssoGroups`/`lastSsoLoginAt`).
- `Registration` — Self-Service-Signup-Staging (`email @unique`, `slug?`, `token @unique`, `company?`, `selectedSubscriptionPriceId?`, Lead-Conversion-Felder, `createdTenantId?`).
- `Blacklist` (email/domain/ip), `TenantIpAddress`, `ImpersonationAudit` (Actor/Target/Tenant SetNull), `WorkspaceAuthProvider` (SSO/SAML/OIDC pro Tenant), `TenantType`.
**permissions.prisma:**
- `Role` (`tenantId?`, `name`, `description`, `type`, `assignToNewUsers`, `isDefault`, `order`, `@@unique([tenantId, name])`), `Permission` (`name @unique` global atomar, `type`, `isDefault`, `order`, `entityId?`), `RolePermission`, `UserRole` (`tenantId?`, partielle Eindeutigkeit via Migration-SQL), `Group` (`tenantId?`, `name`, `description`, `color`), `GroupUser`.
**core.prisma:** `ApiKey` (gehasht), `ApiKeyLog`, `AuditEvent` (Hash-Kette), `IdempotencyKey`, `Credential` (Secret-Store), `Event`/`EventWebhookAttempt`, `Log`, `IpAddress`/`IpAddressLog` (Geolocation für Session-Standort B.5).
**subscriptions.prisma:** `SubscriptionProduct`, `SubscriptionPrice`, `SubscriptionFeature`, `TenantSubscription`, `TenantFeatureOverride` (`@@unique([tenantId, featureKey])`), `TenantProductConfiguration` (`blueprintId` default `crm-saas`) — Block-F.1-Fundament.
**featureFlags.prisma:** `FeatureFlag`, `FeatureFlagFilter`, `FeatureFlagAudit` — Block-F.2-Fundament.
**notifications.prisma:** `NotificationDelivery`, `UserNotificationPreference`, `DeviceToken` — für D.3-Lösch-Notification + B.5-Push-Token-Cleanup.
---
## Lücken / komplett neu zu bauen
Verifiziert per Grep — folgendes existiert in der Boilerplate **NICHT** und ist neu zu bauen:
1. **Server-Side-Sessions in Postgres (B.5) — ARCHITEKTUR-LÜCKE.** Boilerplate nutzt `createCookieSessionStorage` (Cookie-only, `apps/web/app/utils/session.server.ts:124`). Es gibt **kein** `Session`/`UserSession`/`AuthSession`-Prisma-Modell (Grep leer). Spec B.5 verlangt explizit Postgres-Sessions wegen Logout/Revoke, Session-Liste mit Gerät/Browser/IP/Standort/Letzte-Aktivität, globalem „Von allen Geräten abmelden", 4h-Inaktivitäts-Timeout, 30-Tage-Sliding-Window, Auto-Logout bei PW/2FA/Email-Änderung. → **Neues `Session`-Modell + Session-Service neu bauen.** (`profile.session.tsx`-UI existiert als Hülle, Backend fehlt.)
2. **Postgres Row-Level-Security (A.4) — ARCHITEKTUR-LÜCKE.** Kein `current_setting('app.tenant_id')`, kein `SET LOCAL`, keine RLS-Policy-Migrations (Grep nur Doku/Tests, keine `ENABLE ROW LEVEL SECURITY`). Boilerplate-Tenant-Isolation ist **App-Level** (`withTenantScope`, repo-Filter, `tenant-scoped.ts`-Wrapper). Spec A.4 schreibt RLS als bauliche Vorgabe vor (Policy USING/WITH CHECK, App-Role ohne BYPASSRLS, Middleware `SET LOCAL app.tenant_id`). → **RLS-Layer neu (Migration + Request-Middleware + Worker-Job-Context)** — M019.
3. **Tenant-Lifecycle-Statusmaschine (A.5 / 27.11) — FEHLT.** `Tenant` hat nur `active: Boolean` + `deactivatedReason?`, **kein** Status-Enum. Status-Werte `trial/active/past_due/frozen/suspended/deletion_pending/deleted`, Übergänge, Frozen-Read-Only-Rechte-Matrix, Statusabhängige UI-Banner, 30-Tage-Grace-Cron — alles neu. → **Tenant-Status-Feld/Enum + State-Machine + Banner + Cron** — M026.
4. **Reassign-Workflow vor Deaktivierung/Entfernen (D.2 / 27.9.5) — FEHLT.** Kein `reassign`/`handover`/`orphan`-Code in `apps`. Anzeige offener Verknüpfungen pro Typ (Deals/Projekte/Tasks/Termine), Bulk-Reassign, Einzel-Reassign, „verwaist lassen"-Override mit Bestätigung. → Neu (querschnittlich, braucht spätere Modul-Entitäten). — M024.
5. **SHK-Branchen-Rollen-Template (C.2 / 27.7) — FEHLT (generische Seeds vorhanden).** `defaultAppRoleSeeds` = `SuperUser/Admin/Billing Admin/User` (generisch). Die 7-10 SHK-Rollen (GF, Vertriebsleiter, Vertriebler, Projektleiter, Büro, Buchhaltung, Personal, Lagerist, Marketing, Monteur) inkl. Default-UI-Modus (Büro/Baustelle) und Permission-Bundles sind als **deklaratives Branchen-Template** neu zu schreiben (Seeding-Mechanik selbst ist wiederverwendbar). — M023.
6. **Permission-Bundle/Advanced-Mode + Permission-IDs (C.1) — FEHLT.** Permissions sind atomar gespeichert (vorhanden), aber die Variante-2-Bundle-Stufen (`crm.none/read_own/read_all/write/admin`), der Advanced-Mode-Toggle und „Benutzerdefiniert"-Erkennung sind neu. Konkrete HiveGrid-Permission-IDs entstehen modulweise (Spec offen). — M023.
7. **`Tenant.lastAccessedAt` pro Membership (A.3 / D.4) — FEHLT.** Kein Feld für Default-Sortierung des Switchers / „letzter genutzter Tenant". → Feld auf `TenantUser` ergänzen + im Switcher (Suchfeld bei 5+) verdrahten. — M019/M018.
8. **Trust-Device / „30 Tage merken" (B.4) — FEHLT.** Kein `trustedDevice`/`rememberDevice`-Code (Grep leer). Signed-Cookie umgeht 2FA 30 Tage + Trust-Device-Liste mit Revoke neu. — M022.
9. **Pflicht-2FA-Toggle pro Tenant (B.4 / 27.4.3) — FEHLT.** MFA-Enforcement existiert für Admin-Panel (`admin-mfa-gate.server.ts`), aber nicht der Tenant-weite „2FA für alle Pflicht"-Toggle (zwingt User ohne 2FA beim nächsten Login). → Tenant-Setting + Enforcement-Erweiterung. — M022.
10. **Captcha ab 3. Fehlversuch (B.1 / 27.3.5) — FEHLT.** `AppConfiguration.authRecaptchaSiteKey` existiert als Feld, aber keine hCaptcha-Integration/Trigger-Logik. Spec verlangt hCaptcha (DSGVO). — M020.
11. **API-Key-Scopes + Rate-Limit-Override + 365-Tage-Default (Block E) — TEILWEISE.** `ApiKey`-Modell hat keine Scope-Spalte; Scopes (`read_only`/`webhook_inbound`/`read_full`/`automation_trigger`), per-Tenant-Rate-Limit-Override (1000/h Default, erhöhbar) und 365-Tage-Zwangs-Ablauf neu. — M027.
12. **Signup-Formular-Felder (A.1 / 27.1.2-3) — TEILWEISE.** `Registration` hat `company`/`slug`, aber nicht: Branche (SHK-preselect, andere „Coming soon"), Firmengröße-Dropdown, AGB/DSGVO/AVV-Akzeptanz-Checkboxen (AVV elektronisch unterschrieben), USt-Id, Telefon. → Felder + Formular-Validierung ergänzen. — M018.
13. **Slug-Live-Validierung beim Tippen (A.2, debounced 500ms) — TEILWEISE.** Server-Funktionen (`getAvailableTenantSlug`, `isReservedTenantSlug`) vorhanden; debounced Live-Check-Endpoint + Umlaut-Mapping (ae/oe/ue) + Suffix-Erhalt (GmbH) ergänzen. — M018.
14. **HiveGrid-UI-Layer (`packages/ui-hivegrid`, Subframe-Adapter, View-Models, sidebar.config) — NEU.** Boilerplate-UI liegt in `apps/web/app/routes` + `components/core` (klassisch, kein VM/Adapter-Pattern). HiveGrid-DoD verlangt VM-Layer (`packages/view-models`), `ui-hivegrid`-Adapter, `sidebar.config.ts`-Eintrag „Mitarbeiter & Rollen" (Gruppe Einstellungen), i18n-Keys. Boilerplate-Routen sind **als-vorlage** (Logik/Felder), nicht 1:1 (Layer-Disziplin AGENTS.md §A). — alle Milestones.
15. **Domain-/Pseudonymisierungs-Erweiterung der `user-fk-map.ts` — PFLICHT bei neuen User-FKs.** Jedes neue M27-Modell mit User-FK muss in `packages/db/src/dsgvo/user-fk-map.ts` eingetragen werden (CI-Test bricht sonst).
---
## Empfohlenes Vorgehen
**Phase 1 — Fundament 1:1 aktivieren (geringes Risiko, größter Hebel):**
1. Prisma-Modelle `accounts.prisma` + `permissions.prisma` + `core.prisma`(ApiKey/AuditEvent) + `featureFlags.prisma` + `subscriptions.prisma` als HiveGrid-Schema übernehmen (in `prisma/schema/` — sind bereits Teil der Codebase). `githubId`/`googleId` im Zuge der EU-Sterilisation entfernen.
2. Querschnitt-Helper aus `packages/foundation` (`permissions.ts`, `audit`, `notifications`, `flags`, `concurrency`, `errors`) + `packages/db`-Repos + `packages/entitlements` + `packages/shared-store` aktivieren — sie sind die in INTEGRATION-CONTRACTS/AGENTS referenzierten APIs.
3. Auth-Services 1:1 verdrahten: `totp.server.ts` (+ mfa/*), `password-policy.server.ts`, `magic-link.server.ts`, `auth-rate-limit.server.ts` (auf `shared-store`-Rate-Limiter umstellen statt In-Memory-cache).
**Phase 2 — Architektur-Lücken schließen (M019/M021/M026 — die drei echten Neubauten):**
4. **Server-Side-Sessions** (B.5): neues `Session`-Prisma-Modell + Session-Service (Erstellen/Laden/Revoke/Liste, Sliding-Window, Inaktivitäts-Timeout, Auto-Logout-Trigger). Cookie hält nur Session-ID-Referenz. — M021.
5. **RLS-Härtung** (A.4): RLS-Policies als Forward-Migration (Inverse-SQL als Kommentar, AGENTS §F.28), Request-Middleware `SET LOCAL app.tenant_id`, App-Role ohne BYPASSRLS, Worker-Job-Tenant-Context-Wrapper, Cache-Präfix `tenant:{id}:`, S3-Pfad `tenants/{id}/`. Sonderfälle ohne RLS: `users`/`subscription_plans`/`feature_flags`/`branchen_templates`. — M019.
6. **Tenant-Lifecycle** (A.5): `Tenant.status`-Enum + State-Machine + Frozen-Read-Only-Gate (write-blockierend) + Status-Banner + 30-Tage-Grace-Cron. — M026.
**Phase 3 — HiveGrid-Inhalte einfüllen (auf Fundament):**
7. **SHK-Rollen-Template** (C.2): deklaratives Branchen-Seed (10 Rollen + UI-Modus + Bundle-Permissions) über die vorhandene `seedRolesAndPermissions`-Mechanik; bei Tenant-Anlage mit Branche=SHK seeden. — M023.
8. **Permission-Bundles + Advanced-Mode** (C.1) + konkrete HiveGrid-Permission-IDs (modulweise) + `featureKeys`-Erweiterung in entitlements. — M023.
9. **Signup-Formular** (A.1) + Slug-Live-Validierung (A.2) + `lastAccessedAt` + Tenant-Switcher-Suchfeld. — M018.
10. **Mitarbeiter-Lifecycle**: Einladung (D.1, drei Pfade) auf vorhandenem `TenantUserInvitation` + **Reassign-Workflow** (D.2, neu) + Deaktivieren/Entfernen + Last-Admin-Schutz. — M024.
11. **Konto-Löschung** (D.3) auf vorhandener `privacy.delete.*`-Hülle + `user-fk-map.ts`-Pseudonymisierung + Multi-Step/3-Sek-Countdown/Last-Admin-Block. — M025.
12. **2FA-Ergänzungen**: Trust-Device-Cookie + Pflicht-2FA-Tenant-Toggle. — M022. **Captcha** (hCaptcha) bei Login. — M020.
13. **API-Keys** (Block E): Scopes + Rate-Limit-Override + 365-Tage-Ablauf auf vorhandenem `ApiKey`-Modell/UI. — M027.
14. **Feature-Gating** (Block F): entitlements-Engine + `featureFlags`-Modell verdrahten, `requireTenantFeatureAccess`-Loader-Convention + Klartext-Boundary mit Marketing-Link. — M027.
**Querschnitt durchgängig:** Alle UI als VM + `ui-hivegrid`-Adapter (nicht Boilerplate-Routen direkt), `sidebar.config.ts`-Eintrag „Mitarbeiter & Rollen" (Gruppe Einstellungen) mit Permission-Gate, i18n-Keys, Audit auto via Middleware, Smoke-Tests + Hot-Swap-Drill (AGENTS.md DoD).
---
## Notizen / Realitäts-Check
- **EU-Sterilisation:** `apps/web/app/routes/oauth/github` + `User.githubId`/`googleId` sind noch vorhanden (Boilerplate-OAuth). Für M27 (Spec: nur Email+PW+Magic-Link+TOTP, kein Google/GitHub/MS) im Zuge des Pre-Flight-Checks (AGENTS.md) zu entfernen. `WorkspaceAuthProvider` (SAML/OIDC) ist Enterprise-SSO-Vorrat, in Phase 1 nicht beauftragt.
- **Admin-Panel-Auth** (`AdminUser`, `admin-mfa-gate.server.ts`) gehört strenggenommen zu Modul 37 (Plattform-Admin), ist aber als Fundament für `admin.hivegrid.de` direkt vorhanden.
- **Sessions cookie-basiert** ist der wichtigste Befund: Spec B.5 ist hier inkompatibel mit dem Boilerplate-Default und erzwingt einen echten Neubau (Postgres-Session-Tabelle), nicht nur Anpassung.
- **RLS fehlt komplett** — die Boilerplate setzt vollständig auf App-Level-Scoping. Spec A.4 macht RLS zur baulichen Vorgabe; das ist eine substanzielle, separat zu testende (Pen-Test-Suite, M019-Erfolgskriterium) Erweiterung.
@@ -0,0 +1,151 @@
# Boilerplate-Mapping — M28 Subscriptions / Billing
**Modul-ID:** M28
**Name:** Subscriptions / Billing
**Milestones:** M153 (Stripe-Integration EU), M154 (Plan-/Seat-Modell), M155 (Trial + Read-Only), M156 (Metering / Pay-as-you-go)
**Pflichtenheft-Block:** Block 7, Modul 28 (`_bmad-output/pflichtenheft/blocks/block7.md` §28.1–28.Y)
**Detail-Spec:** `_bmad-output/specs/M28-subscriptions-billing.md`
**Reuse-Level:** **HIGH** (Daten-Layer, Stripe-Service-Layer, Pricing-Config-Admin, Pay-as-you-go-Engine fast vollständig vorhanden; Lifecycle-State-Machine, Seat-Modell und Read-Only-Middleware fehlen)
> Verifiziert direkt gegen `/Users/raffaelsheikh/Documents/GitHub/boilerplate` (nicht nur Inventur).
> Wichtig: Die BP-Modelle bilden den **Stripe-Spiegel** struktur-getreu ab, weichen aber in mehreren
> spec-kritischen Punkten ab (kein Status-Enum, kein Seat-Typ, andere Idempotenz-Strategie, sterilere Felder).
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `prisma/schema/subscriptions.prisma` | Komplettes Subscription-Daten-Modell: `SubscriptionProduct`, `SubscriptionPrice`, `SubscriptionUsageBasedPrice`, `SubscriptionUsageBasedTier`, `SubscriptionFeature`, `TenantSubscription`, `TenantSubscriptionProduct`, `TenantSubscriptionProductPrice`, `TenantSubscriptionUsageRecord`, `TenantProductConfiguration`, `TenantFeatureOverride`, `CheckoutSessionStatus`, `Credit` | **anpassen** — Modellnamen 1:1 deckungsgleich mit Spec; Felder müssen erweitert werden (siehe Lücken). Als Migrations-Basis übernehmen. |
| `apps/web/app/utils/stripe.server.ts` (21 KB) | Stripe-Client + ~40 Funktionen: `createStripeCheckoutSession`, `createStripeSetupSession`, `createStripeSubscription`, `cancelStripeSubscription`, `reactivateStripeSubscription`, `createCustomerPortalSession`, `getStripeInvoices`, `getStripeUpcomingInvoice(s)`, `getStripePaymentMethods`, `createStripeCustomer`, Products/Prices CRUD, `createStripeUsageBasedPrice`, `createMeterEvent`, `listMeterEventSummaries`, `getStripeCoupon`/`PromotionCode`, Revenue-Aggregation | **1:1** — deckt fast alle Stripe-Operationen der Spec (Block C, F, H, I, K) ab. API-Version `2025-10-29.clover`. EU-Entity/Tax-Felder konfigurativ. |
| `apps/web/app/utils/services/.server/subscriptionService.ts` (23 KB, mit Tests) | `getPlanFeaturesUsage`, `getPlanFeatureUsage`, `getActiveTenantSubscriptions`, `reportUsage`, `persistCheckoutSessionStatus`, `cancelTenantSubscription`, `getMrr`, `convertToCurrency`, `clearTenantSubscriptionCache` | **anpassen** — Lifecycle-Helper als Basis; Status-Übergänge (Block B) fehlen und sind zu ergänzen. |
| `apps/web/app/utils/services/.server/subscriptionCache.server.ts` | Two-Tier-Cache (L1 in-process Map, L2 SharedStore-Pub/Sub) mit TTL-Default 5 min + Cross-Replica-Invalidation | **anpassen** — Spec verlangt 30 s TTL (Block G) statt 5 min; ansonsten 1:1 das geforderte Cache-Invalidate-Pattern. |
| `apps/web/app/utils/services/.server/pricingService.ts` (23 KB) | Pricing-Berechnung / Plan-Preis-Auflösung | **anpassen** — Basis für Setup-Fee-Differenz-Logik (Block K) und Proration. |
| `apps/web/app/utils/services/.server/tenantPlanLookup.server.ts` (+Test) | Plan-Auflösung pro Tenant | **1:1 / anpassen** — direkt für Quota-/Feature-Lookup. |
| `apps/web/app/utils/db/subscriptions/tenantSubscriptionProducts.db.server.ts` (+Test) | DB-Layer Tenant-Subscription-Products | **als-vorlage** — DB-Zugriffsmuster; in HiveGrid-Layer-Trennung (View-Model) zu überführen. |
| `apps/web/app/utils/db/stripe/checkoutSessions.db.server.ts` | Checkout-Session-Persistenz | **als-vorlage** |
| `packages/entitlements/` (Paket `@toolctrl/entitlements`) | Plan-/Feature-Gate-Engine: `evaluateEntitlement`, `evaluateProductAccess`, `featureGateRegistry`, `featureKeys`, `getFeatureGate`, `standardEntitlementPlans`, `getProductBlueprint` (`blueprints.ts`, aktuell `crm-saas`) | **anpassen** — direkt nutzbar als Feature-Gating-Querschnitt (Spec `SubscriptionFeature`/`SubscriptionProductFeature`); HiveGrid-Plan-Codes/Blueprints einzufüllen. Plan-IDs sind aktuell `free`/`pro`/`enterprise`. |
| `packages/entitlements/src/ai-quotas.ts` (+Test) | `resolveTenantAIQuota`, `getAIQuotaPlanDefaults`, `parseTenantAIQuotaOverride`, `aiQuotaCapabilityKeys` | **1:1 / anpassen** — deckt AI-Credits-Quota (Block J) konzeptionell ab. |
| `packages/entitlements/src/ai-overage.ts` (+Test) | `resolveTenantAIOverage`, `computeMarkedUpCostCents`, `roundUpToBlock`, `standardAIOverageDefaults` (Multiplier 1.5, Block-Cents) | **1:1** — Pay-as-you-go-Aufschlags-Rechnung (Block I/J + Pflichtenheft 28.8) bereits implementiert. |
| `apps/worker/src/jobs/ai-cost-aggregate*.ts`, `ai-cost-persist*.ts`, `ai-cost-stripe-report*.ts`, `ai-cost-stripe-submitter.ts`, `ai-cost-reconciliation*.ts`, `ai-cost-alert.ts` | Voller AI-Cost-Pipeline-Stack: Aggregation pro Period → Persist → Stripe-Usage-Reporting (`createMeterEvent`) → Reconciliation → Kosten-Alert (Notbremse-Kandidat 28.8.6) | **1:1 / anpassen** — deckt Metering-Reporting an Stripe (Block I) + Notbremse/Upsell-Trigger weitgehend ab. |
| `apps/worker/src/jobs/trial-expiring-trigger.ts` (+Test) | Täglicher Cron: scannt Trials mit Ablauf in Horizon (default 3 Tage), emittiert `tenant.trial_expiring`-Outbox-Event; idempotenter dispatchKey `tenant.trial_expiring.<tenantId>.<YYYY-MM-DD>` | **anpassen** — Trial-Ende-3-Tage-/1-Tag-Warnung (Block E / 28.3.5). 1-Tag-Schwelle ergänzen. |
| `apps/api/src/billing/stripe-webhook-route.ts` (+ 24 KB Test) | Stripe-Webhook-Endpoint (Hono): Signatur-Verify (`constructStripeWebhookEvent`), Metrics, transaktionaler Sub-Update + Outbox-Publish, Cache-Invalidation. Handhabt: `customer.subscription.updated/deleted`, `subscription_schedule.canceled` | **anpassen** — solide Endpoint-/Signatur-/Transaktions-/Outbox-Vorlage. Event-Coverage und Idempotenz-Strategie müssen erweitert werden (siehe Lücken). |
| `apps/web/app/components/core/settings/subscription/*` | UI: `ChangeSubscription`, `MyInvoices`, `MyPayments`, `MyPaymentMethods`, `MyUpcomingInvoice`, `MyBillingSession`, `MyPlanFeatureUsage`, `MySubscriptionFeatures`, `MyProducts`, `Plan(s)`, `PlanChangePreviewModal`, `ToggleBillingPeriod`, `CheckPlanFeatureLimit` (alle mit i18n-Tests) | **als-vorlage** — Achtung LP-8/Apple-Compliance: Pricing-/Plan-Vergleich/Upgrade-UI darf NICHT in die App. Nur Read-Only-Teile (`MyInvoices`, `MyPaymentMethods`, Usage-Anzeigen, Billing-Session-Embed) sind in HiveGrid zulässig; Plan-Wechsel/Toggle-Period gehören in Anti-Liste. |
| `apps/web/app/routes/admin/settings/pricing*.tsx` (`pricing.tsx`, `pricing.new.tsx`, `pricing.edit.$id.tsx`, `pricing.features.tsx`, `pricing.ai-overage.tsx`) | Plattform-Admin-Pricing-Konfiguration (datengetrieben, kein Code-Release) | **anpassen** — erfüllt Spec-Grundprinzip Block A (konfig-getriebene Engine) + Pflichtenheft 28.1.6. Gehört nach `admin.hivegrid.de` (M37). |
| `apps/web/app/routes/admin/accounts/subscriptions.tsx`, `subscriptions-revenue.tsx` | Plattform-Admin: alle Tenant-Subscriptions + Revenue-Sicht | **anpassen** — Spec Block N „Plattform-Admin-Sicht" (M37). |
| `apps/web/app/routes/app.$tenant/settings/subscription.tsx` (+Test) | Tenant-seitige Subscription-Settings-Route | **als-vorlage** — Route-Pattern + Loader-Muster; HiveGrid-Routes sind `/{tenant}/admin/subscription/...` (Spec Block M). |
| `packages/shared-store/` (`@toolctrl/shared-store`) | Redis-KV: `createIdempotencyStore`, `createTwoTierCache`, Rate-Limiter | **1:1** — Idempotenz (AV #8) + Status-Cache (Block G) + Cross-Replica-Invalidation. |
| `prisma/schema/outbox.prisma` + `@toolctrl/db/outbox` (`publishOutboxEvent`) | Transactional-Outbox für zuverlässige Side-Effects (Notifications, Lifecycle-Events) | **1:1** — Übergangs-Hooks (Block B) sauber als Outbox-Events. |
| `apps/worker/src/jobs/legacy-event-receiver.ts` | Outbox→BullMQ-Consumer für `subscription.ended`/`cancelled` Lifecycle-Events | **anpassen** — Vorlage für Status-Übergangs-Side-Effects (Audit/Activity/Notification). |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
Alle in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/subscriptions.prisma`:
| BP-Modell | Spec-Pendant | Deckung |
|---|---|---|
| `SubscriptionProduct` | `SubscriptionProduct` | Teilweise — hat `stripeId`, `order`, `title`, `active`, `model`, `public`, `badge`, `billingAddressCollection`, `hasQuantity`, `canBuyAgain`, Relation `assignsTenantTypes TenantType[]`. **Fehlt:** `code`, `tier_order`, `is_self_service_eligible`, `setup_fee_required`, `trial_default_days`, `requires_manual_activation`, `frozen_mode_price_id`, locale-`display_name`, Markdown-`description`, `created_at/updated_at`. |
| `SubscriptionPrice` | `SubscriptionPrice` | Teilweise — `stripeId`, `type`(Int), `billingPeriod`(Int), `price`(Decimal), `currency`, `trialDays`, `active`. **Fehlt:** `interval`-Enum (`month/year/one_time`), `interval_count`, `unit_amount_cents`, `tax_behavior`, `valid_from/valid_until`. |
| `SubscriptionFeature` | `SubscriptionFeature` + teilweise `SubscriptionProductFeature` | Abweichend — BP-Feature ist **direkt** an `SubscriptionProduct` gehängt (`name`, `type`, `value`, `accumulate`), kein globaler Feature-Katalog + M:N-Join. Spec verlangt globalen `SubscriptionFeature` (mit `category`-Enum) + separate `SubscriptionProductFeature` (mit `quota_value` JSON). **Umbau nötig.** |
| `SubscriptionUsageBasedPrice` | `SubscriptionUsageBasedPrice` | Gut — `unit`, `usageType`, `aggregateUsage`, `tiersMode`, `billingScheme`, Tiers. **Fehlt:** `metric_code`-Enum (`automation_trigger`/`ai_token`/`sms_outbound`), `included_amount`, `overage_pricing_strategy`. |
| `SubscriptionUsageBasedTier` | `SubscriptionUsageBasedTier` | 1:1 — `from`, `to?`, `perUnitPrice`, `flatFeePrice`. |
| `TenantSubscription` | `TenantSubscription` | **Stark abweichend** — BP hat nur `tenantId`, `stripeCustomerId`, `products[]`. **Fehlt alles Lifecycle-Relevante:** `status`-Enum, `subscription_product_id`, `stripe_subscription_id`, Period-Felder, Trial-Felder, `cancel_at_period_end`, `cancelled_at/reason`, `frozen_since`, `setup_fee_paid_at`, Dunning-Felder, `created_by_user_id`, `created_via`. In BP liegen Period/Cancel-Felder auf `TenantSubscriptionProduct`, nicht auf `TenantSubscription`. |
| `TenantSubscriptionProduct` | `TenantSubscriptionProduct` (Snapshot) | Teilweise — hat `currentPeriodStart/End`, `cancelledAt`, `endsAt`, `stripeSubscriptionId`, `quantity`, `fromCheckoutSessionId`, `customEntitlementPlanId`. **Fehlt:** `snapshot_data` (JSONB Preisstabilität), `valid_from/valid_until`. |
| `TenantSubscriptionProductPrice` | `TenantSubscriptionProductPrice` | Teilweise — verknüpft Price + UsageBasedPrice + UsageRecords. **Fehlt:** `quantity` (liegt eine Ebene höher), `is_metered`-Flag, `valid_from/valid_until`. |
| `TenantSubscriptionUsageRecord` | `TenantSubscriptionUsageRecord` | Abweichend — BP: `timestamp`(Int), `quantity`, `stripeSubscriptionItemId`. Spec: `metric_code`, `period_start/end`, `usage_amount`, `reported_to_stripe_at`, Composite-Index. **Erweitern.** |
| `CheckoutSessionStatus` | `CheckoutSessionStatus` | Teilweise — BP: `pending`(Bool), `email`, `fromUrl`, `from/createdUserId`, `from/createdTenantId`. **Fehlt:** `stripe_checkout_session_id`, `status`-Enum, `intent`-Enum, `completed_at/expires_at`, `error_message`, `redirect_url_*`. |
| `Credit` | `Credit` | Teilweise — BP: `tenantId`, `userId?`, `amount`(Int), `type`(String), `objectId?`. **Fehlt:** `credit_type`-Enum, `source`-Enum, `valid_from/valid_until`, `balance_after`, `notes`, `created_by_user_id`-Semantik. |
| `TenantProductConfiguration` | (kein direktes Spec-Pendant) | Bonus — `blueprintId` default `crm-saas`. Brauchbar als Branchen-/Blueprint-Anker (M29). |
| `TenantFeatureOverride` | (deckt `quota_value`-Override-Idee) | Bonus — Per-Tenant Feature-Override (`featureKey`/`override`/`reason`). Nutzbar für manuelle Plattform-Admin-Grants. |
**Verwandt, NICHT subscriptions.prisma:**
- `prisma/schema/accounts.prisma` → `model Tenant` hat **nur** `active: Boolean` + `deactivatedReason: String?` — **kein Status-Enum** (trial/active/past_due/cancelled/frozen). Die in Spec Block B geforderte `Tenant.status`↔`TenantSubscription.status`-Synchronisation existiert in der BP NICHT.
- `model TenantType` (accounts.prisma) — ist ein **Tenant-Typ** (von `SubscriptionProduct.assignsTenantTypes` zugewiesen), **nicht** das Büro/Monteur-User-Seat-Konzept der Spec (Block D). Kein Seat-Typ pro User vorhanden.
- `prisma/schema/portals.prisma` → eigenes gespiegeltes `PortalSubscription*`-Subsystem (Endkunden-Billing). Nicht für M28-Tenant-Billing, aber als zweites Muster vorhanden.
---
## Lücken / komplett neu zu bauen
1. **Subscription-Lifecycle-State-Machine (Block B) — fehlt komplett.**
- Kein `status`-Enum auf `TenantSubscription` und kein `Tenant.status`-Enum. BP kennt nur `Tenant.active` (Boolean).
- Keine Übergangs-Tabelle/Guards (`trial→active`, `active→past_due`, `→frozen`, `→cancelled`, Reaktivierung). BP-Webhook setzt nur `cancelledAt`/`endsAt`/Period.
- Tenant-Status-Synchronisation (M27 A.5) neu zu bauen.
2. **Seat-Modell Büro/Monteur (Block D) — fehlt komplett.**
- Kein per-User-Seat-Typ, keine Seat-Counter, keine Rolle↔Seat-Validierung, keine Quota-Enforcement (`max_buero_seats`/`max_monteur_seats`), kein Hard/Soft-Limit. `quantity` existiert nur als undifferenzierte Stripe-Item-Menge.
3. **Read-Only-/Downgrade-Middleware (Block G, AV #11) — fehlt komplett.**
- Grep nach `read.?only|past.?due|frozen|isWritable|402` in `apps/web` + `apps/api` ergibt KEINEN Billing-Status-Gate. Kein `is_writable_status()`-Helper, kein 402-Response auf Write-Endpoints, keine Worker-Job-Blockade (Automation/Notification/Webhook-Outbound), keine UI-Disable-Logik. Komplett neu, gekoppelt an Lifecycle (#1).
4. **Webhook-Event-Coverage + Idempotenz-Tabelle (Block C) — teilweise.**
- BP-Handler deckt nur `customer.subscription.updated/deleted` + `subscription_schedule.canceled`. **Fehlen:** `checkout.session.completed`, `invoice.created`, `invoice.payment_succeeded`, `invoice.payment_failed`, `customer.subscription.trial_will_end`.
- Spec verlangt **`StripeWebhookLog`-Tabelle** mit `stripe_event_id`-Unique-Constraint (Schreib-Idempotenz). BP nutzt stattdessen Outbox-`dispatchKey` (`stripe:<eventId>:...`) — funktional ähnlich, aber NICHT die geforderte Tabelle. Neu anzulegen oder Spec-Abweichung freigeben.
5. **Dunning / Past-Due-Tracking (Block F) — fehlt.**
- Keine `last_dunning_attempt_at`/`dunning_attempt_count`-Felder, keine 14-Tage-3-Versuche-Logik, keine gestaffelte Notification-Strategie. `invoice.payment_failed`-Handling fehlt (siehe #4).
6. **Frozen-Modus / User-Pause (Block H) — fehlt.**
- Kein `frozen_since`, kein `frozen_mode_price_id`, kein „PAUSIEREN"-Multi-Step-Flow, kein Stripe-Price-Wechsel auf reduzierte Gebühr, kein `frozen→past_due`-Pfad.
7. **Trial-Aktivierungs-Flow mit Vertriebs-Freigabe (Block E) — teilweise.**
- `trial-expiring-trigger.ts` deckt die Warnungen ab. **Fehlt:** `requires_manual_activation`-Flow (Vertrieb setzt `trial_start`/`trial_end`), „Trial-Anfragen"-Liste in M37, Trial-Ende→`deletion_pending`-Übergang, 1-Tag-Schwelle.
8. **AI-Credits-Saldo + In-App-Sichten (Block J) — teilweise.**
- Engine-Teile (`ai-quotas`, `ai-overage`, `Credit`-Modell) da. **Fehlt:** Saldo-Berechnung aus `Credit`-Sum mit Carry-Over/`valid_until`-Enforcement, `balance_after`, Pre-Check-Gate beim LLM-Call mit Klartext-Fehler, In-App-Sichten `/{tenant}/admin/usage/` + `/usage/ai-credits/` (Listen, Drill-Down, Trend-Chart, CSV-Export, Hochrechnung).
9. **Setup-Fee-Differenz-Logik (Block K) — fehlt.**
- `setup_fee_required`, `setup_fee_paid_at`, `one_time`-Interval-Handling, Differenz-Berechnung bei Up-Grade (`effective_setup_fee = neu - bisherig`, clamped ≥0).
10. **Layer-Trennung (AGENTS.md DoD A) — kompletter Umbau.**
- BP-Code mischt DB + UI in `apps/web/app/components/core/settings/subscription/*` und nutzt `.db.server.ts`/`.server.ts` direkt. HiveGrid verlangt: View-Models in `packages/view-models/`, UI in `packages/ui-hivegrid/` (kein Prisma-Import), Subframe via Adapter, Routes bauen VM. Alle BP-UI-Komponenten sind daher nur **als-vorlage** brauchbar, nicht 1:1.
11. **Apple-Compliance-Filter (LP-8 / 28.X.10) — neu durchzusetzen.**
- BP-UI enthält explizit Pricing/Plan-Vergleich/Upgrade/Toggle-Period (`Plans.tsx`, `Plan.tsx`, `ChangeSubscription.tsx`, `ToggleBillingPeriod.tsx`, `PlanChangePreviewModal.tsx`). Diese gehören in HiveGrid auf die Anti-Liste; in der App nur Read-Only + externer „Plan-Wechsel über hivegrid.de"-Link (`noopener noreferrer`).
12. **Querschnitt-Wiring (AGENTS.md DoD B/C) — anzudocken.**
- Sidebar-Eintrag „Subscription / Abo" (Gruppe EINSTELLUNGEN, INTEGRATION-CONTRACTS §1) + Permission-Gate, Breadcrumbs, i18n-Keys, `logActivity`, `notify()` statt direkter Mail, `withTenantScope()`/`withPermission()`, Permission-Keys `billing.read`/`tenant.admin` (Block N) in `packages/entitlements/src/permissions.ts`. In der BP nicht in dieser Form vorhanden.
13. **Demo-Buchung (Block L)** — bewusst extern (Calendly), kein Code nötig; nur `created_via=platform_admin`-Pfad.
---
## Empfohlenes Vorgehen (Reihenfolge)
**M153 — Stripe-Integration (zuerst, Fundament):**
1. `prisma/schema/subscriptions.prisma` als Migrations-Basis übernehmen; spec-Felder ergänzen (insb. `TenantSubscription.status`-Enum, fehlende Product/Price/Credit/CheckoutSession-Felder) und `Tenant.status`-Enum in `accounts.prisma` einführen.
2. `apps/web/app/utils/stripe.server.ts` 1:1 übernehmen (in HiveGrid-Domain-Layer `app/domain/billing/`); EU-Entity/Tax konfigurieren.
3. **`StripeWebhookLog`-Tabelle** neu + Webhook-Handler aus `apps/api/src/billing/stripe-webhook-route.ts` als Vorlage erweitern auf alle Pflicht-Events (Block C). Idempotenz über `stripe_event_id`-Unique. Outbox-/Transaktions-Muster der BP beibehalten.
4. Idempotenz/Cache aus `@toolctrl/shared-store` + `subscriptionCache.server.ts` (TTL auf 30 s).
**M154 — Plan-/Seat-Modell:**
5. Pricing-Config-Admin (`routes/admin/settings/pricing*.tsx`) anpassen → `admin.hivegrid.de` (datengetriebene Engine, Block A).
6. `packages/entitlements` als Feature-Gating übernehmen; globalen `SubscriptionFeature`-Katalog + `SubscriptionProductFeature`-M:N umbauen; HiveGrid-Plan-Codes/Blueprints einfüllen.
7. **Seat-Modell (Büro/Monteur) komplett neu** — neue Felder/Counter, Rolle↔Seat-Validierung, Quota-Enforcement, Stripe-`quantity`-Sync. Usage-Sicht `/{tenant}/admin/usage/`.
**M155 — Trial + Read-Only:**
8. Lifecycle-State-Machine + `Tenant.status`-Sync (Block B) neu; Übergangs-Hooks via Outbox (Audit/Activity/`notify()`).
9. **Read-Only-/Downgrade-Middleware (Block G) komplett neu** — Route-Guard + Worker-Guard + 402-API + UI-Disable; an Lifecycle gekoppelt.
10. Trial-Aktivierungs-Flow + Dunning (Block E/F): `trial-expiring-trigger.ts` erweitern (1-Tag), `requires_manual_activation`-Flow + Dunning-Counter + `invoice.payment_failed`-Handling.
11. Frozen-Modus (Block H) + Setup-Fee (Block K).
**M156 — Metering / Pay-as-you-go (vieles vorhanden):**
12. `packages/entitlements/ai-overage.ts` + `ai-quotas.ts` **1:1** übernehmen.
13. AI-Cost-Worker-Pipeline (`ai-cost-*`) übernehmen/anpassen (Stripe-Usage via `createMeterEvent`, Reconciliation, Notbremse-Alert 28.8.6).
14. AI-Credits-Saldo-Logik (`Credit`-Sum, Carry-Over, Pre-Check-Gate) + In-App-Sichten (Usage/AI-Credits, CSV-Export) neu bauen.
**Querschnitt (alle Milestones):** Layer-Trennung (View-Models + `ui-hivegrid` + Adapter), Sidebar/Permissions/Breadcrumbs/i18n, Apple-Compliance-Filter (keine Pricing/Upgrade-UI in der App).
---
## Notizen
- **Reuse-Grad HIGH** trägt v.a. von Daten-Modell, Stripe-Wrapper, Pricing-Admin und Pay-as-you-go-Engine. Die drei spec-kritischsten Bereiche — **Lifecycle-State-Machine, Seat-Modell Büro/Monteur, Read-Only-Middleware** — sind in der BP NICHT vorhanden und komplett neu (das senkt es vom theoretischen „high+" auf solides high).
- **Idempotenz-Konflikt:** Spec fordert `StripeWebhookLog`-Tabelle; BP nutzt Outbox-`dispatchKey`. Vor Bau entscheiden (Spec-Abweichung freigeben oder Tabelle bauen).
- **Apple-Compliance** ist das größte „Anti"-Risiko: die fertigen BP-UI-Komponenten (`Plans.tsx`, `ChangeSubscription.tsx`, `ToggleBillingPeriod.tsx`) verleiten zur Übernahme verbotener In-App-Pricing-UI. Nur Read-Only-Sichten + externer Link erlaubt.
- BP-API-Version `2025-10-29.clover` (`apps/web/app/utils/stripe.server.ts:35` ff.) — beim Übernehmen aktuell halten.
</content>
</invoke>
@@ -0,0 +1,97 @@
# Boilerplate-Abgleich — M29 Onboarding
- **Modul-ID:** M29 (Modul 29)
- **Name:** Onboarding
- **Milestones:** M157 (Wizard-Framework + State-Machine), M158 (SHK-Standard-Daten-Provisionierung), M159 (Branchen-Templates deklarativ), M160 (Demo-Daten + KI-Walkthrough)
- **Pflichtenheft-Block:** Block 7 (Plattform-Außenseite), Abschnitt Modul 29
- **Reuse-Level:** **low**
## Kurz-Verdikt
Die Boilerplate hat ein **vollständiges, reifes Onboarding-Subsystem** — aber es löst ein **anderes Problem** als M29. Das Boilerplate-Onboarding ist ein **Admin-konfigurierbares Produkt-Tour-/Announcement-System** (Modal/Page-Touren, Zielgruppen-Filter, Session-Tracking, Realtime-Trigger). M29 verlangt einen **branchen-getriebenen Post-Signup-Provisioning-Wizard**, der einem frischen Mandanten Pipelines, Rollen, Custom-Felder, Forms, Demo-Daten und eine Erste-Schritte-Checkliste einrichtet — aus **deklarativen YAML-Branchen-Templates**.
Die 7 Prisma-Modelle und die State-Machine-Logik (Session-Lifecycle: active → started → completed/dismissed, Step-Tracking mit seenAt/completedAt, Action-Log, Filter-Matching, Resume) sind als **konzeptionelle Vorlage und teils 1:1-Datenmodell-Basis** sehr wertvoll. Aber die fachliche Kern-Logik von M29 (Provisioning-Transaktion, deklarative Templates, Demo-Daten mit Demo-Flag, Erste-Schritte-Auto-Detection, KI-Walkthrough) existiert **nicht** und ist komplett neu zu bauen.
Querschnitt-Bausteine (Audit, i18n, Feature-Flag `onboarding`, Notifications, Tenant-Scope, Events) sind vorhanden und werden genutzt.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/onboarding.prisma` | 7 Modelle: `Onboarding`, `OnboardingFilter`, `OnboardingStep`, `OnboardingSession`, `OnboardingSessionAction`, `OnboardingSessionFilterMatch`, `OnboardingSessionStep` | **anpassen** — Modell-Namen + Grundstruktur (Session-Tracking, Step-Tracking, Action-Log, Filter-Match, `@@unique([onboardingId, userId, tenantId])`) decken Spec-Block A weitgehend. ABER: Spec verlangt zusätzliche Felder/Modelle (`industry_code`, `version`, `template_directory_path`, `OnboardingDemoProvisioning`, `OnboardingInvitationDispatch`, `OnboardingSessionStep.status`-Enum + `submitted_data`, `validation_rules`, `action_handler`, `step_type`). Felder erweitern. |
| `apps/web/app/modules/onboarding/services/OnboardingSessionService.ts` | Session-Lifecycle-State-Machine: `started`, `dismissed`, `setStep` (fromIdx/toIdx mit completedAt/seenAt), `complete`, `addActions` (Input-Dedup + Timestamps) | **als-vorlage** — exakt das resumable State-Machine-Verhalten aus 29.1.4 / M157. Logik-Muster (Step-Vor/Zurück, Action-Persistierung, Completion) übernehmen, fachlich auf 10-Step-Provisioning-Wizard ummünzen. |
| `apps/web/app/modules/onboarding/services/OnboardingService.ts` | Auto-Match-Logik: `getUserActiveOnboarding` (Erst-Login-Detection + Resume), `generateMatchingOnboardings`, `getMatchingFilters` | **als-vorlage** — deckt 29.1.2 (Auto-Redirect bei erstem Login), Filter-basiertes Onboarding-Matching (Spec `OnboardingFilter`/`OnboardingSessionFilterMatch`) und „nur eine aktive Session pro Tenant". Filter-Typ-Set muss um `industry`/`company_size` erweitert werden. |
| `apps/web/app/modules/onboarding/services/OnboardingFiltersService.ts` | Filter-Matching-Engine (`matches({userId, tenantId, filter})`) | **als-vorlage** — Operator/Filter-Auswertung; Spec braucht `equals/in/greater_than/less_than` (29.3-Branche, Größe). |
| `apps/web/app/modules/onboarding/services/step-registry/registry.server.ts` + `gating.server.ts` | Deklaratives Step-Registry mit Gating via Feature-Flag + Plan-Feature; `getVisibleOnboardingSteps`, `evaluateOnboardingStepGates` | **als-vorlage** — Muster für „Step nur sichtbar wenn Bedingung". Nützlich für „Coming soon"-Branche-Gating und optionale Steps (Logo, Demo-Daten). |
| `apps/web/app/modules/onboarding/routes/onboarding.$id.tsx` (`apps/web/app/routes/onboarding.$id.tsx`) | Route mit Loader/Action (CSRF, Owner-Check, Action-Dispatch: started/dismissed/add-actions/set-step/complete) | **als-vorlage** — Action-Dispatch-Shape + Owner-/CSRF-Guard für die `/onboarding/setup/`-Route (Spec Block I). |
| `apps/web/app/modules/onboarding/db/*.db.server.ts` (`onboarding`, `onboardingSessions`, `onboardingSessionSteps`) | DB-Layer mit `OnboardingWithDetails`/`OnboardingSessionWithDetails`-Includes, `createOnboardingSession` (mit Filter-Match-Anlage), Pagination | **anpassen** — DB-Zugriffsmuster + Include-Typen übernehmen; an erweiterte Felder/Modelle anpassen. |
| `apps/web/app/components/admin/SetupSteps.tsx` + `apps/web/app/application/dtos/setup/SetupItem.ts` | „Getting Started"-Widget: Liste von Items mit completed-Icon + CTA-Link, i18n-Key `onboarding.gettingStarted.title` | **als-vorlage** — direkte Vorlage für die Erste-Schritte-Checkliste (Block F / M158): Item mit Titel/Beschreibung/`completed`/`path`-CTA. ABER: kein Auto-Detection/Status-Tracking, keine Dismiss-Logik — neu zu bauen. |
| `apps/web/app/components/layouts/buttons/OnboardingButton.tsx`, `components/Onboarding*.tsx` (`OnboardingSession.tsx`, `OnboardingBadge.tsx`, `OnboardingSessionBadge.tsx`) | UI-Renderer für Session/Block, Trigger-Button | **als-vorlage** — UI-Mechanik (Modal/Page-Rendering, Block→Step-Mapping). HiveGrid baut UI ohnehin neu via Subframe + ui-hivegrid-Adapter; nur Verhaltensmuster ableiten. |
| `apps/web/app/modules/events/services/.server/EventsService.ts` + DTOs `OnboardingStarted/Completed/DismissedDto.ts` | Event-Emission bei Onboarding-Lifecycle | **1:1** als Wiring-Pattern — fügt sich in M001-Audit / M045-Activity / M011-Notify ein. |
| `apps/web/app/utils/db/appConfiguration.db.server.ts` (Zeile 78, 184: `onboarding.enabled`) | App-Config-Flag `onboarding.enabled` | **1:1** — Modul-Abschaltbarkeit (DoD-Feature-Flag). |
| `packages/foundation` `featureKeys` enthält `onboarding` (Inventur `foundation.md:93`) | Kanonischer Feature-Key `onboarding` für Entitlement-Gating | **1:1** — Feature-Flag-Gate aus DoD. |
| `apps/web/app/utils/db/seed/SeedService.ts` (`seedDemoProductCoreTenant`, `seedDemoTenantRoles`, `seedDemoTenantKnowledgeBase`, `seedDemoSupportTickets`, `assignTenantPlan`) | Per-Tenant-Seeding-Muster (Rollen, KB, Tickets, Plan-Zuordnung, upsert-basiert idempotent) | **als-vorlage** — bestes vorhandenes Vorbild für die SHK-Provisionierung (M158) und Demo-Daten (M160). Aber: hardcoded (kein deklaratives Template), keine Transaktion/Rollback, kein Demo-Flag, kein SHK-Inhalt → nur Struktur-Vorlage. |
| `prisma/schema/imports.prisma` (`ImportJob`) + `@toolctrl/imports` | Async-Import-Job-Infrastruktur (Status, Row-Counts, Error-Report) | **als-vorlage** — relevant für Phase-2-CSV-Bulk-Import (29.X.3, out of scope Phase 1). |
| `packages/email-templates/src/sequences/` (welcome, trial-reminder, re-activation) + `packages/mail/.../Invitation` Template | Lifecycle-/Einladungs-Mails | **als-vorlage / 1:1** — Mitarbeiter-Einladungs-Mail (29.5) läuft über M27/M24-Invitation-Flow; Welcome-Mail vorhanden. |
---
## Vorhandene Daten-Modelle (konkret)
In `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/onboarding.prisma`:
- **`Onboarding`** — `id`, `title`, `type` (modal/page), `realtime`, `active`, `canBeDismissed`, `height`; Relationen zu Filters/Steps/Sessions.
- Spec-Mapping: deckt `Onboarding` teilweise. **Fehlt:** `code`, `display_name`/`description` getrennt, `industry_code`, `version`, `template_directory_path`. (`active` ↔ `is_active`/„Coming soon").
- **`OnboardingFilter`** — `onboardingId`, `type`, `value`.
- Spec-Mapping: ↔ `OnboardingFilter`. **Fehlt:** `criterion_key`/`operator`/`criterion_value`-Struktur (Spec hat explizite Operatoren; BP hat nur `type`+`value`-String).
- **`OnboardingStep`** — `onboardingId`, `order`, `block` (JSON-String mit Block-DTO).
- Spec-Mapping: ↔ `OnboardingStep`. **Fehlt:** `code`, `display_name`, `step_type` (info/form/action/confirmation), `is_skippable`, `validation_rules`, `action_handler`. (BP kodiert UI im JSON-`block`, Spec will strukturierte Felder.)
- **`OnboardingSession`** — `onboardingId`, `userId`, `tenantId?`, `status` (active/started/completed/dismissed), `startedAt`/`completedAt`/`dismissedAt`, `createdRealtime`; `@@unique([onboardingId, userId, tenantId])`.
- Spec-Mapping: ↔ `OnboardingSession`. Sehr gute Basis. **Fehlt:** `current_step_id`, `started_by_user_id` (vs. `userId`), `payload` (JSONB-Antworten), Spec-Unique nur `(tenant_id, status='in_progress')`.
- **`OnboardingSessionAction`** — `onboardingSessionId`, `type`, `name`, `value`.
- Spec-Mapping: ↔ `OnboardingSessionAction`. **Fehlt:** `step_id`, `action_type`-Enum (step_entered/completed/skipped/data_saved/external_call), `payload`-JSON, `actor_user_id`, `created_at`-Index. (Audit/Replay-Anbindung erweitern.)
- **`OnboardingSessionFilterMatch`** — `onboardingFilterId`, `onboardingSessionId`.
- Spec-Mapping: ↔ `OnboardingSessionFilterMatch`. **Fehlt:** `matched_value`.
- **`OnboardingSessionStep`** — `onboardingSessionId`, `stepId`, `seenAt`, `completedAt`.
- Spec-Mapping: ↔ `OnboardingSessionStep`. **Fehlt:** `status`-Enum (pending/in_progress/completed/skipped), `submitted_data`.
**Nicht vorhanden (neue Modelle nötig):** `OnboardingDemoProvisioning` (Demo-Tracking, `entities_created`, `provisioning_log`, `removable_until`) und `OnboardingInvitationDispatch` (FK auf M27 `TenantUserInvitation`, `bulk_batch_id`).
**Kein Branchen-/Industry-Konzept im Schema** (`grep -niE "industry|branche|trade" prisma/schema` → leer). **Kein Demo-Flag** (`isDemo`/`is_demo_data`) auf irgendeiner Entität.
---
## Lücken / komplett neu zu bauen
1. **Branchen-getriebene deklarative Templates (M159, Spec Block C, A.3):** Es gibt **keine** YAML-/Verzeichnis-basierte Template-Engine. Keine `yaml`/`js-yaml`-Dependency (`grep` → leer). Komplett neu: Verzeichnis-Struktur `templates/onboarding/<branche>/{onboarding,pipelines,forms,custom-fields,permissions,demo-data,checklist}.yml`, Loader beim System-Start, Sync nach `Onboarding`/`OnboardingStep`/`OnboardingFilter`, Build-Validierung pro Template. Das BP-Onboarding ist DB-konfiguriert (Admin-UI), nicht template-getrieben.
2. **SHK-Smart-Setup-Provisioning-Transaktion (M158, Spec Block D):** Auto-Anlage von Pipelines, Custom-Feldern, Rollen+Permissions, Form-Templates, Marketing-Templates, Notification-Sets, Dashboards — **als Transaktion (alles oder nichts) mit Rollback + Retry + Plattform-Admin-Alert bei Fehler**. `SeedService.ts` ist nur eine grobe Struktur-Vorlage (hardcoded, keine TX, keine Branchen, keine SHK-Inhalte). Die konkreten SHK-Defaults (10 Rollen, 2 Sales-/3 Auftrags-Pipelines, SHK-Custom-Felder, 8 Form-Vorlagen) existieren nirgends.
3. **Demo-Daten-Provisioning + Demo-Flag (M160, Spec Block E):** Kein `is_demo_data`-Flag auf Entitäten, kein „Demo-Daten ausblenden"-Filter in Listen, kein Bulk-Lösch mit Multi-Step-Confirm, keine `OnboardingDemoProvisioning`-Verfolgung. SHK-Demo-Inhalte (10 Kontakte, 3 Firmen, 5 Deals, 1 Projekt mit Chat/Tasks/Foto-Checkliste, 1 Lead, 5 Dateien, Booking-Page) komplett neu.
4. **Erste-Schritte-Checkliste mit Auto-Detection (M158, Spec Block F):** `SetupSteps.tsx` liefert nur die statische Anzeige. Es fehlt: deklarative Item-Definition (`first-steps.yml`), Detection-Rules (KWP-Verbindung, Mailbox, AirCall, Booking-Page, Einladung, Automation, Form, Deal-Stage-Change), Status-Tracking/Cache, manuelles Erledigt-Markieren, dauerhaftes Ausblenden, 100%-Glückwunsch.
5. **KI-Walkthrough (M160, Spec Block H):** Existiert nicht. Muss als System-Prompt-Modus „onboarding-walkthrough" auf M18-Tenant-Assistant aufsetzen, mit Action-Boundary (AV #20: nur navigieren/erklären, kein Schreiben), Permission-aware Kontext, User-Settings-Toggle `ai_walkthrough_offered`. (Der Boilerplate-`chatbot` existiert, aber kein Walkthrough-Modus.)
6. **Vollbild-Wizard-Route `/onboarding/setup/` (M157, Spec Block B/I):** BP hat nur `onboarding.$id` (Session-by-id, modal/page-Tour) und `admin/onboarding/*` (Admin-Verwaltung). Der 10-Schritte-Vollbild-Wizard ohne Sidebar, mit den HiveGrid-spezifischen Steps (Firmendaten, Branche-Bestätigung mit „Coming soon", Größe, Logo-Upload, Smart-Setup-Vorschau mit Toggle pro Template-Gruppe, Demo-Daten-Frage, Mitarbeiter-Einladung) ist neu — inkl. UI via Subframe + ui-hivegrid-Adapter.
7. **Logo-Upload-Pipeline (Spec 29.2.5):** PNG/JPG/SVG, max 2 MB, Format-/Größen-Validierung mit Klartext-Fehlern — neu (gemeinsam mit M23 Files).
8. **Mitarbeiter-Einladung im Wizard (M160, Spec Block G):** Inline-Email+Rolle-Zeilen, Live-Validierung, Duplikat-Check, „Vertriebler"-Default, „Später"-Skip → nutzt M27/M24-Invitation-Flow; `OnboardingInvitationDispatch`-Tracking neu.
9. **Permissions `onboarding.run` / `onboarding.demo_data.manage` / `onboarding.checklist.dismiss` (Spec Block J):** Im BP-Onboarding läuft Zugriff über User-Owner-Check + `admin`-Flag, nicht über HiveGrid-Permission-Keys (M004). Neu zu definieren in `packages/entitlements/src/permissions.ts` + Bundle-Mapping.
10. **HiveGrid-Layer-Trennung (AGENTS.md DoD):** Das BP-Onboarding mischt DB-Zugriff, Server-Logik und React-Components im selben `modules/onboarding`-Ordner. Für HiveGrid muss alles auf View-Model-Layer (`packages/view-models`), Domain-Layer (`app/domain`), ui-hivegrid + Subframe-Adapter umstrukturiert werden. Kein 1:1-Übernehmen der Datei-Struktur möglich.
---
## Empfohlenes Vorgehen
**Reihenfolge (BP-Vorlage zuerst, dann Neues):**
1. **Datenmodell zuerst (Basis für alle 4 Milestones):** `onboarding.prisma` aus BP als Start kopieren, dann gemäß Spec Block A erweitern: Step-Felder (`code`, `step_type`, `is_skippable`, `validation_rules`, `action_handler`), Session (`current_step_id`, `payload`), Filter (`criterion_key`/`operator`/`criterion_value`), Action (`action_type`-Enum, `payload`, `actor_user_id`). Zwei neue Modelle `OnboardingDemoProvisioning` + `OnboardingInvitationDispatch`. Industry-Felder (`industry_code`, `version`, `template_directory_path`). Forward-Only-Migration mit Inverse-SQL.
2. **M157 Wizard-Framework:** Die State-Machine aus `OnboardingSessionService.ts` (started/setStep/complete/dismissed + Resume-Logik aus `OnboardingService.getUserActiveOnboarding`) als Vorlage in HiveGrid-Domain-Layer portieren. Neue Vollbild-Route `/onboarding/setup/` + `/onboarding/setup/step/{code}/`. View-Model + Subframe-UI. Erst-Login-Detection (29.1.2) aus BP-`generateMatchingOnboardings` ableiten.
3. **M159 Branchen-Templates (Fundament für M158):** Deklarative YAML-Engine **komplett neu** (yaml-Dependency hinzufügen, Verzeichnis-Loader, Sync-Service, Build-Validierung). Phase 1 nur `shk` aktiv, andere als `is_active=false` „Coming soon". Das ist der größte Neubau-Block — kein BP-Vorbild.
4. **M158 SHK-Provisioning:** `SeedService.ts`-Muster (per-Tenant upsert, Rollen/KB/Tickets) als Struktur-Vorlage, aber auf Transaktion + Rollback + Retry umbauen und aus M159-Templates speisen. Erste-Schritte-Checkliste: `SetupSteps.tsx`/`SetupItem` als UI-Vorlage, Detection-Rules + Status-Tracking neu.
5. **M160 Demo-Daten + KI-Walkthrough:** Demo-Flag-Spalte auf relevanten Entitäten + Listen-Filter + Bulk-Lösch neu. Demo-Seeding aus YAML-Template (M159). KI-Walkthrough als neuer System-Prompt-Modus auf M18-Assistant mit Action-Boundary. Mitarbeiter-Einladung über M27/M24-Flow + `OnboardingInvitationDispatch`.
6. **Querschnitt durchgängig:** Feature-Key `onboarding` (foundation) + `appConfiguration.onboarding.enabled` 1:1 nutzen. Events (`EventsService` + Onboarding-DTOs) als Wiring-Pattern → M001-Audit / M045-Activity / M011-Notify. Permission-Keys (`onboarding.*`) neu in M004-Registry. i18n-Keys (BP hat bereits `onboarding.*`-Keys als Namensraum-Vorlage).
**Wichtig:** Das BP-Onboarding NICHT als fertiges Modul „aktivieren" — es ist ein Tour-System, nicht der M29-Provisioning-Wizard. Es dient als **Daten-Modell-Basis + State-Machine-Vorlage + Wiring-Muster**, nicht als lauffähige Lösung.
@@ -0,0 +1,98 @@
# Boilerplate-Abgleich — Modul M30: i18n / Mehrsprachigkeit
- **Modul-ID:** M30 (Spec `_bmad-output/specs/M30-i18n.md`)
- **Name:** i18n / Mehrsprachigkeit
- **Zugehörige Milestones:** M028 (User/Tenant Sprach-Settings + Auflösungs-Reihenfolge), M029 (Sprach-Wechsler + Fallback-Hinweise). Querschnitt-Vorbedingung: M003 (i18n-Pflicht-Wiring).
- **Achtung Numerik-Stolperfalle:** Das Repo-Milestone-Verzeichnis `docs/milestones/M030-hell-dunkel-modus` ist NICHT dieses Modul (das ist Hell/Dunkel-Modus). Modul-Spec „M30" = Milestones M028+M029.
- **Pflichtenheft-Block:** Block 1, Modul 30 (`_bmad-output/pflichtenheft/blocks/block1.md` Abschnitte 30.1–30.6)
- **Reuse-Level:** **HIGH** — vollständige i18n-Library + App-Wiring + CI-Gates bereits vorhanden; HiveGrid füllt vor allem Inhalte (Locale-Keys, TR/PL) ein und ergänzt zwei Datenmodell-Felder + zwei Resolver-Schritte.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `/Users/raffaelsheikh/Documents/GitHub/boilerplate/packages/i18n/` | Eigenständiges `@toolctrl/i18n`-Paket: Locale-Config, ICU-MessageFormat-Plurale, SSR-sichere Intl-Formatter (Zahl/Währung/Datum/RelativeTime/Liste), RTL-Erkennung, `useTranslate`-Factory | **1:1** (Paket nur umbenennen `@toolctrl/i18n`→`@hivegrid/i18n`; Logik unverändert) |
| `…/packages/i18n/src/index.ts` | `defaultLocale="de"`, `supportedLocales=["de","en"]`, `i18nNamespaces`, `i18nConfig`, `isSupportedLocale`, `getDirection`, `isRTL` | **anpassen** — `supportedLocales` auf `["de","en","tr","pl"]` erweitern; Namespaces auf HiveGrid-Module mappen |
| `…/packages/i18n/src/icu/plural.ts` | `formatIcu`, `createIcuFormatter` (FormatJS/CLDR-Plural-Regeln, deckt PL 3-Form-Plurale ab) | **1:1** — erfüllt Spec A.1.3 (ICU-Plural) komplett |
| `…/packages/i18n/src/intl/{number,currency,date,list}.ts` | Locale-explizite Intl-Wrapper, gecacht, SSR-safe | **1:1** — erfüllt Spec A.1.4 (Intl-Formate) |
| `…/packages/i18n/src/useTranslate.ts` | `createUseTranslate(deps)` — react-i18next injiziert (kein harter Dep), automatische ICU-Erkennung | **1:1** |
| `…/apps/web/app/locale/i18next.server.ts` | Server-Loader: `remixI18Next` (remix-i18next), `i18nCookie` (`_i18n`, 30 Tage), `getTranslations(request, ns)`, fs-Backend pro `{{lng}}/{{ns}}.json` | **anpassen** — erfüllt D.3 (Sprache pro Request im Loader) + Cookie-Persistenz (D.4); Resolution-Order erweitern (siehe Lücken) |
| `…/apps/web/app/locale/i18next.global.server.ts` | Globale i18next-Instanz für SSR (verhindert Flash/Hydration-Mismatch, lädt alle Locales synchron vorab) | **1:1** — erfüllt D.3 „SSR rendert sofort korrekt ohne Flash-of-DE" |
| `…/apps/web/app/locale/mail-translator.server.ts` | `getMailTranslator(locale)`, `getMailTranslatorFromRequest(request)`, `resolveMailLocale` (Fallback auf `defaultLocale`), `mail`-Namespace | **anpassen** — Basis für Block F (Outbound-Email i18n); Empfänger-Auflösung D.2 ergänzen (User/Contact/Tenant-Default) |
| `…/apps/web/app/components/ui/selectors/LocaleSelector.tsx` | Sprach-Dropdown, submitted an `/preferences` (`action=setLocale`), iteriert `i18nConfig.supportedLngs` | **als-vorlage** — Public-Page-Switcher (G.3) + Header-Switcher (D.4); UI in HiveGrid-Design neu bauen (Subframe), Logik übernehmen |
| `…/apps/web/app/routes/preferences.tsx` | Root-Action `setLocale`, schreibt Locale-Cookie, Reload-frei | **anpassen** — erfüllt D.4 (Live-Wechsel ohne Reload); zusätzlich `User.locale` in DB persistieren bei eingeloggtem User |
| `…/apps/web/app/modules/users/components/UserProfileSettings.tsx` (Picker referenziert in `…/routes/app.$tenant/settings/profile.locale-picker.test.ts`) | Locale-Picker im User-Profil, `i18nConfig.supportedLngs`, `changedLocale()`, `setLocale`→`/preferences` | **als-vorlage** — Basis für G.1 (`/{tenant}/settings/profile/`); Zeitzone+Datumsformat-Picker fehlen noch |
| `…/scripts/i18n-check.ts` | Locale-Parity-Checker: Key-Divergenz pro Namespace, fehlende/leere Files, **Waisen-Keys** (`unexpected-locale-dir`), **fehlende Source-Keys** (`t("…")` ohne Locale-Eintrag), leere Übersetzungen | **1:1** — erfüllt B.3 (Vollständigkeits-Check, Waisen-Keys, Master=DE) nahezu komplett |
| `…/scripts/verify-hardcoded-strings.mjs` (+ `.test.mjs`) | Build-Lint gegen hartkodierte User-facing Strings über `apps/packages/prisma/server`, mit Allowlist (`reviewedPathPatterns`) | **1:1 / anpassen** — erfüllt A.1.5 (Lint-Regel im Build) + Allowlist-Mechanik (offener Spec-Punkt „Lint-Regel-Allowlist-Spec") |
| `…/scripts/i18n-audit.mjs` + `verify-i18n-hardcoded-baseline.mjs` | Baseline-Scan hartcodierter DE/EN-Strings (zählt, erzwingt nicht — Migrations-Hilfe) | **als-vorlage** — nützlich beim schrittweisen Härten der Lint-Regel |
| `package.json` Script `verify:i18n` | `tsx scripts/i18n-check.ts && node scripts/verify-hardcoded-strings.mjs && eslint --max-warnings=0 packages/ui-*` | **1:1** — fertiges CI-Gate für DoD-Punkt 10 (i18n-Keys Pflicht) |
| `…/apps/web/app/modules/i18n/locale-coherence.test.ts` | Cross-Language-Copy-Leak-Test (DE-Wörter im EN-Catalog etc.) | **als-vorlage** — Inhalt ist boilerplate-spezifisch (Tenant→Konto-Wording), Struktur als Vorlage für HiveGrid-Glossar-Tests |
| `…/apps/web/app/modules/i18n/autogenerated-locale-prune.test.ts` | Prune-Test: Locale-Keys ohne Source-Referenz markieren | **anpassen** — ergänzt Waisen-Key-Erkennung (B.3) |
| `…/apps/web/app/utils/i18n/translateLabel.ts` (+ `.test.ts`) | Helper zum Übersetzen von Label-Keys | **als-vorlage** |
| `…/apps/web/public/locales/{de,en}/*.json` (9 Namespaces: translations, auth, billing, common, crm, emailMarketing, errors, mail, settings) | Befüllte DE+EN-Locale-Files | **als-vorlage** — Struktur/Konvention übernehmen, Inhalte sind ToolCTRL-spezifisch und werden durch HiveGrid-Keys ersetzt |
| `package.json` deps | `i18next ^23`, `react-i18next ^15`, `remix-i18next ^7`, `i18next-fs-backend`, `i18next-http-backend`, `i18next-browser-languagedetector`, `intl-messageformat ^10` | **1:1** — kompletter Stack vorhanden, keine neuen Libs nötig (klärt Spec-offenen-Punkt „i18n-Library-Wahl" → react-i18next ist gesetzt) |
---
## Vorhandene Daten-Modelle
| Modell / Feld | Pfad | Status gegen Spec |
| --- | --- | --- |
| `User.locale` (`String?`) | `…/prisma/schema/accounts.prisma:16` | **vorhanden** — deckt C.1 `language` ab (als freier String, nicht Enum `de\|en\|tr\|pl`). Reicht funktional, ggf. Enum-/Validierung in App-Layer ergänzen |
| `User.lightOrDarkMode` (`String?`) | `…/prisma/schema/accounts.prisma:17` | vorhanden (gehört zu M030 Hell/Dunkel, nicht i18n — nur Kontext) |
| `Tenant` (Modell, Felder `theme`, `themeConfig`) | `…/prisma/schema/accounts.prisma:129` | Tenant existiert, **aber KEIN `default_language`/`locale`-Feld** → siehe Lücken (C.2) |
| `Portal.locale` (`String?`) | `…/prisma/schema/portals.prisma:67` | vorhanden (Customer-Portal-Locale; relevant für Public-Portal-Sprachauflösung) |
| `KnowledgeBase.defaultLanguage` / `.languages` / `Article.language` | `…/prisma/schema/knowledgeBase.prisma:12,16,37,73` | vorhanden — Basis für Help-Center-Mehrsprachigkeit (G.3, Phase 2 / Modul 38) |
| FeatureFlag-Filter-Typ `language.is` | `…/prisma/schema/featureFlags.prisma:37` | vorhanden — Sprache als Targeting-Dimension nutzbar |
**Datenmodell-Fazit:** `User`-Sprache ist da; `Tenant`-Default-Sprache, `User.timezone` und `User.date_format_preference` sind NICHT vorhanden.
---
## Lücken / komplett neu zu bauen
1. **`Tenant.default_language` fehlt** (Spec C.2). `Tenant` (`…/prisma/schema/accounts.prisma:129`) hat nur `theme`/`themeConfig`, kein Sprach-Default. → Neues Feld + Forward-Only-Migration. Blockt M028 (Mandanten-Default in Auflösungs-Reihenfolge).
2. **`User.timezone` und `User.date_format_preference` fehlen** (Spec C.1). Nirgends in `prisma/schema/*.prisma` ein `timezone`/`dateFormat`-Feld (grep leer). → Zwei neue `User`-Felder. Profil-Picker für Zeitzone/Datumsformat (G.1) gibt es noch nicht (nur Locale-Picker vorhanden).
3. **Resolution-Order unvollständig** (Spec D.1 / Pflichtenheft 30.2.4). Boilerplate-`remixI18Next` löst Cookie → Browser-`Accept-Language` → Fallback `de`. Es fehlt der **`Tenant.default_language`-Schritt** (User > **Tenant** > Browser > DE) und das Lesen von `User.locale` aus DB nach Login. → Resolver-Funktion `resolveLocale(user, tenant, browser, default)` in `app/locale/**` ergänzen (M028 Slice-Hinweis nennt exakt diese Signatur).
4. **Outbound-Empfänger-Auflösung D.2 fehlt** (Pflichtenheft 30.2.5). `mail-translator.server.ts` löst nur eine übergebene Locale; die Kette „User.language > Contact.language > Tenant.default_language > de" muss neu gebaut werden. Setzt `Contact.language` voraus (Modul 1 — im aktuellen Schema kein CRM-`Contact`-Modell mit `language` gefunden).
5. **TR/PL-Locales fehlen** (Spec B.4 / Pflichtenheft 30.1.6). Nur `de`+`en` in `…/apps/web/public/locales/`. Phase-1-Pflicht ist nur DE befüllt — aber `supportedLocales` muss `tr`/`pl` als Ziel-Sprachen kennen (für „Coming soon"-Anzeige G.1 und Fallback-Hinweise H/I). Der Parity-Checker würde leere `tr`/`pl`-Files als Divergenz melden → Strenge anpassen (Spec B.4: andere Locales dürfen Phase 1 teilbefüllt sein, Fallback auf DE).
6. **`errors.*`-Namespace-Struktur** (Spec B.2 / LP-10). Boilerplate hat zwar einen `errors`-Namespace, aber die geforderte Sub-Key-Struktur `errors.<x>.headline / .body / .action_label` ist HiveGrid-spezifisch und muss als Konvention etabliert/befüllt werden (koppelt an M002 Error-Catalog).
7. **Fallback-Debug-Marker (Dev) + Klartext-Hinweise** (Spec B.4, H, I.). „Lokalisierung für [Sprache] noch nicht vollständig — Fallback auf Deutsch" (Dev), „Sprache geändert." etc. — als Locale-Keys + Dev-Mode-Marker neu. remix-i18next macht silenten Fallback (Prod ok), Dev-Marker fehlt.
8. **Settings-Routen für HiveGrid-URL-Schema** (Spec G.1/G.2). Boilerplate-Profil-Route liegt unter `app.$tenant/settings/profile`; HiveGrid-Pattern ist `/{tenant}/settings/profile/` und `/{tenant}/admin/general/` (Tenant-Default-Sprache, Permission `tenant.admin`). Tenant-Default-Picker existiert noch gar nicht.
9. **Public-Page-Sprach-Switcher mit Cookie** (Spec G.3 / M029 / Pflichtenheft 30.4.2). `LocaleSelector.tsx` existiert als Footer-Switcher, aber dedizierter Public-Page-Switcher (Help `/help/{lang}/…`, Booking, Forms) mit `httpOnly:false, samesite:lax`-Cookie (M029-Slice-Hinweis) ist neu. URL-Sprachsegment `/help/{lang}/{slug}` (Integration-Contracts §3) noch nicht verdrahtet.
10. **`.server.ts`-Memory-Blocker prüfen.** Boilerplate nutzt `i18next.server.ts` / `i18next.global.server.ts`. Laut User-Memory (S03) ist `.server.ts`-**Re-Export** inkompatibel mit RR7-Client-Build. Diese Dateien sind server-only Logik (kein Client-Re-Export) → vermutlich unkritisch, aber bei Übernahme verifizieren, dass keine Client-Komponente daraus importiert (Component-Extraktion-Regel beachten).
11. **Layer-Trennung (AGENTS.md).** Boilerplate-Locale-Logik liegt in `apps/web/app/locale`. HiveGrid-DoD verlangt VM-Layer + `packages/ui-hivegrid`-Adapter; Locale-Switcher als reusable Component in `packages/ui-hivegrid` (M029-Hinweis) — Adapter neu, Logik aus Boilerplate.
**Explizit NICHT in Phase 1 (Spec 30.5 / Block I.):** DeepL-Pipeline, Auto-Chat-Übersetzung, mehrsprachige Mandanten-Inhalte, mehrsprachiges Help-Center. Boilerplate liefert dafür kein Fundament (außer KnowledgeBase-Sprach-Felder) — korrekt, da out-of-scope.
---
## Empfohlenes Vorgehen
**Phase A — Foundation übernehmen (1:1, schnell):**
1. `packages/i18n` als `@hivegrid/i18n` übernehmen (Plurale, Intl-Formatter, `useTranslate`, ICU) — unverändert.
2. CI-Gate `verify:i18n` (`i18n-check.ts` + `verify-hardcoded-strings.mjs` + ESLint) übernehmen → erfüllt direkt DoD-Punkt 10 + Spec A.1.5/B.3.
3. App-Wiring übernehmen: `i18next.server.ts`, `i18next.global.server.ts` (SSR-Flash-Schutz), `preferences.tsx`-Action (`setLocale` + Cookie). `.server.ts`-Import-Pfade gegen RR7-Client-Build verifizieren (Memory S03).
4. react-i18next-Stack als gesetzte Library-Wahl dokumentieren (schließt Spec-offenen-Punkt).
**Phase B — Datenmodell ergänzen (M028 Vorbedingung):**
5. Migration: `Tenant.default_language`, `User.timezone`, `User.date_format_preference` (Forward-Only + Inverse-SQL-Kommentar). `User.locale` bleibt.
6. `supportedLocales` → `["de","en","tr","pl"]`; `de` als Master. Parity-Checker so konfigurieren, dass teilbefüllte `tr`/`pl` Phase 1 nicht hart blocken (Spec B.4).
**Phase C — Resolver + Outbound (M028 Kern):**
7. `resolveLocale(user, tenant, browser, default)` neu — Tenant-Default-Schritt in die Kette einfügen (D.1).
8. Outbound-Empfänger-Auflösung D.2 in `mail-translator.server.ts`-Erweiterung (User > Contact > Tenant > de). Setzt `Contact.language` aus Modul 1 voraus (Abhängigkeit prüfen).
**Phase D — UI/Settings (M028 G.1/G.2 + M029):**
9. User-Profil-Picker (`/{tenant}/settings/profile/`): Locale-Picker (aus Boilerplate-Vorlage) + neue Zeitzone-/Datumsformat-Picker; „Coming soon" für tr/pl. Reload-free via `/preferences`.
10. Tenant-General-Picker (`/{tenant}/admin/general/`) für `Tenant.default_language`, Permission-Gate `tenant.admin` (M004 `withPermission`).
11. Public-Page-Switcher mit Cookie (`samesite:lax`, nicht-httpOnly) als reusable Component in `packages/ui-hivegrid` (M029); URL-Sprachsegment `/help/{lang}/…` verdrahten.
12. Klartext-Hinweise + Dev-Fallback-Marker als Locale-Keys; `errors.*`-Sub-Key-Struktur (headline/body/action_label) mit M002 koppeln.
**Phase E — Layer-Konformität + Tests:**
13. Locale-Switcher + Picker via VM-Layer + `ui-hivegrid`-Adapter (AGENTS.md A.1–A.5); keine `useLoaderData` in UI-Components.
14. Tests: VM-Builder-Unit-Tests, Smoke-Tests (Sprach-Wechsel live, Public-Booking-Cookie, unvollständiges Test-Locale → 30.6.1-Hinweis), Hot-Swap-Drill, Visual-Snapshot. Parity-/Prune-Tests aus Boilerplate adaptieren.
**Reihenfolge-Kern:** Erst alles aus BP übernehmen (Phase A) → dann die 3 fehlenden DB-Felder + tr/pl (B) → dann die 2 fehlenden Resolver-Schritte (C) → erst danach UI/Settings (D) → Layer/Tests (E).
@@ -0,0 +1,122 @@
# Boilerplate-Abgleich — M31 Branding / Theme
| Feld | Wert |
|------|------|
| **Modul-ID** | M31 |
| **Name** | Branding / Theme |
| **Milestones** | M030 (Hell/Dunkel-Modus + Capacitor-Status-Bar) — Phase 1. (Spec-Quelle `_bmad-output/specs/M31-branding.md`; LP-9. Phase-2-Features E.1-E.6 ohne eigenes Milestone in `docs/milestones/`.) |
| **Block** | Block 1, Abschnitt 31.1 (`_bmad-output/pflichtenheft/blocks/block1.md`) |
| **Reuse-Level Phase 1** | **medium** — solide Light/Dark-Basis vorhanden, aber binär statt drei-modig; "system"-Modus + Live-Reaktivität fehlen |
| **Reuse-Level Phase 2** | **medium-high** — Tenant-Theme-Engine (Farben/CSS-Vars), System-Branding-Felder (Logo/Favicon) und Mail-Brand-Tokens existieren als starke Vorlage |
> **WICHTIG zur Milestone-Benennung:** Das Milestone-Verzeichnis `docs/milestones/M031-feature-flag-schalter-lifecycle/` gehört NICHT zu diesem Modul (das ist Feature-Flags). Das fachlich passende Milestone für M31 Branding/Theme Phase 1 ist **`docs/milestones/M030-hell-dunkel-modus/`**.
> **Hinweis zur PLAN.md-Annahme:** `M030/PLAN.md` nennt „`next-themes` bereits in Boilerplate-Deps". Das ist **falsch verifiziert** — `next-themes` taucht in der Boilerplate nur in `apps/web/app/utils/build/buildWarningTriage.ts` (Build-Warnungs-Triage) auf, NICHT als aktive Theme-Lib. Die reale Boilerplate nutzt ein **eigenes Session/Cookie-basiertes Dark-Mode-System** (siehe unten). Slice-Planung muss das korrigieren.
---
## Was das Modul fachlich braucht (aus Spec)
**Phase 1 (M030 / minimal):**
- A: `User.theme_preference` Enum `system | light | dark`, Default `system`
- B: User-Settings-Route `/{tenant}/settings/profile/` mit 3-Optionen-Picker, sofortige Anwendung ohne Reload
- C: Theme-Auflösung (User-Pref → `prefers-color-scheme` → Light-Fallback), Live-Reaktivität bei `system` (matchMedia-Listener), SSR-No-Flash via Cookie/Header, Anwendung in Web (`dark:`-Class) + Capacitor (Status-Bar/Splash)
- D: keine Permissions in Phase 1
**Phase 2 (E.1-E.6, später):** TenantBranding-Tabelle (Logo, primary/accent-color, display-name), `/{tenant}/admin/branding/`, Logo-Upload, Farb-Picker mit Live-Preview, Branding in Email/Portal/Login/Splash, White-Label (custom_domain, custom fonts, theme_variables JSONB), Permissions `branding.admin` + `branding.white_label` (Enterprise-Gate via M27).
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `prisma/schema/accounts.prisma:17` | `User.lightOrDarkMode String?` — Per-User Hell/Dunkel-Präferenz | **anpassen** — Feld existiert, ist aber binär (`light`/`dark`, nullable). Muss zu 3-Wert-Enum `system\|light\|dark` mit Default `system` erweitert/migriert werden |
| `apps/web/app/root.tsx:97-100` | SSR-Anwendung der `dark`-Klasse auf `<html>` aus `userSession.lightOrDarkMode` (gated über `DARK_MODE_IN_APP`) | **als-vorlage** — No-Flash-Mechanik via Session vorhanden; muss um `system`-Auflösung (`prefers-color-scheme`) + Cookie-Header-Lesung erweitert werden |
| `apps/web/app/root.tsx:184-213` | Action `toggleLightOrDarkMode` (+ `setTheme`, `setLocale`): persistiert in User-DB + Session, feuert `EventsService.create` Audit-Event `user.preferences.updated` | **als-vorlage** — Persist-Pfad + Audit-Wiring vorhanden; Toggle→3-Wert-Set umbauen |
| `apps/web/app/routes/preferences.tsx` | Dedizierte `/preferences`-Action-Route, an die Toggle/Selector posten | **anpassen** — Endpoint-Pattern wiederverwenden; Action für 3-Wert-Preference ergänzen |
| `apps/web/app/utils/theme/userThemePreference.server.ts` | `LIGHT_OR_DARK_MODES`, `normalizeLightOrDarkMode`, `nextLightOrDarkMode`, `persistUserLightOrDarkMode(userId, …)` | **anpassen** — Normalisierung/Persist als Basis; Enum um `system` erweitern, Auflösungs-Helfer (`resolveEffectiveTheme`) neu ergänzen |
| `apps/web/app/components/ui/toggles/DarkModeToggle.tsx` (`DarkModeToggle`, `DarkModeSvgs`) | Binär-Toggle-Button mit i18n-Label + Analytics-Event | **als-vorlage** — für 2-Wert; für Phase-1-Spec (3 Optionen) eher Picker (s. nächste Zeile) |
| `apps/web/app/components/ui/selectors/ThemeSelector.tsx` | Dropdown/Select-Picker, postet `setTheme` an `/preferences` mit Analytics | **als-vorlage** — Picker-Pattern (Dropdown + fetcher.submit) direkt für System/Hell/Dunkel-Auswahl übernehmbar; Optionen-Quelle austauschen |
| `apps/web/app/components/layouts/sidebars/shadcn/nav-user.tsx:152-157` | Theme-Toggle bereits in User-Menü der App-Shell verdrahtet (`onThemeToggle`, gated über `DARK_MODE_IN_APP`) | **als-vorlage** — Einbau-Ort in Shell vorhanden; auf Picker/3-Wert anpassen |
| `apps/web/app/application/Constants.ts:10` | `DARK_MODE_IN_APP = true` — Schalter ob App-Bereich Dark-Mode bekommt | **1:1** als Feature-Gate-Vorbild (Spec D verlangt Feature-Flag-Fähigkeit) |
| `apps/web/app/application/enums/shared/Theme.ts` | `enum Theme { LIGHT, DARK }` | **anpassen** — um `SYSTEM` erweitern |
| `apps/web/app/locale/.../translations.json` (`components.themeToggle.modeLabel.light/dark`, `settings.theme.*`, `shared.theme`) | Vorhandene i18n-Keys de+en für Theme-Labels | **anpassen** — `system`-Label ergänzen; Klartext aus Spec B.1 einsetzen |
| `apps/mobile/package.json:25` (`@capacitor/status-bar` 7.0.1), `@capacitor/splash-screen` 7.0.1 | Capacitor Status-Bar- + Splash-Plugins als Dependency installiert | **als-vorlage** — Plugins vorhanden, aber **keine theme-aware Nutzung** (kein `StatusBar.setStyle`-Aufruf in `apps/mobile/src`) → Spec C.4 muss neu gebaut werden |
### Phase-2-relevante Vorlagen (Tenant-Branding / White-Label)
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `prisma/schema/accounts.prisma:187-188` | `Tenant.theme String?` + `Tenant.themeConfig String?` (serialisierter JSON-String mit Farben inkl. `darkColors`, `sidebar*`) | **als-vorlage** für Phase-2-`TenantBranding` (Farb-Override pro Tenant) |
| `apps/web/app/utils/theme/ThemeConfig.ts` (`parseThemeConfig`, `ThemeConfig`) | Parst Tenant-`themeConfig` → CSS-Variablen (alle Farb-Slots inkl. dark, sidebar, radius, fontFamily) | **als-vorlage** für Phase-2-Live-Theming / `theme_variables` JSONB (E.5) |
| `apps/web/app/utils/theme/defaultThemes.ts` (`themeConfigurations`, `defaultThemes`, `defaultTheme="blue"`) | ~20 vordefinierte Farbschemata (light+dark) als CSS-Var-Sets | **als-vorlage** für Farb-Picker-Presets (E.1) |
| `apps/web/app/themes.css` (~86 KB) | `.theme-{name}` + `.dark.theme-{name}` Klassen | **als-vorlage** für Phase-2-CSS-Var-Theming |
| `apps/web/app/routes/app.$tenant/settings/theme.tsx` + `apps/web/app/components/core/tenants/forms/UpdateTenantFormTheme.tsx` | Tenant-Theme-Verwaltungs-Route (`update-theme`-Action, Permission-gated `app.settings.account.view/update`, Live-Preview via `themePreviewStore`) | **als-vorlage** für `/{tenant}/admin/branding/` (E.1) — Route-/Form-/Permission-Pattern direkt adaptierbar |
| `apps/web/app/stores/themePreviewStore.ts` (`useThemePreviewStore`) | Client-Store für Live-Preview-Override im Shell-Layout | **als-vorlage** für „Preview vor Live-Schalten" (E.6) |
| `apps/web/app/components/layouts/sidebars/shadcn/ShadcnSidebarLayout.tsx` | Liest Tenant-`themeConfig` + Dark-Mode → injiziert `--sidebar*` CSS-Variablen in Shell | **als-vorlage** für Anwendung von Tenant-Branding in Sidebar/Header (E.1) |
| `prisma/schema/core.prisma:6,23-27` (`AppConfiguration.theme`, `brandingLogo`, `brandingLogoDarkMode`, `brandingIcon`, `brandingIconDarkMode`, `brandingFavicon`) | **System-weite** (nicht tenant-bezogene) Branding-Felder | **als-vorlage** für Feld-Shape der Phase-2-`TenantBranding`-Tabelle (Logo + Dark-Logo + Icon + Favicon) |
| `apps/web/app/utils/db/appConfiguration.db.server.ts:90,196,251-292` | Load/Persist + Mapping `branding.logo/logoDarkMode/icon/iconDarkMode/favicon` | **als-vorlage** für DB-Service-Layer Phase 2 |
| `apps/web/app/root.tsx:127-135` | Favicon/Branding-Logo aus `appConfiguration.branding` ins `<head>` injiziert | **als-vorlage** für Tenant-Branding-Anwendung (E.1) |
| `packages/brand/src/index.ts` (`@toolctrl/brand`) | Brand-Tokens (productName, legal/support-Links, assets, **mail.colors**), `createBrandTokens`, `resolveTransactionalBrandTokens`, `brandCssVariables` | **als-vorlage** für E.2 (Email-Branding mit Logo+Primärfarbe) — Mail-Color-Token-Resolution + CSS-Var-Export bereits fertig |
| `prisma/schema/portals.prisma:14-15` (`themeColor`, `themeScheme`) + `apps/web/app/modules/portals/db/portals.db.server.ts:176-209` | Per-Portal `themeColor`/`themeScheme` (separates Customer-Portal-Theming) | **als-vorlage** für E.3 Custom-Login/Portal-Branding (eigenständiges Konzept, nicht 1:1) |
---
## Vorhandene Daten-Modelle
| Modell / Feld | Pfad | Deckung |
|---|---|---|
| `User.lightOrDarkMode String?` | `prisma/schema/accounts.prisma:17` | Phase 1 A.1 — **teilweise** (binär statt 3-Wert-Enum, nullable statt Default `system`) |
| `Tenant.theme String?` | `prisma/schema/accounts.prisma:187` | Phase 2 E.1 — Tenant-Farbschema-Auswahl |
| `Tenant.themeConfig String?` | `prisma/schema/accounts.prisma:188` | Phase 2 E.1/E.5 — serialisierte Tenant-Theme-Vars (Whitelabel-Vorlage) |
| `AppConfiguration.theme / brandingLogo / brandingLogoDarkMode / brandingIcon / brandingIconDarkMode / brandingFavicon` | `prisma/schema/core.prisma:6,23-27` | Phase 2 E.1 — **System-Level** Branding-Feld-Shape (Vorlage für TenantBranding) |
| `Portal.themeColor / themeScheme` | `prisma/schema/portals.prisma:14-15` | Phase 2 E.3 — Customer-Portal-Theming (separat) |
**Existiert NICHT (muss in Phase 2 neu angelegt werden):**
- Keine `TenantBranding`-Tabelle (kein `model TenantBranding/Branding/TenantTheme` in `prisma/schema/*.prisma`).
- Keine `logoFileId`/`primaryColor`/`accentColor`/`custom_domain`/`theme_variables`-Felder als eigene Tenant-Branding-Entität.
- Branding ist heute **system-weit** (`AppConfiguration`), NICHT tenant-scoped.
---
## Lücken / komplett neu zu bauen
### Phase 1 (M030)
1. **3-Wert-Theme-Preference statt binär** — `User.lightOrDarkMode` ist `light`/`dark`. Spec verlangt Enum `system | light | dark` mit Default `system`. → Feld erweitern/migrieren (Forward-Only-Migration), `Theme`-Enum + Normalisierungs-Helfer um `system` ergänzen.
2. **`system`-Auflösung über `prefers-color-scheme`** — existiert NICHT (`grep prefers-color-scheme` = 0 Treffer in apps/packages). Spec C.1 Schritt 3 + Live-Listener C.2 (`matchMedia('(prefers-color-scheme: dark)')`) komplett neu.
3. **Live-Reaktivität bei `system`** — kein Client-Listener für OS-Theme-Wechsel während offener Session (C.2) vorhanden.
4. **SSR-Cookie für No-Flash bei `system`** — heute kommt der Modus aus der **Server-Session** (nur eingeloggt). Für `system`/nicht-eingeloggt + No-Flash braucht es Cookie/Header-Lesung von `prefers-color-scheme` server-seitig (C.3). Neu.
5. **3-Optionen-Picker an `/{tenant}/settings/profile/`** — Picker-Komponente existiert als Vorlage (`ThemeSelector`/`DarkModeToggle`), aber für 3 Optionen mit Spec-Klartext (B.1) neu zusammenzubauen; Einbau in `settings/profile` (nicht nur User-Menü).
6. **Capacitor Status-Bar / Splash theme-aware** — Plugins (`@capacitor/status-bar`, `@capacitor/splash-screen`) sind installiert, aber **kein** `StatusBar.setStyle(...)`-Aufruf in `apps/mobile/src` (C.4). Native-Hook komplett neu.
7. **i18n-Key `system`** — `components.themeToggle.modeLabel` hat nur `light`/`dark`; `system` + Spec-Klartexte fehlen.
### Phase 2 (E.1-E.6) — alles als Vorlage vorhanden, aber neu zu bauen
- `TenantBranding`-Tabelle (Felder per `AppConfiguration`-Shape) — neu.
- `/{tenant}/admin/branding/`-Route — `settings/theme.tsx`-Pattern adaptieren.
- Logo-Upload (PNG/SVG) via Files-Modul (M23/Scaleway) — neu verdrahten.
- Permissions `branding.admin` (über `tenant.admin`) + `branding.white_label` (Enterprise-Gate via M27) — neu.
- White-Label: custom_domain (Caddy-Cert), custom fonts, `theme_variables` JSONB, hide-hivegrid-branding — neu (ThemeConfig-Parser als Vars-Vorlage).
- Email-Branding (E.2): `@toolctrl/brand` mail-tokens als Basis, tenant-scoped erweitern.
---
## Empfohlenes Vorgehen (Phase 1 zuerst)
1. **Daten-Layer:** `User.lightOrDarkMode` → 3-Wert-Preference erweitern. Forward-Only-Migration mit Inverse-SQL-Kommentar; Default `system`; bestehende `null`-Werte → `system`. (`accounts.prisma:17`)
2. **Auflösungs-Helfer neu:** `resolveEffectiveTheme(pref, prefersColorSchemeHeader/Cookie)` aufbauend auf `userThemePreference.server.ts`-Normalisierung. Reihenfolge nach Spec C.1.
3. **SSR-No-Flash erweitern:** `root.tsx:97-100` umbauen — `dark`-Klasse zusätzlich aus aufgelöstem `system`-Wert (Cookie/Header) setzen, nicht nur aus Session. Cookie-Name (offener Spec-Punkt) festlegen.
4. **Client-Reaktivität:** `matchMedia`-Listener für `system`-Modus (neu) — bei OS-Wechsel `dark`-Klasse live toggeln (C.2), Cookie aktualisieren.
5. **UI-Picker:** `ThemeSelector.tsx`/`DarkModeToggle.tsx` als Vorlage → 3-Optionen-Picker mit Spec-Klartext; posten an `/preferences` (vorhandene Action erweitern). Einbau in `settings/profile` + optional User-Menü (`nav-user.tsx`).
6. **Audit/Events:** vorhandenes `user.preferences.updated`-Event (`root.tsx:197`) für neuen 3-Wert weiterverwenden.
7. **i18n:** `system`-Label + Klartexte (de zuerst, AV #4 — alle Strings extern).
8. **Capacitor (C.4):** Native-Hook neu — `@capacitor/status-bar` `StatusBar.setStyle(Style.Dark/Light)` + Splash an aufgelösten Modus koppeln (`apps/mobile/src`). Soft-dependency auf M168.
9. **Feature-Flag:** Modul über Flag abschaltbar machen (DoD); `DARK_MODE_IN_APP`-Konstante als Vorbild.
10. **Phase 2** erst nach eigener Detail-Spec; dann `settings/theme.tsx` + `AppConfiguration`-Branding-Shape + `@toolctrl/brand` als Startpunkte.
---
## Notiz zu Layer-Disziplin (AGENTS.md / INTEGRATION-CONTRACTS)
- Die Boilerplate-Vorlagen liegen in `apps/web` (RR7-Routen, Loader+Action+Component gemischt). HiveGrid-DoD verlangt: Loader baut View-Model, UI-Component (`packages/ui-hivegrid/**`) nur VM+Actions, keine Prisma-/Server-Imports im UI-Paket, Subframe via Adapter. → Vorlagen als Referenz für Logik, **UI-Komponente neu** in `packages/ui-hivegrid` (Subframe-Design pflicht, Memory `feedback_subframe_mandatory`).
- Sidebar/Settings-Wiring: `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` ist Single-Source (INTEGRATION-CONTRACTS §1); Mandant-Settings unter `app.hivegrid.de/settings/<modul>/...` (§ Route-Tabelle). Phase 1 ist eine Profil-Settings-Sektion, kein eigener Sidebar-Top-Eintrag — explizit dokumentieren (DoD-Ausnahme).
@@ -0,0 +1,104 @@
# Boilerplate-Abgleich — Modul M32 (Feature-Flags / Admin-UI)
- **Modul-ID:** M32
- **Name:** Feature-Flags (Plattform-Admin-UI)
- **Milestones:** M031 (Schalter-Definition + Lifecycle), M032 (Filter-Editor + Einzel-Ausnahme), M033 (Massen-Funktionen + Statistik + Audit-Historie)
- **Block:** Block 1 · Modul 32 (Pflichtenheft `block1.md` §32.1-32.9)
- **LP:** LP-9 (Spec-Tiefe), LP-10 (Klartext-Diagnostik); ist selbst die Umsetzung von AV #7 (Feature-Flag-Hülle) und AV #6 (Audit-Default)
- **Reuse-Level:** **medium** — sehr starke, getestete Querschnitt-/Engine-Basis (Paket `@toolctrl/feature-flags`) vorhanden; das fachliche **Datenmodell der Spec** (Lifecycle, Tenant-Override, Filter mit Priority/Criteria/Value, Owner, Audit-Diff) und die **konkrete Admin-UI** müssen aber nennenswert ergänzt/neu gebaut werden.
> Verifiziert direkt in `/Users/raffaelsheikh/Documents/GitHub/boilerplate` (nicht nur Inventur). Hinweis: HiveGrid hat noch **kein** Prisma-Schema (`/Users/raffaelsheikh/Documents/GitHub/HiveGrid-Cloud/prisma` enthält nur `init-db.sql`), keine `packages/ui-hivegrid`, kein `app/routes`. Die Boilerplate-Artefakte sind daher als Vorlage/zu portieren zu betrachten, nicht 1:1-Import in ein bestehendes HiveGrid-Paket.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
| --- | --- | --- |
| `packages/feature-flags/src/evaluate.ts` | `evaluateFlag(name, ctx, opts)` — DB-freie Eval-Engine via `FlagLoader`-Callback, mit Zeitfenster, Parent-Dependency (Zyklus-Schutz `maxDepth=8`), Variant-Bucketing. `EvaluateFlagResult.reason` (`flag-not-found`/`disabled`/…/`matched`) | **anpassen** — Fundament für `tenantHasFlag`. Spec-Auswertungsreihenfolge (Override → Filter nach `priority` → `default_value`) ist NICHT die Paket-Logik; muss als eigene Resolver-Schicht oben drauf oder als neuer Rule-/Loader-Aufbau ergänzt werden. |
| `packages/feature-flags/src/targeting/evaluator.ts` + `targeting/types.ts` | Reiner Regel-Evaluator + Rule-Union (`allowlist`/`percentage`/`plan-tier`/`role-based`/`time-based`/`composite`) | **als-vorlage** — Pattern für deterministische, I/O-freie, total-typsichere Regel-Auswertung. HiveGrid-Filter-Criteria (`tenant_size`, `tenant_industry`, `tenant_status`, `subscription_product_id`, `tenant_created_*`, `tenant_id_in/not_in`) sind andere Leaf-Typen → analog ergänzen. AND-Verknüpfung + Exhaustiveness-Check direkt übernehmbar. |
| `packages/feature-flags/src/targeting/percentage.ts` | `computeBucket`, `isInRolloutBucket` (sticky Hash-Bucketing) | **als-vorlage** — in Phase 1 nicht benötigt (Prozent-Rollout = Phase 2), aber für M032/Phase-2 vorhanden. |
| `packages/feature-flags/src/cache.ts` | `createFlagEvalCache`, `buildCacheKey`, `hashContext`, `FEATURE_FLAGS_INVALIDATION_CHANNEL` | **anpassen** — deckt das Spec-Caching (Block E: Request-scoped, kein wiederholtes DB-Roundtrip) ab. Cache-Key müsste um Resolver-Inputs (TenantOverride/Filter-Set) erweitert werden; Cross-Replica-Invalidierung bei Admin-Save bereits gelöst. |
| `packages/feature-flags/src/publisher.ts` | `publishFeatureFlagInvalidationOnce/ViaStore` (Cross-Process-Invalidierung via shared-store) | **1:1** — bei jeder Flag-/Filter-/Override-Mutation aus Admin-Action aufrufbar. |
| `packages/feature-flags/src/client/context.tsx` | `FeatureFlagProvider`, `useFeatureFlag`, `useFeatureFlagEnabled`, `useFeatureFlagVariant` (SSR-Hydration) | **1:1 / anpassen** — Phase 1 hat kein Tenant-User-UI für Flags, aber das Hydration-Pattern ist für flag-gegatete UI (AV #7) generell nutzbar (deckt INTEGRATION-CONTRACTS §2 `feature_flag?`-Gate ab). |
| `packages/foundation/src/flags.ts` | `defineFlag`, `featureFlag`, `selectFlagRecord` (leichtgewichtige Auflösung user > tenant > global, override) | **als-vorlage** — die `selectFlagRecord`-Präzedenz (User→Tenant→Global) ist konzeptuell nah an der Spec-Reihenfolge (Override→…→Default). Für ein einfaches `tenantHasFlag` ggf. direkt erweiterbar; berücksichtigt aber keine `priority`-sortierten Filter. |
| `packages/foundation/src/audit.ts` | `auditEvent(input, client)`, `withAuditMiddleware(handler, buildEvent, client)` | **1:1** — erfüllt Block G (Audit-Logging mit Vorher/Nachher). Wrappt jede mutierende Flag-/Filter-/Override-Action. Schreibt nach `AuditEvent` (`core.prisma`). |
| `packages/foundation/src/permissions.ts` | `requirePermission`, `withPermission`, `withTenantScope`, `withOwnerCheck` | **1:1** — für Block H (`admin.feature_flags.read/write/lifecycle`). Liefert bei Fehlschlag `CatalogError(notFound)` → 404 statt 403 (deckt DoD „Permission-Denied → 404" / INTEGRATION-CONTRACTS A.5.3). |
| `packages/foundation/src/errors.ts` | `CatalogError`, `foundationErrors`, `catalogRegistry`, `toPinoLogObject` | **1:1** — Klartext-Fehler mit `i18nKey` für Block I (Lookup-auf-unbekannten-Key, Vergangenheits-`expires_at` usw.). |
| `packages/view-models/src/index.ts` | `VMState`, `VMStatus`, `ActionCallbacks`, `RegistryGate`, `SidebarRegistryItem`, `ready/empty`, `assertI18nLabel` | **1:1** — Loader-baut-VM-Disziplin (DoD „Loader baut VM; UI nur VM+Actions"), Empty-/Loading-/Error-States, Sidebar-Registry-Gate. |
| `packages/i18n/src/*` | `useTranslate`-Factory, Intl-Formatter, ICU, Namespaces, `assertI18nKey` | **1:1** — alle UI-Texte als i18n-Keys (DoD/INTEGRATION-CONTRACTS §10). |
| `packages/url/src/canonical.ts` | `adminPath`/`adminUrl` (host-aware) | **1:1** — alle Routen liegen auf `admin.hivegrid.de` (Block B). |
| `prisma/schema/core.prisma` → `AuditEvent` (Z.55) | Append-only Audit-Event mit `actorType`, `action`, `targetType/Id`, `metadata`, `previousHash`/`eventHash` (Hash-Chain), `dataSensitivity`, `regulatoryScope[]` | **1:1** — Ziel-Tabelle für Block G. Diff (`oldValue`/`newValue`) als `metadata` ablegbar. |
| `prisma/schema/core.prisma` → `IdempotencyKey` (Z.84) | `@@unique([scope, key])` | **1:1** — für idempotente Bulk-Override-Actions (M033). |
| `apps/web/app/routes/admin/feature-flags/*` (`feature-flags.tsx`, `index.tsx`, `flags.tsx`, `flags/$id.tsx`, `flags/new.tsx`, `settings.tsx`, `engine.tsx`) | Bestehende Plattform-Admin-Routen-Struktur (Liste/Detail/New, Sidebar-Icon-Layout, Permission-Gate `admin.featureFlags.view/manage`) | **als-vorlage** — Routen-Aufteilung und Admin-Gating als Blaupause. UI deckt jedoch nur einfache An/Aus + analytics-orientierte Filter ab, **nicht** Lifecycle/Tenant-Override/Priority/Statistik/Audit-Historie. |
| `apps/web/app/modules/featureFlags/db/featureFlags.db.server.ts` | `getAllFeatureFlags`, `getFeatureFlag`, `create/update/deleteFeatureFlag`, Cache-Invalidierung | **als-vorlage** — DB-Server-Layer-Muster (`.db.server.ts`). Muss um Lifecycle/Owner/TargetModule/Archiv/Tenant-Override/Filter-Priority erweitert werden. |
| `apps/web/app/modules/featureFlags/components/FeatureFlagForm.tsx`, `FeatureFlagTracker.tsx` | Anlege-/Edit-Form + Analytics-Tracker-Component | **als-vorlage** — Form-Struktur wiederverwendbar; HiveGrid-Felder/UI (Lifecycle-Dropdown, Owner, Modul-Tag, Default-Toggle) ergänzen. |
| `apps/web/app/utils/services/.server/featureFlagEvaluator.server.ts`, `featureFlagCache.server.ts`, `featureFlagsRequest.server.ts` | Server-Bridge zwischen Prisma-`FeatureFlag` und Paket-`evaluateFlag` (OR-Lookup name/id, L2-Cache, Memo 60s) | **anpassen** — Vorlage für die HiveGrid-`tenantHasFlag`-Server-Bridge; Resolution-Order/Loader an Spec-Modell anpassen. |
| `apps/web/app/components/layouts/icons/IconFeatureFlags.tsx` | Sidebar-Icon | **1:1** — Icon übernehmbar. |
| `apps/web/app/application/sidebar/AdminSidebar.tsx` (Z.206-215) | Admin-Sidebar-Registrierung der Feature-Flags-Gruppe inkl. Sub-Items + Permission-Gate | **als-vorlage** — Muster für die HiveGrid-Sidebar (`packages/ui-hivegrid/src/app-shell/sidebar.config.ts`, INTEGRATION-CONTRACTS §2). |
| `packages/shared-store/*` | `createTwoTierCache`, Pub/Sub | **1:1** — Infrastruktur, die `cache.ts`/`publisher.ts` benötigen. |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
Alle in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`:
- **`FeatureFlag`** — `featureFlags.prisma:1`
Felder: `id`, `createdAt`, `updatedAt`, `key String?`, `scope` (default `global`), `tenantId?`, `userId?`, `name`, `description`, `enabled`, sowie v2-Targeting-Felder `rules Json?`, `variants Json[]`, `enabledFrom?`, `enabledUntil?`, `parentFlagId?` (Self-Relation `FeatureFlagDependency`). Relationen: `audits`, `events` (Analytics), `filters`.
Indizes: `@@unique([name, description])`, `[key, scope]`, `[tenantId, key]`, `[userId, key]`, `[parentFlagId]`.
- **`FeatureFlagFilter`** — `featureFlags.prisma:32`
Felder: `id`, `createdAt`, `featureFlagId`, `type String`, `value String?`, `action String?` (onDelete: Cascade).
- **`FeatureFlagAudit`** — `featureFlags.prisma:44`
Append-only: `id`, `flagId`, `actorUserId?`, `action` (create/update/delete/toggle), `oldValue Json?`, `newValue Json?`, `createdAt`. Indizes `[flagId, createdAt]`, `[actorUserId, createdAt]`.
- **`AuditEvent`** — `core.prisma:55` (Querschnitt-Audit, Hash-Chain, tenant-aware).
- **`IdempotencyKey`** — `core.prisma:84`.
- **`Tenant`** — `accounts.prisma` (für Filter-Criteria relevant: `slug`, `name`, `active`, `createdAt`, `subscription`/`productConfiguration`/`featureOverrides`). **Achtung:** Es gibt **kein** `tenant_size`-, `tenant_industry`- oder `tenant_status`(trial/active/past_due/frozen/suspended)-Feld am Boilerplate-`Tenant` — nur `active Boolean` + `deactivatedReason`. Die Spec-Filter-Criteria brauchen also entweder zusätzliche Tenant-Felder (HiveGrid-Tenant-Modell, Modul 37) oder eine Ableitung (z.B. MA-Zahl aus HR-Hub).
---
## Lücken / komplett neu zu bauen
### Datenmodell (größte Lücke)
Das Boilerplate-Schema folgt einem anderen Konzept (An/Aus + Targeting-Rules-Tree pro Flag, Filter = analytics-/session-/percentage-Match). Die Spec verlangt ein **dreiteiliges Modell mit Tenant-Override und priorisierten Filtern**. Konkret fehlt / weicht ab:
- **`FeatureFlag`** — fehlende Spec-Felder: `label`, `default_value` (es gibt nur `enabled`), `lifecycle_stage` Enum (`beta|rollout|ga|deprecated`), `owner_admin_user_id`, `created_by_admin_user_id`, `target_module`, `archived_at` (Soft-Delete). `key` ist `String?` und nur via `[name, description]` unique — Spec verlangt `key` **global unique + non-null + nach Anlage nicht editierbar**.
- **`FeatureFlagFilter`** — fehlende Spec-Felder komplett: `name`, `priority Int`, `value Boolean`, `criteria JSONB` (strukturiertes Schema), `active Boolean`, `created_by_admin_user_id`. Boilerplate-Filter (`type`+`value String`+`action`) ist semantisch ein anderes Konstrukt.
- **`TenantFeatureFlag` (Tenant-Override)** — **existiert nicht**. Komplett neu: `feature_flag_id`, `tenant_id`, `value Boolean`, `set_by_admin_user_id`, `note?`, `expires_at?`, `@@unique([feature_flag_id, tenant_id])`, Indizes `[tenant_id, feature_flag_id]` + `expires_at`.
### Logik
- **Auswertungs-Reihenfolge der Spec** (Override → aktive Filter nach `priority` desc, bei Gleichstand ältester → `default_value`) — neu zu implementieren. Die Paket-Engine (`evaluate.ts`) macht Zeitfenster + Parent-Dependency + Rule-Tree, **nicht** die priorisierte Override/Filter/Default-Kaskade.
- **`tenantHasFlag(tenantId, key)`** (Block E) — neue Convenience-API mit: wirft bei unbekanntem `key` in Dev/Staging (Schema-Drift-Schutz), loggt im Prod; Boolean-Only; Request-scoped-Cache. Vorlage: `featureFlagEvaluator.server.ts`, aber andere Resolution.
- **Filter-Criteria-Matcher** für `tenant_size/industry/status/subscription_product_id/created_*/id_in/id_not_in` — neue Leaf-Evaluatoren (Pattern aus `targeting/evaluator.ts`).
- **Live-Vorschau „X Tenants matchen"** (Block D, §32.5.4) — neue Query gegen Tenant-Tabelle (Spec: Read-Replica). Komplett neu.
- **Effektive-Auswertungs-Statistik** „X aktiv / Y nicht" (Block C, §32.5.3, M033) — neue Aggregation über alle Tenants. Komplett neu.
- **Bulk-Override** „12 Tenants gleichzeitig" (Block C, §32.5.6, M033) — neue idempotente Action (Vorlage `IdempotencyKey`/`withIdempotency`). Komplett neu.
- **Lifecycle-Wechsel-Workflow** mit Bestätigungs-Dialog + Edge-Case-Warnungen (Block F, §32.5.7) + **Archivierung/Reaktivierung** (§32.5.8). Komplett neu (kein Lifecycle-Konzept in BP).
- **Expires-At-Cron** (Tenant-Override-Auto-Reset) — neuer Worker-Job. Komplett neu.
- **Audit-Diff-History pro Flag im Detail-View** (Block G/§32.7) — `FeatureFlagAudit` existiert als Tabelle, aber UI + Befüllung über alle Spec-Aktionen (Filter-, Override-, Lifecycle-, Archiv-Änderungen mit Diff) neu zu verdrahten; alternativ `AuditEvent` (Querschnitt M21) nutzen.
### UI (HiveGrid-spezifisch, komplett neu in `packages/ui-hivegrid` + `app/routes`)
- Listen-View mit Spalten/Filter/Suche/Sortierung/„Archivierte anzeigen"/Quick-Actions (Lifecycle, Archiv) — BP-`flags.tsx` deckt nur Teil ab.
- Detail-View mit Sektionen Stammdaten/Filter/Tenant-Overrides/Statistik/Audit, Inline-Edit, Filter-Editor mit Criteria-Builder + Live-Vorschau, Tenant-Override-Editor mit Tenant-Autocomplete + Expires-At, Bulk-Override-UI, Lifecycle-Confirm-Dialog.
- Alle als View-Model (Loader) + reine UI-Component (kein Prisma-Import in `packages/ui-hivegrid/**`), Subframe-Output via Adapter (DoD).
- Sidebar-Eintrag in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` + Breadcrumbs + Smoke-Test (`e2e/smoke/`) — Infrastruktur existiert in HiveGrid noch nicht.
### Permissions
- BP nutzt `admin.featureFlags.view` / `admin.featureFlags.manage`. Spec verlangt 3 atomare Permissions (`admin.feature_flags.read/write/lifecycle`) + 4 Bundles (`none/viewer/operator/owner`). Mapping/Definition neu (final mit Modul 37 abzustimmen — offener Spec-Punkt).
### Plattform-Admin-Fundament (Vorbedingung, außerhalb M32)
- `admin.hivegrid.de`-Subdomain-App-Shell, Plattform-Admin-Auth (M034), Admin-User-Modell, `AdminAuditLog`/Cross-Tenant-Audit (M037). In Boilerplate liegt Admin als Bereich **innerhalb** `apps/web` — HiveGrid plant separate Admin-App-Surface (INTEGRATION-CONTRACTS §1: „separate Sidebar-Struktur M034-M041"). Diese Vorbedingungen sind nicht Teil von M32 selbst, aber blockierend.
---
## Empfohlenes Vorgehen (Reihenfolge)
1. **Querschnitt 1:1 übernehmen (kein Neubau):** `@toolctrl/foundation` (audit, permissions, errors), `@toolctrl/view-models`, `@toolctrl/i18n`, `@toolctrl/url`, `@toolctrl/shared-store`, `AuditEvent`/`IdempotencyKey` (core.prisma). Das ist das stabile Fundament für DoD-Wiring.
2. **Engine-Paket als Basis ziehen, Resolver anpassen:** `@toolctrl/feature-flags` (cache, publisher, client, percentage, targeting-Pattern) übernehmen; darüber einen **HiveGrid-Resolver** bauen, der die Spec-Reihenfolge (Override → priorisierte Filter → default) implementiert. `tenantHasFlag` als neue Convenience-API mit Schema-Drift-Schutz + Request-Cache (Vorlage: `featureFlagEvaluator.server.ts`).
3. **Datenmodell neu/erweitert (M031-Fundament):** HiveGrid-Prisma-Schema anlegen. `FeatureFlag` um Spec-Felder erweitern (label, default_value, lifecycle_stage, owner/created_by, target_module, archived_at, key unique non-null), `FeatureFlagFilter` um (name, priority, value Boolean, criteria JSONB, active, created_by), **`TenantFeatureFlag` neu**. Forward-Only-Migration mit Inverse-SQL-Kommentar. Entscheiden: bestehende BP-`FeatureFlagAudit` nutzen oder Querschnitt-`AuditEvent` (Empfehlung: `AuditEvent` für plattformweite Durchsuchbarkeit, §32.7.2).
4. **M031 — Schalter-Definition + Lifecycle:** DB-Server-Layer (`.db.server.ts`, Vorlage BP), Permissions (3 atomar + 4 Bundles), Routen Liste/Detail/New/Edit auf `admin.hivegrid.de` (Vorlage BP-Routen-Struktur), Sidebar-Registry, Lifecycle-Wechsel mit Confirm + Audit. Form-Component aus BP `FeatureFlagForm.tsx` als Vorlage.
5. **M032 — Filter-Editor + Einzel-Ausnahme:** Criteria-Builder-UI + neue Leaf-Matcher (Pattern `targeting/evaluator.ts`), Live-Vorschau-Query gegen Tenant-Tabelle, Tenant-Override-Editor mit Autocomplete + Expires-At, priorisierte Auswertung + Edge-Case-Klartext (Block D). **Vorab klären:** welche Tenant-Felder (`tenant_size/industry/status`) das HiveGrid-Tenant-Modell (M37) bereitstellt — BP-`Tenant` hat sie nicht.
6. **M033 — Massen + Statistik + Audit:** Bulk-Override (idempotent via `withIdempotency`), Wirkungs-Statistik-Aggregation, Audit-History-Sektion im Detail-View, Archiv-/Reaktivierungs-Flow, Expires-At-Cron-Worker, Klartext-Hinweise (§32.9 / Block I).
7. **Querschnitt-Wiring + DoD:** `publishFeatureFlagInvalidationOnce` bei jeder Mutation, i18n-Keys durchgängig, Smoke-Tests pro Route, VM-Unit-Tests mit Zod, Visual-Regression. Permission-Denied → 404.
**Kurzfazit:** Das Querschnitt- und Engine-Gerüst (Paket + foundation) ist exzellent und großteils 1:1/als-Vorlage nutzbar — daher nicht „low". Aber das **fachliche Kernmodell der Spec (Lifecycle + Tenant-Override + priorisierte Criteria-Filter + Owner/Audit-Diff + Statistik/Bulk)** und die **komplette Admin-UI** sind in der Boilerplate nicht vorhanden bzw. konzeptuell abweichend → **medium**.
@@ -0,0 +1,120 @@
# Boilerplate-Abgleich: M33a — Apps-Engine / Adapter-Layer
| Feld | Wert |
|---|---|
| **Modul-ID** | M33a (Detail-Spec `_bmad-output/specs/M33a-apps-engine.md`) |
| **Name** | Apps-Engine / Adapter-Layer |
| **Milestones** | M008 (Adapter-Interface + State-Machine), M009 (Setup-Wizard + Sync-Log), M010 (Probe-Scheduler + Webhook-Sturm-Schutz) |
| **Block** | Block 4 — USP-Säulen (`_bmad-output/pflichtenheft/blocks/block4.md` Modul 33a) |
| **LPs** | LP-9 (Apps-Engine-Foundation), LP-10 (Klartext-Diagnostik), LP-11 (Adapter-Pattern für alle Integrationen) |
| **Reuse-Level** | **medium** |
## Reuse-Verdict (kurz)
Das **Daten-Modell ist fast 1:1 vorhanden** (`prisma/schema/appsEngine.prisma`: `App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem`) — exakt die fünf Entitäten aus Spec Block B. **Aber:** Diese Modelle sind in der Boilerplate **schema-only** — es gibt **keine** Domänen-Logik, **kein** Adapter-Interface, **keine** State-Machine, **keinen** Setup-Wizard, **keine** Settings-UI, **keinen** Probe-Scheduler und **keinen** Webhook-Receiver dafür. Verifiziert per Grep: in `apps/web/app`, `apps/worker/src`, `apps/api/src` und allen `packages/*` (außer dem generierten Prisma-Client) existiert **kein** Code, der `DataConnection`/`AppSyncRun`/`sample-data-sync` verwendet.
Das eigentliche Engineering (M008-M010) muss neu gebaut werden — aber auf einem **sehr starken Querschnitt-Fundament**: AES-256-GCM-Credential-Encryption (`CryptoApi`), BullMQ-Queue mit Idempotenz + plan-basierten Prioritäten, Webhook-Signatur-Validierung (Stripe/MailerSend als HMAC-Vorlage), ein periodischer Worker-Scheduler (`metrics-scheduler`), eine fertige Connection-Management-Admin-Route als Vorlage (`marketing-integrations.tsx` inkl. Encryption-at-rest + Secret-Masking) und das Adapter-Registry-Pattern aus `@toolctrl/marketing-outbound`. Der WASM-`plugin-runtime` deckt die **Phase-2-Marketplace**-Foundation ab (M33b/M33c), ist für Phase 1 aber nicht der Kern.
> Wichtige Begriffs-Abgrenzung (auch in der Inventur `docs/boilerplate-inventory/integrations-ki.md` §4 betont): Es gibt in der Boilerplate **zwei** „Apps-Engine"-Bedeutungen. (1) Die **Data-Sync-Engine** (`appsEngine.prisma`, `App`/`DataConnection`/`AppSyncRun`) = das, was M33a Phase 1 will. (2) Das **WASM-Plugin-System** (`plugin-runtime`/`plugin-sdk`/`plugins.prisma`) = M33a Phase 2 (Drittanbieter-Marketplace). Nicht verwechseln.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `prisma/schema/appsEngine.prisma` | `App`, `DataConnection`, `AppSyncRun`, `AppSyncRunLog`, `AppSyncItem` — die fünf Spec-B-Entitäten | **anpassen** — Felder/Enums an Spec angleichen (siehe „Lücken" unten) |
| `apps/web/app/utils/api/.server/CryptoApi.ts` | AES-256-GCM Verschlüsselung (`encrypt`/`decrypt`, versioniert `v1:iv:tag:ct`, `CRYPTO_SECRET`) | **1:1** für `DataConnection.credentials` „verschlüsselt at-rest" (Spec B, 33a.9.3 „Plattform-Schlüssel") |
| `apps/web/app/routes/admin/marketing-integrations.tsx` | Komplette Connection-Management-Admin-Route: Liste, Create/Toggle/Delete, Permission-Gate (`verifyUserHasPermission`), Secret-Masking, `lastDeliveredAt`/`lastError`-Anzeige, i18n | **als-vorlage** für die Settings-UI `/settings/integrationen/` (Spec F) |
| `apps/web/app/utils/leads/marketingIntegrations.db.server.ts` | Encryption-at-rest DB-Layer: `SENSITIVE_KEYS`, `encryptSensitive`/`decryptSensitive`, `maskConfig` über `CryptoApi`, Round-Trip | **als-vorlage** für `DataConnection`-Persistenz mit verschlüsselten Credentials |
| `packages/marketing-outbound/src/index.ts` + `src/types.ts` | Adapter-Interface + Registry-Pattern (`adapterRegistry`, `resolveAdapter(type)`, `MarketingOutboundAdapter<TConfig>` mit `parseConfig`/`send`), `adapterFetch()` (Timeout + SSRF-Allowlist, nur HTTPS, kein RFC1918) | **als-vorlage** für das Adapter-Interface der Apps-Engine (Spec A: `connect`/`syncPull`/`syncPush`/`handleWebhook`/`validateConnection`) + sicheres Outbound-Fetch |
| `packages/queue/*` (`names.ts`, `payloads.ts`, `producers.ts`, `priority.ts`, `runtime.ts`) | BullMQ-Wrapper: typisierte Jobs, **`buildIdempotentJobId()`** (Idempotenz 33a.4.4), `resolveJobPriorityForTenant()` (plan-basiert), `createQueue`/`createWorker` | **1:1** für Sync-Mechanik (Pull/Push/Webhook/Manual), Idempotenz-Keys, Sync-Lock |
| `apps/worker/src/metrics-scheduler.ts` | Periodischer Poller-Pattern (30s/60s-Kadenzen, best-effort, started aus Worker-Entrypoint) | **als-vorlage** für den **Probe-Scheduler** (M010: aktiv 5min / eingeschränkt 1min / defekt 15min) |
| `apps/api/src/webhooks/mailersend-signature.ts` | HMAC-Signatur-Validierung mit `timingSafeEqual`, Timestamp-Toleranz, typisierte Failure-Reasons | **als-vorlage** für Webhook-Signatur-Validierung pro App (Spec E.3, offener Punkt der Spec) |
| `apps/api/src/billing/stripe-webhook-route.ts` | Inbound-Webhook-Route mit Signatur-Konstruktion, Outbox-Publish in DB-Transaktion, Metrics, Error-Envelope | **als-vorlage** für den Webhook-Receiver `app.hivegrid.de/api/v1/webhooks/{app_key}/{tenant_id}/{connection_id}` (Spec E.3) |
| `apps/api/src/webhookDeliveryQueue.ts` + `WebhookDeliveryJob` (`@toolctrl/queue`) | Outbound-Webhook-Delivery über Queue (`buildWebhookDeliveryJob`, signierte/retried Zustellung) | **anpassen** für Push-Sync / signierte Outbound-Webhooks (Spec E.2) |
| `packages/storage/src/index.ts` (`StorageAdapter`, `createS3StorageAdapter`) + `signed/` | Scaleway-S3-Adapter + Signed-URLs | **als-vorlage** für den `kwpsync.exe`-Download im Setup-Wizard (Spec D, connection_type `windows_client`) |
| `apps/web/app/utils/integrations/githubService.ts` | Beispiel eines konkreten externen-Service-Adapters (OAuth/Token-Handling-Muster) | **als-vorlage** für die App-spezifischen Adapter (eigene Module: KWP/Gmail/Stripe) |
| `packages/plugin-runtime/*` + `packages/plugin-sdk/*` + `prisma/schema/plugins.prisma` | WASM-Sandbox, Capability-Gateway (deny-by-default), Manifest+Ed25519-Signatur, Hook-Registry, Audit-Chain, Crash-Tracker; `Plugin`/`TenantPluginCapabilityGrant`/`PluginInvocation` | **als-vorlage** ausschließlich für **Phase 2** (M33b Community-Marketplace mit Sandboxing/Review, Spec Block I) — **nicht** Phase-1-Kern |
| `packages/plugins/src/manifest-schema.ts` (`FeatureManifest`, `validateFeatureManifestShape`) | Feature-/App-Manifest-Schema (Surfaces/Workers/Provider/Permissions/Audit) | **als-vorlage** für das Manifest-Format der Adapter-Registry (Spec C `manifest.ts`) |
### Querschnitt, das M33a wie jedes Modul mitnimmt (separate Foundation-Module M001-M011)
- **Audit:** automatisch via M001-Prisma-Middleware (AGENTS.md §C.13); externe Webhook-Aufrufe manuell via `auditEvent` (INTEGRATION-CONTRACTS §6). Deckt AV #6 (Audit für Connection-Änderungen).
- **Notifications:** via `notify()` (M011, INTEGRATION-CONTRACTS §7) — für `degraded`/`broken`-Alerts (Spec G, 33a.3.5). Realtime/In-App über `@toolctrl/realtime` (SSE).
- **Mail:** `@toolctrl/mail` (MailerSend, EU) für Email-Benachrichtigungen.
- **i18n:** M003-Build-Lint; pro-App `i18n/de.json` Klartext-Übersetzungen (Spec C/G).
- **Feature-Flags:** M006 — pro App `phase`/`requires_subscription_feature` + Feature-Flag (Spec C Tenant-Sicht).
- **Permissions:** M004 `withTenantScope()` + `withPermission()`; `verifyUserHasPermission` als bestehendes Boilerplate-Muster.
---
## Vorhandene Daten-Modelle
**Quelle:** `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/appsEngine.prisma` (generierter Client: `packages/db/generated/prisma/client/models/{App,DataConnection,AppSyncRun,AppSyncRunLog,AppSyncItem}.ts`).
| Boilerplate-Modell | Deckt Spec-Entität | Vorhandene Felder (Auszug) | Bewertung |
|---|---|---|---|
| `App` | App (Spec B) | `id`, `tenantId`, `type`, `config` (JSON), `icon`, `isActive`, `installedByUserId`, `syncRuns[]`, `@@unique([tenantId, type])` | Teilweise. **In BP tenant-scoped** (`tenantId`), Spec will App **plattform-weit** (Code-Registry-Spiegel). Es fehlen `key`/`name`/`description`/`category`/`connection_type`/`connection_schema`/`capabilities[]`/`phase`/`requires_subscription_feature`/`documentation_url`. |
| `DataConnection` | DataConnection (Spec B) | `id`, `tenantId`, `name`, `type`, `config` (Kommentar „Encrypted JSON credentials"), `description` | Solide Basis. **Keine FK zu `App`** in BP (Spec will `app_id`), kein `connection_status`-Enum, keine `last_health_check_*`/`last_sync_at`/`next_sync_at`/`last_error`/`metadata`/`created_by_user_id`. Multi-Connection-pro-App-pro-Tenant ist mit `name` schon angelegt. |
| `AppSyncRun` | AppSyncRun (Spec B) | `id`, `appId`(FK), `tenantId`, `taskKey`/`taskName`, `status` (pending/running/completed/partial/failed/cancelled/reverted), `executionMode` (manual/scheduled/webhook), `startedAt`/`completedAt`, `duration`, `itemsProcessed/Created/Updated/Failed`, `error`, `createdByUserId`, `logs[]`, `syncItems[]` | **Sehr gute Übereinstimmung.** FK zeigt auf `App`, Spec will FK auf `DataConnection` (`data_connection_id`). `kind` (pull/push/webhook/health_check/manual_resync) vs. BP `executionMode` (manual/scheduled/webhook) — angleichen. `summary_text` (Klartext) fehlt; `itemsSucceeded` als `itemsProcessed - itemsFailed` ableitbar. |
| `AppSyncRunLog` | AppSyncRunLog (Spec B) | `id`, `syncRunId`(FK), `type` (info/success/warning/error), `message`, `metadata` | Gute Basis. `severity` der Spec (`debug/info/warn/error/critical`) ⊃ BP-`type`; `timestamp` fehlt (nur `createdAt`); `tech_details` = `metadata`. |
| `AppSyncItem` | AppSyncItem (Spec B) | `id`, `syncRunId`(FK), `objectType`, `sourceId`, `destinationId`, `status` (synced/failed/skipped/pending/reverted), `action`, `sourceData`/`destinationData` (JSON), `error`, **`rolledBackData`/`rolledBackAt`** | **Sehr gute Übereinstimmung, sogar mehr** (Rollback-Felder, die die Spec nicht explizit fordert). Spec `operation` (create/update/delete/upsert/noop) vs. BP `action` (created/updated/deleted/skipped) — angleichen. `entityType`/`entityId` (HiveGrid-Entität) = BP `objectType`/`destinationId`. |
**Phase-2-Modelle (Marketplace) — vorhanden in `prisma/schema/plugins.prisma`:** `Plugin`, `TenantPluginCapabilityGrant`, `PluginUserConfig`, `PluginInvocation`, `PluginCapabilityUse` (mit per-Tenant Hash-Chain). Deckt die Spec-B-Phase-2-Entitäten (`CommunityApp`, `CommunityAppInstall`) konzeptionell ab, aber mit anderem Namensschema. **Nicht** für Phase 1 verwenden.
**Nicht vorhanden:** kein `App`-Registry-Drift-Tracking-Modell, keine Webhook-Rate-Limit-/Anomalie-State-Tabelle, kein Schema-Drift-Sentinel-State.
---
## Lücken / komplett neu zu bauen
### M008 — Adapter-Interface + State-Machine (neu)
1. **Adapter-Interface** (`app/integrations/{app-key}/index.ts`-Konvention, Spec A/C): `connect`/`disconnect`/`refreshTokens`/`validateConnection`/`syncPull`/`syncPush`/`handleWebhook` + `capabilities[]`/`triggers[]`/`actions[]` + `connection_schema`. Vorlage: `marketing-outbound`-Adapter-Registry, aber das konkrete Interface existiert nicht.
2. **5-Status-State-Machine** `pending → active → degraded → broken → revoked` (Spec G, 33a.3.1) mit Audit-Eintrag pro Übergang + Klartext-Hinweis pro Status. Existiert **nicht** (BP `DataConnection` hat keinen Status-Enum). `connection_status`-Feld + Enum neu.
3. **Adapter-Registry / Build-Time-Scan** von `app/integrations/` + DB-Sync der Manifests + Drift-Check (Spec C). Existiert **nicht** (Manifest-Schema-Vorlage in `@toolctrl/plugins`).
4. **Dummy-App** als Referenz-Implementierung (M008-Erfolgskriterium). Neu.
### M009 — Setup-Wizard + Sync-Log (neu)
5. **Generischer Setup-Wizard** pro Connection-Type (OAuth2 / API-Key / Username-Password / Windows-Client), Form-Generator aus `connection_schema` (Spec D, 33a.2). UI komplett neu (Vorlage: `marketing-integrations.tsx` für die Listen-/CRUD-Hülle).
6. **OAuth2-Redirect-Flow + Token-Refresh-Worker** (Spec D). Kein generischer OAuth-Client in BP (`authProviders.server.ts` ist Auth-Login, nicht App-Connection-OAuth; `@ai-sdk/openai`/`remix-auth-google` per EU-Constraint verboten). Neu.
7. **Sync-Log-UI** mit Filter (Status/Operation/Datum) + Detail-Schublade + Klartext-`summary_text` (Spec F, 33a.4). Neu.
8. **Disconnect-Flow** mit Multi-Step-Confirm + Cleanup (Webhook-Subscriptions kündigen, Tokens revoken) + `status=revoked` statt Hard-Delete (Spec D, 33a.7.4). Neu.
### M010 — Probe-Scheduler + Webhook-Sturm-Schutz (neu)
9. **Probe-Scheduler** mit status-adaptiver Frequenz (active 5min / degraded 1min / broken 15min, Spec G/33a.3.4). Vorlage: `metrics-scheduler.ts` (Poller-Pattern) + `@toolctrl/queue` Repeatable-Jobs — die konkrete Logik neu.
10. **Webhook-Sturm-Schutz** (Hard-Limit Events/Stunde pro Mandant, Default 1000) + Verwerfen-mit-Notification (Spec E, 33a.5.1, AV A.16.1). Rate-Limiter-Vorlage existiert in `@toolctrl/ai/guards` (`createFixedWindowAIRateLimiter`), aber nicht für Webhooks. Neu.
11. **Anomalie-Erkennung** (10x normales Volumen → Pause + Klartext, Spec E, 33a.5.2, AV A.16.2): Rolling-Window-Average + Threshold. Neu.
12. **Schema-Drift-Sentinel** (`validateSchema()`, Spec E/33a.6): Pause + Plattform-Admin-Alert (KWP-spezifisch in M092). Neu.
13. **Webhook-Receiver-Route** `api/v1/webhooks/{app_key}/{tenant_id}/{connection_id}` + per-App-Signatur-Validierung (Spec E.3). Routen-/HMAC-Vorlage in `stripe-webhook-route.ts` + `mailersend-signature.ts`, der generische Multi-App-Receiver neu.
### Querschnitt / Wiring (neu pro Modul, aber Foundation vorhanden)
14. **Sidebar-Eintrag** `Einstellungen → Integrationen (Apps)` (INTEGRATION-CONTRACTS §1, Route `app.hivegrid.de/settings/integrationen/`). Neu in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` (Datei selbst existiert in HiveGrid noch nicht — Repo ist Greenfield).
15. **6 Permissions** `integrations.read/connect/disconnect/manual_sync/view_logs/admin` (Spec H) — in `packages/entitlements/src/permissions.ts` + Default-Rollen-Mapping (GF/Vertriebsleiter/Büro). Permission-Pattern existiert (`permissions-seed.server.ts`), die Keys neu.
16. **View-Model-Layer** (`packages/view-models/**`) + UI-Adapter (`packages/ui-hivegrid/src/adapters/`) — gemäß AGENTS.md Code-Layer-Trennung. Diese Pakete existieren in der Boilerplate **noch nicht** (sind HiveGrid-Eigenbau laut AGENTS.md „Du erweiterst, was fehlt").
17. **Klartext-Fehler-Catalog-Einträge** (M002) pro Fehler-Code + Lösungs-Hinweis (Spec G/33a.10). Neu.
### Repo-Realität (wichtig)
18. **Das `HiveGrid-Cloud`-Repo enthält aktuell NUR Planungs-Artefakte** (`_bmad`, `_bmad-output`, `docs`, `prisma`, `AGENTS.md`) — **kein** `apps/`, `packages/`, kein `package.json`. Verifiziert per `ls`. Die Boilerplate muss erst als Codebase ins Repo gezogen werden (AGENTS.md: „Die Boilerplate IST deine Codebase"). Bis dahin gibt es nichts zu erweitern — der Pre-Flight-Check aus AGENTS.md (`package.json`/`apps/`/`packages/` vorhanden) schlägt fehl.
---
## Empfohlenes Vorgehen
**Reihenfolge (zuerst BP übernehmen, dann neu bauen):**
1. **Voraussetzung:** Boilerplate als Codebase ins HiveGrid-Repo bringen + EU-Sterilisations-Check (AGENTS.md Pre-Flight). Vorher kann M008 nicht starten.
2. **Daten-Layer (BP anpassen):** `prisma/schema/appsEngine.prisma` als Basis nehmen und an Spec B angleichen:
- `App`: plattform-weit machen (oder zweigeteiltes Modell App-Definition vs. installierte App klären), Felder `key/name/description/category/connection_type/connection_schema/capabilities/phase/requires_subscription_feature/documentation_url` ergänzen.
- `DataConnection`: FK `app_id`, `connection_status`-Enum (5 Status), `last_health_check_*`/`last_sync_at`/`next_sync_at`/`last_error`/`metadata`/`created_by_user_id`. Credentials über `CryptoApi` (1:1) verschlüsseln — Vorlage `marketingIntegrations.db.server.ts`.
- `AppSyncRun`/`AppSyncRunLog`/`AppSyncItem`: FK-Ziel auf `DataConnection`, `kind`/`severity`/`operation`/`summary_text` angleichen. Forward-Only-Migration mit Inverse-SQL-Kommentar (AGENTS.md §F.28).
3. **M008 (Adapter-Interface + State-Machine):** Adapter-Interface nach Vorbild `@toolctrl/marketing-outbound` (Registry + `parseConfig`/`send` → erweitern zu `connect`/`sync*`/`handleWebhook`/`validateConnection`). Outbound-Calls über `adapterFetch()` (SSRF-/Timeout-Guard). State-Machine als Enum-Transitions mit Audit pro Wechsel (M001-Middleware deckt DB-Audit, externe Calls via `auditEvent`). Dummy-App als Referenz. Manifest-Schema nach Vorbild `@toolctrl/plugins`.
4. **M009 (Setup-Wizard + Sync-Log):** Settings-Route nach Vorbild `marketing-integrations.tsx` (Liste/CRUD/Permission-Gate/Secret-Masking/i18n) → erweitern zum Wizard + Sync-Log-UI. View-Model-Layer + UI-Adapter neu (AGENTS.md Code-Layer-Disziplin). Jobs über `@toolctrl/queue` mit `buildIdempotentJobId` (Idempotenz 33a.4.4) + `resolveJobPriorityForTenant`.
5. **M010 (Probe-Scheduler + Schutz):** Probe-Scheduler nach Vorbild `metrics-scheduler.ts` + Queue-Repeatable-Jobs, status-adaptive Frequenz. Webhook-Receiver-Route nach Vorbild `stripe-webhook-route.ts`, Signatur nach `mailersend-signature.ts`. Rate-Limiter + Anomalie-Detector + Schema-Drift-Sentinel neu. Notifications strikt über `notify()` (M011).
6. **Phase 2 (separat, M33b/M33c):** Erst nach Geerkens-Launch. WASM-`plugin-runtime`/`plugin-sdk`/`plugins.prisma` als Foundation für den Drittanbieter-Marketplace (Sandboxing/Capability-Consent/Signatur/Audit-Chain). Achtung: in der BP sind in Prod nur `log`/`audit`-Capabilities freigeschaltet (`PLUGIN_AVAILABLE_HOST_CAPABILITY_NAMES`).
**Querschnitt nicht vergessen (DoD-Block):** Sidebar-Eintrag (Einstellungen-Gruppe), 6 Permissions, i18n-Keys, Feature-Flag, Klartext-Catalog, Smoke-Tests + Hot-Swap-Drill + Visual-Snapshot, Permission-Denied → 404.
@@ -0,0 +1,119 @@
# Boilerplate-Abgleich: M34 — Public-API & Webhooks
| Feld | Wert |
|---|---|
| **Modul-ID** | M34 |
| **Name** | Public-API & Webhooks |
| **Milestones** | M161 (OpenAPI + Read-Endpoints), M162 (API-Key-Auth + Scopes + Rate-Limit), M163 (Webhooks ein-/ausgehend, signiert) |
| **Pflichtenheft-Block** | Block 7, Modul 34 (`_bmad-output/pflichtenheft/blocks/block7.md` Z. 264-311) |
| **Reuse-Level** | **medium** |
## Kurzfazit
Die Boilerplate bringt ein **echtes, dediziertes Public-API-Fundament** mit: eine eigenständige Hono-App (`apps/api`) mit OpenAPI-Generierung, Service-Token-/Tenant-API-Key-Auth, Scope-Autorisierung, Rate-Limit-Bausteinen (429 + `Retry-After`), Error-Envelope, Request-ID-Korrelation, Idempotenz, API-Request-Logging (`ApiKeyLog` mit den richtigen Indizes), und im Worker eine generische signierte Webhook-Auslieferung mit Backoff (`deliver-webhook`, 5 Versuche). Das deckt das **Querschnitts-Gerüst** der drei Milestones zu großen Teilen ab.
ABER: Die fachliche Substanz von M34 fehlt komplett. Die existierenden API-Endpoints sind **generisch** (`/v1/entities/:entity/rows`, EntityBuilder-Rows) — es gibt **keine** typisierten HiveGrid-Read-Endpoints (`/api/v1/contacts`, `deals`, `projects`, `leads` …), weil die HiveGrid-Domänenentitäten selbst noch nicht existieren (`app.prisma` ist leerer Platzhalter). Cursor-Pagination, Sparse Fieldsets, Embed-Relations und Scope-basiertes Field-Redact sind **nicht** vorhanden (nur page/pageSize-Pagination). Das gesamte M34-Datenmodell (`WebhookSubscription`, `WebhookDelivery`, `WebhookDeliveryAttempt`, `IncomingWebhookIngestion`, `IntegrationApiRequest`) existiert **nicht** in der Boilerplate. Das vorhandene Webhook-Modell ist `Event`/`EventWebhookAttempt` (Legacy) bzw. form-submission-gebunden — kein Subscription-Modell mit Event-Types, Auto-Disable und HMAC-Secret-pro-Subscription. Swagger-UI mit „Try it out" fehlt (nur Roh-JSON-Viewer unter `/v1/docs`).
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `apps/api/src/app.ts` | Hono-App-Komposition (`new Hono<ApiEnv>()`), Middleware-Kette, `register*Routes(app)`-Muster, listener-frei für Tests | 1:1 (App-Host) — neue HiveGrid-Read-Routes via eigenes `registerHiveGridReadRoutes(app)` einhängen |
| `apps/api/src/server.ts` | HTTP-Listener-Entry (`API_PORT`/`PORT`/`3001`) | 1:1 |
| `apps/api/src/openapi.ts` (+ `.test.ts`) | OpenAPI-Contract-Bausteine: Error-Envelope-Schema (`code`/`message`/`requestId`), Rate-Limit-`429`-Response mit `Retry-After`, `apiErrorResponse()`-Helper, `createOpenApiDocument()` | als-vorlage / anpassen — Envelope 1:1; HiveGrid-Endpoint-Schemas (contacts/deals/…) müssen ergänzt werden |
| `apps/api/src/routes/system-routes.ts` | `GET /v1/openapi.json` + `GET /v1/docs` (Doku-Seite) | anpassen — `/openapi.json` 1:1; `/v1/docs` ist NUR Roh-JSON-Viewer → für „Try it out" durch echte Swagger-UI (`swagger-ui-dist`) ersetzen |
| `apps/api/src/auth.ts` | `extractBearerToken`, `extractApiKeyFromRequest`, `isValidServiceToken`, `createServicePrincipal`, Typ `ServicePrincipal` | 1:1 — Bearer/Key-Extraktion direkt nutzbar; Format `Authorization: Bearer hg_<...>` deckungsgleich mit Spec B.1 |
| `apps/api/src/auth/scope.ts` (+ `.test.ts`) | `authorizeApiScope()`, `resolveApiTenantScope()`, deny-Reasons (`api_key_inactive`/`expired`/`tenant_deactivated`/`tenant_mismatch`/…), `ApiScopedPrincipal`-Union | als-vorlage — Tenant-Scope-Resolution + Key-Lifecycle 1:1; HiveGrid-Scopes (`read_only`/`read_full`/`webhook_inbound`/`automation_trigger`) müssen ergänzt werden (BP-Scopes sind read/create/update/delete pro Entity) |
| `apps/api/src/auth/permission.ts`, `login-rate-limit.ts` | Permission-Wiring + Token-/Window-Rate-Limit-Logik (IP-/Account-Scope, `retryAfterSeconds`) | als-vorlage — Rate-Limit-Algorithmus + 429-Shape übernehmen, auf Per-Key/Per-Tenant/Per-Slug erweitern (Redis-Counter) |
| `apps/api/src/routes/shared.ts` | `createLoginRateLimitResponse()` (429 + `Retry-After` + `rate_limited`-Envelope), `authenticateServiceCredentialRequest()`, JSON-Body-Parsing, Idempotenz-Helfer | als-vorlage — 429-Antwort-Shape und Auth-Helfer direkt als Muster |
| `apps/api/src/middleware/error-envelope.ts` | `apiError(c, status, code, message)`, `createErrorHandler`/`createNotFoundHandler` | 1:1 — deckt M34 Antwort-Format `{error:{code,message,details}}` (34.1.7) ab |
| `apps/api/src/middleware/request-id.ts`, `audit-context.ts`, `body-limit.ts`, `metrics.ts`, `sentryTracing.ts` | Standard-API-Middleware-Stack (Request-Korrelation, Audit-Kontext, Body-Limit, Metrik, Tracing) | 1:1 |
| `apps/api/src/route-access-inventory.ts` (+ `.test.ts`) | `routeAccessInventory` + 11 Auth-Klassen inkl. `tenant-api-key`, `service-or-tenant-api-key`, `webhook-signature`, `optional-api-key-validation`, `public-docs` | als-vorlage — neue HiveGrid-Routes müssen hier mit Auth-Klasse eingetragen werden (CI-Gate `verify:api-inventory`/`verify:openapi-security`) |
| `apps/api/src/idempotency/store.ts`, `shared-store-client.ts` | HTTP-Idempotenz-Cache (für Webhook-Empfang + Event-Ingestion-Idempotenz) | 1:1 |
| `apps/api/src/usage/usage-route.ts` | Vollständiger API-Key-Request-Flow als Referenz: Key auflösen → `ApiKeyLog` anlegen → Tenant-Deactivated/Expired/Inactive prüfen → Usage/402 → Log-Status setzen | als-vorlage — exaktes Muster für „Loader prüft Key-Existenz, Scope, Tenant, Aktiv-Status" (Spec B) + Request-Logging |
| `apps/api/src/webhooks/mailersend-signature.ts` (+ `.test.ts`) | HMAC-SHA256-Signatur-Verifikation mit `timingSafeEqual` (Constant-Time-Compare), Timestamp-Toleranz, typisierte Failure-Reasons | als-vorlage — direkt das Muster für M34 eingehende `X-HiveGrid-Signature: sha256=<hex>`-Verifikation (Spec B.2) |
| `packages/marketing-outbound/src/adapters/custom-webhook.ts` | Ausgehender Webhook-POST mit `X-Marketing-Signature: sha256=<hmac(body)>`, SSRF-Guard (`adapterFetch`, HTTPS-only, kein RFC1918) | als-vorlage — exaktes Muster für M34 ausgehende HMAC-signierte Deliveries (Spec E, Signatur + SSRF-Schutz) |
| `apps/worker/src/jobs/form-submission-webhook-delivery.ts` (+ `.test.ts`) | Outbox→Webhook-Delivery-Bridge auf `deliver-webhook`-Job, Tenant-Mismatch-Validierung | als-vorlage — Muster für „BullMQ-Job pro Webhook-Versand" (M163-Hinweis) |
| `packages/queue/src/payloads.ts` → `WebhookDeliveryJob` | Typisierter `webhook.deliver`-Job (`url`/`method`/`headers`/`body`/`eventName`/`metadata`) | 1:1 — direkt für ausgehende Webhook-Deliveries |
| `packages/queue/src/producers.ts` (`buildRetryJobOptions`) | `deliver-webhook`: **5 Versuche, exponentielles Backoff** + Idempotenz via `buildIdempotentJobId()` | anpassen — 5x deckt Spec E ab; Backoff-Stufen (1m/5m/30m/2h/12h) müssen exakt gesetzt werden (BP-Default ist `delay: 2_000` exponentiell) |
| `apps/api/src/webhookDeliveryQueue.ts` | `enqueueJob()`-Wrapper für Webhook-Auslieferung mit Idempotenz-Key + TenantId | als-vorlage |
| `packages/db/src/event-webhook-attempts.server.ts` | Repository-Muster für Webhook-Attempt-Persistenz (tenant-scoped find/update) | als-vorlage — Muster für neue `WebhookDelivery`/`WebhookDeliveryAttempt`-Repos |
| `packages/db/src/api-keys.server.ts` | API-Key-Repository: `findLegacyApiKeyByKey`, Key-Hashing (`keyHash`/`keyPrefix`/`keyLastFour`), `ApiKeyLog`-CRUD, Usage-Aggregation | als-vorlage / anpassen — Key-Hashing + Logging 1:1; HiveGrid-Scope-Spalte ergänzen (BP hat per-Entity-Rechte, kein Scope-Enum) |
| `packages/db/src/outbox.ts` (`publishOutboxEvent`) + `apps/worker/src/outbox/event-router.ts` | Transaktionales Outbox + Event-Routing (`webhook.`-Prefix → `outbox.webhook`-Queue, longest-prefix-wins) | 1:1 — für ausgehende Webhook-Events (`contact.created` etc.) als Dispatch-Mechanismus |
| `packages/db/src/audit-events.server.ts` + `AuditEvent` (Hash-Kette) | Auditierung von Key-Anlage, Webhook-Setup, Inbound-Events (DoD C.13, AV #6) | 1:1 |
| `apps/api/Dockerfile`-Target + `.gitea/workflows/ci.yml` `api`-Job | Build-/CI-Fundament: `verify:openapi-security` → typecheck → `vitest apps/api/src` → build | 1:1 |
| `infra/grafana/api-latency.json` + `infra/prometheus/` | API-Latenz-/Queue-Health-Dashboards (Metering-Vorbereitung Phase 2) | 1:1 |
---
## Vorhandene Daten-Modelle (Prisma)
Alle in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`.
| Modell | Datei | Deckt M34-Bedarf ab? |
|---|---|---|
| `ApiKey` | `core.prisma` (Z. 100-126) | **Teilweise.** Gehashte Keys (`keyHash`/`keyPrefix`/`keyLastFour`/`keyVersion`), `active`, `expires`, `@@unique([tenantId, alias])`. **Fehlt:** Scope-Spalte (BP nutzt `ApiKeyEntity`-Join mit create/read/update/delete pro Entity, NICHT die M34-Scopes `read_only`/`read_full`/`webhook_inbound`/`automation_trigger`) und Per-Key-Rate-Limit-Override-Feld. Spec B verweist explizit auf „API-Key-Verwaltung aus M27 Block E" → Scope-Modell gehört zu M27. |
| `ApiKeyLog` | `core.prisma` (Z. 128-151) | **Gut.** Deckt M34 `IntegrationApiRequest` weitgehend: `endpoint`, `method`, `params`, `status`, `duration`, `error`, `ip`, `tenantId`, `apiKeyId` + passende Indizes inkl. `(apiKeyId, tenantId, createdAt DESC)` und `(tenantId, createdAt)`. **Fehlt ggü. Spec A:** `route_pattern` (statt voll-aufgelöstem `endpoint`), `query_params` als JSONB (BP: `params` String), `request_body_size_bytes`/`response_size_bytes`, `user_agent`, Retention-Logik (90/180 Tage). Entweder `ApiKeyLog` erweitern oder eigenes `IntegrationApiRequest` bauen. |
| `Event` + `EventWebhookAttempt` | `core.prisma` (Z. 152-178) | **Schwach / Legacy.** `Event` (name/data/resource) + `EventWebhookAttempt` (endpoint/success/status/message/body). KEIN Subscription-Modell, keine Event-Types-Liste, kein HMAC-Secret-pro-Subscription, kein Auto-Disable-Counter, kein Idempotenz-`event_id`. Nur als grobe Vorlage für `WebhookDelivery`/`WebhookDeliveryAttempt` tauglich. |
| `EntityWebhook` + `EntityWebhookLog` | `entityBuilder.prisma` (Z. 206-224) | **Nicht passend.** Pro-Entity-Action-Webhook im EntityBuilder (entityId/action/method/endpoint). Kein Tenant-Subscription-Modell, kein Secret, kein Status-Enum/Retry. |
| `Form` + `FormSubmission` | `formWidget.prisma` | **Teil-Vorlage für eingehenden Lead-Push.** `Form` (`key @unique`, `definition` JSON, `fieldMapping`, `allowedOrigins[]`, Anti-Spam-`config`), `FormSubmission` (`payload` JSON, `ipHash`, `status` received/processed/failed/spam). Deckt das Slug-/Mapping-/Status-Muster von M34 `IncomingWebhookIngestion` konzeptionell ab, ist aber Form-Widget-gebunden, nicht Webhook-Slug-gebunden mit 3 Auth-Methoden. |
| `App` / `DataConnection` / `AppSyncRun` | `appsEngine.prisma` | **Randnah.** Externe Datensync-Engine (rest-api/oauth2/webhook, Rollback). Nicht für M34-Public-API, eher Phase-2-Inbound-Connectors. |
| `Lead` / `LeadEvent` | `marketingLeads.prisma` | **Beachten — Konflikt.** BP-`Lead` ist **operator-scoped (KEIN tenantId)**, M34 Lead-Push erzeugt **tenant-scoped** Leads (Modul 2/M042). M34-Webhook muss auf den HiveGrid-CRM-Lead schreiben, nicht auf diesen Marketing-Lead. |
| `OutboxEvent` | `outbox.prisma` | **1:1.** Transaktionaler Event-Auslöser für ausgehende Webhooks. |
| `IdempotencyKey` | `core.prisma` | **1:1.** HTTP-Idempotenz für Webhook-Empfang/Event-Ingestion. |
| `FeatureFlag` | `featureFlags.prisma` | **1:1.** DoD-Pflicht „Feature-Flag-fähig" (M006). |
### Komplett fehlende M34-Modelle (in `prisma/schema/` NICHT vorhanden — verifiziert)
`WebhookSubscription`, `WebhookDelivery`, `WebhookDeliveryAttempt`, `IncomingWebhookIngestion`, `IntegrationApiRequest` — **keiner dieser Namen existiert** im Schema. Anlageort: `prisma/schema/app.prisma` (aktuell leerer Platzhalter `// custom database models`).
---
## Lücken / komplett neu zu bauen
1. **HiveGrid-Domänenentitäten als Voraussetzung.** Die typisierten Read-Endpoints (`/api/v1/contacts|companies|deals|projects|tasks|leads`) setzen voraus, dass diese Module (M042-M074) existieren. `app.prisma` ist leer → es gibt nichts zum Lesen. M161 hängt hart an den Daten-Modulen (PLAN: „Vorbedingung Daten-Module M042-M074").
2. **Typisierte Read-API-Endpoints.** BP bietet nur generische EntityBuilder-Row-Endpoints (`/v1/entities/:entity/rows`). Die HiveGrid-Endpoints müssen neu gebaut werden (eigenes Route-Modul `register*Routes`).
3. **Cursor-Pagination.** BP hat nur `page`/`pageSize` (offset-basiert, `pagination/rows.ts`). Spec verlangt opaque base64-Cursor + `meta.next_cursor` → neu.
4. **Sparse Fieldsets (`fields=`), Embed-Relations (`embed=`, Tiefe 1), erweiterte Filter** (Multi-Value-OR, Date-Range `*_gte/_lte`, `custom.{key}`, `q`-Volltext), **Sortierung (`sort=-feld`)** — keiner dieser Mechanismen existiert generisch. Neu.
5. **M34-Scope-Modell** (`read_only`/`read_full`/`webhook_inbound`/`automation_trigger`) + **Field-Redact** sensibler Felder bei `read_only` (nicht 403, sondern Feld-Auslassung). BP-Scope ist entity-CRUD-basiert. Neu (kommt fachlich aus M27 Block E, hier konkretisiert).
6. **Rate-Limiting Per-Key / Per-Tenant / Per-Slug** mit Token-Bucket + Redis-Counter und Headern `X-RateLimit-Limit/Remaining/Reset`. BP hat nur Login-Rate-Limit (IP/Account) und `Retry-After` im 429. Die generischen `X-RateLimit-*`-Header existieren NICHT (nur in OpenAPI-Schema beschrieben). Neu — Algorithmus von `login-rate-limit.ts` als Vorlage.
7. **`IntegrationApiRequest`-Modell** (oder Erweiterung von `ApiKeyLog`) mit `route_pattern`, `query_params` JSONB, Größen-Feldern, `user_agent` + Retention-Job (90/180 Tage). Neu.
8. **Webhook-Subscription-Modell + UI.** `WebhookSubscription` (Event-Types[], HMAC-Secret, Auto-Disable-Counter, Stats), `WebhookDelivery` (Status-Enum pending/delivered/failed/exhausted/discarded), `WebhookDeliveryAttempt`. Komplett neu. Inkl. Subscription-Verwaltungs-UI unter `/{tenant}/admin/webhooks/`, Delivery-Stats-View, Test-Funktion (`test.ping`), Replay (Phase-2-Out-of-Scope). Auto-Disable nach 10 konsekutiven Fehlern + `notify()` (M011).
9. **Eingehende Webhook-Slugs.** `IncomingWebhookIngestion` + Slug-Verwaltung mit 3 Auth-Methoden (API-Key / HMAC / URL-Token mit Klartext-Warnung), `payload_mapping`, Auto-Lead-Anlage + Duplikat-Check (Email-Match → Activity statt neuem Lead), Default-Tags/Assignee. HMAC-Verify-Muster aus `mailersend-signature.ts` übernehmbar, Rest neu.
10. **Backoff-Stufen exakt.** BP `deliver-webhook` = 5 Versuche exponentiell `delay 2000` — Spec verlangt feste Stufen 1m/5m/30m/2h/12h. Job-Options anpassen/custom-Backoff.
11. **Event-Type-Katalog + Payload-Envelope** (`event_id`/`event_type`/`tenant_id`/`occurred_at`/`data`/`previous_data`) mit Mapping auf die ~30 HiveGrid-Events (`contact.created` … `call.missed`). Neu — Trigger-Punkte in jedem Quell-Modul.
12. **Allgemeine Event-Ingestion** `POST /api/v1/integrations/events` → Automation-Engine-Trigger (M075/M017 `external_event`). Verdrahtung neu.
13. **Swagger-UI mit „Try it out".** BP `/v1/docs` ist nur ein Roh-JSON-`<pre>`-Viewer (kein interaktives Swagger-UI, kein API-Key-Eingabefeld). `swagger-ui-dist` einbinden (M161-Hinweis nennt es als Boilerplate-Dep — als Paket vorhanden, aber NICHT als Route verdrahtet). Neu.
14. **Versionierung/Deprecation-Header** (`Sunset`, `Deprecation: true`, Stability-Tags). Nicht vorhanden. Neu (leichtgewichtig).
15. **402-bei-Frozen-Tenant für Schreib** (Spec 34.1.10) — da Phase 1 read-only ist, primär ein Webhook-Inbound-403-Pfad (frozen → eingehende Webhooks verworfen). BP-`usage-route.ts` hat das 402-Muster bereits (`payment_required`) als Vorlage.
16. **UI-Layer komplett** (DoD A-E): View-Models, Adapter, Sidebar-Eintrag (`packages/ui-hivegrid/src/app-shell/sidebar.config.ts`), Routes, i18n-Keys, Empty/Loading-States, Visual-Regression, Hot-Swap, Smoke-Tests. Reihenfolge & Wiring per `docs/INTEGRATION-CONTRACTS.md`. Nichts davon HiveGrid-spezifisch vorhanden.
---
## Empfohlenes Vorgehen
**Voraussetzung prüfen:** M27 (API-Keys + Scope-Definition) und die Daten-Module M042-M074 müssen stehen — sonst gibt es keine Scopes und keine Entitäten zum Lesen. Ohne diese ist M161 nicht sinnvoll baubar.
### M161 — Read-API + OpenAPI (zuerst aus BP übernehmen)
1. **App-Host 1:1 nutzen:** `apps/api/src/app.ts` + Middleware-Stack + `error-envelope.ts` + Request-ID stehen. Nur ein neues Route-Modul `registerHiveGridReadRoutes(app)` registrieren.
2. **Auth-Flow als Vorlage:** `usage-route.ts` + `auth.ts` + `auth/scope.ts` als Blueprint für „Key auflösen → loggen → Tenant/Active/Expired prüfen". `extractBearerToken`/`extractApiKeyFromRequest` 1:1.
3. **NEU bauen:** typisierte Endpoints, Cursor-Pagination, Filter/Sort/Fields/Embed-Parser, Response-Builder `{data, meta}`.
4. **OpenAPI:** `openapi.ts`-Envelope + `createOpenApiDocument()` erweitern; `swagger-ui-dist` als echte Route ergänzen (BP `/v1/docs` ersetzen).
5. **Route-Access-Inventar** (`route-access-inventory.ts`) für jede neue Route pflegen → sonst CI-Gate `verify:openapi-security`/`verify:api-inventory` rot.
### M162 — Scopes + Field-Redact + Rate-Limit
1. **Scope-Modell** an `ApiKey` koppeln (kommt aus M27); `authorizeApiScope`-Muster für `read_only`/`read_full` adaptieren.
2. **Field-Redact** im Response-Builder (sensible Felder bei `read_only` weglassen) — neu, im VM/Serializer.
3. **Rate-Limit:** Algorithmus aus `auth/login-rate-limit.ts` + Redis-Counter (`@toolctrl/queue` `resolveRedisConnectionOptions`) auf Per-Key/Per-Tenant/Per-Slug ausweiten; `X-RateLimit-*`-Header + 429-Shape aus `routes/shared.ts` übernehmen.
4. **Request-Logging:** `ApiKeyLog` erweitern oder `IntegrationApiRequest` neu + Retention-Cron (Muster: `apps/worker/src/jobs/audit-retention.ts`).
### M163 — Webhooks (signiert)
1. **Neue Prisma-Modelle** in `app.prisma`: `WebhookSubscription`, `WebhookDelivery`, `WebhookDeliveryAttempt`, `IncomingWebhookIngestion` (Forward-Only-Migration mit Inverse-SQL, DoD F.28). DSGVO: neue User-FKs in `packages/db/src/dsgvo/user-fk-map.ts` eintragen (sonst CI rot).
2. **Eingehende Webhooks:** HMAC-Verify aus `webhooks/mailersend-signature.ts` (Constant-Time) übernehmen; Slug-/Mapping-/Auth-Logik neu; Idempotenz via `IdempotencyKey`; Auto-Lead-Anlage auf HiveGrid-CRM-Lead (nicht Marketing-`Lead`) + Activity via `logActivity`.
3. **Ausgehende Webhooks:** Outbox (`publishOutboxEvent`) → `webhook.`-Prefix-Routing → `deliver-webhook`-Job (5 Versuche). HMAC-Sign-/SSRF-Muster aus `marketing-outbound/custom-webhook.ts`. Backoff-Stufen auf 1m/5m/30m/2h/12h anpassen. Auto-Disable nach 10 Fehlern + `notify()` (M011). Delivery-/Attempt-Persistenz à la `event-webhook-attempts.server.ts`.
4. **Event-Ingestion** `POST /v1/integrations/events` → Automation-Trigger (M075/M017).
5. **UI** für Subscriptions/Slugs/Logs (DoD A-E) zuletzt: Subframe → Adapter → View-Model → Route → Sidebar + Permission-Gate + i18n + Tests.
### Querschnitt nicht nachbauen (DoD): Audit (`AuditEvent`/`auditEvent`), Outbox, Idempotenz, FeatureFlag, Notifications (`notify`), Tenant-Scope — alles in BP vorhanden und Pflicht-Wiring.
@@ -0,0 +1,124 @@
# Boilerplate-Abgleich — M35 DSGVO-Export / Account-Löschung
| Feld | Wert |
|---|---|
| **Modul-ID** | M35 |
| **Name** | DSGVO-Export / Account-Löschung / Tenant-Daten-Portabilität |
| **Milestones** | M164 (Daten-Export), M165 (Lösch-Workflow), M166 (AVV/Auskunft) |
| **Block** | Block 7 — Plattform-Außenseite |
| **Reuse-Level** | **medium** |
## Zusammenfassung
Die Boilerplate hat ein **vollständiges, getestetes USER-Level-DSGVO-Subsystem** (Phase 10 Task #68):
Personal-Data-Export-Worker, Lösch-Cascade-Worker, Grace-Period-Cron, Pseudonymisierung, eine
zentrale `USER_FK_MAP` über alle User-FKs, ein Selbst-Service-Privacy-Hub im Web (Export/Delete/Reactivate
mit Email-Confirm-Token + 30-Tage-Grace), CLI-Commands und ein generisches Streaming-Export-Framework
(`packages/exports`: CSV/JSON/XLSX/PDF + S3-Multipart-Sink + Row-Caps + Audit-PDF). Das deckt **M165
(Account-Löschung) und einen Großteil von M164 (User-Daten-Export)** als hochwertige Vorlage ab.
**Komplett fehlend** ist die gesamte **TENANT-Ebene**: Tenant-Voll-Export (`scope=tenant_full`),
Tenant-Lösch-Workflow + Hard-Delete-Cron (Storage/Embeddings/Queues/Webhooks/DB in FK-Reihenfolge),
Tenant-Lifecycle-Status (`deletion_pending`/`deleted`/`frozen`/`past_due` als Enum), **AVV-Vertrag**
(PDF-Generator, `AvvAcceptance`, Re-Akzeptanz-Flow), **Subprozessoren-Liste** (`Subprocessor`-Modell +
UI + Widerspruchs-Frist) und **ZIP-mit-Binär-Dateien**-Bundling (Boilerplate exportiert nur ein
JSON-Bundle, keine `.zip` mit Original-Files). Die HiveGrid-spezifischen DSGVO-Modelle (`GdprExportJob`,
`TenantDeletionRequest`, `AvvAcceptance`, `Subprocessor`) existieren nicht.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `packages/exports/src/index.ts` (+ `csv/`, `json/`, `pdf/`, `xlsx/`, `types.ts`, `limits.ts`, `sinks/s3-multipart.ts`, `format-caps.ts`, `spreadsheet-safety.ts`, `audit-pdf.ts`) | Streaming-Export-Framework: Format-Serializer (Stream + Buffer), `createS3MultipartUploadSink` (8 MB Parts, Auto-Abort), Plan-/Format-Row-Caps, Injection-Safety, Compliance-Audit-PDF-Renderer | **1:1** als Fundament für beide Export-Scopes |
| `apps/worker/src/jobs/dsgvo-export.ts` | User-Personal-Data-Export-Worker: iteriert `USER_FK_MAP.includeInExport`, streamt JSON-Bundle zu S3, mintet Signed-URL, sendet Ready-Email, schreibt `AuditEvent` (action=EXPORT_PERSONAL_DATA, scope=gdpr, sensitivity=pii). Host-agnostisch (DI über `DsgvoExportDependencies`) | **anpassen** — Basis für M164 User-Export; ZIP-statt-JSON + README/manifest.json + Binär-Files ergänzen |
| `apps/worker/src/jobs/dsgvo-deletion.ts` | User-Lösch-Cascade: pro `USER_FK_MAP`-Eintrag `hard-delete`/`pseudonymize`/`retain-tombstone`, User-Row löschen oder pseudonymisieren, Audit. Idempotent, DI-fähig | **anpassen** — deckt M165 Account-Löschung (35.2.4 „PII überschrieben, Audit behält User-ID") direkt ab |
| `apps/worker/src/jobs/dsgvo-grace-cron.ts` | 24h-Cron: scannt `User.pendingDeletion && scheduledDeletionAt<=now`, dispatcht Lösch-Jobs. Idempotenter `upsertJobScheduler` | **als-vorlage** — Muster 1:1 übertragbar auf Tenant-Hard-Delete-Cron (35.3.4) |
| `packages/db/src/dsgvo/user-fk-map.ts` | Zentrale Karte ALLER Modelle mit User-FK + Cascade-Strategie + `includeInExport`. CI-Test (`user-fk-map.test.ts`) erzwingt Vollständigkeit gegen Schema | **anpassen** — Pflicht-Referenz; jedes neue HiveGrid-Modell mit User-FK muss ergänzt werden. Vorlage für eine analoge `TENANT_FK_MAP` (Tenant-Hard-Delete) |
| `packages/db/src/dsgvo/pseudonymize.ts` | Deterministische Pseudonymisierungs-Helfer (`pseudonymize`, `buildUserPseudonymizationUpdate`) | **1:1** für 35.2.4 Anonymisierungs-Strategie |
| `apps/web/app/routes/app.$tenant/settings/privacy.tsx` + `privacy.delete.confirm.$token.tsx` + `privacy.delete-mail.test.tsx` | Selbst-Service-Privacy-Hub: Export-Anstoß, Lösch-Anforderung mit Confirm-Phrase, Email-Confirm-Link, Reactivate, CSRF, Audit pro Intent. Route liegt bereits unter `app.$tenant/settings/privacy` | **als-vorlage** — entspricht Spec-Route `/{tenant}/settings/account/delete` + `.../data-export`; Multi-Step-Flow (35.2.3) + 1.5-Export-Angebot ergänzen. **Achtung:** verstößt noch gegen HiveGrid-Layer-Trennung (Prisma im Loader, kein VM, Inline-Styles statt i18n-VM) → muss in VM/`ui-hivegrid`/Adapter umgebaut werden |
| `apps/web/app/utils/dsgvo/deletionToken.server.ts` | HMAC-signierter Lösch-Confirm-Token (24h, fail-closed, eigene Version gegen Replay) | **1:1** für 35.2.5 24h-Reverse-Window (Magic-Link gegen Auth-Bypass) |
| `packages/storage/src/signed/index.ts` | S3-Signed-URL-Adapter (PUT/GET, Expiry-Clamp, Key-Validierung, Secret-Redaction) | **1:1** für 7-Tage-Download-Window-Signed-URLs (35.1.5) |
| `prisma/schema/imports.prisma` (`ExportJob`-Modell, Migration `20260508000000_export_jobs` + `..._export_job_hard_delete`) | `ExportJob`: tenantId, requestedByUserId, exportType, format, status (`ExportJobStatus`), requestId, artifactBucket/Key, rowCount, `expiresAt`/`deletedAt`/`hardDeletedAt` (Retention), error-Felder, Timestamps. Indizes inkl. `(expiresAt)` für Cleanup-Cron | **anpassen** — sehr nah an Spec-`GdprExportJob`; es fehlen `scope`-Enum, `target_user_id`, `progress_percent`, `download_count`, `last_downloaded_at`, `result_size_bytes` |
| `apps/worker/src/jobs/export.ts` | Generischer Export-Job: Plan-Resolution, Row-Cap-Enforcement, Format-Streamer-Registry, Progress-Publish via SharedStore, S3-Multipart. Liest `ExportJob`-Queue-Payload | **als-vorlage** — Progress-Update-Muster (alle N Rows, 35.1.4) + Tenant-Plan-Auflösung direkt übernehmbar |
| `packages/queue/src/payloads.ts` | Queue-Payloads: `CleanupJob.task` enthält bereits `'expired-export-artifacts'` + `'dsgvo-grace-period-scan'`; `ExportJob` (`kind: "export.run"`); `queueNames.exports` | **anpassen** — Cleanup-Task für ZIP-Ablauf (7 Tage) bereits vorgesehen; neue Tenant-Lösch-/Export-Tasks ergänzen |
| `apps/worker/src/jobs/dsgvo-verify.ts` + `audit-retention.ts` | Verify-/Retention-Muster für DSGVO-Jobs | **als-vorlage** |
| `packages/cli/src/commands/dsgvo/{export,delete,verify,reactivate,index}.ts` + `commands/tenant/anonymize.ts` | Operator-CLI für DSGVO-Aktionen (für Plattform-Admin-initiierte Löschung, 35.3.5) | **als-vorlage** für M37-Plattform-Admin-Pfad (`admin_initiated`, `suspended_violation`) |
| `apps/web/app/routes/app.$tenant/settings.tsx` + `settings/account.tsx` | Tenant-Settings-Shell + Account-Settings-Page-Muster | **als-vorlage** für Privacy-Hub-Routes `/{tenant}/admin/privacy/*` und `/{tenant}/settings/account/*` |
| `packages/db/src/audit-events.server.ts` (`createAuditEvent`) | Audit-Schreiber mit `regulatoryScope`/`dataSensitivity` | **1:1** — entspricht Querschnitt AV #6 (alle Privacy-Aktionen geloggt) |
---
## Vorhandene Daten-Modelle (konkret)
| Modell | Pfad | Deckt M35-Bedarf |
|---|---|---|
| `ExportJob` (+ Enum `ExportJobStatus`) | `prisma/schema/imports.prisma` | ~70 % von `GdprExportJob`: Retention (`expiresAt`/`deletedAt`/`hardDeletedAt`), `requestId`, `artifactBucket/Key`, `rowCount`, Status, Indizes. **Fehlt:** `scope`-Enum (user_self/tenant_full), `target_user_id`, `progress_percent`, `download_count`, `last_downloaded_at`, `result_size_bytes`, FK auf `File` |
| `User` (DSGVO-Felder) | `prisma/schema/accounts.prisma` (Z. 33-38, 95) | `pendingDeletion`, `scheduledDeletionAt`, `deletionRequestedAt`, `pseudonymized`, `pseudonymizedAt`, `deletionRequestId` (unique) + Index `user_pending_deletion_due_idx`. **Deckt 35.2 Account-Löschung-State vollständig** |
| `Tenant` | `prisma/schema/accounts.prisma` (Z. 129-200) | Nur `active: Boolean` + `deactivatedReason: String?`. **KEIN Lifecycle-Status-Enum** (frozen/past_due/deletion_pending/deleted). Kein Grace-Period-Feld |
| `ImportJob` (+ Enum) | `prisma/schema/imports.prisma` | Nur Import-Seite — als Form-Vorlage für `GdprExportJob`-Felder nutzbar |
| `AuditEvent` | `prisma/schema/core.prisma` (logisch, via `user-fk-map`) | `actorId`/`regulatoryScope`/`dataSensitivity` — Plattform-Audit-Metadata nach Hard-Delete (35.3.4) bereits vorgesehen |
**Nicht vorhanden (komplett neu):** `GdprExportJob` (HiveGrid-spezifisch mit `scope`), `TenantDeletionRequest`,
`AvvAcceptance`, `Subprocessor`.
---
## Lücken / komplett neu zu bauen
### Daten-Layer (neu)
- **`TenantDeletionRequest`-Modell** — reason-Enum (voluntary/non_payment/suspended_violation/admin_initiated), `grace_period_ends_at`, status, cancel-Felder, Unique-Constraint „max 1 aktive". Cron-Index `(grace_period_ends_at, status)`.
- **`AvvAcceptance`-Modell** — version, IP/User-Agent, Signer-Name/Rolle, `pdf_file_id`, Unique `(tenant_id, avv_version)`.
- **`Subprocessor`-Modell** — plattform-global (kein RLS), name/purpose/country/is_eu_eea/data_categories[]/dpa_url/active_since/archived_at.
- **Tenant-Lifecycle-Status** — Enum (`active|frozen|past_due|deletion_pending|deleted`) + `grace_period_ends_at` auf `Tenant` (oder via `TenantDeletionRequest`). Aktuell nur Boolean `active`.
- **`GdprExportJob`-Erweiterung** — `scope`-Enum, `target_user_id`, `progress_percent`, `download_count`, `last_downloaded_at`, `result_size_bytes`, FK auf `File` (Modul 23).
### Domänen-Logik / Worker (neu)
- **Tenant-Voll-Export-Worker** (`scope=tenant_full`) — aggregiert ALLE tenant-scoped Entitäten je JSONL-File (CRM/Deals/Projects/HR/… laut Spec Block D) + Original-Files. Es gibt KEINE `TENANT_FK_MAP`-Analogie zur `USER_FK_MAP`.
- **ZIP-mit-Binär-Bundling** — Boilerplate streamt nur ein flaches JSON-Bundle. Es gibt **keine** `archiver`/`yazl`/`jszip`-Nutzung. Spec verlangt ZIP mit README.md, manifest.json, Unterordnern, JSONL + Original-Binär-Files + `schema/` + Multi-Part-ZIP.
- **Tenant-Hard-Delete-Cron + Cascade** — FK-konforme Reihenfolge inkl. Storage rekursiv, Embeddings (pgvector), Worker-Queues purgen, Webhook-Subscriptions, Redis-Cache (`tenant:{id}:*`). User-Cascade existiert, Tenant-Cascade nicht.
- **AVV-PDF-Generator** — dynamisches PDF mit Tenant-Stamm + Subprozessor-Snapshot. `@react-pdf/renderer` ist verfügbar (für Audit-PDF), aber kein AVV-Template.
- **AVV-Re-Akzeptanz-Cron** (14-Tage → Read-Only) + Subprozessor-Widerspruchs-Frist-Cron (14-Tage).
- **Rate-Limit-Logik** — max 3 user_self/24h, max 1 tenant_full aktiv (Spec-Constraint; nicht in Boilerplate).
- **Resume/Checkpoint-Mode** für großen Tenant-Export (Block D Edge-Case) — nicht vorhanden.
### UI (neu / Umbau)
- **Privacy-Hub Tenant-Admin** (`/{tenant}/admin/privacy/*`: export, deletion, subprocessors, avv) — existiert nicht.
- **Multi-Step-Tenant-Kündigung** (5 Steps mit Slug-Confirm-Phrase, Stripe-Hinweis) — nur einfacher User-Delete vorhanden.
- **Subprozessoren-Tabelle** (Filter EU/alle, Suche, CSV/PDF-Export, Drittland-Klartext-Warnung) — neu.
- **AVV-Download + Akzeptanz-History** + Re-Akzeptanz-Modal — neu.
- **Bestehende `privacy.tsx`** muss auf HiveGrid-Layer-Trennung umgebaut werden (VM-Builder, `packages/ui-hivegrid`, Subframe-Adapter, i18n-Keys statt Inline-Strings/Tailwind) — siehe AGENTS.md Anti-Patterns.
### Querschnitt-Wiring (neu)
- **Permissions** `privacy.user_export.self`, `privacy.tenant_export`, `privacy.tenant_deletion`, `privacy.subprocessors.read`, `privacy.avv.read`, `privacy.avv.accept` — nicht im `permissions-seed.server.ts` (nur „account deletion"-Bezug im Admin-Role-Kommentar). Neu in `packages/entitlements`.
- **Sidebar-Eintrag** „DSGVO / Datenschutz" Gruppe EINSTELLUNGEN (INTEGRATION-CONTRACTS §1 → M164-M166) in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` — neu (HiveGrid-Sidebar-Config existiert in Boilerplate nicht, nur ToolCTRL-`application/sidebar`).
- **Notifications via M011 `notify()`** — Boilerplate-Worker rufen `sendEmailMailerSend`/`sendExportReadyEmail` direkt; muss auf HiveGrid-`notify()`-Service umgestellt werden.
- **Mail-Templates** für Export-Ready / Lösch-Bestätigung / AVV-Re-Akzeptanz / Subprozessor-Notification — keine dedizierten DSGVO-Templates in `packages/email-templates` gefunden.
- **Privacy-Manifest (Apple)** — Block I; gehört zu Modul 36, Inhalt-Quelle hier. Keinerlei Boilerplate-Asset.
---
## Empfohlenes Vorgehen
**Phase 1 — Fundament aus BP übernehmen (1:1 / leichte Anpassung):**
1. `packages/exports` als Export-Framework übernehmen (Streamer + S3-Multipart-Sink + Row-Caps + Audit-PDF). Basis für beide Scopes.
2. `packages/storage/src/signed` (Signed-URL) + `utils/dsgvo/deletionToken.server.ts` (Confirm-Token) + `packages/db/src/dsgvo/pseudonymize.ts` 1:1 übernehmen.
3. `ExportJob`-Modell aus `imports.prisma` zu HiveGrid-`GdprExportJob` erweitern (`scope`, `target_user_id`, `progress_percent`, `download_count`, `result_size_bytes`, `File`-FK). Forward-Only-Migration.
**Phase 2 — M165 Account-Löschung (höchste BP-Deckung):**
4. `dsgvo-deletion.ts` + `dsgvo-grace-cron.ts` + `user-fk-map.ts` als Cascade-Basis nutzen; `USER_FK_MAP` um alle neuen HiveGrid-Modelle ergänzen (CI-Test erzwingt das).
5. `privacy.tsx`-Route auf HiveGrid-Layer-Trennung umbauen (VM + `ui-hivegrid` + Subframe-Adapter + i18n) und Multi-Step + Step-1.5-Export-Angebot ergänzen.
**Phase 3 — M164 User-Export, dann Tenant-Export (neu auf BP-Fundament):**
6. `dsgvo-export.ts` zu ZIP-Bundle ausbauen: ZIP-Lib (`archiver`/`yazl`) einführen, README.md/manifest.json + Unterordner-Struktur (Spec Block C) + Original-Binär-Files aus Storage. Progress-Muster aus `export.ts`.
7. Tenant-Voll-Export NEU bauen: `TENANT_FK_MAP`-Analogie + JSONL-pro-Entität + Files + Multi-Part-ZIP + Resume-Checkpoint.
**Phase 4 — M165 Tenant-Lösch-Workflow (neu):**
8. `TenantDeletionRequest`-Modell + Tenant-Lifecycle-Status-Enum + Multi-Step-Kündigung (Slug-Confirm-Phrase, Stripe-Hinweis aus M153) + 30-Tage-Grace-Banner + Cancel.
9. Tenant-Hard-Delete-Cron (Cron-Muster aus `dsgvo-grace-cron.ts`): Storage rekursiv → Embeddings → Queues → Webhooks → DB (FK-Reihenfolge) → Redis-Cache → Plattform-Audit-Metadata.
**Phase 5 — M166 AVV + Subprozessoren (komplett neu):**
10. `AvvAcceptance` + `Subprocessor`-Modelle, AVV-PDF-Generator (`@react-pdf/renderer` ist da), Signup-Checkbox-Hook (M157), Re-Akzeptanz-Cron, Subprozessoren-Tabelle-UI + Widerspruchs-Frist-Cron + Notification-Templates.
**Querschnitt durchgängig:** Permissions in `entitlements` definieren, Sidebar-Eintrag (Gruppe EINSTELLUNGEN), `notify()` statt Direkt-Email, Mail-Templates, alle Aktionen auditieren (BP-`createAuditEvent` ist da).
@@ -0,0 +1,122 @@
# Boilerplate-Abgleich — M36 Mobile-App (Capacitor)
- **Modul-ID:** M36
- **Name:** Mobile-App (Capacitor-Wrapper für iOS + Android)
- **Zugehörige Milestones:** M167 (Capacitor-Setup + Build-Pipeline), M168 (Native-Features Push/Face-ID/Kamera + Universal Links), M169 (Native Tab-Bar + Mobile-First-Routes), M170 (Offline-Modus Phase-1-Pflicht), M171 (App-Store-Compliance + Force-Update)
- **Pflichtenheft-Block:** Block 7 (Plattform-Außenseite), Pflichtenheft-Modul 36
- **Reuse-Level:** **medium**
## Kurzfazit
Die Boilerplate liefert ein **vollständiges, getestetes Capacitor-7-Wrapper-Grundgerüst** (`apps/mobile`) inkl. der serverseitigen Push-Pipeline — das ist genau das Fundament, das M36 braucht und das normalerweise teuer zu bauen ist. **Stark abgedeckt:** Capacitor-Setup (M167), Push-End-to-End (M168 Teil C.1, M22-Dependency), Session-Persistenz, Sentry, Native-Permission-Strings (Info.plist + AndroidManifest), Shell-Mobile-Registry (Tab-Bar-Datenmodell). **Komplett fehlend:** der gesamte **Offline-Modus** (M170 — IndexedDB-Cache, OfflineSyncQueue, Sync-on-Reconnect, Konflikt-Resolution), **Biometrie/Face-ID** (M168 C.2), **Kamera-Plugin + lokales Resize** (M168 C.3), **Apple-Compliance-Schicht** (M171 — Pricing-Hide-Flag, Demo-Account, Privacy-Manifest, Submission-Playbook), **Universal-Links** (AASA/assetlinks Files), **Force-Update / Web-Bundle-Version-Check** und die Prisma-Modelle `MobileAppSession` + Biometrie-Felder.
Wichtig: Die Boilerplate kennt `de.toolctrl.app` als App-ID und `ToolCtrl` als Namen — beim Übernehmen müssen App-ID, Name, Bundle-IDs, Permission-Strings und alle Klartexte auf HiveGrid umgestellt werden. Außerdem ist die Boilerplate-Auth **token-basiert ohne eigene DB-Session-Tabelle**, während M36 (über M27) eine zentrale `Session`-Tabelle voraussetzt — das ist eine Lücke, die in M27 zu schließen ist, nicht in M36.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `apps/mobile/` (gesamtes Workspace) | Capacitor-7-Wrapper-Skeleton: `package.json`, `capacitor.config.ts`, `ios/`, `android/`, `src/`, `tsconfig.json`, `.gitignore` | **als-vorlage** — Struktur 1:1, aber App-ID/Name/Strings auf HiveGrid umstellen |
| `apps/mobile/capacitor.config.ts` | Config mit `webDir: "../web/build/client"`, `readLiveReloadUrl()` (Live-Reload via `MOBILE_LIVE_RELOAD_URL`), Plugin-Config (Splash/StatusBar/Push/Preferences-Suite) | **anpassen** — `appId`/`appName` ändern, Plugins ergänzen (Camera, Network, Biometric) |
| `apps/mobile/src/index.ts` | `bootstrapMobileShell()` — idempotenter Boot: Sentry-Init + Auth-Rehydrate + opt-in Push-Registration; test-seam-fähig | **anpassen** — Boot-Schritte um Offline-Pre-Cache + Biometrie-Rehydrate erweitern |
| `apps/mobile/src/auth.ts` | Session-Persistenz-Adapter (`SessionStorage`-Interface, `MobileSession`-Typ, `rehydrateAuth`/`persistSession`/`clearSession`, `createMobileApiClient`); Capacitor-Preferences + localStorage-Fallback | **anpassen** — Achtung: Preferences ist NICHT verschlüsselt; für Biometrie-Secret muss OS-Keychain-Adapter ergänzt werden (Spec A.2-Constraint) |
| `apps/mobile/src/pushRegistration.ts` | `registerPushNotifications()` — kompletter APNs/FCM-Flow: Permission-Check → Token holen (mit Timeout) → `POST /v1/devices/register`; graceful skip-reasons | **anpassen** — Endpoint-Pfad auf HiveGrid-Konvention (`/api/v1/notifications/mobile-device`), Pre-Prompt-UI ergänzen (Spec C.1) |
| `apps/mobile/src/sentry.ts` | `initMobileSentry()` (Sentry-Capacitor, spiegelt Web-Sentry, platform-Tag) | **1:1** (nur DSN/Projekt-Config) |
| `apps/mobile/ios/App/App/Info.plist` | Permission-Strings: `NSCameraUsageDescription`, `NSFaceIDUsageDescription`, `NSPhotoLibraryUsageDescription`, `UIBackgroundModes: remote-notification`, ATS strict | **anpassen** — Klartexte auf HiveGrid-Wortlaut (Spec C.2/C.3 Pflicht-Strings), Universal-Links-Entitlement + `PrivacyInfo.xcprivacy` ergänzen (fehlen) |
| `apps/mobile/android/app/src/main/AndroidManifest.xml` | Permissions: `INTERNET`, `ACCESS_NETWORK_STATE`, `POST_NOTIFICATIONS` (API 33+), `USE_BIOMETRIC`, `CAMERA`; Custom-URL-Scheme `toolctrl://`-Intent-Filter; FileProvider | **anpassen** — Scheme auf `hivegrid://`, App-Links-Intent-Filter für `app.hivegrid.de` ergänzen, `READ_MEDIA_IMAGES` ergänzen |
| `apps/worker/src/jobs/push-notification.ts` | **Push-Fan-out-Worker** — APNs/FCM/WebPush-Versand pro DeviceToken, provider-pluggable (firebase-admin + node-apn injectable), Auto-Revoke bei `Unregistered`/`InvalidRegistration`/`BadDeviceToken`; ENV: `FCM_SERVICE_ACCOUNT_JSON`, `APNS_KEY/KEY_ID/TEAM_ID/BUNDLE_ID` | **anpassen** — gehört zu M22-Push-Pipeline; M36 ist Konsument. EU-Stack-Vorgabe (Novu, LP-13) prüfen — Boilerplate nutzt direkt FCM/APNs |
| `apps/api/src/routes/devices-routes.ts` | `POST /v1/devices/register` + `/v1/devices/revoke` mit Validierung, Upsert-by-token (idempotent), Klartext-Fehler-Envelope | **anpassen** — Pfad/Auth auf HiveGrid (Self-Scope statt service-credential), `tenant_id`-Multi-Registrierung pro Tenant (Spec A.1) sicherstellen |
| `packages/db/src/device-tokens.server.ts` | `registerDeviceToken`/`revokeDeviceToken`/`parseDevicePlatform`/`parsePushProvider` (188 Zeilen Domänen-Logik) | **anpassen** — RLS/Tenant-Scope nach HiveGrid-Konvention |
| `prisma/schema/notifications.prisma` → `DeviceToken` | Modell für APNs/FCM/WebPush-Token (`token @unique`, `pushProvider`, `appVersion`, `lastSeenAt`, `revokedAt`, User+Tenant-Relation, Indizes) | **als-vorlage** — deckt M36-Block-A.1 (`NotificationDevice`-Konsum) konzeptionell ab; in HiveGrid als M22-Entität spec'd |
| `apps/web/app/lib/pwa/registerServiceWorker.ts` + `apps/web/public/sw.js` | PWA-Service-Worker-Registrierung + minimaler SW (cache-first für `/assets/`, network-first für `/api`/`/app`/`/admin`, Offline-Fallback) | **als-vorlage** — solides Fundament für M170-Offline, aber **kein** Workbox, **keine** IndexedDB/Queue-Integration; muss massiv erweitert werden |
| Shell-Registry: `MobileRegistryItem`, `createMobileRegistry()` (`packages/foundation` / `packages/ui-shell`, siehe Inventur `foundation.md:77`, `ui-shell.md:59/90`) | Typ + Builder für Mobile-Tab-Bar-Einträge (priority-sortiert, `RegistryGate` für Permission+Flag) | **anpassen** — Datenmodell für M169-Tab-Bar vorhanden; eigentliche native Tab-Bar-Rendering + UI-Modus-Logik (Baustelle/Büro) fehlt |
| `apps/web/public/.well-known/` | Verzeichnis existiert (aktuell `security.txt`, `pgp-key.txt`) | **als-vorlage** — Ablageort für `apple-app-site-association` + `assetlinks.json` (Spec I.1) — Dateien selbst fehlen |
| `packages/api-client-ts` | TS-SDK (`createToolctrlClient`, refresh-aware Bearer via `getToken`-Callback) — vom Mobile-Wrapper konsumiert | **1:1** (HiveGrid-eigene SDK-Variante) |
| `docs/operations/capacitor.md` + `docs/improvement-plan/08-mobile-cli-storybook-sdk/capacitor.md` | Build/Signing/Submission-Runbook + Spec/Deviation-Log | **als-vorlage** — Operator-Runbook übernehmen, Submission-Strategie (Spec K) ergänzen |
| `package.json` `test:mobile` + `.gitea/workflows/ci.yml` (Schritt `test:mobile`) | CI-Gate für Mobile-Unit-Tests bereits verdrahtet | **1:1** (Pattern) — native Build-Pipeline (Tag `mobile-vX.Y.Z` → TestFlight/Play, Spec B.3) fehlt |
| `apps/mobile/src/*.test.ts` (auth/push/sentry/index/capacitor-config) | Vitest-Tests als Test-Pattern für Mobile-Layer | **als-vorlage** |
---
## Vorhandene Daten-Modelle
| Modell | Pfad | Deckt M36-Spec ab |
|---|---|---|
| `DeviceToken` | `prisma/schema/notifications.prisma:66` | Block A.1 (`NotificationDevice`-Konsum): `token @unique`, `pushProvider` (fcm/apns/webpush), `platform`, `appVersion`, `lastSeenAt`, `revokedAt`, User+Tenant-Relation. **Lücke:** pro-Tenant-Mehrfachregistrierung (mehrere Einträge gleicher Token + verschiedene `tenant_id`) ist nicht durch `token @unique` abgedeckt — Constraint muss in HiveGrid auf `(token, tenantId)` o.ä. angepasst werden (Spec A.1). |
| `NotificationDelivery`, `UserNotificationPreference` | `prisma/schema/notifications.prisma:14/41` | Notification-Querschnitt (M22) — Channels nur `IN_APP`/`EMAIL`, **kein** `PUSH`-Channel im Preference-Enum (siehe Lücken). |
| `User`, `Tenant`, `TenantUser` | `prisma/schema/accounts.prisma:1/129/286` | Auth-/Tenant-Basis, an die Device-Token + Mobile-Session hängen. |
**Nicht vorhanden (M36-Spec-Modelle fehlen komplett):**
- `MobileAppSession` (Spec A.2) — Device-Kind, biometric_enabled/kind, os_version, app_version, web_bundle_version, last_foreground_at, last_sync_at. **Voraussetzung-Lücke:** Es gibt in der Boilerplate gar keine zentrale Prisma-`Session`-Tabelle (Auth ist token/JWT-basiert, keine `model Session`), auf die `MobileAppSession.session_id` als FK verweisen könnte. Diese zentrale `Session`-Tabelle muss in M27 entstehen.
- `OfflineSyncQueue` (Spec A.3) — ist laut Spec ohnehin client-seitig in IndexedDB (kein Cloud-Modell), aber die client-seitige Implementierung fehlt vollständig.
- Biometrie-Felder/-Speicher (Spec A.2-Constraint, OS-Keychain) — Capacitor-Preferences-Adapter vorhanden, aber **unverschlüsselt**; Keychain-Adapter fehlt.
---
## Lücken / komplett neu zu bauen
### Offline-Modus (M170, Spec Block D + E) — **größte Lücke, Phase-1-Pflicht**
- **Kein IndexedDB / Dexie.js** im gesamten Repo (verifiziert: kein `dexie`, kein `indexeddb`-Store-Code).
- **Kein Read-Cache** (Spec A.4 — `cache.assigned_projects` etc.) und kein Pre-Cache-beim-Login-Flow (Spec D.2).
- **OfflineSyncQueue** (Spec A.3/D.4) — Queue-Entität, Stati (`pending/syncing/synced/failed/conflict`), Binary-Blob-Storage für Fotos, Aufbewahrungs-/Cleanup-Logik (Spec E.6).
- **Sync-on-Reconnect** (Spec E) — Trigger (Network-Wechsel/Foreground/Force-Sync/periodisch), Sync-Reihenfolge (Read-Refresh → Queue-Drain mit Idempotency-Key → Read-Refresh), Backoff-Retry, Konflikt-Erkennung (409), Last-Write-Wins + Konflikt-Resolution-UI (Spec E.3/E.4).
- **Network-Status-Detection** — `@capacitor/network`-Plugin **nicht installiert**.
- **Service-Worker** vorhanden, aber minimal (kein Workbox, keine Offline-Queue-Integration, keine Offline-Fallback-Page für nicht-gecachte Routen).
- **Idempotenz** — HiveGrid hat M007 (Idempotenz-Wrapper) zur Wiederverwendung; Boilerplate-Wrapper hat in `devices-routes` nur Upsert-by-token, kein generischer Idempotency-Key-Mechanismus.
### Native-Features (M168, Spec Block C)
- **Biometrie / Face-ID / Touch-ID (C.2)** — kein Biometric-Plugin installiert (kein `capacitor-biometric-auth` o.ä.); kein OS-Keychain-Secret-Adapter; kein Setup-/Login-Flow; kein 2FA-Interaktions-Wiring.
- **Kamera (C.3)** — `@capacitor/camera` **nicht installiert**; kein Foto-Capture/Galerie-Flow, kein lokales Resize (max 2400×2400, JPEG-85), keine Offline-Foto→Queue-Anbindung.
- **Push-Pre-Prompt-UI (C.1)** — Backend-Flow vorhanden, aber die User-Pre-Prompt-Erklärung VOR System-Dialog (Apple-Compliance) fehlt im Web-Code.
- **Push-Empfangs-Handling** — Token-Registrierung vorhanden, aber Vordergrund-In-App-Banner + Klick→Deeplink-Routing fehlen.
### Routing / Deeplinks (M168/M169, Spec Block I)
- **Universal Links (iOS) / App Links (Android)** — `apple-app-site-association` + `assetlinks.json` Dateien fehlen; nur Custom-Scheme (`toolctrl://`) im AndroidManifest vorhanden.
- **Deeplink-Routing-Logik** — Auth-Status-Check, Deeplink-Park-bei-Login, Tenant-Switch-Prompt, Permission-Mismatch-Fallback (Spec I.3) fehlen.
- **Native Tab-Bar (M169, Spec I.5)** — Datenmodell (`MobileRegistryItem`) vorhanden, aber kein Tab-Bar-Rendering, keine UI-Modus-Logik (Baustelle/Büro, Default-Tabs pro Modus), kein persistierter UI-Modus pro Gerät.
- **Mobile-First-Routes** (M169) — neue Routen `/{tenant}/settings/security/biometric/`, `/settings/mobile/`, `/settings/mobile/storage/`, `/settings/mobile/queue/` fehlen komplett.
### Apple/Google-Compliance (M171, Spec Block F + K)
- **Pricing-UI-Hide-Flag** (`IS_MOBILE_APP=true` / `PRICING_UI=false`) — nicht vorhanden; keine Verdrahtung in M28-Subscription-UI.
- **Privacy-Manifest** (`PrivacyInfo.xcprivacy`) — fehlt; kein Master-Listen-generierter Build-Step; Android Data-Safety-Form ebenfalls.
- **Reviewer-Demo-Account** (`apple-review`-Tenant) — fehlt; gehört zu M37-Plattform-Admin-Pflege.
- **Force-Update / Web-Bundle-Version-Check** (Spec B.3/J) — kein `/api/v1/mobile/version-check`-Endpoint, kein `min_supported_app_version`-Server-State, kein Force-Update-Modal, kein Web-Bundle-Hot-Update mit Atomic-Swap. (Vorhandener `version-check.ts` ist Plugin-Runtime-intern, nicht mobile.)
- **Submission-Playbook + Reject-Recovery** (Spec K) — Doku fehlt (Operator-Runbook `capacitor.md` ist Build-fokussiert, nicht Submission-Strategie).
- **SIWA-Backup-Implementation** (Spec F.2) — nicht vorbereitet.
### Account-Löschung in App (M171, Spec Block G — Verweis M35)
- Lokal-Cleanup bei App-Löschung (IndexedDB leeren, Keychain entfernen, Push-Token revoke) — Push-Revoke-Endpoint vorhanden, IndexedDB+Keychain-Cleanup hängt an noch zu bauenden Offline/Biometrie-Schichten.
### Native-Build-Pipeline (M167, Spec B.3)
- CI-Tag `mobile-vX.Y.Z` → Multi-Pipeline (Web-Build → iOS-TestFlight + Android-Play) fehlt; nur `test:mobile`-Unit-Gate vorhanden. Echte Builds/Signing sind laut Boilerplate-Doku bewusst Operator-Tasks.
### Querschnitt-Wiring (HiveGrid-spezifisch)
- Klartext-Strings (Spec L) als i18n-Keys (M002/M003), Audit-Einträge für Mobile-Aktionen (Biometrie-Setup, Account-Löschung, Konflikt-Resolution — Spec O AV#6), Feature-Flag-Schaltbarkeit (M006), Permission-Snapshot-Cache offline (Spec H.2/M.4) — alles neu zu verdrahten.
---
## Empfohlenes Vorgehen
**Phase 1 — Fundament aus Boilerplate übernehmen (schnell, hoher Hebel):**
1. **`apps/mobile`-Workspace klonen** als HiveGrid-Mobile-App-Vorlage; App-ID (`de.hivegrid.app`), Name, Bundle-IDs, Custom-Scheme (`hivegrid://`) und alle Permission-Strings/Klartexte umstellen. (M167)
2. **Capacitor-Config + Live-Reload + CI-`test:mobile`-Gate** 1:1 übernehmen. (M167)
3. **Push-Pipeline übernehmen:** `devices-routes.ts` + `device-tokens.server.ts` + `DeviceToken`-Modell + Worker `push-notification.ts` — auf HiveGrid-Auth (Self-Scope), Tenant-pro-Token-Constraint und EU-Stack (Novu/LP-13 prüfen) anpassen. Verdrahtung gehört primär zu M22, M36 konsumiert. (M168 C.1)
4. **Sentry-Mobile + Auth-Rehydrate** übernehmen (`sentry.ts`, `auth.ts`-Adapter-Pattern). (M167)
**Phase 2 — Native-Features ergänzen (neu, plugin-getrieben):**
5. **`@capacitor/camera` installieren** + Foto-Flow + lokales Resize; Anbindung an M52-Foto-Upload. (M168 C.3)
6. **Biometric-Plugin + OS-Keychain-Secret-Adapter** (ersetzt/ergänzt den unverschlüsselten Preferences-Adapter); Setup-/Login-Flow mit 2FA-Pflicht (M22). (M168 C.2)
7. **Universal-Links/App-Links:** AASA + `assetlinks.json` in `apps/web/public/.well-known/`, iOS-Entitlement + Android-Intent-Filter, Deeplink-Routing-Logik. (M168/M169 Block I)
**Phase 3 — Offline-Modus neu bauen (größter Aufwand, Phase-1-Pflicht):**
8. **`@capacitor/network` + IndexedDB (Dexie.js)** einführen; Read-Cache-Stores (Spec A.4) + Pre-Cache-beim-Login.
9. **OfflineSyncQueue** (client-seitig) + Sync-Engine mit Idempotency-Key (M007 wiederverwenden) + Backoff + Konflikt-Erkennung + LWW-Resolution-UI.
10. **Service-Worker erweitern** (vorhandene `sw.js`/`registerServiceWorker.ts` als Basis → Workbox-Strategie + Offline-Fallback). (M170 gesamt)
**Phase 4 — Compliance + Mobile-UI:**
11. **Native Tab-Bar** auf vorhandenem `MobileRegistryItem`/`createMobileRegistry` aufsetzen; UI-Modus-Logik (Baustelle/Büro) + Mobile-First-Routes + neue Settings-Routen. (M169)
12. **Pricing-Hide-Build-Flag** (`IS_MOBILE_APP`), Verdrahtung in M28-UI; Privacy-Manifest-Build-Step (Master-Liste M35/M37); Force-Update + `version-check`-Endpoint; Demo-Account (M37); Submission-Playbook-Doku. (M171)
**Reihenfolge-Logik:** Erst das geschenkte Boilerplate-Fundament (Schritte 1-4) übernehmen — das eliminiert das Capacitor-/Push-Risiko sofort. Danach die echten Neubauten (Offline > Biometrie/Kamera > Compliance), bei denen die Boilerplate nur Vorlagen/Querschnitt liefert.
@@ -0,0 +1,149 @@
# Boilerplate-Mapping: M37 — Plattform-Admin
**Modul-ID:** M37 (Detail-Spec) / Milestones **M034–M041** (Block 1, Pflichtenheft 37.x)
**Name:** Plattform-Admin (`admin.hivegrid.de`)
**Reuse-Level:** **medium**
> Verifiziert direkt gegen `/Users/raffaelsheikh/Documents/GitHub/boilerplate` (Schema, Routes, Services). Inventur-Dateien als Ausgangspunkt, alle Aussagen mit eigenem Grep/Read gegengeprueft.
---
## Kernbefund (zuerst lesen)
Die Boilerplate enthaelt eine **substanzielle, fertige Admin-Welt** (eigene Admin-Subdomain-Routing-Schicht, Admin-Sidebar, Admin-Login inkl. SSO + MFA-Step-up, RBAC, tamper-evidenter Audit-Trail, Impersonation-Infrastruktur, Lead-Inbox, Support-Tickets, Subscription-/Metrics-/Dashboard-Routen). Damit sind **Themenblöcke E, F, G(lese), H, I, L, M, O** zu grossen Teilen aus Vorhandenem ableitbar.
**ABER: Die zentrale Architektur-Prämisse von M37 wird vom Boilerplate-Modell verletzt.** Spec Block A/B verlangt eine **eigene, parallele User-Klasse `AdminUser`** mit eigenem `password_hash`, `totp_secret`, eigener `AdminSession`, eigener `AdminRole/AdminPermission`-RBAC und eigener `AdminAuditLog`-Tabelle. Im Boilerplate ist `AdminUser` jedoch **kein eigenständiges Identity-Modell**, sondern eine 1:1-Erweiterung von `User`:
```prisma
// prisma/schema/accounts.prisma:112
model AdminUser {
userId String @id @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ssoProvider String? ssoSubjectId String? ssoGroups String[] lastSsoLoginAt DateTime?
}
```
D.h. Admin-Auth, Passwort, 2FA und Rollen laufen heute **über dasselbe `User`-/`UserRole`-/`Permission`-Fundament wie Tenant-User** (Admin-Status = `User` hat `AdminUser`-Zeile). Das ist exakt der „Generalschlüssel zwischen den Welten", den Spec Block A.2/A.3 und Milestone M034 (37.1) verbieten.
**Konsequenz für die Planung:** Vor jeder Übernahme muss eine **Architektur-Entscheidung** fallen:
- **(a) Spec-treu (empfohlen lt. Spec):** neue, vollständig getrennte `AdminUser`-Identität + `AdminSession` + `AdminRole/AdminPermission/AdminAuditLog` neu bauen. Boilerplate-Admin-Auth dient dann nur als **Vorlage** (Login-Form, MFA-Flow, Rate-Limit, Session-Cookie-Policy), nicht 1:1.
- **(b) Boilerplate-treu:** das bestehende `User`+`AdminUser`-Extension-Modell behalten und Spec Block A/B abschwächen (Change-Request, da es Pflichtenheft 37.1 widerspricht).
Dieser Punkt ist der Haupt-Grund, warum Reuse **medium** und nicht **high** ist.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `apps/web/app/routes/admin.tsx` + `apps/web/app/routes/admin/` (Tree) | Komplette Admin-Route-Welt unter eigener Surface (accounts, users, subscriptions, roles-and-permissions, audit-trails, metrics, feature-flags, marketing-leads, help-desk, settings, events) inkl. host-aware Subdomain-Routing-Tests (`admin.host-aware.test.ts`) | **als-vorlage** (Routen-Skelett + Subdomain-Trennung direkt; Inhalte HiveGrid-fachlich füllen) |
| `apps/web/app/application/sidebar/AdminSidebar.tsx` + `SidebarItem.ts` + `SidebarGroupDto.ts` | Statische, permission-getaggte Admin-Sidebar in Segmenten (Manage/Build/Market/Settings), `data-ui-id`-IDs, `adminPath()`-host-aware | **anpassen** (Sidebar-Hauptpunkte aus Block E übernehmen; HiveGrid nutzt eigene `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` — INTEGRATION-CONTRACTS §2) |
| `packages/url/src/canonical.ts` (`adminPath`/`adminUrl`) + `packages/config/src/routing.ts` + `packages/config/src/session-cookie.ts` | Host-aware Admin-URL-Builder, Domain-Routing, **domain-/pfad-gelockte Session-Cookie-Policy** | **1:1** (Cookie-Trennung `admin.` vs `app.` = Block A.3, M034) |
| `apps/web/app/routes/auth.admin.login.tsx`, `auth.admin.callback.tsx`, `auth.admin.logout.tsx` | Eigener Admin-Login-/Logout-Flow (heute via Authentik-OIDC) | **als-vorlage** (Flow-Gerüst; HiveGrid braucht Email+Passwort+TOTP statt OIDC — Block C; Magic-Link für Admin verbieten) |
| `apps/web/app/services/auth/admin-mfa-gate.server.ts` (`requireAdminMfaForAction`, `SENSITIVE_ADMIN_ACTIONS`) + `apps/web/app/utils/auth/mfa/*` (`enforcement.server`, `step-up.server`) | MFA-Step-up-Gate für sensible Admin-Aktionen — Taxonomie enthält bereits `impersonation.start`, `tenant.delete`, `permission.grant`, `audit.export`, `data.export` | **anpassen** (deckt Block C 2FA-Pflicht + Block Q Reason-Gates fachlich; auf neue `AdminUser`-Identität umhängen) |
| `apps/api/src/auth/login-rate-limit.ts` (`createFixedWindowRateLimiter`) + `packages/shared-store` (`createFixedWindow/SlidingWindowRateLimiter`) | Login-Rate-Limit (IP + Account-Fingerprint, Redis) | **anpassen** (Spec C: 5/15min Lock, Captcha ab 3., 10/24h → `locked`) |
| `apps/web/app/utils/session.server.ts` (Impersonation-State) + `apps/web/app/utils/session.impersonation.test.ts` | Impersonation-Session-State (`impersonatingFromUserId`/`StartedAt`, Validierung) | **anpassen** (Block G Impersonate; Spec: max 30 Min Cap + Reason-Pflicht ergänzen) |
| `apps/web/app/components/impersonation-banner.tsx` + `apps/web/app/root.tsx` (Wiring) + `root.impersonation-banner.test.tsx` | Rotes Klartext-Impersonation-Banner in der App-Shell (i18n, „Acting as … from …", Exit) | **anpassen** (Block G/M037: Banner-Text „Plattform-Support … Ticket #123" + Ticket-/Reason-Bezug) |
| `prisma/schema/accounts.prisma:266` `ImpersonationAudit` + `packages/db/src/impersonation-audit.server.ts` | Impersonation-Audit-Tabelle (`actorUserId`/`targetUserId`/`tenantId`/`action`/`reason`/`requestPath`/`metadata`, SetNull) | **1:1** (deckt M037 Audit-Spur inkl. Reason) |
| `prisma/schema/core.prisma:55` `AuditEvent` + `packages/db/src/audit-events.server.ts` + `packages/db/src/audit-hash.ts` + `packages/foundation/src/audit.ts` (`auditEvent`, `withAuditMiddleware`) | Tamper-evidenter, cross-tenant-fähiger Audit-Trail (Hash-Kette, `tenantId?`, `actorType`, `action`, `targetType/Id`, `metadata`, reiche Indizes) | **1:1 als AdminAuditLog-Ersatz** — `tenantId=null` = plattform-/operator-weit deckt Block B `AdminAuditLog` fachlich vollständig ab; HiveGrid muss **kein** neues Audit-Schema bauen, nur `action_type`/`target_kind`-Vokabular definieren |
| `apps/web/app/routes/admin/audit-trails.tsx` + `audit-export.tsx`/`.helpers.server.ts` + `packages/db/src/logs.server.ts` (`getAdminAuditLogList`, `serializeAdminAuditLogsCsv`) | Cross-Tenant-Audit-Liste mit Filter (Akteur/Action/Zeitraum) + CSV-Export, permission-gated (`admin.auditTrails.view`) | **anpassen** (Block L fast vollständig; Volltext-Suche in `summary_text` + JSON-Export ergänzen) |
| `packages/exports/src/audit-pdf.ts` (`serializeAuditEventsToPdf`) | Compliance-Audit-PDF (SOC2/HIPAA/GDPR-Evidence) | **1:1** (Block L Compliance-Export) |
| `prisma/schema/permissions.prisma` (`Role`/`Permission`/`RolePermission`/`UserRole`/`Group`) + `apps/web/app/routes/admin/accounts/roles-and-permissions.tsx` + `components/core/roles/*` + `packages/db/src/permissions-seed.server.ts` | Editierbares RBAC mit UI-Rollen-Editor + Permission-Seed | **als-vorlage** (Block D Bundle-Modell; bei Architektur (a) als `AdminRole/AdminPermission` parallel neu, Struktur 1:1 übertragbar) |
| `apps/web/app/utils/services/adminDashboardService.ts` + `packages/db/src/kpi.server.ts` (`getAdminTenantKpiCounts`, `getAdminActiveUserKpiCounts`) + `apps/web/app/utils/services/.server/subscriptionService.ts` (`getMrr`) | Admin-Dashboard-KPIs: Tenant-Count + Trend, MRR, aktive User | **anpassen** (Block F: MRR/ARPU/Churn/Expansion + Health-Kacheln + Sales-Pipeline ergänzen) |
| `apps/web/app/routes/admin/accounts/index.tsx` + `$id.tsx` (Tenant) + `users.tsx`/`users.test.tsx` + `blacklist.tsx` + `__ip-addresses/` | Tenant-Liste/-Detail + User-Verwaltung + IP-Logs + Blacklist | **anpassen** (Block G/H: Status-Wechsel mit Reason, Notes, Trial-Verlängerung, Bulk-Actions ergänzen) |
| `apps/web/app/routes/admin/accounts/subscriptions.tsx` + `subscriptions/$id.tsx` + `subscriptions-revenue.tsx` + `prisma/schema/subscriptions.prisma` (14 Modelle) + `apps/web/app/utils/stripe.server.ts` + `packages/entitlements` | Vollständiger Stripe-Billing-Stack + Admin-Subscription-Routen + MRR | **anpassen** (Block I: manueller Override, Force-Sync, Refund, Sonderpreis-Vermerk ergänzen) |
| `prisma/schema/featureFlags.prisma` (3 Modelle, append-only Audit) + `packages/feature-flags` (Engine v2, Targeting, Cache) + `apps/web/app/routes/admin/feature-flags.tsx` | Feature-Flag-Engine + Admin-Verwaltung | **1:1** (Block J = Verweis auf Modul 32; Admin-Routes liegen bereits unter `admin/feature-flags`) |
| `prisma/schema/marketingLeads.prisma` (`Lead`/`LeadEvent`/`TenantAcquisition`/`MarketingIntegration`, **operator-scoped, kein tenantId**) + `apps/web/app/routes/admin/marketing-leads/index.tsx`+`$id.tsx` + `apps/web/app/utils/leads/*` + `packages/marketing-outbound` | Operator-scoped Lead-Inbox + Detail + Status-Pipeline + First-Touch-Attribution + Convert-to-Tenant (`convertedToTenantId`) + Activity-Timeline (`LeadEvent`) + Outbound-Adapter | **anpassen** (Block M: deckt Lead-Inbox/Detail/Promote-to-Tenant/Activity-Stream sehr nah; fehlt: `AdminDeal`-Pipeline/Kanban, `assigned_to`, `industry/company_size`-Enums) |
| `prisma/schema/helpDesk.prisma` (`Feedback`, `SupportTicket` mit `source`/`status`/`priority`/`inboundMessageId`-Dedup) + `apps/web/app/modules/helpDesk/*` + `apps/web/app/routes/admin/help-desk.tsx` + `components/FeedbackTable.tsx` | Support-Ticket-Aufnahme inkl. Inbound-Email-Dedup + PII-Redaction + Admin-Inbox | **anpassen** (Block O: Konversation/Messages, Tags[], Assignee, Reply-Templates, Internal-Notes fehlen → erweitern) |
| `apps/web/app/routes/admin/metrics/` (`summary.tsx`, `logs.tsx`, `settings.tsx`) + `packages/metrics` (prom-client, AI-Cost-Counter) + `apps/worker/src/admin-server.ts`/`health-server.ts` + `infra/grafana` (queue-health, business-kpi Dashboards) | Metrics-Admin-Routen + Worker-Status/Health-Server + Grafana-Dashboards | **anpassen** (Block K Health-Dashboard: Service-Status + Error-Rate als Vorlage; BullMQ-Queue-Steuerung-UI + KWP-Sync neu) |
| `prisma/schema/queue.prisma` (`QueueFailedJob` BullMQ-DLQ, `ExportJob`) + `apps/worker/src/jobs/queue-failed-jobs*` + `packages/db/src/queue-failed-jobs.server.ts` | Failed-Job-Tracking (DLQ) | **anpassen** (Block K Worker-Queues: Liste/Retry/Pause-UI darauf aufbauen) |
| `packages/foundation/src/permissions.ts` (`withTenantScope`/`withPermission`/`requirePermission`) + `apps/web/app/utils/helpers/.server/PermissionsService.ts` (`verifyUserHasPermission`) | Permission-Gating + 404-statt-403 (Existenz-Versteckung) | **1:1** (Querschnitt; Block D-Durchsetzung + AGENTS DoD C.17) |
| `packages/foundation/src/notifications.ts` (`notify`/`bundleNotifications`) + `prisma/schema/notifications.prisma` + `prisma/schema/email.prisma` (`Campaign`/`OutboundEmail`) | Notifications + Outbound-Email/Kampagnen (für Bulk-Email an Tenant-Admins, AVV-Re-Akzeptanz-Mails) | **1:1 / anpassen** (Block G Bulk-Email, Block P AVV-Notification) |
| `packages/exports` (CSV/JSON/XLSX) + `apps/worker/src/jobs/{export,dsgvo-export}.ts` + `prisma/schema/queue.prisma` `ExportJob` | DSGVO-/Daten-Export-Pipeline (Tenant-Daten-Export-Anstoss, Block G) | **1:1** |
| `packages/foundation/src/errors.ts` (`CatalogError`) + `packages/view-models` (`VMState`, `ActionCallbacks`, `RegistryGate`) + `packages/i18n` | Klartext-Diagnostik, View-Model-Vertrag, i18n | **1:1** (Querschnitt; Block Q Klartext-Hinweise, AGENTS DoD A/E) |
---
## Vorhandene Daten-Modelle (konkret, mit Pfad)
| Modell | Pfad | M37-Bezug |
|---|---|---|
| `AdminUser` | `prisma/schema/accounts.prisma:112` | **nur Teilabdeckung** — 1:1-Extension von `User` (`userId @id` + SSO-Felder). Kein eigenes `passwordHash`/`totpSecret`/`status`/`disabledReason`. Block B `AdminUser` braucht NEUES Modell oder Spec-Abschwächung. |
| `User` | `prisma/schema/accounts.prisma:1` | Auth-Fundament: `passwordHash`, `totpSecretEncrypted`, `totpEnabledAt`, `mfaRecoveryCodeHashes`, DSGVO-Flags. Vorlage für AdminUser-Auth-Felder. |
| `ImpersonationAudit` | `prisma/schema/accounts.prisma:266` | **deckt M037-Audit** (actor/target/tenant/action/reason/requestPath/metadata). |
| `AuditEvent` | `prisma/schema/core.prisma:55` | **deckt `AdminAuditLog` fachlich** (Hash-Kette, `tenantId?`, actor/target/action/metadata, Indizes). Kein neues Schema nötig. |
| `Role`/`Permission`/`RolePermission`/`UserRole`/`Group`/`GroupUser` | `prisma/schema/permissions.prisma:1-78` | RBAC-Fundament. Vorlage für `AdminRole/AdminPermission` (Block D). |
| `Tenant`, `TenantUser`, `TenantUserInvitation` | `prisma/schema/accounts.prisma:129/286/298` | Tenant-Verwaltung Block G/H (Status via `active`, Membership, Invitations). |
| `SubscriptionProduct…TenantSubscription`, `TenantFeatureOverride`, `Credit` (14) | `prisma/schema/subscriptions.prisma` | Subscription-Management Block I (Stripe-IDs, Plan, Override). |
| `FeatureFlag`/`FeatureFlagFilter`/`FeatureFlagAudit` | `prisma/schema/featureFlags.prisma` | Block J (Modul 32). |
| `Lead`/`LeadEvent`/`TenantAcquisition`/`MarketingIntegration` | `prisma/schema/marketingLeads.prisma:14-143` | Block M Lead-Inbox/Activity/Convert (operator-scoped, kein tenantId). **Kein `AdminDeal`.** |
| `Feedback`/`SupportTicket` | `prisma/schema/helpDesk.prisma:1/12` | Block O (Aufnahme + Inbound-Email-Dedup). **Keine Ticket-Messages/Konversation.** |
| `QueueFailedJob`/`ExportJob` | `prisma/schema/queue.prisma` | Block K Worker-DLQ + Block G Export. |
| `AppConfiguration`, `Log`, `MetricLog`, `Event` | `prisma/schema/core.prisma` | Health/Config-Querschnitt Block K. |
| `app.prisma` (leerer Platzhalter) | `prisma/schema/app.prisma` | **Vorgesehener Ort** für neue HiveGrid-Admin-Modelle (`AdminRole`, `AdminDeal`, `AdminTenantNote`, `Subprocessor`, `AvvVersion` …). |
---
## Lücken / komplett neu zu bauen
1. **Eigenständige `AdminUser`-Identität (Block A/B, M034)** — separate Tabelle mit `email`/`passwordHash`/`totpSecret`/`totpEnabled`/`status`/`disabledReason`/`createdByAdminUserId` **getrennt von `User`**. Heute existiert nur die `User`-Extension. **Architektur-Entscheidung blockierend** (siehe Kernbefund).
2. **`AdminSession` (Block C, M034)** — server-side Postgres-Sessions mit 8h-Cap, 1h-Inaktivität, **IP-Bindung (Re-Auth bei IP-Wechsel)**, Revoke-Liste. Boilerplate nutzt Cookie-Sessions (`session.server.ts`), kein DB-Session-Modell mit IP-Bindung → neu (Vorlage: `packages/shared-store` `createSessionStore`).
3. **`AdminRole`/`AdminPermission`/`AdminRolePermission`/`AdminUserRole` (Block D)** — eigene parallele RBAC mit Bundle-Stufen (`none/read/write/admin`, `viewer/operator/owner`). Struktur aus `permissions.prisma` ableitbar, aber neue Tabellen + Default-Rollen-Seed (`super_admin/support/sales/engineering/viewer`) + Self-Permission-Schutz (letzter super_admin).
4. **`AdminDeal` + Deal-Pipeline/Kanban (Block M)** — kein Deal-Modell, kein Kanban-View, kein Drag&Drop. `expected_arr_eur`, `seats`, `stage`. Komplett neu (Lead-Teil ist abgedeckt).
5. **`AdminTenantNote` (Block B/G)** — pro-Tenant Markdown-Notizen mit Kategorie/Pin/Archiv. Kein Modell vorhanden. Neu.
6. **Subprozessoren- + AVV-Verwaltung (Block P, M041)** — `Subprocessor`, `AvvVersion` (+ Re-Akzeptanz-Tracking) existieren **nicht** im Schema. Komplett neu (Querschnitt Modul 35).
7. **KWP-Sync-Health (Block K, M041)** — kein KWP/Heartbeat-Modell/Code in der Boilerplate (HiveGrid-spezifische Integration, M089-M095). Komplett neu.
8. **Support-Ticket-Konversation (Block O)** — `AdminSupportTicketMessage` (Reply-Thread, Internal-Notes, Attachments, Reaktions-Templates) fehlt; nur Single-Message-`SupportTicket` vorhanden. Erweitern.
9. **Worker-Queue-Steuerungs-UI (Block K)** — Retry/Pause/Resume/Remove pro BullMQ-Queue: Worker-Runtime + DLQ-Modell existieren, aber **keine Admin-UI/Steuerungs-API**. Neu (auf `apps/worker/admin-server.ts` aufsetzen).
10. **Manuelle Tenant-Anlage mit Sonder-Konditionen (Block N, M036)** — Slug-Live-Validation, Initial-Admin-Anlage, Trial-Dauer, Demo-Daten-Provisionierung, Reason-Pflicht. Tenant-Anlage existiert im Onboarding-/Registrierungs-Pfad, aber kein Admin-„Tenant-anlegen-im-Auftrag"-Flow. Neu (Teile aus Registration/Onboarding wiederverwenden).
11. **Dashboard-Erweiterung (Block F)** — Churn/Expansion-MRR, ARPU, System-Health-Kacheln, Sales-Pipeline-Volumen, Support-Kennzahlen, Redis-Cache (TTL 5 Min) + Hourly-Aggregat-Job. Basis (Tenant/MRR/Active-Users) vorhanden, Rest neu.
12. **`packages/ui-hivegrid` Admin-Shell + Subframe-Adapter** — der HiveGrid-eigene UI-Layer (`ui-hivegrid`, `ui-subframe`-Adapter) existiert in der Boilerplate **nicht** (nur Stubs `ui-app`/`ui-subframe`). Admin-Sidebar muss in `packages/ui-hivegrid/src/app-shell/sidebar.config.ts` registriert werden (INTEGRATION-CONTRACTS §2). Querschnitt für alle M034-M041.
13. **Impersonate 30-Min-Hard-Cap + Reason-Pflicht-Gate** — State + Banner vorhanden, aber Spec verlangt harte 30-Min-Session-Begrenzung und Reason-Pflicht-Block (Block Q). Erweitern.
---
## Empfohlenes Vorgehen (Reihenfolge)
**Phase 0 — Architektur-Entscheidung (blockierend, vor M034):**
- Klären: getrennte `AdminUser`-Identität (Spec-treu, empfohlen) vs. Boilerplate-`User`-Extension (Change-Request gegen Pflichtenheft 37.1). Bis dahin keine Auth-Implementierung beginnen.
**M034 (Auth) — zuerst aus BP übernehmen, Rest neu:**
1. Übernehmen (als-vorlage): `auth.admin.login/logout`-Flow, `admin-mfa-gate.server.ts` (Step-up-Gate inkl. fertiger Sensitive-Action-Taxonomie), `login-rate-limit.ts`, `session-cookie.ts` (Domain-Lock), `adminPath`-Routing.
2. Neu: `AdminUser`/`AdminSession`-Modelle (in `app.prisma`), Email+Passwort+TOTP-Pflicht (Magic-Link verbieten), IP-Bindung, 8h/1h-Timeouts, 5/15 + 10/24h-Lockout, Bootstrap-Seed-Script.
**M039 (Konten & Rollen) — RBAC:**
3. `AdminRole/AdminPermission/AdminUserRole` als parallele RBAC (Struktur aus `permissions.prisma`, UI-Editor aus `roles-and-permissions.tsx`), Bundle-Stufen + Default-Rollen-Seed + Self-Permission-Schutz.
**M035 (Dashboard) + M036 (Mandanten):**
4. Dashboard: `adminDashboardService` + `kpi.server` als Basis, Health-/Pipeline-/Support-Kacheln + Redis-Cache + Hourly-Job ergänzen.
5. Mandanten: `admin/accounts` Liste/Detail erweitern (Status-Wechsel mit Reason → `AuditEvent`/`ImpersonationAudit`, `AdminTenantNote` neu, Trial-Verlängerung, Bulk-Email via `notify`/Campaign, Daten-Export via `packages/exports`). Manuelle Tenant-Anlage (Block N) neu.
**M037 (Stellvertreter-Anmeldung):**
6. Vorhandene Impersonation (Session-State + Banner + `ImpersonationAudit`) übernehmen; 30-Min-Hard-Cap + Reason-Pflicht-Gate (`requireAdminMfaForAction("impersonation.start")` ist schon da) + Banner-Text mit Ticket-Bezug ergänzen.
**M038 (Mitarbeiter-Verwaltung):**
7. `admin/accounts/users` als Vorlage; 2FA-Reset, Session-Revoke, Memberships, Deaktivieren auf neue Permission-Bundles umstellen; AdminUser-Verwaltung (Anlegen via Setup-Mail) neu.
**M040 (Lead-Pipeline):**
8. `Lead`/`LeadEvent`/`marketing-leads`-Routen + Convert-to-Tenant fast 1:1 übernehmen; `AdminDeal`-Modell + Kanban-Pipeline-UI + `assigned_to`/`industry`/`company_size` neu.
**M041 (Health/Audit/Support/Legal):**
9. Audit: `audit-trails.tsx` + `logs.server` + `audit-pdf` übernehmen (Volltext-Suche + JSON-Export ergänzen) — `AuditEvent` deckt `AdminAuditLog`.
10. Subscriptions-Admin: `subscriptions/$id` + Stripe erweitern (Override/Force-Sync/Refund/Diff-View).
11. Support: `helpDesk` + `SupportTicket` übernehmen; `AdminSupportTicketMessage`-Thread + Templates + Internal-Notes neu.
12. Health: `metrics`-Routen + Worker-Admin/Health-Server + Grafana als Basis; BullMQ-Queue-Steuerungs-UI (auf `QueueFailedJob`) + KWP-Sync (HiveGrid-spezifisch) neu.
13. Legal: `Subprocessor`/`AvvVersion`-Modelle + Re-Akzeptanz-Trigger (via `notify`/Campaign) komplett neu (Modul 35).
**Querschnitt (durchgängig):** `withTenantScope`/`withPermission`, `auditEvent`, `notify`, `CatalogError`, `VMState`, i18n-Keys + Sidebar-Registrierung in `packages/ui-hivegrid` (das zuerst angelegt werden muss) — gemäss AGENTS.md DoD + INTEGRATION-CONTRACTS.
---
## Realitäts-Vorbehalte (keine Erfindungen)
- `packages/ui-hivegrid` und ein gebauter `ui-subframe`-Adapter **existieren nicht** in der Boilerplate (nur Stubs). Die in AGENTS.md/INTEGRATION-CONTRACTS referenzierten Pfade sind HiveGrid-Soll, nicht Boilerplate-Ist.
- Boilerplate-`AdminUser` ist **kein** parallel-Identity-Modell — das ist der zentrale Spec-Konflikt.
- **Kein** `AdminRole`/`AdminPermission`/`AdminSession`/`AdminAuditLog`/`AdminLead`/`AdminDeal`/`AdminLeadActivity`/`AdminSupportTicket`/`AdminTenantNote`/`Subprocessor`/`AvvVersion`-Modell existiert (verifiziert via Grep über `prisma/schema/`).
- **Kein** KWP-/Heartbeat-Code existiert (HiveGrid-spezifisch).
- `SupportTicket` ist single-message (kein Konversations-Thread).
- Admin-Login läuft heute via Authentik-OIDC, **nicht** Email+Passwort+TOTP wie Spec C verlangt.
@@ -0,0 +1,146 @@
# Boilerplate-Mapping: M38 — Help-Center (öffentlich)
| | |
|---|---|
| **Modul-ID** | M38 |
| **Name** | Help-Center (öffentlich) |
| **Milestones** | M172 (Markdown-Artikel-Mgmt / Git-PRs), M173 (Routes + Suche + SEO + Kontextuelle Hilfe) |
| **Block** | Block 7 |
| **LPs** | LP-9, LP-10, LP-12, LP-13 |
| **Reuse-Level** | **medium** |
## TL;DR — Verdict
Die Boilerplate enthält ein **vollständiges, produktionsreifes Help-Center / Knowledge-Base-System** (SaaSRock-Stil) unter
`apps/web/app/modules/knowledgeBase/` mit öffentlichen Routen `/help/...`, Kategorien, Artikeln, Suche (Meilisearch + SQL-Fallback),
anonymem Daumen-hoch/runter-Feedback, View-Tracking, SEO-Metatags (OG/Twitter/Canonical), Markdown-Rendering mit Sanitizer,
verwandten Artikeln, Featured-Artikeln **und** kompletter Admin-Editor-UI — sogar mit RAG-Service für KI-Suche.
**Der entscheidende Bruch:** Die Boilerplate ist **DB-getrieben** (Prisma `KnowledgeBase*`-Modelle + Editor-UI in `admin/knowledge-base`).
Die HiveGrid-M38-**Phase-1**-Spec verlangt aber explizit das **Gegenteil**: **file-basierte** Markdown-Artikel im Repo, Pflege via
**Git-PRs**, **kein** Editor-UI, mit **Build-Time-Index + Sitemap + Frontmatter-/Verlinkungs-Validierung als Build-Step**
(Spec Block A.1, A.4; M172-Erfolgskriterium). Dieser file-/build-basierte Daten- und Validierungslayer **existiert in der Boilerplate nicht**.
Konsequenz: Die **UI-, Routing-, Such-, SEO- und Feedback-Schicht** ist als Vorlage exzellent (high reuse), aber der **Daten-Layer
muss komplett ersetzt** werden (DB → File/Frontmatter + Build-Index). Was die Boilerplate liefert, entspricht eher der HiveGrid-**Phase-2**
(DB-getriebene Pflege durch Plattform-Admin, Spec Block I). Daher gesamt **medium**.
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `apps/web/app/routes/help/$slug.($lang).tsx` | Public Help-Home-Route (Kategorien + Suche), Sprach-Segment optional, Meta-Tags aus Loader | als-vorlage (URL-Schema von `/help/$slug/($lang)` → HiveGrid `/help/$lang/$category/$slug` umstellen) |
| `apps/web/app/routes/help/$slug.($lang).categories.$category.tsx` | Public Kategorie-Seite | als-vorlage |
| `apps/web/app/routes/help/$slug.($lang).articles.$article.tsx` | Public Artikel-Detail + `action` (Feedback) | als-vorlage |
| `apps/web/app/routes/help/help-routes.test.tsx` | Route-Smoke-/Render-Tests der Public-Help-Routen | als-vorlage (Test-Pattern für DoD-Smoke-Tests) |
| `apps/web/app/modules/knowledgeBase/routes/api/KbRoutes.Index.Api.tsx` | Loader-VM-Builder: `metatags/kb/search/categories/featured/isAdmin`; 404 bei unsupported lang | als-vorlage (Loader→VM-Konvention 1:1, Datenquelle tauschen) |
| `apps/web/app/modules/knowledgeBase/routes/api/KbRoutes.Category.Api.tsx` | Kategorie-Loader-VM | als-vorlage |
| `apps/web/app/modules/knowledgeBase/routes/api/KbRoutes.Article.Api.tsx` | Artikel-Loader-VM + anonymes `voteArticle` (thumbsUp/Down via `userAnalyticsId`, ohne PII) | als-vorlage (Feedback-Mechanik 1:1, Artikel-Quelle tauschen) |
| `apps/web/app/modules/knowledgeBase/routes/views/KbRoutes.{Index,Category,Article}.View.tsx` | Reine View-Komponenten (empfangen Loader-Data), Layer-getrennt | anpassen |
| `apps/web/app/modules/knowledgeBase/components/KbSearch.tsx` | Such-Eingabe mit `useDebounce` (500ms) + `q`-Query-Param + Ergebnis-Liste | anpassen (Spec verlangt 300ms-Debounce; Ranking Title>Desc>Keyword>Body ergänzen) |
| `apps/web/app/modules/knowledgeBase/components/KbHeader.tsx`, `KbFeaturedArticles.tsx`, `KbSkeletons.tsx` | Header, Featured-Artikel-Block, Loading-Skeletons | anpassen |
| `apps/web/app/modules/knowledgeBase/components/categories/Kb{Categories,CategoriesGrid,CategoriesList,Category,CategoriesTopArticles}.tsx` | Kategorien-Übersicht (Grid/List/„Top-Artikel“) | anpassen (deckt Spec Block C: Kategorie-Karten + „Beliebteste Artikel“) |
| `apps/web/app/modules/knowledgeBase/components/articles/Kb{Article,ArticleContent,Articles,ArticlesBySection}.tsx` | Artikel-Detail: Title/Description/Autor/Datum, Markdown-Body, verwandte Artikel, Daumen-Feedback | anpassen (deckt Spec Block D weitgehend; TOC fehlt — siehe Lücken) |
| `apps/web/app/modules/knowledgeBase/components/KbArticleContent.tsx` | `renderSanitizedMarkdown(content)` + `prose`-Styling + Related-Sidebar + Feedback-Buttons | anpassen |
| `apps/web/app/utils/html-sanitizer.ts` | `renderSanitizedMarkdown()` über `marked` (Renderer), Raw-HTML-Escaping, JS-URL-/rel-Hardening | 1:1 (sichere Markdown→HTML-Pipeline; Spec-Renderer-Frage „unified/remark“ damit faktisch schon gelöst) |
| `apps/web/app/modules/knowledgeBase/components/layouts/KbDocsLayout.tsx`, `KbDocsHeader.tsx` | „docs“-Layout-Variante (Sidebar-Navigation Kategorie→Artikel) | als-vorlage (passt zum Help-Layout; TOC-Spalte ergänzen) |
| `apps/web/app/components/ui/breadcrumbs/BreadcrumbSimple.tsx` (via `KbArticle.tsx`) | Breadcrumb Kategorie > Artikel | 1:1 (Spec Block C/D Breadcrumb) |
| `apps/web/app/modules/knowledgeBase/dtos/{KbArticleDto,KbCategoryDto,KbCategorySectionDto,KbNavLinkDto,KbSearchResultDto,KnowledgeBaseDto}.ts` | Vollständige DTO-/VM-Typen für KB/Kategorie/Artikel/Suche inkl. `metatags` | als-vorlage (Zielform für den neuen File-Index-Loader; Frontmatter→DTO mappen) |
| `apps/web/app/modules/knowledgeBase/service/KnowledgeBaseService.server.ts` | `kbToDto/categoryToDto/articleToDto` mit kompletten **SEO-Metatags** (title/description/OG/Twitter/Canonical), `search()`, `getFeaturedArticles()`, getypte Error-Codes → i18n-Keys | als-vorlage (Metatag-/DTO-Mapping 1:1 übernehmbar, DB-Reads durch File-Index-Reads ersetzen; nutzt `cachified` 24h-TTL — passt zu Spec „langer Cache-Header / SSG“) |
| `apps/web/app/modules/knowledgeBase/services/kbArticleSearch.server.ts` | Such-Service mit Meilisearch-Pfad + transparentem SQL-LIKE-Fallback, gleiche Rückgabeform | als-vorlage (Server-Side-Suche; für file-basiert: SQL-Pfad raus, FlexSearch-Index oder Postgres-FTS-Helper-Tabelle einsetzen) |
| `packages/search/src/{client,query,index-helper,sanitize}.ts` + `schemas/kb-articles.ts` | `@toolctrl/search` Meilisearch-Wrapper, tenant-scoped Query, Index-Schema `kbArticlesIndex`, Outbox-Publisher | anpassen (Spec bevorzugt Phase-1 client-side FlexSearch ODER Postgres-FTS; Meili ist „nice-to-have“ — als Server-Side-Option direkt verfügbar) |
| `apps/web/app/modules/knowledgeBase/services/kbArticleRag.server.ts` | RAG-Service für KB-Artikel (Embeddings-Suche) | als-vorlage (HiveGrid-**Phase-2** „KI-gestützte Artikel-Suche/RAG“, Spec Block I) |
| `packages/ai/src/embeddings/*` (`@toolctrl/ai/embeddings`) | Komplette Embeddings-/RAG-Pipeline (embed-and-upsert, retrieve, chunking, Outbox-Reindex) | als-vorlage (Phase-2 RAG) |
| `apps/web/app/modules/knowledgeBase/db/kbAnalytics.db.server.ts` | `createKnowledgeBaseView`, `voteArticle`, `getArticleStateByUserAnalyticsId` (anonym via `userAnalyticsId`) | als-vorlage (Spec: „helpful?“-Daumen anonym ohne PII; View-Counts = Phase-2) |
| `apps/web/app/utils/analyticsCookie.server.ts` (`getAnalyticsInfo`) | Anonyme Analytics-ID-Quelle für Feedback/Views ohne Login | anpassen (Spec verlangt anonym **ohne Cookies** — prüfen/anpassen) |
| `apps/web/app/components/ui/errors/ServerError.tsx` + Route-`ErrorBoundary` | Fehlerseiten-Pattern für Public-Routen | 1:1 |
| `apps/web/react-router.config.ts` + `apps/web/app/routes.ts` (Flat-Folder-Convention) | RR7-Routing-Setup inkl. „Plugin-Routen deaktivierbar“-Liste (`help/**` ist dort enthalten = Feature-Flag-fähig per Datei-Pattern) | 1:1 (Routing-Fundament; `help/**` aus `defaultDisabledPluginRouteFilePatterns` aktivierbar machen → Feature-Flag-Anforderung DoD erfüllt) |
| `packages/i18n/src/index.ts` (`@toolctrl/i18n`) + `apps/web/app/locale/*` | i18n-Setup (`defaultLocale="de"`, `supportedLocales`, SSR-Integration); Help-Center-Komponenten nutzen durchgängig `t(...)` | 1:1 (deckt LP-12 „alle UI-Strings aus Locale-Files, Phase 1 nur DE“) |
| `packages/view-models/src/index.ts` (`VMState`, `assertI18nLabel`, `ShellRegistries`) | View-Model-Statusmodell + i18n-Key-Zwang | 1:1 (Loader→VM-Konvention, DoD „Unit-Test VM-Builder mit Zod/Schema“) |
| `packages/ui-mock/*` + `apps/web/app/components/ui/hot-swap/HotSwapPageRenderer.tsx` | Hot-Swap-/Visual-Regression-Fundament | als-vorlage (DoD: Hot-Swap-Drill + Visual-Snapshot) |
---
## Vorhandene Daten-Modelle
Quelle: `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/knowledgeBase.prisma`
| Prisma-Modell | Relevanz für M38 |
|---|---|
| `KnowledgeBase` | KB-Container (slug, title, description, defaultLanguage, layout `list/articles/grid/docs`, color, enabled, `languages`, `links`, logo, seoImage, basePath, optional `tenantId`) — entspricht inhaltlich dem Help-Center-Root. **DB-getrieben — Phase-2-Modell, nicht Phase-1.** |
| `KnowledgeBaseCategory` | Kategorie (slug, order, title, description, **icon**, language, seoImage) — deckt Spec-`_category.json` (slug/label/description/icon/order) inhaltlich vollständig ab, aber als DB-Zeile statt File. |
| `KnowledgeBaseCategorySection` | Unter-Sektionen je Kategorie (order/title/description) — über Spec hinaus (optional). |
| `KnowledgeBaseArticle` | Artikel (slug, title, description, order, `contentDraft`/`contentPublished`/`contentPublishedAsText`, `contentType` wysiwyg/markdown, language, `featuredOrder`, seoImage, `publishedAt`, `relatedInArticleId`, `createdByUserId`) — deckt Frontmatter-Felder slug/title/order/description + Body inhaltlich ab. **Kein** Frontmatter-`deprecated`/`redirect_to`/`keywords`/`last_updated`-Äquivalent (siehe Lücken). |
| `KnowledgeBaseRelatedArticle` | Verwandte-Artikel-Relation (Spec: `related_slugs`). |
| `KnowledgeBaseViews` / `KnowledgeBaseArticleViews` | View-Tracking pro `userAnalyticsId` (Spec Block I Phase-2 „View-Counts“). |
| `KnowledgeBaseArticleUpvotes` / `KnowledgeBaseArticleDownvotes` | Anonymes Daumen-Feedback pro `userAnalyticsId` (Spec Block D „War hilfreich?“, ohne PII). |
**Wichtig:** Diese Modelle modellieren die **DB-getriebene** Variante. M38-Phase-1 ist **bewusst file-basiert** (Spec A.1, M172).
Sie sind nur als **Strukturreferenz / Phase-2-Fundament** relevant, nicht als Phase-1-Datenquelle.
Such-Index-Schema vorhanden: `packages/search/src/schemas/kb-articles.ts` (`kbArticlesIndex`).
Vektor/RAG-Speicher (Phase-2): `@toolctrl/vector-store` (pgvector/Qdrant), `prisma/migrations/...enable_pgvector`.
---
## Lücken / komplett neu zu bauen
1. **File-basierter Daten-Layer (Kern-Lücke).** Es gibt **keine** Boilerplate-Mechanik für
`content/help/de/<kategorie>/<slug>.md` + `_category.json`. Komplett neu zu bauen:
- Markdown-File-Loader mit **YAML-Frontmatter-Parser** (Pflicht: slug/title/category/order/description/last_updated; optional related_slugs/keywords/deprecated/redirect_to).
- `_category.json`-Loader (slug/label/description/icon/order).
- Mapping Frontmatter/File → bestehende DTO-Form (`KbArticleDto`/`KbCategoryDto` als Zielform wiederverwendbar).
2. **Build-Time-Index + Validierung (M172-Erfolgskriterium).** Nicht vorhanden. Neu:
- Build-Step erzeugt Volltext-Index (FlexSearch ~50KB) **oder** Postgres-FTS-Helper-Tabelle, Kategorien-Manifest, `sitemap.xml`.
- **Build-Fail**, wenn eine Kategorie-Referenz oder ein `related_slug` nicht existiert (harte Validierung — existiert in BP nicht).
3. **Frontmatter-spezifische Felder ohne DB-Pendant:** `last_updated` (BP nutzt `updatedAt`), `keywords`, `deprecated` + Deprecated-Banner, `redirect_to`/Redirect-Handling. Neu zu bauen (Spec A.2, Block D, Block H).
4. **Inhaltsverzeichnis (TOC) aus H2-Headings, klickbar/scroll-spy.** In `KbArticleContent`/`KbDocsLayout` **nicht** vorhanden (kein toc/heading-Generator gefunden). Neu zu bauen (Spec Block D).
5. **`/help/sitemap.xml`-Route.** Keine Sitemap-/robots-Route in der Boilerplate (`grep sitemap/robots` → leer). Neu (Spec Block B, A.4).
6. **URL-Schema-Umstellung.** Boilerplate-Routen sind `/help/$slug/($lang)/...` (KB-Slug zuerst, Sprache optional/hinten).
HiveGrid verlangt `/help/$lang/$category/$slug` mit `/help/` → `/help/de/`-Redirect und 404 für nicht-DE (Spec Block B, INTEGRATION-CONTRACTS §3). Routen-Files müssen neu strukturiert werden.
7. **Public-Mounting / Auth-Vorlagerung.** `help/**` steht aktuell in `defaultDisabledPluginRouteFilePatterns` (`apps/web/app/routes.ts`), also **standardmäßig deaktiviert**. Muss aktiviert und als **vor-Auth**, kein Tenant-Context, kein RLS gemountet werden (Spec Block G). IP-Rate-Limiting (1000 Req/Min/IP) ist neu zu ergänzen.
8. **Deep-Link-Mapping Route→Slug aus der App (Block F).** Hilfe-Icon-Pattern + Mapping-Konfiguration (`/{tenant}/...` → `kategorie/slug`) mit Fallback auf `/help/de/`. Komplett neu (kein Pendant in BP).
9. **Anonymes Feedback ohne Cookies.** BP-Feedback nutzt `getAnalyticsInfo` (`userAnalyticsId`, i.d.R. Cookie-gestützt). Spec fordert „anonym ohne Cookies“ — Mechanik prüfen/ersetzen.
10. **Such-Ranking & Highlight.** BP-SQL-Fallback ist einfaches LIKE ohne explizites Ranking; Spec verlangt Title>Description>Keyword>Body + Snippet-**Highlight** + Empty-State-Text mit `support@hivegrid.de`. Ranking/Highlight neu (Meili liefert Ranking, aber Phase-1 ist client-side FlexSearch vorgesehen).
11. **„Sprache nicht verfügbar“-Klartext-Banner / Redirect** (`/help/en/...` → `/help/de/` mit Hinweis, Spec Block H). BP wirft stattdessen hartes 404 bei unsupported lang.
12. **HiveGrid-Querschnitt-Pakete.** `@hivegrid/*` (activity/audit/notifications/entitlements/i18n) sind **HiveGrid-eigene** Pakete (INTEGRATION-CONTRACTS) und in der Boilerplate **nicht** vorhanden (dort `@toolctrl/*`). Für M38 großteils irrelevant (öffentlich, keine Permissions, keine schreibende Domain-Aktion außer anonymem Vote) — aber Feature-Flag-Schaltbarkeit (M006) und ggf. Audit für externe Effekte müssen HiveGrid-konform verdrahtet werden.
13. **Initial-Artikel-Inventar (~10-20 DE-Artikel).** Inhalt, nicht Code — neu zu schreiben (Spec A.5, M172). BP hat `DefaultKbsTemplate.ts` als Demo-Content, aber inhaltlich irrelevant.
---
## Empfohlenes Vorgehen
**Phase A — Daten-/Build-Fundament zuerst neu bauen (die Kern-Lücke):**
1. Verzeichnis-Layout `content/help/de/<kategorie>/<slug>.md` + `_category.json` festlegen.
2. Frontmatter-Parser + File-Loader bauen; Ausgabe **direkt auf die vorhandenen DTOs** (`KbArticleDto`/`KbCategoryDto`/`KbSearchResultDto` als Zielform) mappen — so bleibt die UI-Schicht wiederverwendbar.
3. Build-Step: Kategorien-Manifest + Volltext-Index (FlexSearch oder Postgres-FTS) + `sitemap.xml`; harte Validierung (Kategorie-Refs, related_slugs) → Build-Fail. (M172-Erfolgskriterium.)
**Phase B — UI/Routing aus Boilerplate übernehmen und umbauen:**
4. `renderSanitizedMarkdown` aus `apps/web/app/utils/html-sanitizer.ts` **1:1** übernehmen (löst die Renderer-Wahl der Spec).
5. View-Komponenten + DTO-Mapping aus `modules/knowledgeBase/{components,routes/views,service}` als Vorlage übernehmen; Daten-Reads von DB auf den File-Index umstellen.
6. SEO-Metatag-Builder (`kbToDto/categoryToDto/articleToDto`) **als-vorlage** übernehmen — OG/Twitter/Canonical sind fertig.
7. Routen neu nach HiveGrid-Schema `/help/$lang/$category/$slug` strukturieren (Vorlage: `routes/help/*`); `/help/`→`/help/de/`-Redirect, nicht-DE-Klartext-Banner, `sitemap.xml`-Route; `help/**` aus der Disabled-Liste aktivieren und **public/vor-Auth** mounten + IP-Rate-Limiting.
**Phase C — Ergänzungen, die der Boilerplate fehlen:**
8. TOC-aus-H2-Generator (klickbar/scroll-spy) in die Artikel-View ergänzen.
9. Deep-Link-Mapping Route→Slug (Block F) inkl. Hilfe-Icon-Pattern + Fallback `/help/de/`.
10. Such-Ranking (Title>Desc>Keyword>Body) + Snippet-Highlight + Empty-State-Klartext; Debounce auf 300ms.
11. Deprecated-Banner + `redirect_to`-Handling + Artikel-404-Klartextseite (Block D/H).
12. Anonymes Feedback (Daumen) aus `kbAnalytics.db.server.ts` übernehmen, aber **cookieless** umbauen.
**Phase D — DoD-Querschnitt (HiveGrid):**
13. Feature-Flag-Schaltbarkeit (M006), i18n-Keys (DE-Locale, LP-12), Smoke-Test (`routes/help/help-routes.test.tsx` als Vorlage), Hot-Swap-Drill + Visual-Snapshot. Sidebar-Eintrag entfällt (Help ist außerhalb Tenant-Sidebar, Spec Block B) — als „nur Inline-Verlinkung“ dokumentieren (DoD-Erreichbarkeit erlaubt das explizit).
**Phase-2 (später, BP liefert das Fundament fertig):** DB-getriebene Pflege + Editor-UI (`admin/knowledge-base/*` + `KnowledgeBase*`-Prisma-Modelle), Mehrsprachigkeit, **RAG-Suche** (`kbArticleRag.server.ts` + `@toolctrl/ai/embeddings` + `@toolctrl/vector-store`), View-Counts/Helpful-Statistik.
---
## Anti-Erfindung / Vorbehalte
- Die Boilerplate-Help-Center-Implementierung ist **DB-getrieben**; die file-/Git-PR-basierte Phase-1-Architektur der M38-Spec ist **nicht** vorhanden und muss neu gebaut werden.
- `help/**`-Routen sind in `apps/web/app/routes.ts` **standardmäßig deaktiviert** (`defaultDisabledPluginRouteFilePatterns`).
- **Kein** TOC-/H2-Generator, **keine** `sitemap.xml`-/robots-Route, **kein** Deep-Link-Route→Slug-Mapping in der Boilerplate gefunden.
- Such-SQL-Fallback ist LIKE-basiert ohne explizites Title>Desc>Keyword>Body-Ranking; Highlight fehlt.
- HiveGrid-Querschnitt-Pakete (`@hivegrid/*`, `packages/ui-hivegrid`, `packages/db/src/search/indexed-entities.ts`) sind **HiveGrid-eigen** und in der Boilerplate (`@toolctrl/*`) nicht enthalten.
@@ -0,0 +1,120 @@
# Boilerplate-Abgleich — M39 Help-Desk / Support
| Feld | Wert |
|---|---|
| **Modul-ID** | M39 |
| **Name** | Help-Desk / Support |
| **Milestones** | M174 (Email-/Feedback-Ticket-Eingang + Plattform-Admin-Bearbeitung), M175 (In-App-Feedback-Widget „Problem melden") |
| **Block** | Block 7 — Plattform-Außenseite (Betrieb, Compliance, Zugang) |
| **Reuse-Level** | **HIGH** |
| **Spec-Quellen** | `_bmad-output/specs/M39-help-desk.md`, `_bmad-output/pflichtenheft/blocks/block7.md` §39 |
## Kurzfazit
Die Boilerplate enthält ein **fast vollständiges Phase-1-Help-Desk-Modul** unter
`apps/web/app/modules/helpDesk` + `prisma/schema/helpDesk.prisma`. Es deckt im Kern:
- In-App-Feedback-Widget (`AddFeedbackButton.tsx`) mit Trigger + Modal + Submit-Action,
- Submit-Action verdrahtet in `app.$tenant.tsx` (`add-feedback` + `create-support-ticket`),
- Service- und DB-Layer für `Feedback` und `SupportTicket` (inkl. Inbound-E-Mail-Dedup),
- **PII-/Secret-Redaction** beim Submit (DSGVO-relevant, spec-konform),
- Plattform-Admin-Bereich `admin/help-desk` (Overview + Tickets-Liste + Feedback-Liste) mit Filter/Suche/Bulk-Delete,
- Admin-Sidebar-Eintrag bereits registriert,
- Notification an Admin-Rolle bei neuem Feedback/Ticket,
- Sliding-Window-Rate-Limiter als Package vorhanden.
Was fehlt, sind die **HiveGrid-spezifischen Spec-Erweiterungen** (reicheres Status-/Assignee-/
Internal-Notes-Datenmodell, Attachments, Browser-Info/App-Version-Capture, Screenshot, Detail-View,
10/h-Rate-Limit) und die **HiveGrid-Layer-Disziplin** (View-Model-Layer, ui-hivegrid-Adapter,
Permission-Keys statt `adminOnly`-Bool). Es ist eine **Anpass- und Erweiterungsaufgabe, kein Neubau.**
---
## Übernehmbar aus Boilerplate
| Boilerplate-Pfad | Was | Wie nutzbar |
|---|---|---|
| `prisma/schema/helpDesk.prisma` → `model Feedback` | Tenant/User-scoped Feedback (`message`, `fromUrl`, `tenantId?`, `userId?`, `createdAt`) | **als-vorlage** — Felder fehlen (status, assignee, internal_notes, route_path, browser_info, app_version, attachments, resolved_*) → erweitern |
| `prisma/schema/helpDesk.prisma` → `model SupportTicket` | Ticket mit `source`/`status`(default open)/`priority`, `subject`, `message`, `fromEmail`, `fromUrl`, `inboundMessageId @unique`, Indizes `[tenantId,status,createdAt]` + `[source,createdAt]` | **anpassen** — näher an Spec-Lifecycle als `Feedback`; entweder konsolidieren oder als Mailbox-Ticket-Modell weiterführen |
| `prisma/schema/core.prisma` → `model FileObject` (Z.213) | S3-Storage-Objekt (provider/bucket/key/contentType/byteSize/checksum/publicUrl) + `FileUploadProgress`/`FileChunk` | **anpassen** — Basis für Attachments (Spec A.3); Join `Feedback↔FileObject` + Storage-Pfad-Konvention `tenants/{id}/feedback/{id}/...` ergänzen |
| `apps/web/app/modules/helpDesk/services/Feedback.server.ts` | Submit-Service: Redaction, Rate-Limit-Count, Notification-Call, `createFeedback` | **anpassen** — Rate-Limit ist `count`-basiert ohne Zeitfenster (Spec: 10/User/h) → auf Sliding-Window umstellen; Browser-Info/App-Version/route_path ergänzen |
| `apps/web/app/modules/helpDesk/services/SupportTickets.server.ts` | `createInline` + `createFromInboundEmail` (E-Mail-Dedup via `inboundMessageId`), Subject-Normalisierung, Redaction | **anpassen** — `createFromInboundEmail` ist Phase-2-Vorgriff; `createInline`-Pattern 1:1 nutzbar |
| `apps/web/app/modules/helpDesk/services/FeedbackRedaction.ts` (+ `.server.ts`) | `redactFeedbackText` — entfernt E-Mails, Bearer-Tokens, API-Keys, Hex/Base64-Secrets aus Body+URL | **1:1** — deckt DSGVO-Transparenz/Query-Param-Stripping (Spec B.3) zum großen Teil ab |
| `apps/web/app/modules/helpDesk/db/feedback.db.server.ts` | `getAllFeedback` (Filter/Suche/Pagination, include tenant+user), `createFeedback` | **anpassen** — Volltextsuche nur über `message`; Spec will Body+Tenant-Name+User-Name; Cursor- statt Offset-Pagination |
| `apps/web/app/modules/helpDesk/db/supportTickets.db.server.ts` | `getAllSupportTickets`, `createSupportTicket`, `updateSupportTicketStatus` | **anpassen** — Status-Update-Funktion vorhanden; Assignee/Notes/Bulk-Status fehlen |
| `apps/web/app/components/layouts/buttons/AddFeedbackButton.tsx` | In-App-Trigger (Top-Bar-Button) + Feedback-Modal + Fetcher-Submit + Toast bei Erfolg/Fehler, autoerfasst `fromUrl`+`tenantId` | **als-vorlage** — UI-Grundgerüst da; nach `packages/ui-hivegrid` portieren (Layer-Disziplin), Attachments/Screenshot/Route-Anzeige/DSGVO-Hinweis/10-Zeichen-Validierung ergänzen |
| `apps/web/app/routes/admin/help-desk.tsx` | Plattform-Admin-Shell (SidebarIconsLayout: Overview/Tickets/Feedback) + Outlet | **anpassen** — Reihenfolge/Labels an HiveGrid; „Mailbox"-Direktlink-Tab (Spec D.1 `/feedback/mailbox`) ergänzen |
| `apps/web/app/routes/admin/help-desk/feedback.tsx` | Listen-Route: Loader baut Filterable-Props (tenant/user), Pagination, Action mit Bulk-`delete` + CSRF | **anpassen** — Status/Assignee/Zeitraum-Filter + CSV-Export + Bulk-Status/Bulk-Assign fehlen |
| `apps/web/app/routes/admin/help-desk/tickets.tsx` | Tickets-Listen-Route mit TableSimple (createdAt/subject/status/source/message) | **als-vorlage** — Listen-Pattern für die Feedback-Tabelle |
| `apps/web/app/modules/helpDesk/components/FeedbackTable.tsx` | Tabelle: Suche, Filter, Bulk-Select+Delete, Spalten (createdAt/tenant/user/message), Redaction in Anzeige | **anpassen** — Spalten Body-Preview(100), #Attachments, Route, Status-Badge, Assignee ergänzen; nach ui-hivegrid portieren |
| `apps/web/app/application/sidebar/AdminSidebar.tsx` (Z.287-294) | Admin-Sidebar-Eintrag „Help-Desk" + Sub „Feedback" via `enabledAdminRouteItem` / `UI_NAV_IDS.adminHelpDesk` | **anpassen** — Reachability-Anker vorhanden; auf HiveGrid-Permission-Gate (`platform_admin.feedback.read`) statt reinem Route-Enable umstellen |
| `packages/shared-store/src/helpers/rate-limit-sliding.ts` | Sliding-Window-Rate-Limiter (`consume`/`reset`, windowSeconds, max, Redis-Lua) | **1:1** — exakt für Spec B.5 „10 Submissions/User/h" |
| `apps/web/app/modules/notifications/services/.server/NotificationService.ts` → `sendToRoles` | Notify-Channel `admin-users` bei neuem Feedback/Ticket | **anpassen** — in HiveGrid über M011 `notify({...})` statt Direkt-Aufruf (AGENTS.md Querschnitt) |
| `apps/web/app/routes/help/$slug.($lang).tsx` (+ Geschwister) | Öffentliche Help-Center-Routen (Modul 38) | **als-vorlage** — Andock-Punkt für den mailto:`support@hivegrid.de`-Trigger (Spec B.1, Public-Kontext) |
| `apps/web/app/routes/admin/help-desk.auth.test.ts` | Auth-Test-Gerüst (Loader/Action gemockt, adminOnly) | **als-vorlage** — Test-Pattern für die HiveGrid-Smoke/Unit-Tests |
---
## Vorhandene Daten-Modelle
Alle in `/Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/`:
- **`helpDesk.prisma` → `Feedback`** — `id`, `createdAt`, `tenantId?`, `tenant`, `userId?`, `user`, `message`, `fromUrl`. Minimal: deckt Spec-Felder `id/created_at/tenant_id/tenant_user_id/body/route_path` ab. **Fehlt:** `updated_at`, `status`, `assignee_admin_user_id`, `internal_notes`, `resolved_at/_by`, `browser_info`, `app_version`, `attachments[]`, Spec-Indizes.
- **`helpDesk.prisma` → `SupportTicket`** — `id`, `createdAt`, `updatedAt`, `tenantId?`, `userId?`, `source`, `status`(@default open), `priority`, `subject`, `message`, `fromEmail?`, `fromUrl?`, `inboundMessageId? @unique`; Indizes `[tenantId,status,createdAt]`, `[source,createdAt]`. Näher am Spec-Status-Lifecycle als `Feedback`. **Fehlt:** Assignee, internal_notes, resolved-Felder, browser_info, app_version, Attachments-Relation.
- **`core.prisma` → `FileObject`** (Z.213) + `FileUploadProgress`/`FileChunk` (Z.195-211) — S3-Storage-Fundament für Attachments (Spec A.3). Es gibt **kein** dediziertes `FeedbackAttachment`-Join-Modell → neu.
**Permission/Plan-Hinweis:** `packages/entitlements/src/features.ts` definiert `supportTickets`+`prioritySupport` als Feature-Keys, `plans.ts` weist Quotas zu. Das ist Tenant-Entitlement-Logik, **nicht** die in Spec E.2 geforderten Plattform-Admin-Permissions (`platform_admin.feedback.read/write/delete`) — die fehlen komplett.
---
## Lücken / komplett neu zu bauen
**Datenmodell (Erweiterung, kein Neubau):**
- `status`-Enum (`open|in_progress|resolved|wontfix`), `assignee_admin_user_id`, `internal_notes` (Markdown), `resolved_at`, `resolved_by_admin_user_id` an Feedback ergänzen.
- `browser_info` (JSON: UA, Viewport, Screen, Sprache, Timezone), `app_version`, `route_path` als eigenes Feld.
- `FeedbackAttachment`-Relation auf `FileObject` + Storage-Pfad-Konvention `tenants/{tenantId}/feedback/{feedbackId}/...`, Typ-/Größen-Limits (5×10 MB, PNG/JPG/WEBP/PDF).
- Spec-Indizes `(status, created_at DESC)`, `(tenant_id, created_at DESC)`, `(assignee_admin_user_id, status)`.
**Widget (Spec B):**
- Browser-Info-Sammler-Util (UA/Viewport/Sprache/Timezone) — nicht vorhanden.
- App-Version-Einbettung zur Build-Zeit (Build-Hash) — nicht vorhanden.
- Screenshot-Funktion (`html2canvas-pro`, One-Click, mit Consent) — nicht vorhanden.
- Attachment-Upload-Komponente (Drag&Drop, max 5) im Modal — nicht vorhanden.
- Aktuelle-Route-Anzeige (read-only) + DSGVO-Hinweis + 10-Zeichen-Min-Validierung (Send disabled) im Modal — nur Basis-Modal da.
- Query-Param-Stripping bei `route_path` (Redaction deckt Secrets ab, aber nicht generelles Query-Strip).
- mailto:`support@hivegrid.de`-Trigger in Public-`/help/`-Routen — nicht vorhanden.
**Plattform-Admin-Detail-View (Spec D.3) — fehlt komplett:**
- Route `admin.hivegrid.de/feedback/:id`, Body-Anzeige mit URL-Link-Detection, Attachment-Preview (Image/PDF), Tenant-Block + User-Block mit Direktlinks ins Tenant-Admin (Impersonation M37), Browser-/Tech-Kontext ausklappbar, Inline-Edit Status/Assignee/Internal-Notes, Hard-Delete mit Warnung.
- **Activity-Log pro Feedback** (Status-/Assignee-/Notes-Wechsel mit Actor+Timestamp) — kein Audit/Activity-Wiring im BP-Modul; via M001/M045 neu verdrahten.
- Mailbox-Direktlink-Tab (Spec C/D.1 `/feedback/mailbox`) — nicht vorhanden.
**Liste (Spec D.2) — Teil-Lücken:**
- Status-/Assignee-/Zeitraum-Filter, Volltextsuche über Body+Tenant+User, Cursor-Pagination (50), Bulk-Status-Wechsel, Bulk-Assign, CSV-Export. Nur Tenant/User-Filter + Body-Suche + Offset-Pagination + Bulk-Delete vorhanden.
**Querschnitt / Layer-Disziplin (AGENTS.md DoD) — durchgehend neu:**
- View-Model-Layer (`packages/view-models`) — BP-Routen geben rohe DB-DTOs direkt aus.
- UI nach `packages/ui-hivegrid` (Widget+Tabelle), Subframe-Adapter — BP-UI liegt in `apps/web/app/components` + `modules/helpDesk/components`.
- Permission-Keys `platform_admin.feedback.read/write/delete` (M004) statt `requireAuth({adminOnly})`-Bool; Permission-Denied → 404 (Spec E.2).
- Audit auto (M001) + Activity (M045) + Notification via M011 `notify()` statt Direkt-`sendToRoles`.
- i18n-Keys (M003): BP nutzt `feedback.*`/`helpDesk.*`-Keys, aber HiveGrid-Klartexte (Spec B.4/F/39.Y) neu pflegen.
- Feature-Flag (M006), Smoke-Test/Hot-Swap (M017), Visual-Regression (M016) — neu pro DoD.
**Bewusst NICHT bauen (Phase 2 / Anti-Liste, Spec F + 39.X):**
Live-Chat, voll-Ticket-System mit User-Notifications, User-facing-Status-Page, NPS/CSAT, Mailbox-Sync in der App, KI-Auto-Triage, Feedback-Voting, Bug-Tracker-/Slack-Integration. → `SupportTickets.createFromInboundEmail` (Inbound-Mail→Ticket) ist ein Phase-2-Vorgriff und in M174 **nicht** zu aktivieren (Spec C.1/C.2: Mailbox extern lesen, nur Direktlink).
---
## Empfohlenes Vorgehen
**M174 zuerst (Backend + Plattform-Admin), dann M175 (Widget) — entspricht der Milestone-Abhängigkeit (M175 setzt M174 voraus).**
1. **Daten-Layer erweitern (M174):** `Feedback`-Modell der Boilerplate als Vorlage nehmen und um Spec-Felder (`status`-Enum, `assignee_admin_user_id`, `internal_notes`, `resolved_*`, `browser_info`, `app_version`, `route_path`) + Spec-Indizes + `FeedbackAttachment`→`FileObject`-Relation erweitern. Forward-Only-Migration mit Inverse-SQL-Kommentar. Entscheiden: `SupportTicket` als separates (Mailbox-)Modell behalten oder Feedback-Modell konsolidieren.
2. **Service-Layer übernehmen + härten (M174):** `Feedback.server.ts` + `feedback.db.server.ts` + `FeedbackRedaction.ts` aus BP übernehmen; Rate-Limit auf `rate-limit-sliding` (10/User/h) umstellen; Status-Lifecycle-Übergänge, Assignee-Set, Internal-Notes-Edit ergänzen; Notification auf M011 `notify()`, Audit/Activity über M001/M045 verdrahten.
3. **Plattform-Admin-Liste (M174):** Routen `admin/help-desk.tsx` + `feedback.tsx` + `FeedbackTable.tsx` als Vorlage nach View-Model + ui-hivegrid portieren; Status/Assignee/Zeitraum-Filter, Volltextsuche (Body+Tenant+User), Cursor-Pagination, Bulk-Status/Bulk-Assign, CSV-Export ergänzen; Permission-Gate `platform_admin.feedback.read`.
4. **Plattform-Admin-Detail-View neu (M174):** `/feedback/:id` mit allen Spec-D.3-Blöcken + Activity-Log + Hard-Delete; Mailbox-Direktlink-Tab (`/feedback/mailbox`) + Klartext-Hinweis (Spec C).
5. **In-App-Widget (M175):** `AddFeedbackButton.tsx` als Vorlage nach `packages/ui-hivegrid` portieren; Route-Anzeige, DSGVO-Hinweis, 10-Zeichen-Validierung, Browser-Info-Util, App-Version, Attachment-Upload (M052/M23-`FileObject`), `html2canvas-pro`-Screenshot ergänzen; Submit-Action (BP `app.$tenant.tsx`-Pattern) anpassen; mailto-Trigger in Public-`/help/`-Routen.
6. **Querschnitt + Tests + Reachability (beide):** Sidebar-Permission-Gate, i18n-Keys (HiveGrid-Klartexte), Feature-Flag, Smoke-/Unit-/Hot-Swap-/Visual-Regression-Tests, Empty-/Loading-/Permission-Denied(404)-States pro DoD.
---
*Erstellt durch Boilerplate-Abgleich-Agent. Geprüft direkt gegen `/Users/raffaelsheikh/Documents/GitHub/boilerplate` (nicht nur Inventur). Es wurde nichts in der Boilerplate oder im HiveGrid-Code geändert.*
@@ -0,0 +1,196 @@
# Boilerplate-Abgleich — Gesamt-Übersicht (alle HiveGrid-Module)
> Konsolidierung aller 38 Modul-Verdicts + Detail-Reports gegen die Boilerplate
> (`/Users/raffaelsheikh/Documents/GitHub/boilerplate`). Stand: 2026-06-04.
> Grundlage: `docs/boilerplate-mapping/M*.md` + `docs/boilerplate-inventory/*.md`.
> Wichtig: Das HiveGrid-Cloud-Repo enthält aktuell **nur Spec-/Docs-Artefakte** (kein `apps/`, `packages/`).
> Die Boilerplate ist die zu übernehmende Code-Basis — sie muss als Pre-Flight zuerst eingezogen werden.
---
## Executive Summary
**Grobe Gesamt-Abdeckung: ca. 35–45 % der HiveGrid-Planung ist durch die Boilerplate substanziell vorbereitet** — aber sehr ungleich verteilt:
- **Querschnitt/Fundament (M00): ~80–90 % fertig.** Audit (Hash-Kette), Permissions (404-statt-403, Tenant-Scope, Owner-Check), i18n (ICU/Plurale + CI-Gate), Feature-Flags (Engine v2), Idempotenz, Notifications, Outbox, View-Models, Adapter-Registry, DSGVO-FK-Map, CI-Gates — alles real, getestet, code-verifiziert. HiveGrid füllt überwiegend Inhalte ein (Error-Texte, Flags, i18n-Keys, Sidebar-Einträge). Dies ist das Fundament, auf dem **alle** Fachmodule stehen.
- **Plattform-/Infrastruktur-Module: hoch.** Auth/Multi-Tenancy (M27), Subscriptions/Billing (M28), Automation-Engine (M17), KI-Layer (M18), i18n (M30), Feature-Flags-Admin (M32) sind **high** — nahezu vollständige Architektur-Skelette vorhanden, HiveGrid baut Inhalte + gezielte Lücken.
- **Kommunikations-/Mid-Tier-Module: medium.** CRM (M01), Lead-Inbox (M02), Form-Builder (M05), Email (M09/M10), Telefonie (M12), HR-Hub (M14)*, Notifications (M22), Files (M23), Suche (M24), Tasks (M26)*, Apps-Engine (M33a), Public-API (M34), DSGVO (M35), Mobile (M36), Plattform-Admin (M37), Help-Center (M38), Help-Desk (M39), KWP (M19): **starkes Querschnitt-Fundament + Vorlagen, aber Domänen-Kern neu.**
- **Fachliche Greenfield-Module: low.** Sales-Pipeline (M03), Auftrags-/Projektpipeline (M04), Booking (M06), Angebot/Signatur (M07), Team-Chat (M13), Customer-Portal (M15), Calendar-Sync (M25): **kein Domänen-Modell in der Boilerplate** — nur Querschnitt + Bau-Vorlagen, der fachliche Kern (Daten + Logik + UI) ist Neubau.
**Die zentrale Erkenntnis:** Die Boilerplate bietet ein **erstklassiges Plattform-Fundament** (Auth, Billing, Automation, KI, Audit, i18n, Flags, Notifications, Files-Storage, Apps-Engine-Schema) — aber **fast keine HiveGrid-Fachdomäne** (kein typisiertes CRM-Modell, kein Deal/Pipeline/Project, kein Calendar, kein Booking, kein Chat). Boilerplate-Eigen-CRM ist Entity-Builder-basiert (Entity+Row+RowValue) und **architektonisch inkompatibel** mit den geforderten First-Class-Tabellen → muss entfernt, nicht angepasst werden.
### Vier wiederkehrende Quer-Befunde (für alle Module relevant)
1. **Naming-Drift (Blocker, kein Code-Gap):** HiveGrid-Doku referenziert durchgängig `@hivegrid/*`, `packages/ui-hivegrid`, `sidebar.config.ts`. Real existiert `@toolctrl/*`, `packages/ui-app` (Stub), Sidebar in `apps/web/app/application/sidebar/`. **Rebranding/Rename muss vor M018ff erfolgen**, sonst greifen alle DoD-Pfade ins Leere.
2. **Zwei falsche AGENTS.md-Pfadbehauptungen:** `packages/db/src/search/indexed-entities.ts` existiert nicht (Search liegt in `packages/search/`); durchgängiges `ui-hivegrid/sidebar.config.ts` entspricht nicht der Realität.
3. **Fehlende HiveGrid-Querschnittsdienste:** `logActivity()` / `@hivegrid/activity` (M045) und `Mass-Action-Schutz` (Bulk-Vorschau/Bestätigung) existieren **nirgends** in der Boilerplate — distinkt vom Audit-Log, Neubau.
4. **Falsche PLAN.md-Dependency-Annahmen (verifiziert):** `signature_pad` (M069), `browser-image-compression` (M052), `react-grid-layout` (M147), `react-beautiful-dnd` (M061→ist `@dnd-kit`), `kbar` (M058), `next-themes` (M030) sind **NICHT** in den Boilerplate-Deps. Vor Bau korrigieren.
### Subframe-Adapter (M15) — Querschnitt-Lücke für *alle* UI-Module
`packages/ui-subframe` ist ein **bewusst leerer Platzhalter** (nur README, leere Registry). Der Type-Bridge-Adapter (Subframe-Props ↔ HiveGrid-VM) existiert als Code nicht. Da User-Memory **jede Route via Subframe** verlangt, ist dieser Adapter-Layer eine harte Vorbedingung für jedes Fachmodul mit UI. Hoch priorisieren.
---
## Master-Tabelle
| Modul | Reuse | Wichtigste übernehmbare Assets | Größte Lücken | Milestones |
|---|---|---|---|---|
| **M00** Querschnitt-Hülle | 🟢 high | foundation (audit/permissions/errors/flags/idempotency/adapters/notifications/outbound-http), view-models, DB-Layer (Hash-Chain), CI-Gates (verify:i18n/ui-visual/hot-swap/tenant-isolation) | `logActivity`, indexed-entities-Registry, Subframe-Adapter-Code, Mass-Action-Schutz; Naming-Drift | M001–M017 |
| **M17** Automation-Engine | 🟢 high | komplette graph-Workflow-Engine (xyflow+dagre), Outbox-Trigger-Fanout, Block-Registry, Conditions, Resume, Credential-Crypto, Promptfoo-Gate | ~45 Domänen-Trigger + ~40 Actions, 24h-Undo, Anomalie/Loop-Guard, KI-NL-Erstellung, Dry-Run-Schwelle, Metering, Layer-Umbau | M075–M082 |
| **M18** KI-Layer / LLM-Gateway | 🟢 high | EU-Provider-Stack (Mistral/Scaleway/Aleph), 30-Min-Fallback, RAG+pgvector(HNSW), Promptfoo-OWASP-Suite, Pay-as-you-go-Overage-Engine | Whisper-EU-Transkription, Runtime-Input-Sanitizer, deterministischer Output-Cache, Sprach-zu-Automation, gesamtes UI | M083–M088 |
| **M27** Auth / Multi-Tenancy / RBAC | 🟢 high | Identity/Tenancy/RBAC-Modelle, TOTP+Recovery, HIBP-Policy, Magic-Link, Slug-Service, Settings-UI, ApiKey gehasht | Postgres-Sessions (BP=Cookie!), Postgres-RLS (fehlt!), Tenant-Lifecycle-Status-Enum, SHK-Rollen-Template, Reassign-Workflow | M018–M027 |
| **M28** Subscriptions / Billing | 🟢 high | Subscription-Datenmodell (13 Modelle), Stripe-Wrapper (~40 fn), Entitlements-Engine, Pricing-Admin, AI-Overage-Pipeline | Lifecycle-State-Machine, Seat-Modell (Büro/Monteur), Read-Only-Middleware (402), Frozen/Dunning, Apple-Compliance (LP-8!) | M153–M156 |
| **M30** i18n / Mehrsprachigkeit | 🟢 high | @toolctrl/i18n (ICU/Plurale/RTL), Server-Wiring (No-Flash), verify:i18n-Gate, Locale-Switcher | `Tenant.default_language`, `User.timezone/date_format`, TR/PL-Locales, Resolution-Order-Schritt, Empfänger-Auflösung | M028, M029 |
| **M01** CRM-Kern | 🟡 medium | Querschnitt komplett, Search+Fallback, Export/DSGVO, Import/Dedupe, Form-Capture, ~707 shadcn-Komponenten | Contact/Company/Lead/Activity/Tag/CustomField als **typisierte Tabellen** (BP-CRM=Entity-Builder, inkompatibel!), Merge, Promotion | M042–M046 |
| **M02** Lead-Inbox | 🟡 medium | Marketing-Lead-Inbox als Strukturmuster, Honeypot, Rate-Limiter, Realtime, Outbox, Retention-Cron, Apps-Engine-Schema | tenant-scoped Lead-Modell (BP-Lead operator-scoped!), Status-State-Machine, Routing-DSL, Anomalie-Detection, Statistik | M047–M051 |
| **M05** Form-Builder | 🟡 medium | Form-Widget (Builder+Drag&Drop, Zod, Origin-Allowlist, Honeypot, Captcha, Outbox-Routing), Submit-Endpoint | 9/17 Feldtypen, normalisiertes Datenmodell, 7/11 Submission-Aktionen, Conditional-Display, SHK-Vorlagen, hCaptcha statt Turnstile | M121–M125 |
| **M09** Email-Inbox + Sync | 🟡 medium | Mail-Stack (MailerSend EU), Adapter-Pattern (marketing-outbound), Bounce-Webhook-Vorlage, Storage, appsEngine (OAuth-DataConnection) | Gmail/Outlook-Mailbox-OAuth, Push-Sync (Pub/Sub/Graph), DKIM/SPF/DMARC, Auto-Match, gesamtes Inbox-Modell | M096–M099 |
| **M10** Email-Marketing | 🟡 medium | Campaign/OutboundEmail-Modelle, Kampagnen-CRUD+Editor, Sender-Mgmt, Tracking-Pixel/Unsubscribe-Token, Bounce-Webhook | Unsubscribe/Suppression/Segment/Template-Modelle, Daily-Cap/Frequenz/Spam-Score, BullMQ-Worker, EU-Provider-Tausch | M100–M104 |
| **M12** Telefonie (AirCall/HalloPetra) | 🟡 medium | Adapter-Registry-Pattern, OAuth-Flow-Template, AES-Crypto, HMAC-Webhook, Push/Notify-Pipeline, Workflow-Trigger | beide Provider-Adapter, Auto-Match (CRM-abhängig), E.164 (libphonenumber fehlt), gesamtes UI/Datenmodell | M105–M109 |
| **M14** HR-Hub | 🟡 medium | AES-Crypto, Storage+Signed-URLs, Audit, Notify, Exports/DSGVO, Imports, Form-Widget, Apps-Engine | alle 21 HR-Modelle, Per-Tenant-KMS/DEK, Leave-Approval-Engine, DE-Feiertage, Calendar-Modell, gesamtes UI | M133–M140 |
| **M19** KWP-Integration | 🟡 medium | Apps-Engine-Schema (DataConnection/AppSyncRun), gehashte API-Keys, Idempotenz, Storage, Worker-Cron, Webhook-Pattern | echtes Adapter-Interface (=M33a!), KWP-Modelle, Sync-/Konflikt-/Drift-Logik, gesamtes UI; **API-Variante vereinfacht stark** | M089–M095 |
| **M20** Dashboard / Reporting | 🟡 medium | Tenant-Dashboard-Route, dashboard-registry (Gating), Stat-DTO (Drill-Down), Export-Infra (PDF/CSV), Realtime/SSE, Recharts | Widget-/Layout-Engine, WidgetCatalog, Drag&Drop-Grid (react-grid-layout fehlt!), Aggregat-Cache, 7 Rollen-Layouts | M147–M150 |
| **M21** Audit-Log (Anzeige) | 🟡 medium | AuditEvent (Hash-Chain) ~70%, Write-Path, verifyAuditChain, Export-Pipeline (audit-pdf/csv), Retention-Cron | Diff-Felder (before/after), summary_text, AuditRetentionPolicy+Legal-Hold, Saved-Views, gesamte Anzeige-UI, Prisma-Middleware | M151, M152 |
| **M22** Notifications (3-Kanal) | 🟡 medium | In-App-Slice (DB→SSE→Center), Push-Pipeline (FCM/APNs/WebPush), DeviceToken, BullMQ-Queue, Mail-Stack | reiches Notification-Aggregat, Bundle/QuietHours-Modelle, PUSH-Enum, Bundle-/Quiet-Hours-Logik, fast alle UI | M115–M117 |
| **M23** Files / Dokumentenablage | 🟡 medium | packages/storage (S3/Scaleway, SigV4, Tenant-Isolation, ClamAV-Pipeline, Magic-Bytes) — sehr reif | File/Version/EntityLink-Modelle, Chunked-Upload+Resume, Preview/Thumbnail (keine Libs!), OCR/Suche, gesamtes UI | M052–M055 |
| **M24** Globale Suche (Cmd+K) | 🟡 medium | packages/search (Index/Outbox/Fallback), EU-Embeddings (Scaleway 1024d), pgvector tenant-scoped, RAG-Indexer, CommandPalette | FTS-Pfad (BP=Meilisearch!), Hybrid-RRF-Fusion, SearchIndexEntry-Modell, Permission-Query-Filter, Domain-Adapter | M056–M058 |
| **M26** Tasks / Aufgaben | 🟡 medium* | Querschnitt reif (Notify+Push/Email, Outbox, RBAC+Groups, Cron), shadcn-Tabellen, appsEngine (ERP-Sync) | Task-Modell (RowTask ist Falle!), TaskWatcher, Reminder-Anti-Spam, Listen/Detail/Archiv-Logik, ERP-Sync | M071–M074 |
| **M33a** Apps-Engine / Adapter | 🟡 medium | appsEngine.prisma (5 Modelle, App/DataConnection/AppSyncRun), AES-Crypto, Queue, HMAC-Webhook, Probe-Scheduler-Pattern | Adapter-Interface, 5-Status-State-Machine, Setup-Wizard, Probe-Scheduler, Webhook-Sturm-Schutz, Schema-Drift | M008–M010 |
| **M34** Public-API & Webhooks | 🟡 medium | dediziertes apps/api (Hono, OpenAPI, API-Key/Scope-Auth, Error-Envelope, ApiKeyLog), signierte Webhook-Delivery+Backoff | typisierte Read-Endpoints, Cursor-Pagination, 5 Webhook-Modelle, X-RateLimit-Header, Swagger-UI, Event-Katalog | M161–M163 |
| **M35** DSGVO-Export / Löschung | 🟡 medium | User-Level-DSGVO komplett (Export/Delete/Grace/Pseudonymize), USER_FK_MAP, Streaming-Export-Framework, Privacy-Hub | gesamte Tenant-Ebene, ZIP-mit-Binär-Bundling, AVV-PDF+Subprozessoren, Tenant-Lifecycle, Tenant-Hard-Delete-Cron | M164–M166 |
| **M36** Mobile-App (Capacitor) | 🟡 medium | Capacitor-7-Wrapper komplett, Push-Pipeline serverseitig, Session-Persist, Sentry, Permission-Strings | Offline-Modus (IndexedDB/Queue), Biometrie/Face-ID, Camera/Network-Plugins, Apple/Google-Compliance, Force-Update | M167–M171 |
| **M37** Plattform-Admin | 🟡 medium | Admin-Subdomain-Routing, AdminSidebar, MFA-Step-up, ImpersonationAudit+Banner, RBAC, KPIs, Stripe-Stack, Lead-Inbox | **getrennte AdminUser-Identität (Spec verlangt, BP verletzt!)**, AdminSession (IP-Bind), AdminDeal/Pipeline, ui-hivegrid | M034–M041 |
| **M38** Help-Center (öffentlich) | 🟡 medium | komplettes KB-System (Routes/Suche/SEO/Feedback/Markdown-Render/RAG) — als Vorlage | file-/build-basierter Daten-Layer (BP=DB-getrieben!), Build-Index+Sitemap, Frontmatter-Validierung | M172, M173 |
| **M39** Help-Desk / Support | 🟢 high | Feedback+SupportTicket-Modelle, Service/DB-Layer, In-App-Widget, PII-Redaction, Admin-Bereich, Sliding-Rate-Limiter | Detail-View, Status/Assignee/Notes/Attachments, reichere Liste, Browser-Info/Screenshot, Permission-Keys | M174, M175 |
| **M31** Branding / Theme | 🟡 medium | Light/Dark-Basis (binär), Capacitor-Status-Bar/Splash, Tenant.themeConfig+Parser, ~20 Themes, settings/theme-Route | 3-Wert-Theme (system), prefers-color-scheme, matchMedia-Listener, TenantBranding-Tabelle (Phase 2) | M030 |
| **M03** Sales-Pipeline | 🔴 low | Querschnitt 1:1, @dnd-kit (vorhanden!), SSE (kein WS), EntityView-board-Pattern, Row.folio-Sequenz, BullMQ-Cron | Deal/Pipeline/Stage/Forecast komplett neu, Stage-Engine, Won-Handoff, Field-Level-Volumen-Redaction, Stuck-Detection | M059–M064 |
| **M04** Auftrags-/Projektpipeline | 🔴 low | DB-Layer, RBAC+Groups, Notify+Push-Worker, appsEngine (KWP), @react-pdf, @dnd-kit, App-Shell | alle 8 Kern-Modelle neu, **Plantafel-Grid (kein Lib!)**, signature_pad (fehlt!), Abnahme-State-Machine, Konflikt-Detektor | M065–M070 |
| **M06** Booking (Calendly) | 🔴 low | Querschnitt, handlebars (Template-Engine!), Redis-Cache, Form-Widget, Lead, Site (booking), shadcn-Calendar | gesamtes Booking-Modell, Slot-Engine, EXCLUDE-gist-Doppelbuchungsschutz, Public-Flow, .ics, Calendar-Sync (M25 fehlt!) | M126–M129 |
| **M07** Angebot + e-Signatur | 🔴 low | defineAdapter, SSRF-outbound-http, Stripe/MailerSend-Webhook als Blaupause, appsEngine (OAuth), Storage, Outbox | PandaDoc/DocuSign-Adapter, 5 Signatur-Modelle, **hängt an M3-Deal-Modell**, Inbound-Webhook-Routen, 5-Schritt-Wizard | M130–M132 |
| **M13** Team-Chat (intern) | 🔴 low | Querschnitt (Notify/Permissions/Audit/Storage+AV), RowComment-Muster, realtime-Client (Reconnect), hocuspocus-Blueprint | 7 TeamChat-Modelle, **generischer WebSocket-Room-Layer (realtime=SSE!)**, Mention-Resolver, Link-Previews, gesamtes UI | M110–M114 |
| **M15** Customer-Portal | 🔴 low | Querschnitt (Auth-Primitiven, Storage+ClamAV, Mail, DSGVO, Rate-Limit), TenantUser-Auth als Referenz | gesamte CustomerPortal-Datenebene, **kein Session-Modell!**, getrennte PortalUser-Auth, alle Endkunden-Views | M141–M146 |
| **M25** Calendar-Sync (Google/MS) | 🔴 low | Querschnitt, AES-Crypto, SSRF-http, Outbox+Cron, HMAC-Webhook-Pattern, defineAdapter, appsEngine (Feld-Vorlage) | 5 Calendar-Modelle, CalendarAdapter, Google/MS-Adapter (kein googleapis/Graph!), OAuth2-Flow, Sync, Verfügbarkeit | M118–M120 |
*M14/M26 sind grenzwertig low→medium (starker Querschnitt, Domäne neu); M37 medium mit blockierender Architektur-Entscheidung.
---
## Gruppierung nach Reuse-Level (Quick-Wins zuerst)
### 🟢 HIGH — Quick-Wins (Fundament aktivieren + Inhalte füllen)
- **M00 Querschnitt-Foundation** — Pre-Flight für *alles*; ~80–90 % fertig. Vier echte Neubauten (`logActivity`, indexed-entities, Subframe-Adapter, Mass-Action) + Naming-Drift-Fix.
- **M30 i18n** — Mechanik komplett; 3 DB-Felder + TR/PL + 2 Resolver-Schritte.
- **M27 Auth/Tenancy/RBAC** — fast vollständig; 3 echte Architektur-Neubauten (Postgres-Sessions, RLS, Lifecycle-Status).
- **M28 Billing** — Datenmodell+Stripe+Pricing+Overage fertig; Lifecycle/Seat/Read-Only neu. **Achtung Apple-Compliance LP-8.**
- **M17 Automation-Engine** — Engine 1:1; Domänen-Trigger/Actions + USP-Mechaniken + Layer-Umbau.
- **M18 KI-Layer** — EU-Stack+RAG+Promptfoo+Overage fertig; Whisper + Sanitizer + Sprach-zu-Automation + UI.
- **M39 Help-Desk** — fast komplettes Phase-1-Modul; Anpass-/Erweiterungsaufgabe.
### 🟡 MEDIUM — solides Fundament + Vorlagen, Domänen-Kern neu
M01 CRM, M02 Lead-Inbox, M05 Form-Builder, M09 Email-Inbox, M10 Email-Marketing, M12 Telefonie, M14 HR-Hub, M19 KWP, M20 Dashboard, M21 Audit-Anzeige, M22 Notifications, M23 Files, M24 Suche, M26 Tasks, M31 Branding, M33a Apps-Engine, M34 Public-API, M35 DSGVO, M36 Mobile, M37 Plattform-Admin, M38 Help-Center.
### 🔴 LOW — Greenfield-Fachdomäne (Querschnitt + Bau-Vorlagen, Kern neu)
M03 Sales-Pipeline, M04 Auftrags-/Projektpipeline, M06 Booking, M07 Angebot/Signatur, M13 Team-Chat, M15 Customer-Portal, M25 Calendar-Sync.
> **Es gibt kein „none".** Selbst die low-Module profitieren erheblich vom Querschnitt + konkreten Bau-Vorlagen.
---
## Querschnitt-Foundation (M00) — Status separat
**Reuse-Level: HIGH, code-verifiziert (nicht nur Inventur).** Fast die gesamte Querschnitt-Hülle existiert als lauffähige, getestete Patterns + CI-Gates:
| Baustein | Status | Quelle |
|---|---|---|
| M001 Audit (Hash-Chain pro Tenant) | ✅ 1:1 | `foundation/src/audit.ts` + `db/src/audit-events.server.ts` |
| M002 Klartext-Diagnostik (CatalogError) | ✅ Engine fertig, `catalogRegistry` **leer** (HiveGrid füllt) | `foundation/src/errors.ts` |
| M003 i18n-Pflicht | ✅ 1:1 + CI-Gate | `packages/i18n` + `verify:i18n` |
| M004/M005 Tenant-Scope/Owner-Check/404-statt-403/SSRF | ✅ 1:1 | `foundation/src/permissions.ts` + `outbound-http.ts` |
| M006 Feature-Flag-Engine | ✅ 1:1 (v2: Targeting/Cache/Hooks) | `packages/feature-flags` |
| M007 Idempotenz-Wrapper | ✅ 1:1 | `foundation/src/idempotency.ts` |
| M008–M010 Apps-Engine-Adapter + State-Machine | ⚠️ Registry+Status-Enum da, **Interface+State-Machine neu** (=M33a) | `foundation/src/adapters.ts` + `appsEngine.prisma` |
| M011 Notification-Service | ✅ 1:1 | `foundation/src/notifications.ts` |
| M012–M014 Workspace/View-Model/Action-Contract | ✅ 1:1 | `packages/view-models` |
| M015 Subframe-Adapter | ❌ **nur leere Boundary, Adapter-Code fehlt** | `packages/ui-subframe` |
| M016/M017 Visual-Regression + Hot-Swap-Drill | ✅ 1:1 + CI-Gates | `packages/ui-mock` + `ci.yml` |
**Vier echte M00-Neubauten:** (1) `logActivity()`/Activity-Stream (distinkt vom Audit), (2) Search-indexed-entities-Registry (falscher AGENTS-Pfad), (3) Subframe-Adapter-Code, (4) Mass-Action-Schutz. **Plus durchgängiger Naming-Drift** (`ui-hivegrid`/`@hivegrid/*` vs. Realität) — Phase-0-Pflicht.
---
## KWP (M19) — API-Anbindungs-Auswirkung (hervorgehoben)
**Stand 2026-06-04: Mit der NEUEN offiziellen KWP-API kippt die Architektur von 3 auf 2 Komponenten** — aus `Cloud ↔ kwpsync.exe ↔ MSSQL` wird `Cloud ↔ KWP-API`. Das verändert den Milestone-Schnitt erheblich:
| Milestone | Auswirkung der KWP-API |
|---|---|
| M089 (Sync-Programm / kwpsync.exe) | **ENTFÄLLT** (kein Windows-Client, kein MSSQL-Treiber, kein NSSM/DPAPI/Auto-Update mehr) |
| M090 (Heartbeat + Recovery) | **stark vereinfacht** → generischer M33a-Health-Probe |
| M091 (Warteschlange + Replay) | **vereinfacht** — lokale SQLite-Queue entfällt, nur Cloud-Outbox bleibt |
| M092 (Schema-Drift-Sentinel) | **vereinfacht bis ENTFÄLLT** → API-Versions-/Contract-Check (zod-Response-Validierung) |
| M093 (Konflikt „KWP gewinnt") | **BLEIBT unverändert** — transport-unabhängige Domain-Regel |
| M094 (Plantafel-Sync) | **vereinfacht** via API-Endpoints/Webhooks |
| M095 (Dokumenten-Archiv-Sync) | **vereinfacht** via API-Endpoints/Webhooks |
**Netto:** Aufwand sinkt deutlich. Bei API-Variante passt `DataConnection.type='rest-api'/'oauth2'` **nahtlos** ins generische Apps-Engine-Muster (statt `windows_client`-Sonderweg) — das **hebt das faktische Reuse-Level Richtung medium-high**. **Empfehlung:** M089–M095 vor Umsetzung gegen die API **neu schneiden**. Harte Vorbedingung bleibt **M008–M010 (M33a Adapter-Interface)**.
---
## Empfohlene Übernahme-Reihenfolge (gemäß Cluster-Reihenfolge der Planung)
### Phase 0 — Pre-Flight (blockierend, vor jedem Fachmodul)
1. **Boilerplate als Code-Basis einziehen** (HiveGrid-Repo ist heute Docs-only). AGENTS Pre-Flight-Check schlägt sonst fehl.
2. **Naming-Drift beheben:** `@toolctrl/*` → `@hivegrid/*`, `ui-app`-Stub → `ui-hivegrid`, Sidebar-Konvention, AGENTS-Pfade (`indexed-entities.ts`, `sidebar.config.ts`) korrigieren.
3. **EU-Sterilisation:** OAuth-GitHub/Google-Login-Reste entfernen (M27), Anthropic/OpenAI-Embedding-Stubs deaktiviert lassen (M18, DSGVO-Hard-Constraint).
4. **M00 aktivieren:** foundation/view-models/i18n/feature-flags 1:1 + CI-Gates verdrahten. Die 4 echten Neubauten (`logActivity`, indexed-entities-Registry, **Subframe-Adapter** [Quer-Voraussetzung für alle UI], Mass-Action-Schutz) anlegen.
### Phase 1 — Plattform-Fundament (high reuse, ermöglicht alles Weitere)
5. **M27 Auth/Tenancy/RBAC** — inkl. 3 Architektur-Neubauten (Postgres-Sessions, **RLS**, Lifecycle-Status). Vorbedingung für *alle* tenant-scoped Module.
6. **M30 i18n** + **M31 Branding** (Quick-Wins, klein).
7. **M28 Billing** (Lifecycle/Seat/Read-Only; Apple-LP-8 beachten).
8. **M32 Feature-Flags-Admin**, **M33a Apps-Engine** (Adapter-Interface — Vorbedingung für M07/M09/M12/M19/M25), **M34 Public-API**.
### Phase 2 — Daten-Kern (Cluster „CRM-Fundament")
9. **M01 CRM-Kern** — Contact/Company/Lead als typisierte Tabellen; **Boilerplate-Entity-Builder-CRM entfernen**. Vorbedingung für M02/M03/M07/M09/M12.
10. **M23 Files**, **M24 Suche** (FTS-Architektur-Entscheid), **M26 Tasks**, **M22 Notifications**, **M21 Audit-Anzeige**.
### Phase 3 — Vertriebs-/Auftrags-Kette (Cluster „Sales→Projekt")
11. **M02 Lead-Inbox** → **M03 Sales-Pipeline** (low, Greenfield) → **M04 Auftrags-/Projektpipeline** (low, Plantafel-Neubau) → **M07 Angebot/Signatur** (hängt an M3-Deal).
### Phase 4 — Kommunikation & Termine
12. **M05 Form-Builder**, **M06 Booking**, **M25 Calendar-Sync** (low; M25 ist harte Vorbedingung für M06-Verfügbarkeit), **M09/M10 Email**, **M12 Telefonie**, **M13 Team-Chat** (WebSocket-Layer neu).
### Phase 5 — USP & Integration
13. **M17 Automation-Engine** (high; nach Domänen-Events der Vormodule), **M18 KI-Layer** (high), **M19 KWP** (gegen API neu schneiden).
### Phase 6 — Endkunden & Plattform-Betrieb
14. **M14 HR-Hub**, **M15 Customer-Portal** (low), **M20 Dashboard** (nach Domänen-Modulen), **M29 Onboarding**, **M35 DSGVO** (Tenant-Ebene), **M36 Mobile**, **M37 Plattform-Admin**, **M38 Help-Center**, **M39 Help-Desk**.
---
## Risiken / Offene Fragen
### Architektur-Risiken (blockierend)
- **R1 — Naming-Drift:** Ohne Rename greifen *alle* DoD-Pfade ins Leere. Höchste Priorität (Phase 0).
- **R2 — Postgres-RLS fehlt (M27/A.4):** Boilerplate macht nur App-Level-Scoping. RLS-Migration + `SET LOCAL app.tenant_id` + App-Role ohne BYPASSRLS + Worker-Job-Context komplett neu — Sicherheits-Fundament.
- **R3 — Kein Session-Prisma-Modell (M27/M15):** Weder TenantUser noch PortalUser haben server-side Postgres-Sessions (BP=Cookie-only). Betrifft M021 + das gesamte Customer-Portal.
- **R4 — AdminUser-Identität (M37):** Spec 37.1 verlangt vollständig getrennte AdminUser/AdminSession/AdminRole/AdminAuditLog; BP nutzt User-Extension + dasselbe RBAC + Authentik-OIDC. **Blockierende Entscheidung vor M034:** spec-treu neu bauen vs. Change-Request.
- **R5 — CRM-Architektur-Konflikt (M01):** BP-CRM ist Entity-Builder (Entity+Row+RowValue), Spec verlangt typisierte First-Class-Tabellen. **BP-CRM entfernen, nicht anpassen** — sonst sickert fremde Architektur ein.
- **R6 — Operator-scoped Lead (M02/M06/M37):** `marketingLeads.Lead` ist operator-scoped (globales `@@unique([email])`, Funnel-Status). Der tenant-scoped HiveGrid-CRM-Lead gehört fachlich zu M01 und muss neu — NICHT mitbenutzen.
### Technische Risiken
- **R7 — Subframe-Adapter fehlt (M15-Querschnitt):** Pflicht für jede UI-Route (User-Memory), aber nur leere Boundary. Quer-Voraussetzung.
- **R8 — realtime = SSE, kein WebSocket:** Präjudiziert M03 (Live-Kanban) und blockiert M13 (Team-Chat braucht generischen WS-Room-Layer). hocuspocus ist CRDT, kein Chat-Broadcast.
- **R9 — Fehlende Dependencies (PLAN.md falsch):** `signature_pad`, `browser-image-compression`, `react-grid-layout`, `kbar`, `next-themes`, `libphonenumber`, `googleapis`/`@microsoft/microsoft-graph`, `ics/ical-generator`, `EXCLUDE USING gist/btree_gist` — alle NICHT vorhanden. Vor Bau ergänzen.
- **R10 — Apple-Compliance (M28/M36, LP-8):** Fertige BP-Pricing/Upgrade-UI darf NICHT in die Mobile-App. Größtes Anti-Risiko bei Billing.
- **R11 — Suche-Architektur (M24):** BP=Meilisearch+SQL-ILIKE, Spec=Postgres-FTS+pgvector-RRF-Fusion. Phase-0-Entscheid nötig (FTS-Pfad + Fusion komplett neu).
- **R12 — Per-Tenant-KMS fehlt (M14/M25):** CryptoApi ist Single-Env-Key. Spec verlangt Per-Tenant-DEK/KMS (Password-Safe, OAuth-Tokens).
### Offene fachliche Fragen (für User/Noah)
- **F1:** EU-SMTP-Provider final wählen (M10/M22) — MailerSend ist nicht-EU (LP-13-Konflikt); Scaleway TEM evaluieren.
- **F2:** Captcha-Provider — BP=Cloudflare Turnstile, Spec=hCaptcha (EU). Tausch nötig (M05/M125).
- **F3:** KWP-API-Verfügbarkeit/Vertrag bestätigen → M089–M095 neu schneiden.
- **F4:** SHK-Custom-Field-/Tag-Defaults + 7-10 SHK-Rollen-Template finalisieren (M01/M27/M29 — deklaratives Seeding).
- **F5:** Idempotenz-Strategie Stripe-Webhook (M28): StripeWebhookLog-Tabelle (Spec) vs. Outbox-dispatchKey (BP) — Konflikt klären.
- **F6:** Saved-Searches Phase 1 vs. 2 (M24); `url/email` als eigene field_type vs. Validierung (M01); Activity-Tabelle vs. Audit-Wiederverwendung (M01/M45).
@@ -0,0 +1,135 @@
# Boilerplate Readiness
This page enumerates the verification gates that protect the boilerplate's
"safe to fork and build on" contract. The default CI lane is asserted by
`.gitea/workflows/ci.yml`; heavier gates such as coverage, Lighthouse,
Storybook, a11y, and deploy verification may live in dedicated workflow files.
Failing a required gate blocks the PR or deployment lane that owns it — there
is no silent override mode.
## How to read this page
Each section names a gate, what it enforces, the canonical command, and the
file(s) that contain the contract. Gates marked **anti-regression** keep the
current floor; they only allow improvements, never regressions. Gates marked
**hard contract** assert structural invariants that must always hold.
## Onboarding & branding
| Gate | Contract | Command |
| --- | --- | --- |
| Brand env layer (hard) | All user-facing brand strings are overridable via `BRAND_*` env vars in `packages/config/src/catalog.ts`. `packages/brand/src/env.ts` is the source-of-truth loader. | `pnpm --filter @toolctrl/brand test` |
| Infra-host parametrization (hard) | Gitea runner label, registry host, image prefix, DB user/name read from `scripts/infra-config.mjs` with `TOOLCTRL_*` env overrides — forks can sed-replace strings and set vars without rewriting `verify-gitea-*-workflow.mjs`. | `pnpm run verify:gitea-ci-workflow && pnpm run verify:deploy-workflow` |
| `.env.example` drift (hard) | Every key in `.env.{example,local.example,production.example,fly.example,test}` exists in the catalog. | `pnpm exec vitest run packages/config/src/env-examples.test.ts` |
## Internationalization
| Gate | Contract | Command |
| --- | --- | --- |
| DE/EN namespace parity (hard) | All registered `i18nNamespaces` stay in lockstep across `de` and `en`, including app, auth, billing, CRM, settings, email marketing, mail, and error namespaces. | `pnpm run verify:i18n` |
| Hardcoded user-facing strings (anti-regression) | `brand-literal`, `jsx-text`, `user-facing-prop-literal`, and `user-facing-response-literal` counts may only decrease over the ratcheted baseline; current zero-floor categories must stay at zero. | `node scripts/verify-hardcoded-strings.mjs` |
| RTL/direction (hard) | `getDirection(locale)` covers known RTL languages; `isRTL` matches. | `pnpm --filter @toolctrl/i18n test` |
| Mail templates (hard) | `renderTransactionalEmail(template, props)` (React-Email) renders subject + body for every supported template. Host-app translator hooks are reserved for a future i18n pass; templates emit English copy directly. | `pnpm --filter @toolctrl/mail test` |
## Security
| Gate | Contract | Command |
| --- | --- | --- |
| Secret scanning (hard) | 10 secret-shaped patterns (GitHub PAT, AWS, Stripe `sk_live_*`, Slack, PEM, JWT, generic) scanned across `apps/packages/scripts/prisma/docs/`. Allowlist for `.env.example` variants and test fixtures. | `pnpm run verify:secrets` |
| CSRF inventory (hard) | Every mutating `POST/PATCH/DELETE` route is classified in `docs/security/csrf-inventory.md`. | `pnpm exec vitest run apps/api/src/csrf-inventory.test.ts apps/web/app/utils/session-csrf-sweep.test.ts` |
| Route access inventory (hard) | Every API route has an access class (`tenant-api-key`, `service-token`, `public-health`, …) documented + matching OpenAPI security. | `pnpm run verify:api-inventory && pnpm run verify:openapi-security` |
| Tenant isolation (hard) | 1115 lines of runtime tests in `apps/api/src/isolation/` + `apps/web/app/test/isolation/` exercise cross-tenant mutations and assert 403/401. | `pnpm exec vitest run apps/api/src/isolation apps/web/app/test/isolation` |
| Permission matrix (hard) | Role/permission seed is the source-of-truth, docs and tests stay aligned. | `pnpm run verify:permission-matrix` |
| Auth error envelope (hard) | Auth middleware throws `{ error: { code, messageKey } }` envelopes, never free strings. | `pnpm exec vitest run apps/web/app/utils/loaders.middleware.test.ts` |
| STRIDE threat model (doc) | `docs/security/threat-model.md` lists Spoofing/Tampering/Repudiation/Information-disclosure/DoS/Elevation threats and links each to a gate. | (review) |
## Quality & code hygiene
| Gate | Contract | Command |
| --- | --- | --- |
| Phase-C boundaries (hard) | UI packages cannot import `@toolctrl/db`; runtime separation enforced. | `pnpm run verify:phase-c-boundaries` |
| ESLint baseline (anti-regression) | `eslint-disable`, `@ts-ignore`, `@ts-expect-error` counts only decrease from the ratcheted baseline. | `pnpm run verify:eslint-baseline` |
| Test density (anti-regression) | 12 workspace dirs maintain a minimum test-file-per-source-file ratio. | `pnpm run verify:test-density` |
| Test coverage (anti-regression) | `scripts/coverage-targets.json` defines active per-path thresholds and release-floor sprint targets; `@vitest/coverage-v8` emits `coverage-summary.json` for the gate. The latest backlog has no critical-path positive gaps. | `pnpm run verify:test-coverage` |
| Hot-swap mock visual (hard) | `verify:ui-visual` + `test:hot-swap` keep mock-driven surface stable. | `pnpm run verify:ui-visual && pnpm run test:hot-swap` |
## Operations & runtime
| Gate | Contract | Command |
| --- | --- | --- |
| Docker non-root (hard) | All runtime stages (`web`, `api`, `worker`) drop to `USER node` before `CMD/ENTRYPOINT`. | `pnpm run verify:docker-non-root` |
| Docker baseline (hard) | Dockerfile stage layout + healthchecks per stage stable. | `pnpm run verify:docker-baseline` |
| Bundle-size budget (anti-regression) | JS chunks (root/route/vendor/other) and CSS stay under the +20% headroom budget from current state. | `pnpm run verify:bundle-size` |
| Production seed artifact (hard) | The compiled production seed bundle is present and idempotent. | `pnpm run verify:production-seed-artifact` |
| Migration rollback drill (manual) | `scripts/drill-migration-rollback.sh` replays forward chain + seed against an ephemeral postgres and asserts a re-apply is a no-op. | `./scripts/drill-migration-rollback.sh` |
| Health baseline (hard) | `/healthz` and `/readyz` shapes are stable. | `pnpm run verify:health-baseline` |
## Supply chain
| Mechanism | Coverage | File |
| --- | --- | --- |
| Dependabot | weekly npm (grouped: ai-providers / react-stack / test / lint), weekly github-actions, monthly docker | `.github/dependabot.yml` |
| Renovate (Gitea) | same group config for self-hosted Gitea forks | `.gitea/renovate.json` |
| SBOM | `scripts/generate-sbom.mjs` writes `sbom-pnpm.json` during the build stage; every runtime image carries the SBOM at `/myapp/sbom-pnpm.json` and labels it via `org.opencontainers.image.sbom`. | `Dockerfile`, `scripts/generate-sbom.mjs` |
| Production seed bootstrap | `SEED_ADMIN_EMAIL`/`SEED_ADMIN_PASSWORD` required for prod seed; rejects the local default. | `packages/db/src/production-seed.server.ts` |
## Documentation
| Doc | Purpose |
| --- | --- |
| `docs/quickstart.md` | Install-to-first-local-run path |
| `docs/new-project.md` | Adapting the boilerplate to a new project |
| `docs/deployment-porting.md` | Gitea infra-string fork procedure |
| `docs/security/threat-model.md` | STRIDE per subsystem with mitigation links |
| `docs/operations/database-rollback.md` | Forward-only rollback runbook + drill script reference |
| `docs/ui-inventory/hot-swap-status.md` | UI registry activation state |
## Optional gates (opt-in)
Some verification surfaces require a deployed environment or an external
binary and are therefore not wired into the default CI lane. They SKIP
cleanly (exit 0) when their prerequisites are missing, so they are safe to
run locally or to enable in a downstream pipeline.
| Gate | Contract | Command | Prerequisites |
| --- | --- | --- | --- |
| Load tests (opt-in) | k6 scripts in `tests/load/` exercise login, tenant-scoped read, and tenant-scoped mutating paths against a deployed target. Thresholds: `http_req_failed: rate<0.05`, login `p(95)<400ms` / `p(99)<800ms`, read `p(99)<300ms` and `>200 req/s`, mutating `p(99)<600ms`. | `pnpm run verify:load` | `k6` binary on PATH (`brew install k6` or <https://k6.io/docs/getting-started/installation/>); reachable `K6_TARGET_URL` (default `http://localhost:3000`); `LOAD_TENANT_API_KEY` for the read/mutating scripts. See `tests/load/README.md` for the full env contract. |
To activate the load suite in a release lane, set `K6_TARGET_URL` and
`LOAD_TENANT_API_KEY` for that lane and add `pnpm run verify:load` to the
job. The runner SKIPs when k6 is missing or the target is offline — the
gate never blocks a CI run on environments that cannot satisfy its
prerequisites.
## Composite "all gates" run
There is no single mega-script — gates are intentionally separate so CI can
parallelise. The canonical CI order is asserted by
`verify:gitea-ci-workflow` against `.gitea/workflows/ci.yml`. To dry-run the
local subset:
```sh
pnpm run verify:ci-baseline
pnpm run verify:env-drift
pnpm run verify:health-baseline
pnpm run verify:i18n
pnpm run verify:hardcoded-strings
pnpm run verify:phase-c-boundaries
pnpm run verify:api-inventory
pnpm run verify:openapi-security
pnpm run verify:tenant-isolation-report
pnpm run verify:docker-baseline
pnpm run verify:docker-non-root
pnpm run verify:secrets
pnpm run verify:eslint-baseline
pnpm run verify:test-density
pnpm run verify:bundle-size
pnpm run verify:test-coverage
pnpm typecheck
pnpm lint
pnpm test
```
Any failure here is a CI failure. Anti-regression gates lock the current
ratchet; hard contracts assert immutable structural invariants. Adopt and
extend gates rather than disable them.
@@ -0,0 +1,167 @@
# Boilerplate Version Tracking
## Purpose
This guide defines how maintainers and downstream projects record the boilerplate baseline they adopted, how they review drift from that baseline, and how they preserve upgrade evidence without inventing runtime migrations. It complements the SemVer policy in [docs/versioning.md](./versioning.md), the upgrade path in [docs/upgrade.md](./upgrade.md), release entries in [docs/releases/README.md](./releases/README.md), the migration registry in [docs/migration-registry.md](./migration-registry.md), the documentation porting guide in [docs/porting-guide.md](./porting-guide.md), post-port verification in [docs/porting-verification.md](./porting-verification.md), and the public [CHANGELOG.md](../CHANGELOG.md).
Version tracking is documentation evidence. It proves that a downstream project recorded a reviewed source version, release entry, drift review, and verification summary; it is not package publish proof, artifact provenance, deployment proof, or migration execution proof unless it links to immutable artifact, tag, or deployment evidence that is separately reviewed and redacted.
## SemVer source version
The source boilerplate version is the upstream boilerplate release that downstream work starts from or last reconciled against. Use the SemVer decision rules in [docs/versioning.md](./versioning.md) and the matching release note in [CHANGELOG.md](../CHANGELOG.md) before classifying a baseline as patch, minor, or major.
Record the source version with placeholder-only fields such as:
| Field | Safe placeholder format | Notes |
| --- | --- | --- |
| Source version label | `<source-version-label>` | Example shape only; use the released SemVer label when the record is finalized. |
| Source release entry path | `<docs/releases/vMAJOR.MINOR.PATCH.md>` | Link to a local release entry under [docs/releases/README.md](./releases/README.md) when available. |
| Source changelog heading | `<CHANGELOG.md#version-heading>` | The heading must match [CHANGELOG.md](../CHANGELOG.md). |
| Source commit SHA label | `<source-commit-sha-label>` | Store a short or full SHA label only after it is public to the project and safe to disclose. |
| Source artifact label | `<artifact-or-tag-label>` | Optional; do not imply artifact proof unless the tag, digest, or signed artifact record exists. |
Do not use this record to claim that a package was published, an image was pushed, a database migration ran, or a production deployment succeeded. Those claims require their own immutable evidence and manual runtime notes where applicable.
## Tracked fields
Each downstream baseline record should include these fields with redacted, placeholder-only values until the release owner fills them with safe evidence:
| Field | Required value | Redaction rule |
| --- | --- | --- |
| Downstream project label | `<downstream-project-label>` | Use a generic project label, not a tenant, customer, or private domain. |
| Downstream baseline version | `<downstream-baseline-version>` | Name the adopted boilerplate SemVer label or reviewed release train. |
| Adopted release entry | `<docs/releases/release-entry.md>` | Use a resolvable local Markdown link when the entry exists. |
| Changelog alignment | `<changelog-entry-label>` | Point to the Keep a Changelog entry that describes the baseline. |
| Commit SHA label | `<commit-sha-label>` | Record the label only; avoid private repository URLs with credentials. |
| Migration registry IDs | `<migration-registry-id-list>` | Use IDs from [docs/migration-registry.md](./migration-registry.md); do not invent runtime database migrations for docs-only drift. |
| Drift review status | `<drift-review-status>` | Summarize whether downstream differences are accepted, pending review, or blocked. |
| Verification command names | `<verification-command-list>` | Name supported non-E2E commands only. |
| Manual runtime status | `<manual-runtime-status>` | State whether manual runtime review is not required, pending, passed, failed, or separately recorded. |
| Rollback note label | `<rollback-note-label>` | Link to release or deployment rollback notes when the baseline affects runtime behavior. |
Allowed values are labels, local file paths, command names, release names, registry IDs, and redacted statuses. Prohibited values include secrets, URLs with credentials, UUID-like deploy endpoints, tenant identifiers, customer identifiers, provider payloads, raw environment values, cookies, bearer tokens, private registry credentials, database connection strings, Redis connection strings, webhook secrets, and screenshots containing sensitive data.
## Downstream version ledger
Downstream projects should keep one ledger row per adopted source baseline or material drift review. The ledger belongs in downstream documentation, while this guide defines the contract it should follow.
Suggested ledger shape:
| Field | Value |
| --- | --- |
| Record ID | `<version-record-id>` |
| Source version label | `<source-version-label>` |
| Downstream baseline version | `<downstream-baseline-version>` |
| Adopted release entry | `<docs/releases/release-entry.md>` |
| Commit SHA label | `<commit-sha-label>` |
| Migration registry IDs | `<migration-registry-id-list>` |
| Drift review status | `<drift-review-status>` |
| Verification command names | `<verification-command-list>` |
| Manual runtime status | `<manual-runtime-status>` |
| Rollback note label | `<rollback-note-label>` |
A ledger row may be marked complete only when its links resolve, its SemVer classification matches [docs/versioning.md](./versioning.md), its changelog reference matches [CHANGELOG.md](../CHANGELOG.md), and its release entry or downstream handoff notes explain any manual runtime boundary.
## Changelog alignment
Every baseline record must point back to an adopted release entry and the corresponding changelog entry. Use [docs/releases/README.md](./releases/README.md) for release-entry structure and [CHANGELOG.md](../CHANGELOG.md) for the public release summary.
Alignment means:
- Breaking changes in the baseline have migration steps, rollback limits, and verification evidence.
- Minor and patch changes do not silently introduce downstream work outside their SemVer class.
- Empty Keep a Changelog categories remain explicit instead of using unfinished marker text.
- Drift review notes explain whether downstream differences are accepted local customizations or pending upgrade work.
- Release artifact or tag references are immutable labels when they are used as proof.
Version records must not replace release notes. They are an index that lets downstream maintainers find the source release, compare local drift, and decide which upgrade notes apply.
## Update workflow
Use this workflow whenever a downstream project adopts a new boilerplate baseline or refreshes drift against an existing baseline:
1. Confirm the source release and SemVer class using [docs/versioning.md](./versioning.md) and [CHANGELOG.md](../CHANGELOG.md).
2. Confirm the adopted release entry under [docs/releases/README.md](./releases/README.md) and capture only a local release-entry path or release label.
3. Compare downstream documentation and runtime boundaries against [docs/porting-guide.md](./porting-guide.md) and [docs/porting-verification.md](./porting-verification.md).
4. Review any migration or docs-only tracking IDs in [docs/migration-registry.md](./migration-registry.md).
5. Record the drift review status, verification command names, and manual runtime status using placeholder-safe labels.
6. Run supported automatic non-E2E checks when the files exist and the package manager is pinned through `pnpm@10.33.0`.
7. Link rollback notes from the release entry, upgrade guide, or downstream handoff record when runtime behavior, data, deployment, or security posture can be affected.
Supported automatic checks for this documentation contract are:
```sh
pnpm run verify:release-docs
pnpm run verify:docs-links
```
These checks are local, static, non-E2E verification. They do not drive a browser, open provider consoles, run live deployments, push registries, or prove production readiness.
## Downstream project handoff
A downstream handoff should include a short baseline summary, the ledger row, the chosen upgrade path, and the evidence boundary. The handoff should tell the next maintainer what was adopted, what drift remains, which release entry governs the change, and which checks were automatic versus manual.
Minimum handoff content:
- Source boilerplate version label and downstream baseline version.
- Adopted release entry path and changelog entry label.
- Migration registry ID list or a clear statement that no runtime migration registry entry applies.
- Drift review status with accepted local differences separated from pending upgrade work.
- Verification command names, exit status labels, and redacted diagnostic labels.
- Manual runtime status, owner label, and date label when manual review is required.
- Rollback note label that names whether rollback is redeploy-only, migration-sensitive, backup-dependent, or unsupported.
The handoff must avoid tenant names, customer names, private hostnames, provider request payloads, raw environment values, tokens, passwords, and URLs that embed credentials. If a downstream operator needs those values, reference the environment-owned secret store by role label rather than copying values into Markdown.
## Compatibility with SemVer/changelog/release entries
The version ledger is compatible with SemVer, changelog, and release-entry records because it stores references rather than redefining policy. [docs/versioning.md](./versioning.md) defines the meaning of major, minor, and patch. [CHANGELOG.md](../CHANGELOG.md) gives the public release summary. [docs/releases/README.md](./releases/README.md) defines the operator evidence expected around the release.
If these sources disagree, do not finalize the downstream baseline. Correct the release documentation first, then update the version record. For example, a downstream record cannot classify a breaking database behavior change as patch if the versioning policy requires a major release decision and rollback notes.
## Migration-registry relationship
Version tracking may reference migration registry IDs from [docs/migration-registry.md](./migration-registry.md), but it does not create migrations by itself. Documentation-only baseline records should stay marked as docs evidence unless a real database, deployment, configuration, API, worker, provider, or security migration exists.
Use migration registry IDs for ownership, status, rollback, and verification labels. Use the version ledger for adopted baseline and drift history. When a migration is runtime-affecting, the registry owns the migration detail and this guide should only link the ID and summarize the redacted status.
## Upgrade and rollback notes
Upgrade notes should point to [docs/upgrade.md](./upgrade.md), the adopted release entry, and any downstream handoff record. They should explain whether the downstream project is adopting a patch, minor, or major baseline and whether additional drift work is required before rollout.
Rollback notes must state the safest known path:
- Redeploy a prior immutable artifact or tag when no data or configuration migration changed.
- Restore from backup or forward-fix when database state changed and downgrade is not automatic.
- Pause or drain worker queues when queue behavior affects safe rollback.
- Treat provider, security, and mail changes as manual runtime concerns with redacted request or status labels only.
Do not present a version record as rollback proof. It is a pointer to the release and upgrade evidence that defines rollback behavior.
## Redacted evidence
Record only safe, reviewable evidence:
- Version labels, release-entry paths, changelog labels, migration registry IDs, and command names.
- Commit SHA labels and artifact or tag labels that are safe for the project to disclose.
- Exit code labels, pass or fail labels, redacted diagnostic codes, and manual runtime status labels.
- Owner role labels and date labels when downstream policy requires them.
Do not record secrets, URLs with credentials, UUID-like deploy endpoints, tenant identifiers, customer identifiers, customer data, provider payloads, raw environment values, cookies, bearer tokens, API keys, private registry credentials, database URLs, Redis URLs, webhook token paths, or full environment dumps.
If evidence arrives with sensitive values, redact it before adding it to the ledger. If redaction would remove the fact needed to prove the claim, link to an approved secure evidence store by role label and keep only the non-secret status in Markdown.
## Non-E2E verification
The verifier from the Release Docs release-documentation slice rejects missing required tracking terms, unresolved local links, forbidden placeholder markers, automatic browser or E2E command recommendations, and secret-shaped values. Documentation-only verification reads Markdown locally and does not need runtime services.
For this guide, use:
```sh
test -s docs/boilerplate-version-tracking.md
grep -n "^## Tracked fields" docs/boilerplate-version-tracking.md
pnpm run verify:release-docs
pnpm run verify:docs-links
```
Browser journeys, Playwright, Cypress, provider-console checks, live Docker runs, registry pushes, Coolify execution, production deploys, and customer-impacting request paths are manual runtime evidence only unless a human explicitly approves the exact focused run. When manual review is needed, record the manual runtime status, owner role, target label, immutable tag or request label, and redacted outcome only.
+97
View File
@@ -0,0 +1,97 @@
# GDPR Data Inventory
## Scope
This inventory documents the current data-protection-relevant surfaces in the boilerplate for follow-up work in Compliance Retention. It is a repository evidence map, not legal advice and not an implemented retention schedule. Entries are grounded in the current security and production evidence boundaries from `docs/security.md` and `docs/production-checklist.md`, the Compliance Retention baseline inventory report, and Prisma schema surfaces under `prisma/schema`.
The inventory covers user, tenant, authentication, API, file, entity-row, import/export, notification, billing, audit, email, analytics, worker, and optional plugin-candidate surfaces that can contain personal data or personal-data-adjacent operational evidence. Unknown retention periods are listed as implementation gaps for later Compliance Retention slices rather than invented durations.
## Data categories
| Category | Current examples | Primary schema evidence | Sensitivity notes |
| --- | --- | --- | --- |
| Account identity | User email, first name, last name, phone, avatar, locale, OAuth identifiers, active status, default tenant | `User`, `AdminUser`, `TenantUser`, `TenantUserInvitation`, `Registration` in `prisma/schema/accounts.prisma` | Direct personal data; invitations and registrations may contain people who never complete signup. |
| Authentication and MFA | Password hash, verification token, encrypted TOTP secrets, MFA recovery code hashes, MFA timestamps, session-adjacent identity relationships | `User` in `prisma/schema/accounts.prisma` | Credentials and recovery material are security-sensitive and must not appear in support evidence. |
| Tenant and organization profile | Tenant slug, name, icon, logo, active/deactivated state, tenant type, tenant membership status | `Tenant`, `TenantType`, `TenantUser` in `prisma/schema/accounts.prisma` | Tenant names, slugs, and branding can identify customers and must be redacted from shared evidence. |
| API keys and API access logs | API key alias, key hash/prefix/last-four, expiry, active status, endpoint, method, params, IP, duration, error, tenant/user linkage | `ApiKey`, `ApiKeyLog`, `TenantIpAddress`, `Log` in `prisma/schema/core.prisma` and `accounts.prisma` | API key values, hashes, prefixes, params, and IPs are sensitive; only generic status and request IDs are safe in docs. |
| Files and media | File names, object keys, content types, checksums, public URLs, upload/download timestamps, row media, email attachments | `FileObject`, `FileUploadProgress`, `FileChunk`, `RowMedia`, `EmailAttachment` | Object keys, URLs, filenames, and attachments can expose customer content or identifiers. |
| Dynamic entity data | Entity row values, comments, tasks, tags, permissions, relationships, formulas, custom fields, tenant settings rows | `Row`, `RowValue`, `RowComment`, `RowTask`, `RowPermission`, `RowRelationship`, `FormulaLog`, `TenantSettingsRow` in `prisma/schema/entityBuilder.prisma` | Entity rows are tenant-defined and can contain arbitrary personal data. |
| Imports and error reports | Uploaded import source bucket/key, file name, content type, row counts, rejected row counts, error names/messages, error report bucket/key | `ImportJob` in `prisma/schema/imports.prisma` | Import files and rejection reports can contain bulk personal data. |
| Exports | Export jobs and generated files referenced by Compliance Retention baseline tests (`packages/db/src/export-jobs.server.test.ts`, `packages/exports/*`) | Export-related package tests and file-object storage surfaces | Export artifacts can aggregate tenant personal data and require explicit expiration/deletion follow-up. |
| Notifications | In-app/email delivery category, message key, status, provider message ID, payload metadata, sent/read timestamps, user preferences | `NotificationDelivery`, `UserNotificationPreference` in `prisma/schema/notifications.prisma` | Payload metadata and provider message IDs can identify users or message content. |
| Email and campaigns | Inbound/outbound addresses, names, subjects, text/html bodies, reads, CC recipients, senders, campaigns, opens, clicks, unsubscribe/bounce timestamps | `Email`, `EmailRead`, `EmailCc`, `EmailSender`, `Campaign`, `OutboundEmail`, `OutboundEmailOpen`, `OutboundEmailClick` in `prisma/schema/email.prisma` | Email bodies, addresses, tracking events, and provider configuration are high-risk personal-data surfaces. |
| Billing and subscriptions | Stripe customer/subscription/session identifiers, checkout email, from URL, usage records, credits, product entitlement state | `TenantSubscription`, `TenantSubscriptionProduct`, `TenantSubscriptionUsageRecord`, `CheckoutSessionStatus`, `Credit` in `prisma/schema/subscriptions.prisma` | Billing records can link identity, payment-provider identifiers, and usage. |
| Audit, events, metrics, and impersonation | Logs, event payloads, webhook attempts, metric logs, IP geolocation, impersonation actor/target, request path, metadata | `Log`, `Event`, `EventWebhookAttempt`, `MetricLog`, `IpAddress`, `IpAddressLog`, `ImpersonationAudit` | Audit data is needed for security but can include personal data, URLs, IPs, and payload fragments. |
| Optional plugin-candidate surfaces | Knowledge base, workflow, app engine, forms, portals, sites, prompt/AI, help desk, analytics, feature-flag, and onboarding surfaces | baseline report plus schema roots such as `prisma/schema/appsEngine.prisma`; additional schema files under `prisma/schema` | Treat as potentially personal until each plugin-candidate surface has a dedicated data review. |
## Processing surfaces
- Web, API, worker, and provider surfaces follow the redaction boundaries documented in `docs/security.md`; diagnostics may record route class, endpoint label, status, stable error code, request ID, job ID, queue name, and provider category only.
- Account and tenant flows process identity data for registration, invitation, authentication, organization membership, admin access, MFA, and impersonation audit trails.
- API key flows process credentials and access telemetry for tenant-scoped API usage, including endpoint, method, params, status, duration, error, IP, and tenant/user relationships.
- Entity-builder flows process tenant-defined rows, values, comments, tasks, tags, permissions, files, formulas, relationships, and custom schemas. These rows may contain arbitrary personal data because field semantics are tenant-defined.
- File, import, export, and email flows process uploaded documents, generated artifacts, object metadata, message bodies, attachments, campaign tracking events, and provider delivery identifiers.
- Worker, queue, notification, mail, analytics, metrics, app sync, and webhook flows process operational metadata and delivery state. Raw job payloads, provider payloads, webhook bodies, and source/destination sync data are not safe evidence.
- Billing and subscription flows process Stripe-linked customer/subscription/session identifiers, usage records, checkout emails, credits, and entitlement state.
## Storage evidence
| Surface | Evidence path | Current storage notes |
| --- | --- | --- |
| Users and tenants | `prisma/schema/accounts.prisma` | `User`, `Tenant`, `TenantUser`, `TenantUserInvitation`, `Registration`, `TenantIpAddress`, and `ImpersonationAudit` store identity, membership, invite, IP, and audit state. Most tenant/user relations cascade on delete, but impersonation audit uses `SetNull` for actor/target/tenant references. |
| Security and API telemetry | `prisma/schema/core.prisma` | `ApiKey`, `ApiKeyLog`, `Log`, `Event`, `EventWebhookAttempt`, `MetricLog`, `IpAddress`, and `IpAddressLog` store credential metadata, request traces, event payload strings, webhook attempts, metrics, and IP geodata. |
| Files and uploads | `prisma/schema/core.prisma`; `entityBuilder.prisma`; `email.prisma` | `FileObject`, `FileUploadProgress`, `FileChunk`, `RowMedia`, and `EmailAttachment` store object metadata, binary chunks, public URLs, and attachment content. External object-store deletion behavior needs implementation evidence. |
| Entity records | `prisma/schema/entityBuilder.prisma` | `Row` has `deletedAt` for soft deletion; many child tables use cascade relations. `RowComment` has `isDeleted`, while values, tasks, permissions, formulas, and relationships can retain personal data until deletion paths are enforced. |
| Imports | `prisma/schema/imports.prisma` | `ImportJob` stores source bucket/key, filename, content type, row counts, error messages, and error-report bucket/key. Source and error-report object retention is not defined here. |
| Notifications | `prisma/schema/notifications.prisma` | `NotificationDelivery` uses nullable tenant/user references with `SetNull`; `UserNotificationPreference` cascades with tenant/user. Payload metadata and provider IDs require redaction. |
| Email and campaigns | `prisma/schema/email.prisma` | Email, campaign, attachment, open/click, read, sender, and outbound records store addresses, names, message bodies, and delivery/tracking state. |
| Billing | `prisma/schema/subscriptions.prisma` | Subscription, checkout, usage, and credit records store payment-provider identifiers, checkout email, from URL, usage quantities, and tenant/user references. |
| App/plugin candidates | `prisma/schema/appsEngine.prisma`; other schema roots listed by baseline | App config, encrypted connection config, sync runs, logs, items, source/destination data, workflow, knowledge-base, portal, form, site, prompt/AI, help-desk, analytics, and feature-flag schemas need individual classification before extraction or removal work. |
## Export surfaces
- Package tests in the baseline identify existing export-related code paths in `packages/db/src/export-jobs.server.test.ts` and `packages/exports/src/*`. Later Compliance Retention export work should preserve those contracts while adding data-subject scoped inventory coverage.
- Entity rows, row values, row media, comments, tasks, tenant settings rows, email records, imports, notifications, audit logs, billing references, app sync items, and files are potential export inputs when tied to a user, tenant, or data subject request.
- Export artifacts must be treated as sensitive generated files. Evidence may name export job labels, file-object IDs, object-store class labels, status, row counts, request ID, and expiration status; it must not paste exported row data, email content, customer identifiers, object keys, signed URLs, or provider payloads.
- Export scope gaps for later slices: define data-subject selector behavior, tenant-admin export permissions, artifact expiration, object-store deletion, queue retry handling, and safe audit evidence for export completion/failure.
## Deletion and erasure surfaces
- Existing schema evidence includes cascade relationships for many tenant/user-owned records, `Row.deletedAt` for soft-deleted entity rows, `RowComment.isDeleted` for comment deletion state, and nullable `SetNull` references on notification delivery and impersonation audit records.
- User erasure must account for direct user records plus created rows, comments, tasks, entity views, notifications, preferences, API keys, tenant memberships, invitations, impersonation audit actor/target links, email reads, analytics sessions, metrics, formulas, workflows, apps, forms, portals, sites, and optional plugin-candidate surfaces.
- Tenant erasure must account for tenant profile, tenant users, invitations, API keys/logs, rows, row permissions, entity views, imports, exports, files, email/campaign data, notifications, billing/subscription records, app sync data, workflow data, portals, forms, sites, and object-store artifacts.
- Operational audit data may need restricted retention rather than immediate deletion when required for security, fraud, accounting, or legal defense. That policy is not implemented in this task and must be explicitly defined later.
- External providers and object stores require deletion hooks beyond database cascades: Mailjet/mail providers, Stripe, Redis/queue payloads, object storage, analytics providers, and deployment/runtime logs.
- Deletion evidence must be redacted to status labels, request IDs, job IDs, model/surface labels, counts, and redacted diagnostic codes. Do not include deleted data samples, emails, tenant names, object keys, signed URLs, API keys, webhook bodies, or raw queue payloads.
## Retention status
Repository-level retention defaults now live in [`docs/data-retention.md`](../data-retention.md). That document is the current default schedule for engineering follow-up work, not legal advice and not evidence that deletion automation already exists.
| Surface | Current default schedule | Remaining implementation gap |
| --- | --- | --- |
| Account, tenant, invitations, registration | Covered by account lifecycle, tenant lifecycle, and short-lived operational artifact defaults. | Implement account/tenant closure, inactive-account handling, invitation expiry/deletion, and registration cleanup jobs. |
| API keys and logs | Covered by account lifecycle and operational telemetry defaults. | Implement API telemetry TTL, API key revocation/deletion cleanup, parameter scrubbing, and secure diagnostic aggregation. |
| Entity rows and comments | Covered by tenant lifecycle defaults with a limited restoration window before hard deletion. | Implement hard-delete jobs, restoration rules, and child-table erasure behavior. |
| Files, imports, exports | Covered by short-lived operational artifact defaults. | Implement upload/import/export artifact expiration, signed URL limits, and object-store deletion verification. |
| Email, notifications, webhooks, queues | Covered by operational telemetry and short-lived operational artifact defaults. | Implement provider-side deletion, retry/dead-letter cleanup, payload redaction, and redacted evidence. |
| Billing and subscriptions | Covered by provider/accounting exception defaults. | Define approved accounting retention separately and ensure erasure workflows preserve only required minimal references. |
| Audit, impersonation, metrics, IP geolocation | Covered by security/audit evidence and operational telemetry defaults. | Implement security-retention basis, minimization, aggregation, IP truncation or deletion, and access controls. |
| Plugin-candidate surfaces | Covered by plugin-candidate review defaults. | Classify each candidate before migration, deletion, export, or pause/resume implementation. |
## Operational redaction rules
- Follow `docs/security.md` and `docs/production-checklist.md`: automatic evidence is local/static unless a human explicitly approves a runtime check; browser, provider-console, Docker, registry, Coolify, production traffic, Playwright, and Cypress activities remain manual records.
- Safe evidence includes model or surface labels, route or endpoint labels, status, stable error code, request ID, job ID, queue name, counts, command name, exit code, redacted diagnostic code, and pass/fail labels.
- Unsafe evidence includes secrets, bearer tokens, cookies, API keys, key hashes/prefixes/last-four, raw authorization headers, tenant identifiers, customer domains, customer names, email addresses, phone numbers, IP addresses, object keys, signed URLs, webhook signatures, provider payloads, queue payloads, raw logs, screenshots with personal data, import/export contents, and email/message bodies.
- Examples in compliance docs must stay placeholder-shaped and must not include realistic personal data, tenant slugs, domains, UUID-like deploy endpoints, provider IDs, or secret-shaped strings.
- If sensitive evidence is accidentally captured, remove it from shared records, rotate credentials where applicable, and record only containment status, affected surface label, and redacted remediation facts.
## Downstream follow-ups
- Implement focused verification that rejects missing data-protection inventory sections and placeholder markers while staying non-E2E.
- Define data-subject export scope across users, tenants, entity rows, files, imports, exports, email, notifications, audit data, billing references, and plugin-candidate surfaces.
- Define erasure workflows for user and tenant deletion, including soft delete, hard delete, object-store cleanup, provider deletion, queue cleanup, and audit-minimization exceptions.
- Define retention policy defaults for accounts, invitations, registrations, API logs, entity rows, imports, exports, files, emails, notifications, webhooks, queues, metrics, IP geodata, billing records, and audit trails.
- Add operational runbooks for redacted data-protection request evidence, approval boundaries, failure handling, retry behavior, and escalation when provider-side deletion is blocked.
- Classify optional plugin-candidate surfaces before extracting, pausing, exporting, or deleting them, especially knowledge base, workflows, app engine, forms, portals, sites, prompt/AI, help desk, analytics, and onboarding data.
+114
View File
@@ -0,0 +1,114 @@
# Configuration
This project keeps runtime configuration explicit and treats secret diagnostics
as a
public surface. Application code should receive configuration through typed
loaders, provider options, or handler dependencies instead of reading
`process.env` inside package internals.
## Explicit environment access
Only runtime entrypoints should read from `process.env`. Worker-owned
environment reads are limited to the worker runtime and health entrypoints.
Those entrypoints immediately validate values and convert them into explicit
dependencies for queues, Mailjet, browser runtime integrations, and
handlers.
Package adapters must accept an environment object or concrete provider options
from their caller. This keeps hidden environment coupling out of packages such
as `packages/mail` and `packages/queue`, makes tests deterministic, and lets
diagnostics report configuration status without exposing raw secret values.
## Worker enablement
The worker is opt-in. Set `WORKER_ENABLED=true` only on deployments that should
start queue processors. In multi-resource platforms such as Coolify, do not put
`WORKER_ENABLED=false` into a shared production environment group that is also
injected into the worker resource; leave it unset for web/API resources and set
it explicitly to `true` on each worker resource.
When the worker is enabled, provide queue configuration explicitly:
- `WORKER_QUEUE_GROUP` must be one of `general`, `mail`, `import`, `webhook`,
or `plugin` for grouped worker resources.
- `WORKER_HEALTH_PORT` and `WORKER_ADMIN_PORT` should be unique per Coolify
worker app when those endpoints are exposed.
- `BULLMQ_REDIS_URL` is the worker Redis URL.
- `REDIS_URL` is accepted only as a fallback when `BULLMQ_REDIS_URL` is absent.
If the worker is disabled, queue processors should not start. Health and runtime
diagnostics may still report enablement and readiness status, but they must not
include the Redis URL value.
## Mailjet configuration
Mailjet is the transactional mail provider boundary for API and worker
runtimes. Mail delivery requires the provider key and verified sender domain
address to be configured together:
- `MAILJET_API_KEY` is the Mailjet API token.
- `MAILJET_FROM_EMAIL` is the default sender email from a verified Mailjet
domain.
- `MAILJET_FROM_NAME` is an optional sender display name.
Treat `MAILJET_API_KEY` and `MAILJET_FROM_EMAIL` as a required pair
whenever Mailjet delivery is enabled. Supplying only one of the pair is
invalid configuration because the provider cannot safely send mail without both
authentication and an approved sender address.
## Browser runtime configuration
Worker browser runtime settings are explicit worker configuration, not
package-global state:
- `BROWSER_RUNTIME_URL` identifies the browser runtime service endpoint when
that integration is enabled.
- `BROWSER_RUNTIME_API_KEY` authenticates requests to the browser runtime
service.
- `BROWSER_SESSION_TIMEOUT_MS` configures the session timeout and must parse as
a valid millisecond duration.
Browser runtime diagnostics should report key names, parse status, and
diagnostic codes only. They must never include endpoint secrets, API keys,
tokens, or raw values.
## Safe diagnostics and redaction
Configuration validation must use the central redacted secret validator for
required and optional secret-like values. Errors, health JSON, and logs may
include:
- environment key names;
- diagnostic codes;
- parse status;
- high-level messages;
- queue readiness or worker enablement status.
They must not include raw secret values. This applies to Redis URLs, database
URLs, API keys, tokens, Mailjet sender secrets, browser runtime credentials,
and any future provider secret.
When reporting invalid or missing configuration, prefer messages such as
`missing required env key MAILJET_API_KEY` or
`invalid duration for BROWSER_SESSION_TIMEOUT_MS`. Do not interpolate the
rejected value into thrown errors, logs, or health responses.
## Seed Credentials
Local demo users and demo tenants are opt-in through `SEED_DEMO_DATA=true` and
are refused in production. Production bootstrap seeding requires
`SEED_ADMIN_EMAIL` and `SEED_ADMIN_PASSWORD`, creates only the configured admin
plus roles, permissions, and entitlements, and refuses local default seed emails
or the default `password`.
## Adding new configuration
When adding a new runtime setting:
1. Decide which runtime entrypoint owns the environment read.
2. Validate and normalize the value before passing it to packages or handlers.
3. Use the redacted secret validator for secret-like required or optional values.
4. Pass normalized values through explicit options or dependencies.
5. Add tests that assert diagnostics expose keys and codes, not raw values.
6. Update this document and the environment inventory when the setting is
user-facing.
+203
View File
@@ -0,0 +1,203 @@
# CLI ops (`toolctrl ops`)
The operator CLI under `packages/cli/` ships three ops sub-commands
for day-to-day operational work:
- `toolctrl ops doctor` — local environment health check
- `toolctrl ops migrate-tenant` — resumable tenant migration between envs
- `toolctrl ops impersonate` — audit-logged debug magic-link for a tenant user
All three commands accept the `--env` flag to target a named env file
under `~/.toolctrl/envs/`. The default is the current working-dir's
`.env` (i.e. whatever is already loaded into `process.env`).
## Env-file layout
Named envs live under:
```
~/.toolctrl/envs/<name>.env
```
Example layout:
```
~/.toolctrl/envs/
├── staging.env # chmod 600
├── staging-backup.env
└── dev.env
```
- File format: standard `KEY=VALUE` `.env` syntax with `#` comments.
No shell interpolation, no multi-line values.
- File permissions must be `0600` (owner-only). The CLI emits a warning
when world-readable bits are set; rotate the file with
`chmod 600 ~/.toolctrl/envs/<name>.env`.
- Production env files **must not** live in CI. The `impersonate`
command refuses any env named `prod`, `production`, or `live`
regardless of file content.
## `toolctrl ops doctor`
Runs eight probes against the resolved env and emits an ok/warn/fail
table:
| Probe | Description | Fail hint |
|---|---|---|
| `node.version` | Process Node version >=20 | `nvm use` |
| `pnpm.version` | `pnpm --version` >=9 | `corepack enable` |
| `docker.reachable` | `docker info` succeeds | `docker compose up -d` |
| `postgres.reachable` | DATABASE_URL ping (injected) | Check `docker compose ps postgres` |
| `redis.reachable` | REDIS_URL ping (injected) | `docker compose up -d redis` |
| `sentry.dsn` | HEAD against SENTRY_DSN origin | Check Coolify Sentry container |
| `env.required` | Required env keys present | Update your `.env` |
| `prisma.migrations` | `prisma migrate status` (injected) | `pnpm db:migrate dev` |
Examples:
```bash
# Default — read local .env
node packages/cli/bin/toolctrl-cli.mjs ops doctor
# Read ~/.toolctrl/envs/staging.env
node packages/cli/bin/toolctrl-cli.mjs ops doctor --env=staging
# JSON output for CI
node packages/cli/bin/toolctrl-cli.mjs ops doctor --json
```
**Exit codes:** `0` when every probe is `ok` or `warn`; `1` when any
probe reports `fail`.
### Troubleshooting
| Symptom | Likely cause |
|---|---|
| `postgres.reachable: fail (DATABASE_URL not set)` | Forgot to export `.env` before running CLI |
| `docker.reachable: warn` | Docker Desktop not running |
| `sentry.dsn: warn` | SENTRY_DSN intentionally unset — error reporting disabled |
| `env.required: fail Missing required env vars: SESSION_SECRET` | Bootstrap your `.env` from `.env.example` |
| `prisma.migrations: warn N pending` | Run `pnpm db:migrate dev` |
## `toolctrl ops migrate-tenant`
End-to-end tenant migration between named envs. Reuses the
`packages/cli/src/commands/tenant/snapshot.ts` infrastructure from
phase 03 (#44). Resumable via a state file under
`~/.toolctrl/migrations/<tenantId>-<timestamp>.state.json`.
Steps (each is an atomic resume-point):
1. `preflight` — both envs resolve, names validated
2. `snapshot` — `tenant snapshot` exports source tenant to a local archive
3. `restore` — archive is restored into the target env
4. `verify` — row-counts compared source vs target
5. `cleanup` — snapshot kept on disk for audit
Examples:
```bash
# Plan a migration (no writes)
node packages/cli/bin/toolctrl-cli.mjs ops migrate-tenant tenant-abc \
--from=staging --to=staging-backup --dry-run
# Real migration
node packages/cli/bin/toolctrl-cli.mjs ops migrate-tenant tenant-abc \
--from=staging --to=staging-backup
# Resume after a crash
node packages/cli/bin/toolctrl-cli.mjs ops migrate-tenant tenant-abc \
--from=staging --to=staging-backup \
--resume ~/.toolctrl/migrations/tenant-abc-2026-05-17T120000000Z.state.json
```
The CLI emits a per-step status line:
```
state-file=/home/op/.toolctrl/migrations/tenant-abc-2026-05-17T120000000Z.state.json
preflight: ok — from=staging to=staging-backup
snapshot: ok — /home/op/.toolctrl/migrations/tenant-abc-….tar.gz
restore: ok — restored 12345 rows
verify: ok — row-counts match
cleanup: ok — snapshot kept on disk for audit
```
The Prisma + S3 backends are injected at the `bin/toolctrl-cli.mjs`
entrypoint. The standalone subcommand prints a warning if invoked
without that injection (no destructive operations happen).
## `toolctrl ops impersonate`
Generates a 15-minute HMAC-signed magic-link the engineer can paste
into a browser to debug a specific tenant user. Every issued link
writes an `AuditEvent` so the action is reviewable.
```bash
node packages/cli/bin/toolctrl-cli.mjs ops impersonate \
tenant-abc user-123 \
--env=staging \
--reason="support-ticket-456"
```
**Safety guarantees:**
- Production env names (`prod`, `production`, `live`) are hard-blocked.
No `--force` bypass exists. Use the admin self-service impersonate
UI for production.
- TTL is fixed at 15 minutes. Tokens cannot be longer-lived.
- HMAC signing key must be at least 16 characters (the CLI throws
otherwise).
- The audit-event captures:
- `actorType = admin`
- `action = ops.impersonate.issued`
- `targetType = user`, `targetId = <userId>`
- `regulatoryScope = [soc2]`
- `dataSensitivity = pii`
- `outcome = success`
- `metadata = { envName, tokenId, expiresAt, ttlMinutes, reason, userEmail }`
The audit-event is written before the link is returned, so even a
crashing operator workflow leaves a trail.
### Backend route
The corresponding loader at
`apps/web/app/routes/api.auth.impersonate.tsx` (to be wired in a
follow-up phase) is responsible for:
1. Verifying the HMAC signature against the same secret
2. Looking up the audit-event by `tokenId` and refusing if absent
3. Refusing if the audit-event's `outcome ≠ success`
4. Setting a session cookie scoped to the impersonated user with a
short lifetime
## Where things live
```
packages/cli/src/commands/ops/
├── _env.ts # env-file resolver (local vs named)
├── _env.test.ts
├── doctor.ts # doctor command + 8 probes
├── doctor.test.ts
├── migrate-tenant.ts # 5-step resumable migration
├── migrate-tenant.test.ts
├── impersonate.ts # HMAC magic-link + audit-event
├── impersonate.test.ts
└── index.ts # citty subcommand group
```
The bin entrypoint in `packages/cli/bin/toolctrl-cli.mjs` is responsible
for injecting the live Prisma client + S3 client + URL base into the
commands. The standalone files keep zero dependencies on `@prisma/client`
or AWS SDK so the package builds without optional peers.
## Security considerations
- **Never commit `~/.toolctrl/envs/*.env`.** The path is outside the
repo on purpose.
- The `migrate-tenant` state files contain tenant IDs but no secrets.
They are safe to retain for audit.
- Snapshot archives may contain PII. Apply the standard PII-retention
policy (chmod 600, delete after migration audit window).
- The `impersonate` audit-event is admin-visible and forms part of
the soc2 audit trail. Do not redact it after the fact.
@@ -0,0 +1,163 @@
# CLI scaffolding (`toolctrl create` / `toolctrl add`)
The operator CLI under `packages/cli/` ships scaffolding generators that
emit minimal, opinionated skeletons for the most common boilerplate
chores. Each generator writes to **new paths only** and supports a
`--dry-run` flag for testability.
## Quickstart
```bash
# from the repo root, after `pnpm install`
pnpm --filter @toolctrl/cli build
# show the full command tree
node packages/cli/bin/toolctrl-cli.mjs --help
```
## Command reference
### `toolctrl create module <name>`
Scaffolds a feature-module skeleton under `apps/web/app/modules/<name>/`:
- `db/<name>.db.server.ts` — tenant-scoped CRUD helpers
- `services/<Name>Service.ts` — service layer with tenant guards
- `components/<Name>List.tsx` — list UI stub
- `<name>.db.server.test.ts` — vitest smoke test
- `README.md` — manual follow-up checklist
- `apps/web/app/routes/app.$tenant.<name>.tsx` — React-Router-v7 tenant route
Flags:
- `--permission-prefix <prefix>` — override the permission key prefix.
- `--dry-run` — print the would-be writes without touching the filesystem.
Example:
```bash
node packages/cli/bin/toolctrl-cli.mjs create module inventory
```
Manual follow-up (the README in the generated module repeats this):
1. Add a Prisma model and run `pnpm prisma migrate dev`.
2. Replace the `TODO` markers in the db layer.
3. Add the permission keys to
`packages/db/src/permissions-seed.server.ts` (see
`toolctrl add permission` below).
4. Run `pnpm typecheck` to generate the route types.
### `toolctrl create route <filename>`
Scaffolds a single React-Router-v7 route file at
`apps/web/app/routes/<filename>.tsx` plus a co-located vitest stub.
Flags:
- `--top-level` — generate a route without the `:tenant` param.
- `--dry-run` — print the would-be writes without touching the filesystem.
Example:
```bash
node packages/cli/bin/toolctrl-cli.mjs create route app.\$tenant.dashboard
```
The generated file imports `Route` types from `./+types/<filename>`
(NEVER relative paths) and contains loader, action, default component
and an `ErrorBoundary` export. Run `pnpm typecheck` after generation
to populate the `./+types/` directory.
### `toolctrl create entity <name>`
Emits a Prisma model snippet plus a timestamped migration directory:
- `prisma/_generated/<name>.prisma` — model snippet to paste into
`prisma/schema.prisma`
- `prisma/migrations/<timestamp>_create_entity_<name>/migration.sql` —
placeholder SQL
- `prisma/migrations/<timestamp>_create_entity_<name>/README.md` —
step-by-step apply instructions
The generator does NOT modify `prisma/schema.prisma` automatically.
After generation:
```bash
# paste the model from prisma/_generated/<name>.prisma into prisma/schema.prisma
pnpm prisma migrate dev --name create_entity_<name>
pnpm prisma generate
```
### `toolctrl add hook <name>`
Scaffolds a plugin-hook registration file under
`apps/web/app/hooks/_generated/`. The output is a **forward-compatible
stub** for generated hook registration. The plugin runtime and
`@toolctrl/plugin-sdk` now exist; keep generated hooks on the scaffolded
no-op shape until a focused task wires that specific hook into the runtime.
Example:
```bash
node packages/cli/bin/toolctrl-cli.mjs add hook onContactCreated
```
### `toolctrl add event-type <name>`
Scaffolds an EntityWebhook event-type definition with a zod schema, an
example payload, and an async handler stub under
`apps/web/app/modules/workflowEngine/eventTypes/_generated/`. Event
names MUST be dotted lowercase, e.g. `crm.contact.created`.
Example:
```bash
node packages/cli/bin/toolctrl-cli.mjs add event-type crm.contact.created
```
### `toolctrl add permission <name>`
Scaffolds two fragments for a new permission key:
- `packages/db/src/_generated-permissions/<key>.ts` — seed fragment
(paste into `defaultPermissionSeeds`).
- `apps/web/app/utils/permissions/_generated/<key>.ts` — frontend
permission-mapper stub.
The generator intentionally does NOT patch the hand-curated
`defaultPermissionSeeds` array in `permissions-seed.server.ts`. The
README notes spell out the manual paste step.
Example:
```bash
node packages/cli/bin/toolctrl-cli.mjs add permission app.inventory.view
```
## Safety guarantees
- Generators **never** overwrite existing files. Re-running a generator
with the same name throws and exits non-zero.
- All generators support `--dry-run`, which prints the would-be writes
without touching the filesystem.
- Generators only produce new paths inside the repo (or
`prisma/_generated/` snippets that you paste in by hand). The
hand-curated source-of-truth files (`schema.prisma`,
`permissions-seed.server.ts`, `routes.ts`) are never patched
automatically.
## Adding a new generator
1. Create `packages/cli/src/commands/{create,add}/<command>.ts` with a
`generate<Name>` function and a `defineCommand` export.
2. Re-use the helpers from `_lib/scaffold.ts`:
- `validateIdentifier`, `pascalCase`, `kebabCase`, `snakeCase`
- `createScaffoldResult` + `writeScaffoldFile`
3. Add a vitest suite next to the command file that asserts:
- The expected file paths (in dry-run mode).
- Content invariants (e.g. correct import paths).
- Refuses to overwrite existing files.
4. Register the command in the relevant `create/index.ts` or
`add/index.ts` and update the root `index.test.ts` to keep the
registry shape pinned.
@@ -0,0 +1,190 @@
# i18n Audit — Baseline 2026-05-25
> Generated by `scripts/i18n-audit.mjs`. Counts hardcoded user-facing
> strings in `apps/web/app/{routes,modules,routeModules,components}/**/*.{ts,tsx}`.
> Baseline only — not yet
> enforced. See `docs/contributing/i18n.md` for the migration playbook.
>
> Historical baseline note: this file is a point-in-time audit artifact from
> 2026-05-25. Re-run the audit script before using counts as current status.
>
> Current re-run note: `node scripts/i18n-audit.mjs --json` on 2026-05-29
> reports `0` hardcoded strings and `0` locale-formatting signals.
## Current Re-Run Snapshot
| Metric | Value |
| ---------------------------------------------- | ----: |
| Files scanned | 1774 |
| Files with hardcoded strings | 0 |
| Total hardcoded JSX text fragments | 0 |
| Files with locale-sensitive formatting signals | 0 |
| Total locale-sensitive formatting signals | 0 |
## Historical Baseline Summary
| Metric | Value |
| ---------------------------------------------- | ----: |
| Files scanned | 1752 |
| Files with hardcoded strings | 4 |
| Total hardcoded JSX text fragments | 26 |
| Files with locale-sensitive formatting signals | 91 |
| Total locale-sensitive formatting signals | 213 |
## Historical Top Offenders
| File | Hits |
| ----------------------------------------------------------------------- | ---: |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts` | 22 |
| `apps/web/app/routes/__test.client-throw.tsx` | 2 |
| `apps/web/app/components/ai-elements/open-in-chat.tsx` | 1 |
| `apps/web/app/modules/workflowEngine/utils/DefaultWorkflowTemplates.ts` | 1 |
## Historical Disposition
The 26 hardcoded text fragments from the 2026-05-25 baseline are no longer
reported by the current audit. The historical dispositions below explain why
they were not treated as active product UI debt at the time:
| File | Disposition |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts` | Seed/template article content. Migrate only if knowledge-base templates gain locale variants. |
| `apps/web/app/routes/__test.client-throw.tsx` | Test-only Sentry smoke route. Keep diagnostic copy literal. |
| `apps/web/app/components/ai-elements/open-in-chat.tsx` | Provider/brand name (`Scira AI`). Proper noun, do not translate. |
| `apps/web/app/modules/workflowEngine/utils/DefaultWorkflowTemplates.ts` | English-scoped default workflow payload. Migrate only if workflow templates gain locale variants. |
## Detail (first 100 hits)
| File:Line | Hardcoded text |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `apps/web/app/components/ai-elements/open-in-chat.tsx:45` | Scira AI |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:120` | Watch the demo video: |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:122` | Watch the demo video: |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:142` | Static Knowledge Base |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:142` | And the knowledge base |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:142` | Dynamic Knowledge Bases |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:142` | For all other knowledge bases, the |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:142` | You can change the hard-coded |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:144` | Static Knowledge Base |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:144` | And the knowledge base |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:144` | Dynamic Knowledge Bases |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:144` | For all other knowledge bases, the |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:144` | You can change the hard-coded |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:163` | Beispiel 2 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:164` | Beispiel 2 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:182` | Sample 2 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:183` | Sample 2 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:227` | Beispiel 1 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:228` | Beispiel 1 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:246` | Beispiel 3 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:247` | Beispiel 3 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:265` | Sample 3 |
| `apps/web/app/modules/knowledgeBase/utils/DefaultKbsTemplate.ts:266` | Sample 3 |
| `apps/web/app/modules/workflowEngine/utils/DefaultWorkflowTemplates.ts:760` | It works! |
| `apps/web/app/routes/__test.client-throw.tsx:33` | Sentry client smoke test |
| `apps/web/app/routes/__test.client-throw.tsx:34` | Triggering a synchronous client-side error. The root ErrorBoundary should pick it up and report to Sentry. |
## Formatting migration signals (first 100 hits)
| File:Line | Signal |
| ----------------------------------------------------------------------------------------------- | ---------------------------- |
| `apps/web/app/components/aceternity/infinite-moving-cards.tsx:214` | `DateUtils.dateYMDHMSMS` |
| `apps/web/app/components/aceternity/infinite-moving-cards.tsx:215` | `DateUtils.dateAgo` |
| `apps/web/app/components/analytics/AnalyticsOverview.tsx:25` | `NumberUtils.intFormat` |
| `apps/web/app/components/analytics/AnalyticsOverview.tsx:32` | `NumberUtils.intFormat` |
| `apps/web/app/components/analytics/AnalyticsOverview.tsx:64` | `NumberUtils.intFormat` |
| `apps/web/app/components/analytics/AnalyticsOverview.tsx:132` | `NumberUtils.intFormat` |
| `apps/web/app/components/analytics/AnalyticsOverviewChart.tsx:8` | `NumberUtils.intFormat` |
| `apps/web/app/components/app/events/LogsTable.tsx:92` | `DateUtils.dateAgo` |
| `apps/web/app/components/app/events/LogsTable.tsx:95` | `DateUtils.dateYMD` |
| `apps/web/app/components/app/events/LogsTable.tsx:96` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/apiKeys/ApiKeyLogsTable.tsx:30` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/core/apiKeys/ApiKeyLogsTable.tsx:31` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/core/apiKeys/ApiKeysTable.tsx:162` | `NumberUtils.intFormat` |
| `apps/web/app/components/core/apiKeys/ApiKeysTable.tsx:188` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/core/apiKeys/ApiKeysTable.tsx:192` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/apiKeys/ApiKeysTable.tsx:197` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/core/apiKeys/ApiKeysTable.tsx:198` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/core/pricing/UsageBasedPricesUnit.tsx:109` | `NumberUtils.numberFormat` |
| `apps/web/app/components/core/settings/members/MembersListAndTable.tsx:48` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/core/settings/members/MembersListAndTable.tsx:49` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/settings/subscription/ChangeSubscription.tsx:51` | `NumberUtils.intFormat` |
| `apps/web/app/components/core/settings/subscription/MyInvoices.tsx:28` | `DateUtils.dateYMD` |
| `apps/web/app/components/core/settings/subscription/MyInvoices.tsx:31` | `DateUtils.dateYMD` |
| `apps/web/app/components/core/settings/subscription/MyInvoices.tsx:32` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/settings/subscription/MyPayments.tsx:31` | `DateUtils.dateYMD` |
| `apps/web/app/components/core/settings/subscription/MyPayments.tsx:32` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/settings/subscription/MyPayments.tsx:42` | `NumberUtils.decimalFormat` |
| `apps/web/app/components/core/settings/subscription/MyPayments.tsx:60` | `DateUtils.dateYMD` |
| `apps/web/app/components/core/settings/subscription/MyPayments.tsx:63` | `DateUtils.dateYMD` |
| `apps/web/app/components/core/settings/subscription/MyPayments.tsx:64` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/settings/subscription/MyPlanFeatureUsage.tsx:38` | `DateUtils.dateYMD` |
| `apps/web/app/components/core/settings/subscription/MyUpcomingInvoice.tsx:30` | `DateUtils.dateMonthDayYear` |
| `apps/web/app/components/core/settings/subscription/Plan.tsx:138` | `NumberUtils.numberFormat` |
| `apps/web/app/components/core/settings/subscription/PlanFeatureDescription.tsx:86` | `NumberUtils.intFormat` |
| `apps/web/app/components/core/settings/subscription/TenantProduct.tsx:43` | `DateUtils.dateMonthDayYear` |
| `apps/web/app/components/core/settings/subscription/TenantProduct.tsx:76` | `DateUtils.dateYMDHMSMS` |
| `apps/web/app/components/core/settings/subscription/TenantProduct.tsx:77` | `DateUtils.dateMonthDayYear` |
| `apps/web/app/components/core/settings/subscription/TenantProduct.tsx:78` | `DateUtils.dateMonthDayYear` |
| `apps/web/app/components/core/settings/subscription/TenantProduct.tsx:85` | `DateUtils.dateMonthDayYear` |
| `apps/web/app/components/core/settings/subscription/TenantProduct.tsx:89` | `DateUtils.dateMonthDayYear` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:104` | `NumberUtils.decimalFormat` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:115` | `DateUtils.dateMonthDayYear` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:119` | `DateUtils.dateMonthDayYear` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:176` | `NumberUtils.decimalFormat` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:214` | `DateUtils.dateYMD` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:215` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:244` | `DateUtils.dateYMD` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:246` | `NumberUtils.decimalFormat` |
| `apps/web/app/components/core/tenants/TenantsTable.tsx:251` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/users/UsersTable.tsx:118` | `DateUtils.dateDM` |
| `apps/web/app/components/core/users/UsersTable.tsx:120` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/core/users/UsersTable.tsx:121` | `DateUtils.dateAgo` |
| `apps/web/app/components/core/users/UsersTable.tsx:224` | `DateUtils.dateAgo` |
| `apps/web/app/components/entities/properties/FormulaControl.tsx:39` | `NumberUtils.decimalFormat` |
| `apps/web/app/components/entities/relationships/EntityRelationshipsTable.tsx:63` | `NumberUtils.intFormat` |
| `apps/web/app/components/entities/rows/cells/RowDateCell.tsx:8` | `DateUtils.dateYMD` |
| `apps/web/app/components/entities/rows/cells/RowDateCell.tsx:12` | `DateUtils.dateYMD` |
| `apps/web/app/components/entities/rows/cells/RowDateCell.tsx:14` | `DateUtils.dateDMY` |
| `apps/web/app/components/entities/rows/cells/RowDateCell.tsx:16` | `DateUtils.dateMDY` |
| `apps/web/app/components/entities/rows/cells/RowDateCell.tsx:18` | `DateUtils.dateAgo` |
| `apps/web/app/components/entities/rows/cells/RowDateCell.tsx:20` | `DateUtils.dateYMD` |
| `apps/web/app/components/entities/rows/cells/RowFolioCell.tsx:6` | `NumberUtils.pad` |
| `apps/web/app/components/entities/rows/cells/RowNumberCell.tsx:11` | `NumberUtils.intFormat` |
| `apps/web/app/components/entities/rows/cells/RowNumberCell.tsx:13` | `NumberUtils.decimalFormat` |
| `apps/web/app/components/entities/rows/cells/RowNumberCell.tsx:15` | `NumberUtils.decimalFormat` |
| `apps/web/app/components/entities/rows/cells/RowNumberCell.tsx:15` | `NumberUtils.currencyFormat` |
| `apps/web/app/components/entities/rows/cells/RowNumberCell.tsx:17` | `NumberUtils.percentFormat` |
| `apps/web/app/components/entities/rows/RowActivity.tsx:132` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/entities/rows/RowActivity.tsx:132` | `DateUtils.dateAgo` |
| `apps/web/app/components/entities/rows/RowLogComment.tsx:88` | `DateUtils.dateAgo` |
| `apps/web/app/components/entities/rows/RowLogComment.tsx:89` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/entities/rows/RowLogComment.tsx:89` | `DateUtils.dateAgo` |
| `apps/web/app/components/entities/rows/RowLogs.tsx:24` | `DateUtils.dateDM` |
| `apps/web/app/components/entities/rows/RowLogs.tsx:79` | `DateUtils.dateYMDHMS` |
| `apps/web/app/components/entities/rows/RowLogs.tsx:80` | `DateUtils.dateHMS` |
| `apps/web/app/components/entities/views/EntityViewsTable.tsx:188` | `NumberUtils.intFormat` |
| `apps/web/app/components/ui/input/InputDate.tsx:172` | `DateUtils.dateYMD` |
| `apps/web/app/components/ui/multi-select.tsx:312` | `NumberUtils.intFormat` |
| `apps/web/app/components/ui/tables/PreviewTableSimple.tsx:45` | `NumberUtils.decimalFormat` |
| `apps/web/app/components/ui/tables/PreviewTableSimple.tsx:52` | `DateUtils.dateYMD` |
| `apps/web/app/modules/api/components/ApiKeyLogsDetails.tsx:96` | `DateUtils.dateYMDHMS` |
| `apps/web/app/modules/api/components/ApiKeyLogsDetails.tsx:98` | `DateUtils.dateYMDHMS` |
| `apps/web/app/modules/api/components/ApiKeyLogsDetails.tsx:166` | `NumberUtils.custom` |
| `apps/web/app/modules/api/components/ApiKeyLogsSummary.tsx:123` | `NumberUtils.intFormat` |
| `apps/web/app/modules/api/components/ApiKeyLogsSummary.tsx:136` | `NumberUtils.custom` |
| `apps/web/app/modules/chatbot/components/chat-list.tsx:52` | `DateUtils.dateAgo` |
| `apps/web/app/modules/codeGenerator/service/CodeGeneratorService.ts:243` | `DateUtils.dateYMD` |
| `apps/web/app/modules/codeGenerator/templates/routes/api/CodeTemplate.Routes.Edit.Api.ts:59` | `NumberUtils.pad` |
| `apps/web/app/modules/codeGenerator/templates/routes/views/CodeTemplate.Routes.Edit.View.ts:69` | `NumberUtils.pad` |
| `apps/web/app/modules/emailMarketing/components/EmailMarketingSummaryRoute.tsx:21` | `NumberUtils.decimalFormat` |
| `apps/web/app/modules/emailMarketing/components/EmailMarketingSummaryRoute.tsx:27` | `NumberUtils.decimalFormat` |
| `apps/web/app/modules/emailMarketing/components/EmailMarketingSummaryRoute.tsx:36` | `NumberUtils.numberFormat` |
| `apps/web/app/modules/emailMarketing/components/OutboundEmailsTable.tsx:177` | `DateUtils.dateYMDHMS` |
| `apps/web/app/modules/emailMarketing/components/OutboundEmailsTable.tsx:178` | `DateUtils.dateYMD` |
| `apps/web/app/modules/emailMarketing/components/OutboundEmailsTable.tsx:179` | `DateUtils.dateAgo` |
| `apps/web/app/modules/emailMarketing/components/OutboundEmailsTable.tsx:213` | `DateUtils.dateYMDHMS` |
| `apps/web/app/modules/events/components/EventDetails.tsx:26` | `DateUtils.dateYMDHMS` |
| `apps/web/app/modules/events/components/EventDetails.tsx:62` | `DateUtils.dateYMDHMS` |
| `apps/web/app/modules/events/components/EventDetails.tsx:68` | `DateUtils.dateYMDHMS` |
| `apps/web/app/modules/events/components/EventsTable.tsx:103` | `DateUtils.dateAgo` |
+213
View File
@@ -0,0 +1,213 @@
# i18n Contribution Guide
> Foundation work for #72 i18n-foundation. Targets contributors adding new
> UI surfaces or extending existing copy. The infrastructure is in place
> (ICU plurals, Intl helpers, RTL Tailwind variants, audit script, locale
> picker, Storybook toolbar) — what follows is the playbook.
## When to translate
Translate every user-facing string that:
- Renders in a React component that ships to the browser
- Renders in a transactional email body or subject (see
`packages/mail/src/templates-react/`)
- Renders in an error/help/empty-state surface accessible to a tenant user
Do **not** translate:
- Brand names (`ToolCTRL`, `Stripe`, `GitHub`, `Scira AI` etc.) — they're
proper nouns and stay constant across locales
- Developer-only tooling labels (Storybook globals, CLI prompts, log
messages, internal admin operator screens marked as such)
- Protocol identifiers (Authentik group names, SDK constants, etc.)
## Where translations live
```
apps/web/public/locales/
├── de/
│ ├── translations.json # Legacy single-file namespace (4000+ keys)
│ ├── errors.json
│ ├── mail.json
│ ├── common.json # Shared UI (buttons, generic labels) + ICU plurals
│ ├── settings.json # Per-area: settings surfaces
│ ├── billing.json # Per-area: billing surfaces
│ ├── crm.json # Per-area: CRM surfaces
│ ├── auth.json # Per-area: authentication surfaces
│ └── emailMarketing.json # Per-area: email marketing surfaces
└── en/ (same structure)
```
`translations.json` is the historical bag-of-keys. **New work should land
in the most specific existing namespace**: `common`, `settings`, `billing`,
`crm`, `auth`, or `emailMarketing`. Use namespace names exactly as registered
in `packages/i18n/src/index.ts`; the email marketing namespace is camelCase
(`emailMarketing`), not kebab-case.
When you add a brand-new namespace:
1. Create `apps/web/public/locales/{de,en}/<namespace>.json`
2. Add the namespace to `i18nNamespaces` in
`packages/i18n/src/index.ts`
3. Run `pnpm verify:i18n` so the registered namespace is checked for
locale/file parity
## Key naming
- Scope-first, dotted: `locale.title`, `action.save`,
`impersonation.actingAs`
- Keep keys stable; if copy changes drastically, mint a new key rather
than reusing the old one (helps with translation memory)
- Avoid embedding tokens in keys: prefer `memberCount` (plural-aware) over
`oneMember` + `manyMembers`
## ICU plurals
Use the ICU-MessageFormat syntax for pluralisation. The CLDR ruleset
covers 60+ languages; never re-implement.
```json
{
"itemsSelected": "{count, plural, =0 {No items selected} one {# item selected} other {# items selected}}"
}
```
In components:
```tsx
import { useTranslation } from "react-i18next";
import { formatIcu } from "@toolctrl/i18n";
function SelectionToolbar({ count }: { count: number }) {
const { t, i18n } = useTranslation("common");
return <span>{formatIcu(t("itemsSelected"), { count }, { locale: i18n.language })}</span>;
}
```
A `createUseTranslate()` factory in `@toolctrl/i18n` returns a hook that
auto-detects ICU syntax and routes to FormatJS — useful when most of your
keys are ICU-shaped. Plain (non-ICU) messages keep working through
i18next's `{{name}}` interpolation, so a gradual migration is safe.
### Pluralisation per language
| Locale | Cases used by CLDR |
| ------ | -------------------------------- |
| en | one, other |
| de | one, other |
| pl | one, few, many, other |
| ar | zero, one, two, few, many, other |
| ja, zh | other (no plural distinction) |
Polish and Arabic are the canonical test cases for "is the ICU pipeline
actually wired up correctly?" — see
`packages/i18n/src/icu/plural.test.ts`.
## Intl helpers
The `@toolctrl/i18n` package exports locale-aware Intl-API wrappers:
```tsx
import { formatCurrency, formatDate, formatNumber, formatPercent, formatList, formatRelativeTime } from "@toolctrl/i18n";
formatCurrency(1234.5, { locale, currency: "EUR" }); // "1.234,50 €" / "$1,234.50"
formatDate(new Date(), { locale, dateStyle: "medium" });
formatNumber(0.42, { locale, style: "decimal" });
formatPercent(0.25, { locale }); // "25%" / "25 %"
formatList(["A", "B", "C"], { locale }); // Oxford-comma aware
formatRelativeTime(earlier, { locale, relativeTo: new Date() });
```
All helpers accept an explicit `locale` so they're SSR-safe and produce
identical output on server + client.
## Locale resolution
When a request arrives, the server picks a locale in this order:
1. `User.locale` (set via the profile locale-picker)
2. `Tenant.defaultLocale`
3. `Accept-Language` header (browser default)
4. App fallback (`de`, see `packages/i18n/src/index.ts#defaultLocale`)
The resolved locale is stored in the i18n cookie (`_i18n`) and surfaced
to React via `useTranslation().i18n.language`. The picker UI lives in
`apps/web/app/modules/users/components/UserProfileSettings.tsx` (rendered
from `/app/<tenant>/settings/profile`) and POSTs to `/preferences`, which
calls the root `setLocale` action.
## RTL considerations
- `<html dir={getDirection(locale)}>` is set server-side in `root.tsx`
- Tailwind ships `rtl:` and `ltr:` modifier variants (registered in
`tailwind.config.ts`); use them where direction matters:
```tsx
<div className="ml-4 rtl:ml-0 rtl:mr-4">…</div>
<ChevronRightIcon className="rtl:rotate-180" />
```
- For text, prefer logical CSS properties (`ps-4` / `pe-4`) when adding
new layouts — even simpler than `rtl:` overrides
- Bidi-spoofing: never trust user-controlled strings to be LTR. The
display layer must strip Unicode bidi-override characters
(`U+202A`–`U+202E`, `U+2066`–`U+2069`) — see `compliance-audit-export`
and the security notes in the foundation spec
## Adding a new locale
1. Add the locale code to `supportedLocales` in
`packages/i18n/src/index.ts`. If it's RTL, also add it to
`rtlLocales`
2. Create `apps/web/public/locales/<locale>/{translations,errors,mail,common,settings,billing,crm,auth,emailMarketing}.json`
by copying the English files
3. Run `pnpm verify:i18n` — the parity check enforces all namespaces have
matching keys
4. Have the translation reviewed before merging
## Auditing hardcoded strings
The `scripts/i18n-audit.mjs` script scans web routes, modules,
route modules, and components for hardcoded English/German JSX text, then
records locale-sensitive formatting signals that should move to the
`@toolctrl/i18n` helpers over time. Run it before starting an extraction
sprint to baseline the count, and again after to measure progress:
```bash
node scripts/i18n-audit.mjs
# → docs/contributing/i18n-audit-<YYYY-MM-DD>.md
```
The current audit snapshot is `docs/contributing/i18n-audit-2026-05-25.md`.
Its re-run section is the active product-UI baseline: 0 hardcoded
route/component strings and 0 locale-formatting signals as of 2026-05-29.
The 2026-05-25 findings remain in that file as historical disposition only.
The repo-wide `pnpm verify:i18n` script enforces three guards:
- `scripts/i18n-check.ts` — per-locale namespace key parity
- `scripts/verify-hardcoded-strings.mjs` — repo-wide hardcoded-string
ratchet (active historical baseline: 14 brand/protocol/package
identifiers; JSX text, prop literals, and response literals are 0)
- `eslint --max-warnings=0` on the design-system packages
## Storybook
The Storybook `preview.tsx` exposes a `Locale` toolbar global. Switching
the value re-renders all stories with `<html lang/dir>` updated, so
`rtl:` Tailwind variants light up immediately. The default value is `en`;
`ar` is included as an RTL preview even though we don't ship Arabic
translations yet.
## Reference checklist for a new translated component
- [ ] Strings live in a per-area namespace (`common`, `settings`, …)
- [ ] DE and EN are both filled in (parity test enforces this)
- [ ] Plural-aware copy uses ICU syntax
- [ ] Currency/date/number rendering uses `@toolctrl/i18n` helpers,
passing the active locale
- [ ] Direction-sensitive Tailwind classes use `rtl:` / `ltr:` modifiers
(or logical CSS properties)
- [ ] No hardcoded Latin-alphabet copy in JSX text (audit clean for that
file)
@@ -0,0 +1,242 @@
# WCAG 2.2 AA — Internal Audit & Fix Backlog
**Status:** infrastructure shipped (Phase 10 #73), fix-sprint deferred
**Spec:** [`docs/improvement-plan/10-compliance-polish/wcag-internal.md`](../improvement-plan/10-compliance-polish/wcag-internal.md)
**Last updated:** 2026-05-26
This document is the operator-facing companion to the WCAG-internal task. It
explains:
1. What automation now guards accessibility on every PR (the "infrastructure"
that just shipped).
2. The current baseline of pre-existing violations and their severity.
3. The top-10 prioritized fix backlog.
4. How to add a new component with a11y coverage from day-1.
5. Manual screen-reader testing setup for NVDA (Windows) and VoiceOver (macOS).
## 1. What's wired today
| Surface | Tool | Trigger |
| -------------------------------- | ------------------------------------------------------------------- | ---------------------- |
| Component unit tests (jsdom) | `vitest-axe` via `@toolctrl/ui-app/test-setup/axe.ts` | `pnpm test:a11y` |
| Static heuristic checks | `apps/web/app/utils/test/a11y.tsx` (`expectNoA11yViolations`) | `*.a11y.test.tsx` |
| Live design feedback | Storybook `@storybook/addon-a11y` (WCAG-2.2-AA preset) | `pnpm --filter storybook dev` |
| Lint rules | `eslint-plugin-jsx-a11y` (`plugin:jsx-a11y/recommended`) | `pnpm lint` |
| Brand-token contrast | `scripts/verify-color-contrast.mjs` (zero-dep, fails CI on regress) | `pnpm verify:color-contrast` |
| Browser-level WCAG sweep | `@axe-core/playwright` | `pnpm verify:e2e-a11y` |
| CI gate | `.github/workflows/a11y.yml` + `.gitea/workflows/a11y.yml` | every PR + push to main |
| Keyboard "skip to content" | `apps/web/app/components/core/SkipToContent.tsx` | rendered in `AppLayout` |
Browser-level mobile public coverage includes landing, legal pages, error
pages, the feature-disabled page, footer menus, narrow viewports, and a compact
320px viewport. These checks assert no horizontal overflow and no critical axe
violations for the public mobile surface.
The six high-traffic components have an explicit axe-core spec:
- `apps/web/app/components/ui/Button.axe.test.tsx`
- `apps/web/app/components/ui/Input.axe.test.tsx`
- `apps/web/app/components/ui/Card.axe.test.tsx`
- `apps/web/app/components/ui/Dialog.axe.test.tsx`
- `apps/web/app/components/ui/Alert.axe.test.tsx`
- `apps/web/app/components/ui/Form.axe.test.tsx`
Focus-visible was added to the top-5 custom components missing it:
| Component | Change |
| ---------------------------------------------------------------------- | ------ |
| `apps/web/app/components/ui/card.tsx` | New `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` when the card is wrapped as a link / button. |
| `apps/web/app/components/ui/dialog.tsx` (DialogClose) | Migrated legacy `focus:` ring to `focus-visible:` (keyboard-only ring). |
| `apps/web/app/components/layouts/buttons/ProfileButton.tsx` | Same migration. |
| `apps/web/app/components/layouts/buttons/SearchButton.tsx` | Same migration. |
| `apps/web/app/components/layouts/buttons/NotificationsButton.tsx` | Same migration. |
## 2. Baseline of pre-existing violations
`eslint-plugin-jsx-a11y/recommended` is now applied as errors; rules with
existing violations are listed in
[`.eslintrc.a11y-baseline.json`](../../.eslintrc.a11y-baseline.json) and
temporarily disabled until each count reaches zero. The current totals:
| Rule | Count | Severity |
| --------------------------------------------- | ----- | -------- |
| `jsx-a11y/no-autofocus` | 81 | P2 |
Both P0 rules (`anchor-has-content`, `heading-has-content`) and all four P1
rules (`label-has-associated-control`, `click-events-have-key-events`,
`no-static-element-interactions`, `no-noninteractive-element-interactions`)
reached zero on 2026-05-20 and have been promoted back to `error` — see the
changelog below. `anchor-is-valid` and `no-noninteractive-tabindex` reached
zero on 2026-05-25 and have also been promoted. `media-has-caption` reached
zero on 2026-05-28 and has also been promoted. Baseline total: **81**
(P2-only remaining).
**Reduction policy** (also encoded in the baseline file):
- The baseline is monotonically non-increasing. `scripts/verify-a11y-baseline.mjs`
compares current counts to baseline and fails CI on regression.
- When a rule's violation count reaches **zero**, remove the override from
`.eslintrc`, drop the entry from `.eslintrc.a11y-baseline.json`, and add a
changelog line below.
### Promotion changelog
- **2026-05-20** — `jsx-a11y/anchor-has-content` and
`jsx-a11y/heading-has-content` reached **0** violations and were
promoted back to `error` (the `off` overrides were removed from
`.eslintrc`). The two reusable wrappers that tripped the rules —
`PaginationLink` (`apps/web/app/components/ui/pagination.tsx`) and
`StepperTitle` (`apps/web/app/components/ui/stepper.tsx`) — now
destructure `children` and render it explicitly inside the `<a>` /
`<h3>` so the rules can statically verify content instead of seeing
a bare `{...props}` spread.
- **2026-05-20** — all four **P1** rules
(`label-has-associated-control` 15, `click-events-have-key-events` 13,
`no-static-element-interactions` 11, `no-noninteractive-element-interactions`
2) reached **0** violations and were promoted back to `error` (the `off`
overrides were removed from `.eslintrc`) as part of the #73 WCAG sweep.
Fix patterns: form `<label>`s were associated with their controls via
`htmlFor`+`id` or had nested accessible text flattened to within depth-2;
decorative/empty labels captioning non-form widgets (Monaco/TipTap editors,
options tables) became `<div>` / `role="group"` captions, and `ConfirmModal`
inputs gained an `aria-label`; genuine div-buttons became `<button>`; rows
with nested interactive children gained `role="button"` + `tabIndex` +
`onKeyDown`; dead `onClick={e => e.preventDefault()}` handlers on plain
divs were removed; the `HoverEffect` `<img>` lightbox trigger was wrapped
in a `<button>`. Four genuine mouse-only click-to-focus hit-area
enhancements — `input-group`, `InputRichTextEditor`, the Novel editor
wrapper, and the `SiteTypeCard` wrapper — keep a justified inline
`eslint-disable` because adding a focusable role there would create a
redundant tab stop ahead of an already-keyboard-accessible control.
- **2026-05-25** — `jsx-a11y/anchor-is-valid` and
`jsx-a11y/no-noninteractive-tabindex` reached **0** reproduced
violations and were promoted back to `error`. The Storybook Popover
demo now uses real button controls for menu actions, and the stale
tabindex baseline entry was removed after the verifier reproduced zero
hits across the tracked app + Storybook scope.
- **2026-05-28** — `jsx-a11y/media-has-caption` reached **0** violations
and was promoted back to `error`. `AudioOrVideoPlayer`
(`apps/web/app/components/ui/media/AudioOrVideoPlayer.tsx`) now renders
a captions track for both audio and video media by default, while still
allowing callers to pass real VTT caption metadata.
- **2026-05-28** — `jsx-a11y/no-autofocus` dropped from **86** to **81**
violations. `ButtonPrimary` and `LinkOrAhref` now preserve their
`autoFocus` caller contract through programmatic focus instead of passing
`autoFocus` to DOM nodes.
## 3. Top-10 prioritized fix backlog
Severity tiers are deliberately small so a single PR can usually clear one.
| # | Severity | File | Rule / issue | Recommended fix |
| - | -------- | ---------------------------------------------------------------------- | --------------------------------------- | ---------------- |
| ~~1~~ | ~~P0~~ | ~~`pagination.tsx`~~ | ~~`anchor-has-content`~~ | ✅ Fixed 2026-05-20 — see promotion changelog. |
| ~~2~~ | ~~P0~~ | ~~`stepper.tsx`~~ | ~~`heading-has-content`~~ | ✅ Fixed 2026-05-20 — see promotion changelog. |
| ~~3~~ | ~~P1~~ | ~~`apps/web/app/components/**`~~ | ~~`label-has-associated-control` (15x)~~ | ✅ Fixed 2026-05-20 — see promotion changelog. |
| ~~4~~ | ~~P1~~ | ~~Custom div-buttons in editors~~ | ~~`click-events-have-key-events` (13x)~~ | ✅ Fixed 2026-05-20 — see promotion changelog. |
| ~~5~~ | ~~P1~~ | ~~Same set~~ | ~~`no-static-element-interactions` (11x)~~ | ✅ Fixed 2026-05-20 — see promotion changelog. |
| ~~6~~ | ~~P1~~ | ~~List-row click handlers~~ | ~~`no-noninteractive-element-interactions` (2x)~~ | ✅ Fixed 2026-05-20 — see promotion changelog. |
| ~~7~~ | ~~P2~~ | ~~Marketing demo videos~~ | ~~`media-has-caption` (2x)~~ | ✅ Fixed 2026-05-28 — reusable media player renders captions tracks. |
| 8 | P2 | Modal/search/onboarding inputs | `no-autofocus` (81x) | Replace with programmatic focus on mount so screen-reader hears modal title first. |
| ~~9~~ | ~~P2~~ | ~~Single tabindex container~~ | ~~`no-noninteractive-tabindex`~~ | ✅ Promoted 2026-05-25 — stale baseline entry reproduced as 0. |
| ~~10~~| ~~P2~~ | ~~`packages/ui/src/stories/Popover.stories.tsx`~~ | ~~`anchor-is-valid`~~ | ✅ Fixed 2026-05-25 — demo menu actions use buttons. |
Track per-fix PRs in the [Issue tracker](https://github.com/toolctrl-labs/boilerplate/issues) with
label `area:accessibility`.
## 4. Adding a new component with a11y coverage from day-1
```tsx
// packages/ui-app/src/MyWidget.tsx
export function MyWidget({ label }: { label: string }) {
return <button aria-label={label}>...</button>;
}
```
```tsx
// packages/ui-app/src/MyWidget.a11y.test.tsx
import { MyWidget } from "./MyWidget";
import { generateA11yTest } from "../test-utils/a11y-template";
generateA11yTest(MyWidget, { label: "Close" });
```
That's it — the helper renders the component, runs axe-core with the
project's WCAG-2.2-AA preset, and fails if any rule is violated.
For interactive components (Modal, Combobox, DataTable), add manual tests for:
- Focus trap (Tab cycles within, Escape closes).
- Keyboard activation (Enter/Space).
- Screen-reader-announced state changes (`aria-live`).
Storybook stories are picked up automatically by `addon-a11y` so designers
see the same violations live.
## 5. Manual screen-reader testing
WCAG 2.2 AA cannot be fully verified by axe-core — roughly 57 % is automatable
[per Deque](https://www.deque.com/axe/#what-can-axe-catch). The remaining
~43 % needs an actual screen reader.
### NVDA on Windows (Chrome)
1. Install [NVDA](https://www.nvaccess.org/download/) (free, open-source).
2. Start NVDA before opening Chrome (`Insert + N` opens the menu).
3. Navigate the staging site keyboard-only:
- `Tab` advances focus, `Shift+Tab` reverses.
- `H` jumps between headings; `K` between links; `F` between form fields.
- `Insert + F7` opens the elements list (headings/links/landmarks).
4. Validate flows: Login, Magic-Link, CRM contact create, DSGVO export.
5. Capture findings in `docs/accessibility/screen-reader-smoke-tests-<date>.md`
(one section per flow; format: `Pass / Findings`).
### VoiceOver on macOS (Safari)
1. Enable: `System Settings → Accessibility → VoiceOver → On` (or `⌘+F5`).
2. Navigate with `VO` modifier (`Control+Option` by default):
- `VO+→` next item; `VO+←` previous.
- `VO+Space` activate.
- `VO+U` opens the rotor; `VO+H` toggles per-heading navigation.
3. Same flows + findings template as NVDA.
### Keyboard-only without screen reader
1. Disconnect / disable the mouse (or `System Settings → Accessibility →
Pointer Control → Mouse Keys` to fall back to keyboard pointer).
2. Login → Dashboard → Settings → Sign out — every action must be reachable.
3. Critical: `Skip to main content` link appears when you press Tab on a
fresh page load; pressing Enter moves focus inside `<main>`.
## 6. Useful commands
```bash
pnpm verify:color-contrast # brand token contrast audit
pnpm test:a11y # vitest + axe-core suites
pnpm lint # incl. jsx-a11y/recommended (baseline-respecting)
pnpm verify:a11y-coverage # component + browser-level a11y coverage ratchet
pnpm verify:a11y-baseline # jsx-a11y baseline regression gate
pnpm verify:e2e-a11y # SKIP-safe playwright + axe-core route sweep
pnpm --filter @toolctrl/storybook dev # designer-facing live a11y feedback
```
`pnpm verify:e2e-a11y` exits successfully with a SKIP message when Playwright is
not installed or no target URL is reachable. Staging/CI lanes that must prove the
browser sweep should set `REQUIRE_E2E_A11Y=1`; that converts missing Playwright
or an unreachable `E2E_BASE_URL` into a failing gate. Authenticated browser specs
require `E2E_TENANT_EMAIL` and `E2E_TENANT_PASSWORD`; set `REQUIRE_AUTH_E2E=1`
when those specs must fail instead of silently skipping without credentials.
Set `REQUIRE_E2E_READINESS=1` for the non-mutating staging/readiness lane. That
requires tenant credentials plus `E2E_TENANT_DASHBOARD_PATH`,
`E2E_TENANT_SUBSCRIPTION_PATH`, API overview/keys/docs/logs paths,
`E2E_TENANT_POLICIES_PATH`, `E2E_PLUGIN_SETTINGS_PATH`,
`E2E_WORKFLOW_BUILDER_PATH`, `E2E_WORKFLOW_RUN_API_PATH`,
`E2E_WORKFLOW_RUN_MANUAL_PATH`, `E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH`, and
`PLUGIN_BINARY_BASE_PATH`, and turns
missing inputs or unavailable 400/403/404/login responses into failures.
Scheduled GitHub/Gitea readiness workflows run this contract against staging
with `E2E_BROWSER_MATRIX=1`, `E2E_A11Y_STRICT=1`, and
`E2E_ALLOW_WORKFLOW_RUN=1`.
Stripe/provider-live paths and mutating account/workflow actions stay separately
opt-in through their own env vars. Set `SKIP_E2E_A11Y=1` when a local or CI lane
should explicitly bypass the browser sweep.
+75
View File
@@ -0,0 +1,75 @@
# Data Retention Defaults
## Scope
This document defines repository-level default retention classes for privacy- and data-protection-relevant surfaces in this boilerplate. It is an operational engineering schedule for future implementation work, not legal advice and not a statement that retention automation already exists. Product owners must confirm jurisdiction-specific requirements before enabling deletion jobs, provider cleanup, exports, or audit evidence workflows.
The defaults apply to application data, operational evidence, generated artifacts, and optional plugin-candidate surfaces unless a narrower product policy is approved. Examples in this document are intentionally placeholder-shaped, such as `<surface-label>`, `<request-id>`, `<job-id>`, and `<count>`. Do not replace them with real customer identifiers, email addresses, tenant slugs, domains, object keys, signed URLs, API keys, provider message identifiers, or exported data samples.
## Default retention classes
| Class | Default handling | Evidence allowed |
| --- | --- | --- |
| Account lifecycle | Retain while the account or required security relationship is active; queue erasure or minimization after closure once required audit/accounting holds are resolved. | `<surface-label>`, closure status, redacted request ID, affected row count. |
| Tenant lifecycle | Retain while the tenant is active; after tenant closure, delete tenant-owned content and minimize retained operational references. | Tenant surface label, deletion job status, counts, redacted diagnostic code. |
| Short-lived operational artifact | Expire temporary uploads, import sources, export files, retry payloads, and generated reports as soon as the workflow no longer needs them. | Artifact class label, expiration status, job ID, count. |
| Operational telemetry | Keep logs, metrics, webhook attempts, API access logs, queue state, and notification delivery state only for troubleshooting, abuse prevention, reliability, and security windows. | Route class, endpoint label, status, duration bucket, request ID, queue name. |
| Security and audit evidence | Restrict and minimize audit, impersonation, fraud, and security records; retain only the fields needed to reconstruct security events. | Audit surface label, actor/target relationship state, status, redacted event code. |
| Provider/accounting exception | Retain minimal external-provider, billing, tax, chargeback, and legal-hold references when deletion is not allowed by an approved business or legal requirement. | Provider category, local reference status, accounting status, redacted support code. |
| Plugin-candidate review | Treat optional plugin and app-engine data as potentially personal until classified; do not export, migrate, or delete with broad defaults before review. | Candidate surface label, classification status, owner, review date label. |
## Surface-by-surface defaults
| Surface | Default class | Default retention behavior | Implementation follow-up |
| --- | --- | --- | --- |
| Account users | Account lifecycle | Keep identity, profile, credential metadata, MFA state, and membership links while the account is active; after closure, delete or minimize direct personal fields while preserving only approved audit/accounting references. | Add account closure workflow, credential invalidation, MFA secret cleanup, and redacted erasure evidence. |
| Tenants and organization profile | Tenant lifecycle | Keep tenant profile and membership state while the tenant is active; after tenant closure, delete tenant-owned content and minimize organization identifiers in retained evidence. | Add tenant closure job, tenant-owned cascade review, and object/provider cleanup checks. |
| Invitations | Short-lived operational artifact | Expire pending invitations after a short configured window; delete accepted, revoked, or expired invite records when they no longer support abuse prevention or audit review. | Add invitation expiry/deletion job and safe invite-status diagnostics. |
| Registration records | Short-lived operational artifact | Remove incomplete registration records after the signup window closes; retain only redacted abuse-prevention evidence when approved. | Add registration cleanup job and duplicate-signup evidence minimization. |
| API keys | Account lifecycle | Keep key metadata while keys are active; delete revoked/expired key material and retain only minimal non-secret audit references. | Verify key hash/prefix/last-four redaction and key revocation cleanup. |
| API logs | Operational telemetry | Keep access telemetry for a bounded troubleshooting/security window; aggregate or delete endpoint params, IP references, and user/tenant links when no longer needed. | Define log TTL, param scrubbing, IP minimization, and diagnostic aggregation. |
| Entity rows, values, tasks, relationships, and permissions | Tenant lifecycle | Treat tenant-defined entity data as arbitrary personal data; soft-delete only for a limited restoration window, then hard-delete rows and child data. | Add hard-delete job, child-table erasure verification, and restore-window controls. |
| Entity comments | Tenant lifecycle | Mark comments deleted for user-facing removal, then hard-delete content after the restoration/audit window. | Ensure `isDeleted` records do not retain visible personal content indefinitely. |
| Files and uploads | Short-lived operational artifact | Delete upload progress, chunks, derived media, object metadata, and object-store blobs when no active row, email, import, export, or audit exception needs them. | Add object-store deletion hooks, checksum-safe evidence, and orphan cleanup. |
| Imports | Short-lived operational artifact | Expire source files, parsing state, rejected-row reports, and import error artifacts after review or retry windows close. | Add import artifact TTL, error-report deletion, and redacted failure summaries. |
| Exports | Short-lived operational artifact | Treat export files as sensitive generated artifacts; expire and delete them promptly after download or request completion. | Add export artifact expiration, object-store deletion verification, and data-subject scope controls. |
| Email and campaigns | Operational telemetry | Retain message bodies, recipients, sender state, campaign events, opens, clicks, and attachments only for delivery, consent, abuse, or product-support windows; minimize tracking data afterward. | Define provider deletion, unsubscribe/bounce preservation, body redaction, and campaign-event TTL. |
| Notifications | Operational telemetry | Keep notification preferences while active; retain delivery attempts and payload metadata only for delivery/retry and support windows, then aggregate or delete. | Add payload metadata scrubbing and provider-message deletion evidence. |
| Webhooks | Operational telemetry | Keep webhook attempt metadata and response status for retry/debug windows; remove raw payloads, signatures, and provider details from durable evidence. | Add webhook attempt TTL, payload redaction, and dead-letter cleanup. |
| Queues and background jobs | Short-lived operational artifact | Keep queued, retry, and dead-letter payloads only while required for workflow completion or troubleshooting; purge stale payloads and redact evidence. | Add queue TTLs, poison-message review, and safe job failure summaries. |
| Metrics, analytics, IP, and geodata | Operational telemetry | Prefer aggregation, truncation, or deletion once troubleshooting/security windows close; avoid retaining precise IP or geolocation values in shared evidence. | Add IP minimization, metric aggregation, analytics retention, and access review. |
| Billing and subscriptions | Provider/accounting exception | Retain minimal billing identifiers, usage records, credits, and checkout state only for accounting, chargeback, tax, entitlement, or provider reconciliation requirements. | Define approved accounting retention, provider deletion limits, and local identifier minimization. |
| Audit, impersonation, and security events | Security and audit evidence | Keep restricted audit trails needed for security investigation and accountability; minimize actor/target personal fields and apply access controls. | Add audit retention basis, impersonation review, access logging, and redacted export behavior. |
| Plugin-candidate surfaces | Plugin-candidate review | Treat knowledge-base, workflow, app-engine, form, portal, site, prompt/AI, help-desk, analytics, feature-flag, onboarding, and similar candidate data as personal until classified. | Require per-candidate classification before migration, export, deletion, pause/resume, or provider sync work. |
## Erasure and deletion handling
- Deletion workflows must distinguish active product records, soft-deleted records, generated artifacts, provider-held data, and restricted audit/accounting exceptions.
- User erasure must cover direct user records plus memberships, invitations, registrations, credentials, API keys, notifications, email reads, created entity data, comments, tasks, files, metrics, impersonation references, and plugin-candidate ownership where applicable.
- Tenant erasure must cover tenant profile, memberships, API keys/logs, entity data, imports, exports, files, email/campaign data, notifications, webhooks, queues, billing references, plugin-candidate data, and object-store artifacts.
- Object-store and provider deletion must be implemented explicitly; database cascades alone are not evidence that blobs, provider messages, queue payloads, analytics records, or billing-provider records were removed.
- Security, fraud, accounting, chargeback, and legal-hold exceptions must retain the minimum fields needed and must not hide unresolved deletion work.
- Erasure evidence must use placeholder-shaped operational facts only: `<request-id>`, `<job-id>`, `<surface-label>`, `<count>`, `<status>`, and `<redacted-code>`.
## Operational evidence
Safe evidence includes surface labels, model labels, route or endpoint labels, command names, exit codes, request IDs, job IDs, queue names, counts, status labels, duration buckets, and redacted diagnostic codes.
Unsafe evidence includes secrets, bearer tokens, cookies, API keys, key hashes, key prefixes, key suffixes, authorization headers, customer names, tenant identifiers, customer domains, email addresses, phone numbers, precise IP addresses, object keys, signed URLs, webhook signatures, provider payloads, queue payloads, raw logs, screenshots containing personal data, import/export contents, and message bodies.
Retention diagnostics should report omissions, missing headings, unsupported placeholders, and checked-file observations without connecting to providers, browsers, production systems, Docker, registries, or E2E environments.
## Manual approval boundaries
- This document does not authorize production deletion jobs, provider-console actions, billing-provider deletion, legal holds, customer notices, or data-subject response decisions.
- Runtime checks against browsers, provider consoles, object stores, queues, production traffic, Docker, registry services, Playwright, or Cypress remain manual unless a human explicitly approves the exact check in the current session.
- Changes that alter retention periods, legal bases, accounting exceptions, or customer-facing promises require product/legal review outside this repository task.
- Implementation work must preserve unresolved gaps rather than implying that object-store deletion, provider deletion, export expiration, or queue cleanup already exists.
## Implementation follow-ups
1. Extend the data-protection documentation verifier to require this document, required headings, required surface coverage, and absence of placeholder markers.
2. Add account, tenant, invitation, registration, entity, import, export, file, email, notification, webhook, queue, metrics/IP, billing, audit, and plugin-candidate retention jobs or explicit non-automation decisions.
3. Add object-store deletion hooks and provider deletion/minimization evidence for files, imports, exports, email, notifications, webhooks, analytics, and billing where supported.
4. Add data-subject export and erasure workflows that use redacted evidence and separate deletable data from approved audit/accounting exceptions.
5. Add tests that fail when required retention headings are missing, surface classes are omitted, or forbidden placeholder markers are introduced.
+102
View File
@@ -0,0 +1,102 @@
# Production Database Operations Guide
## Purpose
This guide is the operator-facing entrypoint for PostgreSQL and Prisma database operations in production-facing environments. It explains the safe command boundaries for checking migration status, deploying migrations, generating Prisma client artifacts, and triaging database failures without exposing credentials or customer data.
Use this guide with the existing database CI and local setup references:
- `docs/db-ci.md` documents the CI migration-status verifier and redacted status output.
- `docs/local-dev.md` documents local PostgreSQL and Prisma setup for day-to-day development.
## Required environment keys
Database operations are driven by key names only. Documentation, issues, CI logs, and runbooks must never include connection strings, database passwords, usernames, hosts, database names, or customer data.
Required or relevant keys:
- `DATABASE_URL`: Prisma PostgreSQL connection setting used by migration status, migration deploy, runtime database access, and local development.
- `DIRECT_URL`: optional direct Prisma connection setting for maintenance flows when the deployment environment requires a separate direct connection.
Operators should confirm that the required key names exist in the target secret store, CI setting, or platform environment. Do not paste raw values into tickets, chat, docs, screenshots, or verifier output.
## Local PostgreSQL and Prisma workflow
For local development, follow `docs/local-dev.md` first. Start a local PostgreSQL instance, create a disposable local database, set `DATABASE_URL` to a local placeholder value, and keep production credentials out of local files.
Local-only Prisma commands may be used while developing schema changes:
```sh
pnpm exec prisma migrate dev
pnpm exec prisma migrate status
pnpm exec prisma db push
pnpm exec prisma generate
```
Use `pnpm exec prisma migrate dev` for normal local migration authoring. Use `pnpm exec prisma migrate status` to inspect local drift. Use `pnpm exec prisma db push` only for deliberate disposable local database bootstraps where migration history is not the source of truth. Refresh generated Prisma client artifacts with `pnpm exec prisma generate` when local schema or dependency changes require it.
These local commands are not production deployment commands. They must not be copied into production migration automation.
## CI migration-status gate
The CI status gate is a read-only verification boundary for migration status:
```sh
pnpm run verify:db-migrate-status
```
This command is documented in `docs/db-ci.md` and routes through the repository-owned `@toolctrl/db` package surface. It checks that Prisma can report migration status for the configured PostgreSQL database and emits bounded, redacted diagnostic fields such as phase, command, exit code, status outcome, and sanitized output excerpts.
A passing migration-status gate means the status command completed. It does not deploy migrations, seed data, reset schemas, or prove browser-facing behavior. Do not confuse status checks with deploy migrations.
## Production migration deployment
Production migration deployment uses the package-level deployment command:
```sh
pnpm run db:migrate:deploy
```
Run this only in the approved deployment path for the target environment, with the environment's secret store providing `DATABASE_URL` and any required `DIRECT_URL`. The command applies already-authored Prisma migrations; it is not a schema authoring workflow and must not be mixed with local-only commands such as `pnpm exec prisma migrate dev` or `pnpm exec prisma db push`.
Before deploying, confirm that the intended migration set has passed review and that the CI migration-status gate is healthy. After deployment, inspect the approved deployment logs and the migration-status verifier output for redacted success or failure signals. If deployment fails, do not paste connection details or table contents into the incident record; capture the command name, exit code, diagnostic phase, and sanitized Prisma excerpt only.
## Backup and restore expectations
Backups and restores are environment-owned operational responsibilities. Before running production migrations, operators must confirm that the target environment has a current backup or restore point according to the hosting provider or database platform policy.
Restore drills should use platform-approved tooling and should document only high-level evidence: environment name, backup timestamp or restore-point identifier, operator, result, and redacted error code if applicable. Never include database dumps, customer rows, credentials, hosts, usernames, passwords, or full connection strings in repository docs or issue trackers.
For local testing of destructive schema work, use disposable local databases instead of production data. Synthetic fixtures and redacted samples are acceptable; customer data is not.
## Prisma client generation
Generated Prisma client artifacts are refreshed through the repository script:
```sh
pnpm run db:generate
```
The root build also generates the database client before building the web runtime. Run `pnpm run db:generate` after schema changes, dependency updates, or deployment preparation when generated types or client artifacts may be stale.
Generation does not connect to production data and must not be treated as a migration or data validation step. If generation fails, inspect package install state, Prisma schema validity, and sanitized compiler output before investigating database connectivity.
## Troubleshooting and safe observability
Use these safe inspection surfaces for database failures:
- CI logs from `pnpm run verify:db-migrate-status` with sanitized stdout and stderr excerpts.
- Approved deployment logs for `pnpm run db:migrate:deploy` with secrets redacted.
- Local command output from `pnpm exec prisma migrate status` against a disposable local PostgreSQL database.
- Package script names, exit codes, diagnostic phases, and redacted Prisma error codes.
- `docs/db-ci.md` for the CI status verifier contract and `docs/local-dev.md` for local setup checks.
Common failure patterns:
- Missing `DATABASE_URL`: confirm that the key exists in the CI secret or platform environment and is scoped only to the database operation that needs it.
- Connection or authentication failure: confirm the secret store and database service health without copying the value, username, password, host, or database name into logs.
- Migration drift: run the status gate, inspect the sanitized Prisma status message, and distinguish read-only status output from deploy behavior before deciding on `pnpm run db:migrate:deploy`.
- Stale Prisma client: run `pnpm run db:generate` and re-run the focused check that reported stale generated artifacts.
- Local schema mismatch: use local-only `pnpm exec prisma migrate status`, `pnpm exec prisma migrate dev`, or a deliberate disposable `pnpm exec prisma db push` according to `docs/local-dev.md`.
If any troubleshooting output reveals a connection string, credential, hostname, username, password, database name, or customer data, treat it as a secret leak. Remove the exposed material from the shared surface, rotate affected credentials, preserve only redacted evidence, and fix the logging or documentation path before continuing.
+61
View File
@@ -0,0 +1,61 @@
# Database CI runbook
## Purpose
The database CI gate verifies that the repository can reach its configured database and that Prisma reports migration status through the package-owned database boundary. The root command `pnpm run verify:db-migrate-status` shells through `pnpm --filter @toolctrl/db prisma:migrate:status`, so CI and local checks use the same `@toolctrl/db` command surface instead of invoking Prisma directly.
This is a non-E2E gate. Auto-Mode does not run browser, Playwright, Cypress, or other E2E checks for this task, and the database job must not include browser automation commands.
## Required secret
Configure a CI secret named `DATABASE_URL` for the database status check. The secret must be scoped to the migrate-status step, not the whole job, so unrelated CI steps do not receive database credentials.
Use placeholder names in documentation and CI examples only:
- secret name: `DATABASE_URL`
- command: `pnpm run verify:db-migrate-status`
Do not paste connection strings, credentials, usernames, passwords, hosts, or database names into docs, issues, comments, or logs.
## Local dry run
To run the same status verifier locally, set `DATABASE_URL` in your shell environment using your normal secret-management workflow and run:
```sh
pnpm run verify:db-migrate-status
```
The verifier fails before spawning Prisma when `DATABASE_URL` is absent. It does not print the secret value.
## CI job order
The Gitea workflow includes a `database` job that depends on `quality`. The job installs dependencies with the pinned pnpm setup, generates the database client through `@toolctrl/db`, and then runs the non-E2E status verifier.
The database job runs migration status only. It must not run `migrate deploy`, seed commands, browser automation, or E2E checks.
## Status outcomes
`pnpm run verify:db-migrate-status` reports diagnostic fields intended for CI logs and local troubleshooting:
- `phase`: where the verifier is running or failing, such as preflight, spawn, or completion
- command: the package-bound command being executed
- exit code: the process exit code when Prisma finishes
- status outcome: success or failure classification
- sanitized stdout/stderr excerpt: a bounded output excerpt with secrets redacted
A success means Prisma migration status completed through the `@toolctrl/db` boundary. It does not apply migrations, seed data, or prove browser-facing behavior.
## Failure diagnostics
Common failures and first checks:
- Missing `DATABASE_URL`: confirm the CI secret exists and is scoped to the migrate-status step.
- Spawn failure: confirm pnpm dependencies are installed and the `@toolctrl/db` package scripts are available.
- Non-zero Prisma exit: inspect the sanitized stdout/stderr excerpt and the Prisma status message in CI logs.
- Workflow contract failure: run `pnpm run verify:gitea-ci-workflow` and fix the database job shape before changing the database verifier.
The primary inspection surfaces are CI job logs, `pnpm run verify:db-migrate-status`, `pnpm run verify:gitea-ci-workflow`, and this runbook.
## Redaction expectations
Diagnostics must never print `DATABASE_URL` or embedded credentials. The verifier redacts the literal configured value and credential-like URL components before showing output excerpts. If a failure report includes secret material, treat it as a security incident: remove the exposed value from the report, rotate the credential, and fix the redaction path before sharing more logs.
+54
View File
@@ -0,0 +1,54 @@
# DB Legacy Boundary: apps/web/app/utils/db
This document defines the durable legacy boundary for `apps/web/app/utils/db/` while database ownership continues to migrate into the workspace DB package. It is intended as the first stop for future slices that add, move, or review files in this directory.
## Scope
`apps/web/app/utils/db/` is a legacy server-only database helper area. Files in this tree may contain database access helpers, persistence-oriented business queries, seed-adjacent helpers, and server-side type glue needed by routes or services that have not yet moved to `@toolctrl/db`.
The directory is not a UI layer, component layer, route module, browser bundle entrypoint, or markup rendering boundary. Treat every file under `apps/web/app/utils/db/**/*.ts` as server-only application infrastructure until it is migrated or deleted.
## Allowed DB and Type Imports
Code in `apps/web/app/utils/db/` may import DB-safe types and helpers when they preserve the server-only boundary:
- Prisma browser/type boundary imports from `@toolctrl/db/prisma-browser` are allowed for type-safe model enums and generated Prisma types that are intentionally safe to reference outside the generated client runtime.
- Server package helpers from `@toolctrl/db/* .server` modules are allowed when the importing file remains server-only and does not expose those helpers to browser code.
- Local server utilities, validation helpers, and framework request/session types may be used when they support DB access and do not pull UI dependencies into this tree.
Do not restate or depend on generated-client internals here. Prefer package-level exports from `@toolctrl/db` over deep generated-client paths.
## Disallowed UI, React, and Markup Patterns
Files under `apps/web/app/utils/db/` must not introduce UI or markup responsibilities. In particular, `apps/web/app/utils/db/**/*.ts` must not contain:
- `.tsx` files anywhere in the directory tree.
- React imports, including `react`, `react/jsx-runtime`, `@remix-run/react`, React component libraries, or UI package component imports.
- JSX elements or fragments such as `<div>`, `<Component />`, `<>...</>`, or any returned JSX tree.
- HTML/markup construction that belongs in UI code, especially `dangerouslySetInnerHTML`.
- Browser-only DOM APIs used for rendering, hydration, or component behavior.
If a DB helper needs to return display content, return plain data, identifiers, or normalized strings. Route modules and UI components are responsible for rendering that data.
## Existing Server-Only Constraints
The legacy boundary assumes server execution. Keep database calls behind server-only modules, use ORM-safe or parameterized access patterns, and validate request-derived values before they reach DB filters or identifiers.
Do not make `apps/web/app/utils/db/` importable by client components or shared browser bundles. If a helper is required from both server and browser code, split the safe type-only contract into a package/type module and keep the DB implementation in a `.server` module.
## Checklist for Changes Under apps/web/app/utils/db
Before adding or changing a file under `apps/web/app/utils/db/`, verify the following:
1. The file is `.ts`, not `.tsx`.
2. The code has no React, UI component, route-rendering, JSX, fragment, or `dangerouslySetInnerHTML` usage.
3. Imports use accepted DB/type boundaries such as `@toolctrl/db/prisma-browser` or server helpers from `@toolctrl/db/* .server` rather than generated-client internals.
4. Inputs from URL params, query params, request bodies, environment variables, or DB identifiers are validated or normalized before DB access.
5. The helper returns data, not rendered markup.
6. Any remaining migration concern is documented in the caller or follow-up plan instead of weakening this boundary.
## Future Migration Notes
Future slices should move stable DB ownership toward `@toolctrl/db` server modules and leave `apps/web/app/utils/db/` as a shrinking compatibility layer. When moving code, preserve the accepted Prisma browser/type boundary for safe types and keep runtime database clients inside server-only modules.
Static guards for this slice should fail loudly with the violating path and reason whenever `.tsx`, React imports, JSX/HTML indicators, or `dangerouslySetInnerHTML` appear in `apps/web/app/utils/db/`.
+141
View File
@@ -0,0 +1,141 @@
# Deploying Meilisearch
This document covers production deployment of the Meilisearch service that
backs `packages/search` and the `search-indexer` worker job. Local
development uses the bundled docker-compose service — see
`docker-compose.yml` and `docs/features/search.md` for the application-side
contract.
## Requirements
- Disk: 1–10 GiB per million indexed documents, SSD strongly recommended.
- Memory: 1 GiB minimum; allocate ~30 % of the dataset size for hot reads.
- Network: reachable only from `apps/web`, `apps/api` and the
`worker-general` service. **Never** expose port 7700 to the public
internet. The master key is the only auth boundary; if the port is open,
the index is open.
- TLS: terminate at the platform's ingress / load balancer; Meilisearch
itself does not speak TLS.
## Environment Variables
| Variable | Required | Purpose |
| --- | --- | --- |
| `MEILI_URL` | yes | URL the search wrapper and worker dial. Internal DNS only (e.g. `http://meilisearch.internal:7700`). |
| `MEILI_MASTER_KEY` | yes | Server-side secret. Min 32 bytes of entropy. Rotate via blue/green; never log. |
| `MEILI_ENV` | yes | `production` in production. Disables the unauthenticated dashboard. |
| `MEILI_NO_ANALYTICS` | recommended | Set to `true` to disable upstream telemetry. |
## Coolify (Hetzner) — recommended for the staging stack
Coolify ships a "custom docker-compose" service type that maps 1:1 onto
the local layout. Paste the snippet below into the project's
docker-compose override (Coolify → Service → Configuration → Compose).
```yaml
services:
meilisearch:
image: getmeili/meilisearch:v1.8
environment:
- MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
- MEILI_ENV=production
- MEILI_NO_ANALYTICS=true
volumes:
- meilisearch-data:/meili_data
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:7700/health | grep -q 'available'"]
interval: 10s
timeout: 5s
retries: 6
start_period: 15s
restart: unless-stopped
networks:
- backend
volumes:
meilisearch-data:
networks:
backend:
external: true
```
Operator steps:
1. Set `MEILI_MASTER_KEY` as a Coolify "secret" (Project → Variables →
"Encrypt"). Length ≥ 32 bytes; generate with `openssl rand -hex 32`.
2. Set `MEILI_URL=http://meilisearch:7700` on `apps/web`, `apps/api` and
the `worker-general` service so they resolve via the internal Docker
network. Do **not** map the port to the host.
3. Add a backup cron that snapshots the `meilisearch-data` volume:
```bash
# /etc/cron.daily/meilisearch-snapshot
docker exec meilisearch \
curl -X POST -H "Authorization: Bearer $MEILI_MASTER_KEY" \
http://127.0.0.1:7700/snapshots
```
4. Verify `/health` is `available` after deploy; the worker logs
`meilisearch.connected` on cold start.
## Dedizierte VM — alternative (Hetzner Cloud)
For traffic spikes that outgrow the central Coolify host, run Meilisearch on
a dedicated Hetzner Cloud VM (e.g. CX22) with a 10 GB volume:
```bash
# Hetzner: attach a 10 GB volume and mount at /meili_data
ssh root@<instance> docker run -d --name meilisearch \
--restart unless-stopped \
-p 7700:7700 \
-v /meili_data:/meili_data \
-e MEILI_MASTER_KEY="$(cat /etc/meilisearch/master-key)" \
-e MEILI_ENV=production \
-e MEILI_NO_ANALYTICS=true \
getmeili/meilisearch:v1.8
```
Front the instance with a private network; never expose
port 7700 to the public internet. Apps reach it via the private DNS name
configured in the `MEILI_URL` env.
## Master-Key Rotation
1. Generate a new key: `openssl rand -hex 32`.
2. Set the new key as `MEILI_MASTER_KEY_NEW` in the secret store.
3. Bring up a parallel Meilisearch container with the new key; replicate
indexes via the snapshot/restore endpoint:
```bash
curl -X POST -H "Authorization: Bearer $OLD_KEY" \
"$OLD_MEILI/snapshots"
# copy the snapshot file, start new container pointing at it
```
4. Flip `MEILI_URL` on the apps to point at the new container.
5. Decommission the old container; rotate the secret-store key (move
`MEILI_MASTER_KEY_NEW` to `MEILI_MASTER_KEY`).
The wrapper's transparent SQL-LIKE fallback (`searchWithFallback`) keeps
search degraded-but-up during the cutover.
## Disaster Recovery
The Postgres database remains the source of truth. To rebuild Meilisearch
from scratch:
```bash
pnpm cli search reindex --tenant=all --index=all
```
This writes `search.index.<entity>.bulkreindex` outbox events only for the
currently supported bulk-reindex set. Today `--index=all` expands to KB
articles, CRM contacts, rows, and users. Entities are rejected until their
tenant-scoped projection has an explicit pager design. The `search-indexer` job
consumes the accepted events and pages over Postgres in 1000-row chunks.
Expected throughput: ~10 k documents per minute per worker replica.
## Backup Retention
Snapshots are full copies of the data directory. Retain at least 7 daily
snapshots offsite (S3 / Backblaze). The volume itself is also covered by
the platform's nightly volume backup if you keep that enabled.
+156
View File
@@ -0,0 +1,156 @@
# Deployment Porting Guide
## Purpose
Use this guide when updating boilerplate deployment and CI documentation. It keeps deployment prose aligned with the current static evidence without overstating live proof, registry state, or production runtime behavior.
This document is a documentation entrypoint only. It does not authorize runtime workflow changes, secret creation, live registry pushes, Coolify deployments, browser checks, Playwright runs, Cypress runs, or other E2E execution from Auto-Mode.
## Source Evidence
Use these sources as the deployment-porting baseline:
- `README.md` for the active Gitea CI summary, Corepack/pnpm setup, Docker smoke description, and manual E2E policy.
- `docs/porting-guide.md` section `Deployment and CI docs` for wording rules that distinguish Gitea CI, local Docker smoke validation, static verification, and manual browser/E2E review.
- `docs/deployment.md` for the current deploy workflow contract, Gitea Package Registry placeholder slots, Coolify runner-side triggering, and health/readiness inspection guidance.
- `.tc/baselines/porting-docs/baseline-health.md` for the Porting Docs baseline gate evidence and documentation inventory scope.
When a later document claims stronger proof than these sources, it must name the later human-approved run or slice evidence that provided that proof.
## CI Terminology
Describe the active pipeline as Gitea CI on the self-hosted `toolctrl-ci` runner. The baseline CI contract starts with Corepack and the pinned pnpm version, installs dependencies with `pnpm install --frozen-lockfile`, then runs static/package gates for quality, API, worker, database, and Docker smoke validation.
Use these safe local verification references for CI documentation updates:
```sh
pnpm run verify:ci-baseline
pnpm run verify:gitea-ci-workflow
pnpm run verify:production-seed-artifact
```
Do not describe baseline CI as publishing images, pushing to a registry, triggering Coolify, running browser automation, or proving live deployment. Those claims require separate focused evidence.
## Docker Smoke Boundaries
Document Docker smoke as local build and configuration validation. It may prove that Dockerfile targets and Docker Compose configuration are shaped correctly, but it is not proof of a live runtime, registry publish, artifact upload, production rollout, or browser/E2E behavior.
Safe static/package verifier references include focused scripts and tests such as:
```sh
node scripts/verify-docker-baseline.mjs
pnpm exec vitest run scripts/verify-docker-baseline.test.mjs
```
If a human later performs a live Docker build/run smoke check, record it as manual evidence and keep secrets out of commands, logs, screenshots, and summaries.
## Deploy Workflow Verification
The deploy workflow contract is static unless a later human-approved run proves otherwise. Use this command as the supported shorthand when documenting the deploy workflow verifier:
```sh
pnpm run verify:deploy-workflow
```
Focused verifier references may also include:
```sh
node scripts/verify-gitea-deploy-workflow.mjs
pnpm exec vitest run scripts/verify-gitea-deploy-workflow.test.mjs
```
The static deploy workflow may document versioned image targets for `web`, `api`, and `worker`, immutable full-SHA tags, stage-and-promote alias behavior, and required placeholder configuration slots. Do not turn this into a claim that a registry login, image push, artifact publication, or production deployment actually happened unless the corresponding Gitea run evidence is attached separately.
## Placeholder-Only Secret Handling
Deployment docs must use placeholder names only. These names identify required CI variable or secret slots; they are not example values:
- `REGISTRY_USER`
- `REGISTRY_PAT`
- `COOLIFY_API_URL`
- `COOLIFY_RESOURCE_UUIDS`
- `COOLIFY_TOKEN`
- `COOLIFY_STAGING_API_URL`
- `COOLIFY_STAGING_RESOURCE_UUIDS`
- `COOLIFY_STAGING_TOKEN`
Do not include real registry hosts, namespaces, usernames, passwords, Coolify URLs, UUIDs, tokens, webhook query strings, deploy endpoints, database URLs, Redis URLs, Supabase keys, mail provider keys, or secret-shaped sample values. Use the image placeholder form `{registry}/{namespace}/{web|api|worker}:{tag}` when explaining image naming.
## Coolify Runner Trigger
Coolify triggering is runner-side after image publication. Document staging as the push-to-`main` path and production as the `workflow_dispatch` `promote` path. Promotion must require `staged_sha` as a full 40-character staged commit SHA, require `backup_confirmed=true` after backup or restore-point readiness is confirmed, verify the requested SHA image set against the current `staging` alias, and then move production `main` aliases without rebuilding.
The workflow must read endpoint and resource data only from configured CI variables and secrets. Documentation must not imply Coolify triggers from pull requests, schedules, or release tags unless a later approved workflow change and verifier evidence explicitly establish that behavior.
## Health and Readiness Inspection
Health/readiness inspection is separate from deployment proof. Static verification can document repository contracts for `GET /healthz`, `GET /readyz`, API health/readiness responses, and worker health reporting, while live runtime inspection remains manual.
Safe verifier references include:
```sh
node scripts/verify-health-baseline.mjs
pnpm exec vitest run scripts/verify-health-baseline.test.mjs
```
Manual runtime inspection may check web or API health/readiness endpoints, compare request IDs, or inspect worker health output. It must not record database connection strings, Redis URLs, stack traces, tokens, passwords, provider secrets, or other deployment-only values.
## Auto-Mode Exclusions
Auto-Mode-safe proof for deployment documentation is limited to static and package-level commands. Auto-Mode must not run live Docker builds, live Docker containers, registry pushes, Coolify dispatches, browser automation, Playwright, Cypress, or E2E commands unless a human explicitly approves that exact focused run in the current session.
When porting deployment docs, write manual-only language for those excluded checks. If a document needs to mention a future live deployment, phrase it as a manual verification step and keep the current static contract clear.
## Negative Verification Terms
Deployment documentation must fail review when it introduces any of these contract breaks:
- Real deploy endpoints, real registry hosts, real Coolify URLs, UUIDs, tokens, passwords, or secret-shaped examples.
- Claims that baseline CI, pull requests, schedules, or static verifiers prove production deployment.
- Claims that Coolify triggers before all deployable images are published.
- Browser, Playwright, Cypress, or E2E commands presented as Auto-Mode defaults.
- Unresolved marker words from the common task-note vocabulary.
## Forking To A Different Infra
When a fork runs the boilerplate on its own Gitea/registry/runner, the literal
infra strings in `.gitea/workflows/*.yml` must change but the verify gates
(`scripts/verify-gitea-ci-workflow.mjs`, `scripts/verify-gitea-deploy-workflow.mjs`)
keep enforcing structural correctness. Both gates read their expected
infra strings from `scripts/infra-config.mjs`, which honours the following
optional env vars (fallback values shown in parentheses):
| Env var | Default | Used for |
| --- | --- | --- |
| `TOOLCTRL_CI_RUNNER` | `toolctrl-ci` | `runs-on:` label asserted in every job |
| `TOOLCTRL_IMAGE_PREFIX` | `toolctrl` | Docker smoke tags `<prefix>-{web,api,worker}:smoke` |
| `TOOLCTRL_GIT_HOST` | `git.tool-ctrl.de` | `REGISTRY:` and `docker login` host pattern + `/etc/hosts` override |
| `TOOLCTRL_REGISTRY_NAMESPACE` | `toolctrl-labs` | Gitea Package Registry namespace expectations |
| `TOOLCTRL_DB_USER` | `toolctrl` | Preflight `DATABASE_URL` user component |
| `TOOLCTRL_DB_NAME` | `toolctrl` | Preflight `DATABASE_URL` database name |
Fork procedure:
1. `sed`-replace the literal `toolctrl-ci`, `toolctrl-{web,api,worker}:smoke`,
`git.tool-ctrl.de`, `toolctrl-labs`, and DB credentials in
`.gitea/workflows/*.yml` to match the fork's infra.
2. Export the matching `TOOLCTRL_*` env vars in CI so the verify gates assert
against the new strings instead of the ToolCTRL defaults.
3. Run `pnpm run verify:gitea-ci-workflow` and `pnpm run verify:deploy-workflow`
locally to confirm the fork's workflow shape still satisfies the contract.
No verify-gate assertions change. Forks inherit the same structural guarantees
without rewriting the Gitea verification scripts.
## Static Verification
After editing this guide, use non-E2E checks only:
```sh
test -s docs/deployment-porting.md
grep -Eq "Gitea|Docker smoke|verify:ci-baseline|verify:gitea-ci-workflow|verify:deploy-workflow|REGISTRY_PAT|COOLIFY_TOKEN|workflow_dispatch|promote|staging|main" docs/deployment-porting.md
pnpm run verify:production-docs
pnpm run verify:docs-links
```
These checks confirm the guide exists, keeps required deployment contract terms visible, and stays covered by the active docs gates. They do not prove live deployment.
@@ -0,0 +1,40 @@
# Deployment Region And Provider Pinning
This document records the deployment-region and provider policy for hosted environments. The historical filename is retained for link compatibility; the policy language is provider- and region-neutral. It is an operator contract: keep raw secret values, account IDs, customer data, provider payloads, and full environment dumps out of docs, tickets, logs, and CI output.
## Runtime Policy
| Surface | Required setting | Operational rule |
| --- | --- | --- |
| AI primary | Provider selected by `AI_PROCESSING_REGION` and deployment record | Primary generation uses the approved AI adapter for the deployment. |
| AI fallback | Provider-specific region markers such as `SCALEWAY_REGION` | Fallback generation must use the provider endpoint and region approved in the deployment record. |
| Additional AI fallback | Provider approved by deployment record | Production enablement requires provider/account evidence before traffic is routed. |
| Transactional mail | Mailjet (Sinch), EU-hosted (Frankfurt/Belgium); no region env var | Outbound mail requires confirmed processing posture and approved subprocessors for the deployment. |
| Billing | Billing provider with `STRIPE_ACCOUNT_COUNTRY` when Stripe is used | Billing integration requires an account country approved for the deployment. |
| Subprocessors | Approved subprocessors only | Any provider contract or add-on that routes customer data outside the approved deployment policy blocks production enablement until approved or replaced. |
## Required Region Variables
These keys are deployment-policy markers. Include the keys that apply to the selected providers in production secret/config stores and deployment review evidence:
```sh
AI_PROCESSING_REGION=<deployment-region>
SCALEWAY_REGION=<provider-region>
STRIPE_ACCOUNT_COUNTRY=<billing-account-country>
```
The keys are not secrets. They are drift markers for deployment review and CI/env inventory checks. Do not treat them as runtime proof by themselves; production evidence must still come from provider account settings, contracts, and approved subprocessors.
## Provider Evidence
Provider endpoint and region settings must be reviewed against the deployment record. Do not infer data residency or processing posture from a hostname alone.
Mailjet remains the current transactional email provider. Production mail must not be enabled until account/contract evidence confirms the approved processing posture and subprocessor list for the deployment.
Stripe remains the current billing provider. Stripe country requirements differ by account country, so deployment review must confirm the approved account country before production billing is enabled.
## Verification Notes
- Run `pnpm run verify:env-drift` after editing env examples or catalog entries.
- Run the standard CI baseline gates before merging deployment-region/provider changes.
- Record only pass/fail status, key names, region labels, and provider/account evidence labels. Never paste secrets, account IDs, raw provider exports, or customer data.
+134
View File
@@ -0,0 +1,134 @@
# Deployment
## Docker Baseline Health
The current baseline is enforced by static Docker, workflow, health, and artifact checks. Historical Docker Baseline evidence is archived in milestone records; this page lists the commands agents should use for the current repository state.
### Baseline Commands
The table below is the current command contract and expected result for agents/operators. It is not live-environment proof unless a dated CI or deployment run is attached separately.
| Command | Expected exit code | Expected result | Failure fingerprint |
| --- | ---: | --- | --- |
| `node scripts/verify-docker-baseline.mjs` | 0 | Pass | Enforces web, API, and worker Docker target shape, runtime start commands, healthcheck paths, and runtime `tsx` exclusions. |
| `pnpm run lint` | 0 | Pass | ESLint runs with `--max-warnings=0`; warnings are failures. |
| `pnpm run test` | 0 | Pass | Runs the repository non-watch Vitest/script suite configured in `package.json`. |
| `pnpm typecheck` | 0 | Pass | Prisma client generation, React Router typegen, and `tsc -b` complete successfully. |
| `pnpm run verify:production-seed-artifact` | 0 | Pass | Verifies the Coolify production seed artifact build contract and, after build, checks for forbidden bare `@toolctrl/*` runtime imports. |
Use focused Vitest files for narrow route, API, worker, and verifier changes; keep browser/E2E checks manual unless explicitly approved for a focused run.
### Current Dockerfile Targets and Runtime Commands
- `base`: `node:20-bookworm-slim` on `linux/amd64`, production-oriented environment variables, OpenSSL, Corepack, and `pnpm@10.33.0`.
- `deps`: installs all workspace dependencies from `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `prisma`, and each workspace `package.json` manifest with `pnpm install --frozen-lockfile --prod=false --prefer-offline` behind a BuildKit `/pnpm/store` cache mount.
- `production-deps`: copies the `deps` filesystem and runs `pnpm prune --prod` for the final runtime images.
- `build`: generates Prisma client, runs the root `pnpm build` command, and runs `pnpm --filter @toolctrl/api build` plus `pnpm --filter @toolctrl/worker build`; the root build produces the React Router build and web server runtime artifact at `build/server-runtime/server.js`, while the API and worker builds produce `apps/api/dist/server.js` and `apps/worker/dist`.
- `web`: copies production node_modules, `build`, `build/server-runtime`, `apps/web/build`, `apps/web/public`, `package.json`, `start.sh`, workspace package runtime dependencies, and `prisma`; runs `ENTRYPOINT ["./start.sh"]`.
- `worker`: copies production node_modules, workspace manifests, `apps/worker/package.json`, `apps/worker/dist`, queue/mail/db packages, generated Prisma client artifacts, and `prisma`; sets `WORKER_ENABLED=true`; uses `CMD ["node", "apps/worker/dist/index.js"]`; healthcheck runs `node apps/worker/dist/health.js`.
- `api`: copies production node_modules, workspace manifests, `apps/api/package.json`, `apps/api/dist`, `packages/config`, `packages/queue`, `packages/db`, `packages/db/generated/prisma/client`, and `prisma`; sets `PORT=8080`; uses `CMD ["node", "apps/api/dist/server.js"]` without `tsx`.
Runtime start command observations:
- `start.sh` delegates to root `pnpm start`.
- Root `package.json` defines `start` as `pnpm --filter @toolctrl/web start`; `apps/web/package.json` owns the concrete `node ../../build/server-runtime/server.js` runtime command, so the web Docker target starts the built Production-Runtime artifact instead of `tsx`.
- `apps/api/package.json` defines `build` as `node scripts/build.mjs` and `start` as `node dist/server.js`; the Docker `api` target depends on that built `apps/api/dist` artifact and on the package-owned Prisma client copied from `packages/db/generated/prisma/client`.
- `node scripts/verify-docker-baseline.mjs` enforces the web Production-Runtime contract and the S03 API Docker contract, including the explicit Dockerfile `api` target, required API/package/Prisma copy paths, BuildKit pnpm-store cache usage in the `deps` target, and absence of `tsx` from the API runtime start command.
- `apps/worker/package.json` defines production `start` as `node dist/index.js` and `health` as `node dist/health.js`; the worker Docker target depends on these compiled runtime artifacts and must not rely on `tsx` at container runtime.
## Registry Push Baseline Gitea Package Registry Push Contract
Registry Push Baseline starts from the existing Gitea CI runner baseline and hardens the S03 deploy workflow contract at `.gitea/workflows/deploy.yml`. The active workflow builds and pushes versioned `web`, `api`, and `worker` Docker image targets to the Gitea Package Registry on the self-hosted `toolctrl-ci` runner, then triggers configured Coolify resources. Web and API resources must pass redacted `/healthz` service-and-revision checks where those resources expose HTTP health URLs; worker resources are separate Coolify applications for `WORKER_QUEUE_GROUP=general`, `mail`, `import`, `webhook`, and `plugin`, and each worker rollout must pass Coolify execute-based `node apps/worker/dist/health.js` readiness with matching revision, enabled worker state, expected group, and expected queue. Image alias/tag checks prove only non-HTTP resource configuration; they are not a pass-open worker health fallback. These static and workflow checks are deployment contract proof; they are still not browser-flow or manual production acceptance evidence.
Auto-Mode-safe verification commands:
| Command | Expected result |
| --- | --- |
| `node scripts/verify-gitea-ci-workflow.mjs` | Passes with every `.gitea` workflow job using `runs-on: toolctrl-ci` and with forbidden browser/E2E deploy commands absent. |
| `node scripts/verify-gitea-deploy-workflow.mjs` | Emits deterministic PASS/FAIL lines for the `.gitea/workflows/deploy.yml` static contract, including runner, registry-login, image-target, tag-policy, and forbidden-command checks. |
| `pnpm run verify:deploy-readiness` | Confirms the package scripts, CI workflow, deploy workflow, deployment docs, worker matrix docs, and worker-group provisioner stay consistent while distinguishing the lightweight deploy preflight from CI/readiness coverage for staging/promotion, health, i18n, hot-swap, Docker, seed, and migration gates. |
| `pnpm exec vitest run scripts/verify-gitea-deploy-workflow.test.mjs` | Exercises verifier fixtures for missing deploy contract pieces, forbidden literal secret exposure, forbidden browser/E2E commands, required Coolify trigger coverage, and required image tag behavior. |
| `grep -R "ubuntu-latest" .gitea` | Returns no matches; any match means the self-hosted runner baseline has regressed. |
| `grep -n "verify:deploy-workflow" docs/deployment.md && grep -n "main" docs/deployment.md` | Confirms this deployment document keeps the deploy verifier command and tag-policy language visible. |
The package script `verify:deploy-workflow` is the supported shorthand for the deploy workflow verifier. The deploy workflow follows a stage-and-promote contract on the self-hosted `toolctrl-ci` runner. Every deploy job depends on `deploy-preflight`, which intentionally runs only lightweight static deploy wiring checks before any image is published or promoted: the Gitea deploy workflow verifier, deploy-readiness docs/worker/provisioner consistency verifier, Docker baseline, Docker non-root runtime verifier, Coolify helper syntax checks, worker-group provisioner syntax checks, and pre-pull script syntax checks. Broader gates such as API inventory, OpenAPI security, env drift, health baseline, i18n, Phase C boundaries, Host Aware URL baseline, Feature Manifest plugin-removal gates, hot-swap, UI visual, build, and production seed artifact verification belong to the CI/readiness contract rather than the deploy preflight job. Because `.gitea/workflows/ci.yml` intentionally does not run on `push`, production readiness requires Gitea main branch protection: direct pushes to `main` must be disabled and merges to `main` must require the pull-request CI workflow to pass. Without that repository setting, a direct `main` push can stage after only the lightweight deploy preflight. Pushes to `main` then build `web`, `api`, and `worker`, push immutable full-SHA tags plus the `staging` alias, trigger the staging web resource first, wait for web health/revision proof, then trigger dependent API plus `worker-general`, `worker-mail`, `worker-import`, `worker-webhook`, and `worker-plugin` resources. Production promotion is a separate `workflow_dispatch` action named `promote`; operators must provide the full staged commit SHA as `staged_sha` and set `backup_confirmed=true` only after production backup or restore-point readiness is confirmed. The workflow verifies that full-SHA image set against `staging`, moves the production `main` alias without rebuilding, configures the production web pre-deployment migration command, deploys the production web resource first, waits for web health/revision proof, verifies production migration status with the database URL scoped to that step, and only then deploys dependent API plus all five worker-group resources. These names identify required CI variable or secret slots only; documentation must not include real deploy endpoint structures, UUIDs, tokens, passwords, credentials, or secret-shaped examples.
The deploy image tag contract is static and versioned. Every staging run must publish full-SHA immutable tags for each image target and update the `staging` alias. Promotion must move the `main` alias to the exact full-SHA image set verified by that workflow run; it must not rebuild from source. Images are recorded by safe target labels such as `web`, `api`, and `worker`; do not paste registry URLs, project IDs, credentials, or token-shaped examples into docs or tickets. The mutable `latest` alias is not part of the deployment contract.
Coolify deployment is runner-side and follows successful image publication. The `.gitea/workflows/deploy.yml` stage path runs on pushes to `main`; the promote path runs only through `workflow_dispatch` with `action=promote`, `staged_sha=<full 40-character staged SHA>`, and `backup_confirmed=true`. Staging Coolify must be called only after `web`, `api`, and `worker` staging images publish successfully: the workflow patches the staging web resource so `pre_deployment_command` runs Prisma migrations and `post_deployment_command` runs the guarded seed, triggers the staging web resource first, waits for web health/revision proof, then triggers and waits on dependent API plus `worker-general`, `worker-mail`, `worker-import`, `worker-webhook`, and `worker-plugin` resources. Production follows the same web-first/dependent-resource order after the runner verifies the requested full-SHA image set and moves the `main` aliases; migration status is confirmed after the production web resource has run its pre-deployment migration command and post-deployment seed command and before dependent API/worker deploys start. The trigger must read endpoint and resource information from configured CI variables and secrets and must not expose deploy URLs, UUIDs, bearer tokens, passwords, or registry credentials in docs, logs, tickets, or verification summaries.
If the self-hosted runner needs an internal hosts override for the Gitea registry, provide `TOOLCTRL_GITEA_HOST_IP` as a CI variable. The workflows leave `/etc/hosts` untouched when that variable is absent.
The web application uses separate Coolify command slots. `pre_deployment_command` must run only reviewed Prisma migrations from the final image: `cd /myapp && pnpm exec prisma migrate deploy --config prisma.config.ts`. `post_deployment_command` must run the guarded production seed artifact from the final image: `cd /myapp && NODE_ENV=production node packages/db/dist/production-seed.server.js`. Docker healthchecks use `/healthz`; `/readyz` remains the stricter readiness endpoint for manual/runtime probes after dependencies are available. That production seed path creates only the configured bootstrap admin plus roles, permissions, and entitlements, and requires `SEED_ADMIN_EMAIL` / `SEED_ADMIN_PASSWORD` in the Coolify environment.
Staging may opt into the CRM SaaS demo seed through the guarded command `SEED_DEMO_DATA=true ALLOW_STAGING_DEMO_SEED=true NODE_ENV=production node packages/db/dist/production-seed.server.js`. That staging-only path adds demo Starter, Pro, and Business tenants, users, plan assignments, product blueprint configuration, tenant-owned roles, KB fixtures, and support tickets. The staging web runtime must also keep `SEED_DEMO_DATA=true` and `ALLOW_STAGING_DEMO_SEED=true` so those seeded demo users can log in while `NODE_ENV=production`. Do not set `SEED_DEMO_DATA=true` or `ALLOW_STAGING_DEMO_SEED=true` on production resources.
Set `WORKER_ENABLED=true` only on worker Coolify resources. Leave it unset on web/API resources and avoid sharing a production env group that forces `WORKER_ENABLED=false` into worker containers. The deploy resource list must include separate worker applications with `WORKER_QUEUE_GROUP=general`, `mail`, `import`, `webhook`, and `plugin`; the deploy workflow identifies them by the group env marker or by resource names such as `worker-general`, `worker-mail`, `worker-import`, `worker-webhook`, and `worker-plugin`. `worker-import` is allowed to be a healthy idle topology placeholder with an empty queue list until `IMPORT_QUEUE_ENABLED=true` and `IMPORT_QUEUE_RUNTIME_READY=true` are backed by wired import, export, and scan services. Treat this deployment paragraph as a summary: `docs/ops/worker-queues.md` is the canonical worker topology and queue ownership reference.
The plugin worker belongs to the topology even while production plugin
execution is gated: with `PLUGIN_RUNTIME_PRODUCTION_ENABLED` unset or `false`,
`worker-plugin` is an idle service, not evidence that tenant plugin execution is
approved.
If `COOLIFY_STAGING_PROVISION_WORKER_GROUPS=true` or `COOLIFY_PROVISION_WORKER_GROUPS=true` is set with the matching project, server, environment, and destination UUID variables, the deploy workflow creates any missing worker-group applications through the Coolify Docker Image API before patching image tags. The provisioner copies runtime env from an existing worker source app when it creates a group resource, then overwrites the group-specific worker env, including distinct health and admin ports, so operators should keep an env-source worker app configured until all group resources have their own Coolify env.
B-012 is not a repository code blocker. The deploy workflow and Docker
verifiers cover the repository contract, while the permanent fix for runner
Docker network exhaustion is operator-owned host hardening: enlarge Docker
`default-address-pools` on the self-hosted runner and add scheduled cleanup for
orphaned Gitea Actions networks.
Deploy secrets, registry credentials, Coolify URLs, UUIDs, personal access tokens, webhook tokens, and other deploy-only values must be supplied through CI secrets or a secret manager. Documentation, workflow logs, tickets, and verification summaries must use placeholder configuration only and must not commit, echo, or paste real URLs, UUIDs, tokens, passwords, registry credentials, or secret-shaped examples. Auto-Mode-safe proof is limited to repository/static checks and the configured CI workflow contract such as `pnpm run verify:deploy-workflow`, `node scripts/verify-gitea-deploy-workflow.mjs`, and focused verifier tests. Browser flows, Playwright, Cypress, and E2E checks remain manual exclusions unless a human explicitly approves the exact focused run.
## Production Docs Production Deployment Runbook
Production Docs/S04 treats deployment proof as a layered contract. Auto-Mode may run `pnpm run verify:production-docs` to verify the production documentation set, plus existing static deploy verifiers such as `pnpm run verify:deploy-workflow`, `node scripts/verify-gitea-deploy-workflow.mjs`, `node scripts/verify-gitea-ci-workflow.mjs`, and focused verifier tests. These checks prove repository shape, documentation coverage, image-target policy, placeholder usage, and forbidden automatic browser/E2E command exclusions; they do not prove that a live Docker build, registry push, Coolify execution, or browser runtime succeeded.
The production image set remains the `web`, `api`, and `worker` Docker targets; staging uses the same target set with isolated runtime configuration in Coolify. Published image references must stay placeholder-only in docs and tickets, using safe target labels and tag labels rather than real registry hosts, namespaces, project IDs, UUIDs, credentials, or token-shaped examples. Every deployable image needs an immutable full-SHA tag; the `staging` alias identifies the currently staged image set, and the `main` alias is updated only by promotion. Operators should inspect workflow output for redacted target names, tag lists, and PASS/FAIL verifier lines only, never raw secrets.
Coolify execution is part of the main-branch stage path and the `workflow_dispatch` promote path. The workflow may trigger Coolify only after image publication or promotion prerequisites pass, and it deploys the migration-bearing web application first, verifies web health/revision, then deploys dependent API/worker resources by reading endpoint/resource data from configured CI variables and secrets. Documentation must not include Coolify URLs, webhook paths, UUIDs, bearer tokens, passwords, registry credentials, or derived endpoint examples. Pull requests and schedules must not invoke Coolify.
Ad hoc live Docker builds, registry pushes, Coolify runs, browser flows, Playwright, Cypress, and E2E checks outside the configured Gitea deploy workflow are manual runtime activities unless a human explicitly approves the exact focused run. The Gitea deploy workflow may run Docker, registry, Coolify, migration, and web/API health steps automatically; record only redacted evidence: deployment status, image target names, immutable tag IDs, web/API service and revision health, worker execute-health status, worker group/queue summary, API `requestId` or `x-request-id` correlation, and queue/job identifiers. Worker live-revision proof is checked automatically through Coolify execute returning worker health JSON with revision data; if execute is unavailable, empty, or non-JSON, the automated rollout fails closed and operators must fix Coolify execute access before accepting the worker deploy. Do not paste database URLs, Redis URLs, Supabase keys, mail provider credentials, stack traces with secrets, webhook tokens, or container environment values.
Rollback uses immutable image tags instead of mutable guesses. Select the previous known-good SHA-derived tag for the affected target, redeploy it through the approved manual path, and confirm web/API health with `GET /healthz` and `GET /readyz` plus the documented API health endpoint while correlating request IDs across logs. For worker incidents, inspect the redacted worker health output, queue names, failed-job counts, and job/request correlation IDs; pause or drain queues through the operational tooling before replaying jobs. If readiness fails, capture the non-secret dependency diagnostic code, target name, tag, and correlation ID, then escalate with secret values omitted.
## Health Baseline Health Baseline Gate
S02 enforces CI-safe static contracts for the web and API liveness/readiness endpoints. The web app exposes `GET /healthz` through `apps/web/app/routes/healthz.tsx` as a lightweight liveness route with no database dependency, and `GET /readyz` through `apps/web/app/routes/readyz.tsx` as a database-backed readiness route. The API exposes unauthenticated `GET /healthz` and `GET /readyz`; successful responses include non-secret JSON with `service: "api"`, `revision`, and `requestId`, and `/readyz` returns HTTP 503 with a redacted dependency diagnostic code when readiness fails.
Auto-Mode-safe verification commands:
| Command | Purpose |
| --- | --- |
| `pnpm exec vitest run apps/web/app/routes/healthz.test.ts apps/web/app/routes/readyz.test.ts` | Focused web route coverage for liveness/readiness response behavior. |
| `pnpm exec vitest run apps/api/src/app.test.ts apps/api/src/openapi.test.ts` | Focused API route and OpenAPI coverage for `/healthz` and `/readyz`. |
| `pnpm exec vitest run scripts/verify-health-baseline.test.mjs` | Static verifier fixture coverage, including missing web/API endpoint regressions and worker DB-readiness regressions. |
| `node scripts/verify-health-baseline.mjs` | Static repository baseline check with deterministic path-specific `PASS`, `OBSERVE`, and `FAIL` lines for web, API, and worker health contracts. |
The verifier output must no longer contain web/API observations saying `/healthz` or `/readyz` is not present, and worker DB readiness must be reported as an enforced pass/fail baseline rather than an observation. Missing web routes, missing API routes, web liveness database usage, missing web readiness database checks, missing API `/readyz` 503 dependency diagnostics, and missing worker DB readiness are all baseline failures.
Manual runtime inspection remains separate from Auto-Mode. A human may inspect a deployed web or API runtime with `GET /healthz` and `GET /readyz` and may compare the API `x-request-id` response header with the JSON `requestId` value. Readiness diagnostics are intentionally non-secret: do not record database URLs, Supabase keys, Redis URLs, tokens, passwords, stack traces, or other secret values in docs, logs, tickets, or verification summaries. Live Docker smokes, browser flows, Cypress, Playwright, and other E2E checks remain manual runtime verification and are not part of this Auto-Mode health baseline gate unless a human explicitly approves the exact focused run.
### Worker Docker Target Verification and Inspection
The worker target is statically/package verified by these CI-safe commands:
| Command | Purpose |
| --- | --- |
| `pnpm --filter @toolctrl/worker build` | Confirms the worker package emits compiled runtime files for `node dist/index.js` and `node dist/health.js`. |
| `pnpm exec vitest run apps/worker/src/index.test.ts apps/worker/src/health.test.ts scripts/verify-docker-baseline.test.mjs scripts/verify-health-baseline.test.mjs` | Exercises worker runtime/package tests and static verifier coverage, including worker `tsx` leakage and DB-readiness regressions. |
| `node scripts/verify-docker-baseline.mjs` | Performs the static Dockerfile/package contract check and emits path-specific PASS/FAIL output for worker Docker target copy paths, start scripts, health scripts, and runtime `tsx` usage. |
| `node scripts/verify-health-baseline.mjs` | Enforces that worker health statically reports Redis parse state and DB readiness without requiring live DB access. |
Manual Docker smoke verification is intentionally separate from Auto-Mode. A human may run a focused build such as `docker build --target worker -t toolctrl-worker:smoke .`, then run the image with safe environment key names only, for example `WORKER_ENABLED`, `WORKER_QUEUE_GROUP`, `WORKER_HEALTH_PORT`, `WORKER_ADMIN_PORT`, `DATABASE_URL`, `BULLMQ_REDIS_URL`, fallback `REDIS_URL`, group-specific fail-loud keys, and mail provider keys supplied by the local shell or secret manager rather than written into docs. To inspect the worker healthcheck inside a container, run `node apps/worker/dist/health.js`; the JSON output is expected to report whether the worker is enabled/configured, whether Redis configuration parses, the DB readiness state, queue names, and redacted dependency codes, without echoing Redis URLs, passwords, database connection strings, stack traces, tokens, or other deployment secret values. Auto-Mode did not run live Docker build/run smoke checks, browser flows, Playwright, Cypress, or E2E commands for S04; the recorded proof is limited to static/package verification.
### API Docker Target Verification and Inspection
The API target is statically/package verified by these task-scoped commands:
| Command | Purpose |
| --- | --- |
| `pnpm --filter @toolctrl/api build` | Confirms the API package can compile `apps/api/src` into `apps/api/dist`, including `dist/server.js` for the Docker runtime start path. |
| `pnpm exec vitest run scripts/verify-docker-baseline.test.mjs` | Exercises the Docker baseline verifier test coverage, including API-target PASS/FAIL cases for required copies and `tsx` runtime leakage. |
| `node scripts/verify-docker-baseline.mjs` | Performs the static Dockerfile/package contract check and emits path-specific PASS/FAIL output for the API target. |
Runtime inspection for a manually run API container should use `GET /v1/health`. A healthy response is expected to identify `service: "api"` and include a `requestId`; startup logs should include the structured `API listening` message with a redacted `port` field. Safe environment keys that may be supplied by deployment automation include `PORT`, `NODE_ENV`, database connection variables, Supabase keys, and JWT/password-hash configuration keys; documentation and logs must not include secret values.
### Scope Notes
Docker build/run smoke verification belongs to focused manual target checks. A manual Docker smoke check may optionally build and run the focused API target with `docker build --target api ...`, start the container, and request `GET /v1/health`, or build the focused worker target with `docker build --target worker ...` and inspect `node apps/worker/dist/health.js` inside the container. Auto-Mode must not run live Docker smoke, Playwright, Cypress, browser, or E2E commands unless a human explicitly approves that exact run. The recorded Auto-Mode verification for S03 and S04 is static/package verification only; it does not claim a live Docker runtime proof.
@@ -0,0 +1,274 @@
# Deployment: boiler.saas-ctrl.com
Reference for the public boilerplate-demo deployment. The values below
are **what to configure in Coolify**, not what to commit. Real secrets
(`SESSION_SECRET`, `CRYPTO_SECRET`, etc.) are generated once and live only
in Coolify's encrypted env store and the optional `.env` on the host.
## Hostnames
Single web-facing host with split admin/api subdomains so the cookie-scope
helper in `@toolctrl/config` can still produce canonical URLs.
| Var | Value |
| ----------------- | ---------------------------------------------------------------- |
| `APP_BASE_DOMAIN` | `saas-ctrl.com` |
| `WEB_APP_HOST` | `boiler.saas-ctrl.com` |
| `ADMIN_APP_HOST` | `admin-boiler.saas-ctrl.com` |
| `API_HOST` | `api-boiler.saas-ctrl.com` |
| `APP_PROTOCOL` | `https` |
| `PORT` | `8080` (Dockerfile default; Coolify routes 443 → container 8080) |
| `API_PORT` | `8080` (api service runs separately if api host is wired) |
DNS records (A or CNAME to the Coolify ingress IP):
- `boiler.saas-ctrl.com`
- `admin-boiler.saas-ctrl.com`
- `api-boiler.saas-ctrl.com`
For a single-host minimal demo the admin/api hosts can resolve to the
same IP — the runtime checks the `Host:` header to pick the surface.
## Required core secrets
Generate once with `openssl rand -hex 32` and store in Coolify:
- `SESSION_SECRET` — cookie signing
- `CRYPTO_SECRET` — AES key for stored workflow credentials (separate from SESSION_SECRET)
- `API_ACCESS_TOKEN` — `openssl rand -base64 32`
- `JWT_SECRET` — `openssl rand -hex 32`
- `ANALYTICS_TOKEN` — `openssl rand -hex 24`
## Brand
`BRAND_*` overrides set the public face. For the demo:
```
BRAND_PRODUCT_NAME=ToolCTRL Boilerplate
BRAND_SITE_URL=https://boiler.saas-ctrl.com
BRAND_SUPPORT_EMAIL=info@tool-ctrl.de
BRAND_SUPPORT_FROM_NAME=ToolCTRL Boilerplate
BRAND_LEGAL_IMPRINT_URL=https://saas-ctrl.com/impressum
BRAND_LEGAL_PRIVACY_URL=https://saas-ctrl.com/datenschutz
BRAND_LEGAL_TERMS_URL=https://saas-ctrl.com/agb
```
## Database
Provision via Coolify's managed Postgres (or external):
- `DATABASE_URL` — `<pooled-postgres-url>` for application/runtime traffic
- `DIRECT_URL` — optional `<direct-postgres-url>` for migrations against the primary DB
## Mail (optional but expected for boilerplate demo)
```
MAILJET_API_KEY=<from Mailjet>
MAILJET_FROM_EMAIL=noreply@saas-ctrl.com
# Deployment policy/evidence marker only; the mail provider does not read it.
MAILJET_SECRET_KEY=<mailjet-secret-key>
MAILJET_WEBHOOK_SECRET=<mailjet-webhook-secret>
```
The managed `worker-mail` Coolify readiness check verifies that the mail queue
runtime can boot, reach Redis/Postgres, and, when fail-loud mail mode is
enabled, see the required Mailjet environment variables. Treat actual
transactional mail delivery as ready only after sender-domain/provider
verification and a real delivery smoke have passed.
## Seed (production bootstrap)
```
SEED_ADMIN_EMAIL=admin@saas-ctrl.com
SEED_ADMIN_PASSWORD=<one-time bootstrap; rotate on first login>
SEED_DEMO_DATA=false
NODE_ENV=production
```
## Storage (optional)
If file uploads should work in the demo:
```
STORAGE_ENDPOINT=https://s3.eu-central-1.wasabisys.com
STORAGE_REGION=eu-central-1
STORAGE_BUCKET=boiler-saas-ctrl-com-uploads
STORAGE_ACCESS_KEY_ID=<from Wasabi>
STORAGE_SECRET_ACCESS_KEY=<from Wasabi>
STORAGE_PUBLIC_BASE_URL=https://uploads.boiler.saas-ctrl.com
STORAGE_FORCE_PATH_STYLE=false
```
## Workers
The active Gitea/Coolify deploy path expects separate worker applications
for general, mail, import, webhook, and plugin queue groups.
Create five persistent Coolify worker applications from the same worker
image. They are internal runtime apps only: do not configure public FQDNs
for any worker app.
This deployment-specific table is a summary. The canonical worker topology,
queue ownership, concurrency, and scaling guide is
[Worker queue groups](../ops/worker-queues.md).
| Coolify app | Required group | Health port | Admin/metrics port |
| ---------------- | ---------------------------- | ----------- | ------------------ |
| `worker-general` | `WORKER_QUEUE_GROUP=general` | `9101` | `9201` |
| `worker-mail` | `WORKER_QUEUE_GROUP=mail` | `9102` | `9202` |
| `worker-import` | `WORKER_QUEUE_GROUP=import` | `9103` | `9203` |
| `worker-webhook` | `WORKER_QUEUE_GROUP=webhook` | `9104` | `9204` |
| `worker-plugin` | `WORKER_QUEUE_GROUP=plugin` | `9105` | `9205` |
Keep all five services present in staging and production-like environments.
`worker-mail` is a permanent service for transactional mail and mail-backed
outbox/webhook work; it must not be treated as a manual workaround started only
after mail backlog appears.
`worker-plugin` is also permanent topology, but production plugin execution
remains blocked until external WASM sandbox signoff and an explicit
`PLUGIN_RUNTIME_PRODUCTION_ENABLED=true` rollout. Before that flip, a healthy
`worker-plugin` only proves the idle service can boot; it is not tenant plugin
runtime approval. During the rollout, run `pnpm run verify:plugin-runtime-live`
inside both `worker-general` and `worker-plugin` before enabling tenant plugins.
`worker-import` is also permanent topology but stays idle until import/export
processing is fully wired. Leave `IMPORT_QUEUE_ENABLED` and
`IMPORT_QUEUE_RUNTIME_READY` unset on `worker-import` until the worker bootstrap
injects import, export, and scan services; the service may then report healthy
with an empty queue list instead of consuming jobs it cannot process.
Each worker needs the same database and queue connection, plus its own queue group/ports:
```
WORKER_ENABLED=true
DATABASE_URL=<pooled-postgres-url>
BULLMQ_REDIS_URL=<redacted-redis-url>
WORKER_QUEUE_GROUP=<general|mail|import|webhook|plugin>
WORKER_HEALTH_PORT=<9101|9102|9103|9104|9105>
WORKER_ADMIN_PORT=<9201|9202|9203|9204|9205>
```
`EXPORT_RETENTION_QUEUE_ENABLED=true` is a legacy standalone-worker fallback.
Do not set it as part of the Coolify queue-group contract; when
`WORKER_QUEUE_GROUP` is valid, export-retention ownership comes from
`packages/queue/src/groups.ts`.
Add these only on `worker-general` in staging/production so outbox dispatch and
workflow trigger fanout fail loud instead of appearing healthy while rows pile
up:
```
OUTBOX_DISPATCHER_ENABLED=true
OUTBOX_DISPATCHER_REQUIRED=true
WORKFLOW_TRIGGER_OUTBOX_ENABLED=true
WORKFLOW_TRIGGER_OUTBOX_REQUIRED=true
WORKFLOW_TRIGGER_EXECUTOR_MODE=http
WORKFLOW_TRIGGER_EXECUTOR_URL=<app-origin> # or APP_URL/WEB_ORIGIN=<app-origin>
API_ACCESS_TOKEN=<same value as web API_ACCESS_TOKEN>
```
Keep this unset or false on `worker-mail` until seed data, persisted stats
aggregation, Playwright E2E, Mailjet delivery evidence/reconciliation,
scheduler/deploy evidence, and the deployment record approve production
delivery. The bundled renderer and `@toolctrl/mail` Mailjet adapter exist,
but the sequence subscribers do no work unless the value is explicitly `true`;
stale-lead/trial-expiring cron runner functions are not production
scheduler-registered yet:
```
MARKETING_SEQUENCES_ENABLED=false
```
Add this only on `worker-mail` in staging/production so transactional mail
outbox bootstrap fails loud when Redis, database, or mail-outbox wiring is not
ready. The worker-group provisioner sets and drift-checks this value for
managed `worker-mail` Coolify applications:
```
TRANSACTIONAL_MAIL_OUTBOX_REQUIRED=true
```
`scripts/coolify-ensure-worker-groups.mjs` treats missing `DATABASE_URL` or
`BULLMQ_REDIS_URL` on any managed worker group (`worker-general`,
`worker-mail`, `worker-import`, `worker-webhook`, `worker-plugin`) as
deployment drift. It also treats missing `worker-general` workflow-trigger
bridge configuration (`WORKFLOW_TRIGGER_EXECUTOR_URL`, `APP_URL`, or `WEB_ORIGIN` plus `API_ACCESS_TOKEN`) and missing mail outbox fail-loud configuration as drift. If
`WORKFLOW_TRIGGER_EXECUTOR_MODE=native` is selected instead of `http`, keep that
choice explicit in Coolify and verify the workflow graph uses only the
worker-native safe-core blocks.
Use `WORKFLOW_TRIGGER_EXECUTOR_MODE=hybrid` to canary native safe-core execution
while routing unsupported workflow graphs through the signed HTTP bridge.
Coolify worker readiness checks should target `/readyz` on
`WORKER_HEALTH_PORT`; `/healthz` remains the lightweight liveness probe for
manual inspection. This confirms queue/runtime readiness for the selected
worker group, including Redis and database reachability. In fail-loud mail mode,
managed `worker-mail` readiness also checks the required Mailjet env
presence. It still does not prove sender-domain verification or live provider
delivery. Admin/metrics endpoints, when enabled, must remain on
`WORKER_ADMIN_PORT` and must not be exposed publicly.
## Observability (opt-in)
```
SENTRY_DSN=<from Sentry>
SENTRY_ENVIRONMENT=production
# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel.internal:4318/v1/traces
OTEL_SERVICE_NAME=boiler-saas-ctrl-com
```
## Coolify configuration checklist
1. Resource type: Docker Image applications for the published web, api, and worker images.
2. Source: this Gitea repository, `main` branch.
3. Domain: `boiler.saas-ctrl.com` (Coolify handles Let's Encrypt).
4. Port: container `8080` → public `443`.
5. Healthcheck: web/api use `GET /healthz`; worker apps use `GET /readyz` on `WORKER_HEALTH_PORT`. Container HEALTHCHECK directives cover liveness; Coolify readiness should wait for the service-specific endpoint.
6. Web resource command slots: `pre_deployment_command` runs `cd /myapp && pnpm exec prisma migrate deploy --config prisma.config.ts`; `post_deployment_command` runs `cd /myapp && NODE_ENV=production node packages/db/dist/production-seed.server.js` (staging adds `SEED_DEMO_DATA=true ALLOW_STAGING_DEMO_SEED=true`). Do not combine the seed into the pre-deploy slot.
7. Env: paste the block above.
8. Worker apps: create `worker-general`, `worker-mail`, `worker-import`, `worker-webhook`, and `worker-plugin` as separate persistent Coolify applications with no public FQDN. Set `WORKER_QUEUE_GROUP`, `WORKER_HEALTH_PORT`, and `WORKER_ADMIN_PORT` per the worker table, and point the Coolify health check at `/readyz`. Do not rely on the worker image's standalone `WORKER_QUEUE_GROUP=general` default for grouped Coolify apps; missing per-app overrides make non-general apps drain the wrong queues.
9. Mail delivery: after `worker-mail` is healthy, verify `MAILJET_API_KEY` and `MAILJET_FROM_EMAIL` exist on the mail-capable runtime and run a real transactional-mail smoke. A green worker health check alone is not mail-provider readiness.
## Trigger a deploy
- Staging: push to `main` builds and pushes images in Gitea CI, patches the staging Coolify image tags, then triggers staging deploys.
- Production: use the Gitea `workflow_dispatch` promote action with a reviewed `staged_sha` and `backup_confirmed=true`. Promotion retags the verified staging images; do not treat a plain `main` push as a production deploy.
- Manual emergency deploy: Coolify dashboard → resource → **Deploy**, only after image tags and migration command state are verified against the runbook.
## Verify the deploy
After Coolify reports "healthy":
```sh
curl -fsS https://boiler.saas-ctrl.com/healthz
curl -fsS https://api-boiler.saas-ctrl.com/healthz
# Image-signing — optional, with cosign installed:
cosign verify --key cosign.pub {registry}/{namespace}/web:<sha>
```
If web/API `/healthz` returns the expected `revision`, verify Coolify also
reports `worker-general`, `worker-mail`, `worker-import`, `worker-webhook`,
and `worker-plugin` healthy. The Gitea deploy workflow must also show that
each worker passed Coolify execute-based `node apps/worker/dist/health.js`
readiness with the expected `revision`, `WORKER_QUEUE_GROUP`, and queue list.
`worker-import` may have an empty queue list while its import runtime gates are
unset; if it reports `toolctrl.imports` or `toolctrl.exports`, both import gates
and service wiring must be intentional.
Image alias/tag output is configuration evidence only; it is not an acceptable
fallback for worker health. If Coolify execute is unavailable, empty, or
non-JSON, fix execute access and rerun the workflow before accepting the
worker rollout. Then log into `boiler.saas-ctrl.com` with the seeded admin
credentials and confirm the tenant switcher loads.
## Current deploy latency levers
Gitea CI pre-builds immutable `web`, `api`, and `worker` images and
Coolify deploys those published tags. Production promotion retags the
verified staging images and does **not** rebuild them.
Known remaining latency levers:
- Docker target builds are already launched in parallel with PID aggregation and per-target cache refs; remaining CI-side gains likely come from cache warmup, runner CPU/network capacity, or splitting buildx work across dedicated runners.
- Registry cache is already enabled through `--cache-from/--cache-to` with stable target-specific cache refs, so cache misses usually mean a cold registry cache or Dockerfile layer invalidation.
- Coolify pre-pull is best-effort and bounded by the helper timeout; a slow host can still spend time pulling layers during deploy.
- Staging and production dependent resources are deployed separately so web health gates worker/API rollout; this is safer, but not the fastest possible rollout.
- Optional post-deploy throughput probes remain in `scripts/run-load-tests.mjs`.
@@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>HiveGrid Cloud – Entwicklungs-Regeln &amp; Infrastruktur</title>
<style>
@page {
size: A4;
margin: 22mm 18mm 20mm 18mm;
@bottom-center { content: counter(page) " / " counter(pages); font-size: 9px; color: #9a958c; }
@bottom-left { content: "HiveGrid Cloud – Engineering Handbook"; font-size: 9px; color: #9a958c; }
}
@page :first { margin: 0; @bottom-center { content: ""; } @bottom-left { content: ""; } }
* { box-sizing: border-box; }
html { font-size: 11px; }
body { font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif; color: #231F20; line-height: 1.5; margin: 0; }
/* Cover */
.cover { height: 297mm; padding: 40mm 24mm 0 24mm; background: linear-gradient(158deg, #2b2521 0%, #1c1813 60%, #14110d 100%); color: #fff; page-break-after: always; position: relative; }
.cover .logo { width: 250px; margin-bottom: 46mm; }
.cover .kicker { font-size: 13px; letter-spacing: 3px; text-transform: uppercase; color: #F1B729; font-weight: 600; }
.cover h1 { font-size: 39px; line-height: 1.1; margin: 14px 0 10px 0; font-weight: 700; color: #fff; }
.cover h1 .accent { color: #F1B729; }
.cover h2 { font-size: 17px; font-weight: 400; color: #d8d3c8; margin: 0 0 32px 0; }
.cover .meta { font-size: 12px; color: #ddd8cd; line-height: 1.9; border-top: 2px solid #F1B729; padding-top: 18px; max-width: 122mm; }
.cover .meta strong { color: #F1B729; }
.cover .badge { display:inline-block; background: rgba(241,183,41,.12); border:1px solid rgba(241,183,41,.55); color:#F1B729; border-radius: 20px; padding: 4px 13px; font-size: 11px; margin: 2px 4px 2px 0; }
/* TOC */
.toc { page-break-after: always; }
.toc .logo-dark { width: 150px; display:block; margin: 4mm 0 8mm 0; }
.toc h2 { border:none; }
.toc ol { list-style: none; padding-left: 0; counter-reset: toc; }
.toc > ol > li { counter-increment: toc; padding: 6px 0; border-bottom: 1px dotted #ddd6c5; font-weight: 600; color:#231F20; font-size: 12px; }
.toc > ol > li::before { content: counter(toc) ". "; color:#c69412; font-weight:700; }
/* Headings */
h2.section { font-size: 19px; color: #231F20; border-bottom: 3px solid #F1B729; padding-bottom: 5px; margin-top: 28px; break-after: avoid; }
h3 { font-size: 14px; color: #9a720b; margin-top: 18px; break-after: avoid; }
p { margin: 6px 0; }
ul, ol { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code { font-family: "SF Mono", "Menlo", Consolas, monospace; background: #fbf1d4; padding: 1px 5px; border-radius: 3px; font-size: 9.5px; color: #82610a; }
strong { color: #231F20; }
.lead { font-size: 12.5px; color: #5a544a; }
/* Tables */
table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 9.7px; break-inside: avoid; }
th { background: #231F20; color: #fff; text-align: left; padding: 6px 8px; font-weight: 600; border-bottom: 2px solid #F1B729; }
td { border-bottom: 1px solid #e7e1d2; padding: 5px 8px; vertical-align: top; }
tr:nth-child(even) td { background: #faf6ec; }
/* Callouts */
.callout { border-left: 4px solid #F1B729; background: #fdf6e2; padding: 9px 14px; margin: 12px 0; border-radius: 0 5px 5px 0; break-inside: avoid; }
.callout.warn { border-left-color: #c2410c; background: #fdf0e8; }
.callout.ok { border-left-color: #15803d; background: #edf8ef; }
.callout .t { font-weight: 700; display:block; margin-bottom: 2px; color:#231F20; }
.pill { display:inline-block; font-size:8.5px; font-weight:700; padding:1px 7px; border-radius:10px; vertical-align:middle; }
.pill.block { background:#fadddd; color:#9b1c1c; }
.pill.local { background:#fbe9b0; color:#82610a; }
.small { font-size: 9.5px; color:#6b665c; }
hr { border:none; border-top:1px solid #e7e1d2; margin: 18px 0; }
/* Page-break protection */
section.sw { break-inside: avoid; }
.group { break-inside: avoid; }
</style>
</head>
<body>
<div class="cover">
<img class="logo" src="assets/hivegrid-logo-white.svg" alt="HiveGrid">
<div class="kicker">Engineering Handbook</div>
<h1>Entwicklungs-Regeln<br>&amp; <span class="accent">Infrastruktur</span></h1>
<h2>Verbindliche Standards für die gemeinsame Entwicklung</h2>
<div>
<span class="badge">0 Warnings</span>
<span class="badge">TypeScript strict</span>
<span class="badge">Hohe Test-Coverage</span>
<span class="badge">Pflicht-E2E</span>
<span class="badge">Accessibility AA</span>
<span class="badge">EU-Stack</span>
<span class="badge">~80 Quality-Gates</span>
</div>
<div class="meta">
<strong>Produkt:</strong> HiveGrid 2.0 – B2B-SaaS für CRM &amp; projektbasiertes Arbeiten<br>
<strong>Codebasis:</strong> Git-Downstream des ToolCTRL-Boilerplates (pnpm-Monorepo)<br>
<strong>Stand:</strong> 14. Juni 2026<br>
<strong>Grundsatz:</strong> Regeln sind durch automatische Gates erzwungen, nicht durch
guten Willen. Was rot ist, wird nicht gemerged.
</div>
</div>
<div class="toc">
<img class="logo-dark" src="assets/hivegrid-logo.svg" alt="HiveGrid">
<h2 class="section">Inhalt</h2>
<ol>
<li>Was HiveGrid Cloud ist (Produktkontext)</li>
<li>Codebasis-Modell: Git-Downstream &amp; Package-Namen</li>
<li>Grundprinzipien</li>
<li>Code-Qualität &amp; Linting (0 Warnings)</li>
<li>TypeScript &amp; Typsicherheit</li>
<li>Formatierung (Prettier)</li>
<li>Unit- &amp; Integrationstests + Coverage</li>
<li>End-to-End-Tests (Playwright)</li>
<li>Accessibility (a11y)</li>
<li>Quality-Gates: Commit → Push → Merge → Deploy</li>
<li>CI/CD-Pipeline</li>
<li>Infrastruktur &amp; Hosting</li>
<li>Architektur-Konventionen (inkl. EU-Stack)</li>
<li>Onboarding-Checkliste</li>
</ol>
</div>
<!-- 1 -->
<section class="sw">
<h2 class="section">1. Was HiveGrid Cloud ist</h2>
<p class="lead">HiveGrid 2.0 ist ein B2B-SaaS für Kontaktmanagement (CRM) und projektbasiertes Arbeiten – mit konsequent europäischem Datenschutz- und Hosting-Anspruch.</p>
<ul>
<li><strong>Kern-Domäne:</strong> Kontakte/Leads → projektbasierte Pipelines (Vertrieb, HR-Onboarding, Kundenbetreuung) → Auftragsabwicklung.</li>
<li><strong>Datenmodell:</strong> typisierte First-Class-Tabellen (z. B. <code>Kontakt</code>, <code>Projekt</code>) statt generischer Entity-Builder.</li>
<li><strong>EU-Stack (nicht verhandelbar):</strong> AI über EU-Provider (Scaleway, Mistral, Aleph Alpha; keine US-Provider), EU-Hosting auf Hetzner/Coolify, Auth über E-Mail+Passwort / Magic-Link / TOTP (kein Apple/Google/Microsoft-OAuth), Per-Tenant-Envelope-Encryption, Postgres mit Row-Level-Security als Defense-in-Depth.</li>
<li><strong>Bauweise:</strong> aufgeteilt in 176 Milestones (Pflichtenheft v1.1), Phase 0 → Fundament → Wertschöpfung.</li>
</ul>
<div class="callout">
<span class="t">Begriffsklärung</span>
<strong>HiveGrid Cloud</strong> ist die SaaS-Anwendung (dieses Repo, in aktiver Entwicklung). Die Marketing-Website <code>hivegrid.de</code> ist ein separates Projekt auf einem anderen Host – nicht zu verwechseln.
</div>
</section>
<!-- 2 -->
<section class="sw">
<h2 class="section">2. Codebasis-Modell: Git-Downstream &amp; Package-Namen</h2>
<p>HiveGrid Cloud ist <strong>kein klassischer Fork</strong>, sondern ein <strong>Git-Downstream</strong> des ToolCTRL-Boilerplates: Verbesserungen aus dem Boilerplate werden per <code>git merge upstream/main</code> übernommen. Damit das funktioniert, bleiben Package-Namen und Konfiguration absichtlich kompatibel.</p>
<div class="callout warn">
<span class="t">Wichtig zu den Package-Namen (häufiges Missverständnis)</span>
Es gibt <strong>kein flächiges Rename</strong> auf <code>@hivegrid/*</code>. Geteilte Boilerplate-Pakete behalten <code>@toolctrl/*</code> – sonst brechen Upstream-Merges. Entscheidung dokumentiert in <code>docs/ARCHITECTURE-DECISIONS.md</code> und <code>AGENTS.md</code>.
</div>
<table>
<tr><th>Scope</th><th>Wofür</th><th>Beispiele</th></tr>
<tr><td><code>@toolctrl/*</code></td><td>Alle vom Boilerplate geteilten Pakete (bleiben so, für Mergebarkeit)</td><td><code>@toolctrl/web</code>, <code>@toolctrl/api</code>, <code>@toolctrl/db</code>, <code>@toolctrl/foundation</code>, <code>@toolctrl/ui-app</code>, …</td></tr>
<tr><td><code>@hivegrid/*</code></td><td><strong>Nur</strong> genuin neue HiveGrid-Pakete, die es upstream nie gibt</td><td><code>@hivegrid/activity</code> (Activity-Stream-Service)</td></tr>
</table>
<p class="small">Mapping-Hinweis: Wo ältere Docs/Pläne <code>packages/ui-hivegrid</code> oder <code>@hivegrid/foundation</code> sagen, ist real <code>packages/ui-app</code> bzw. <code>@toolctrl/foundation</code> gemeint. Ein späterer Naming-Drift-Fix ist in den ADRs als Option notiert, aber bewusst noch nicht umgesetzt.</p>
<p><strong>Neu in HiveGrid:</strong> <code>@hivegrid/activity</code> – zentraler Activity-Stream/Änderungs-Historie, hängt nur an <code>@toolctrl/db</code>. Weitere HiveGrid-spezifische Pakete folgen demselben Muster.</p>
<p><strong>Node-Version:</strong> HiveGrid nutzt <code>v22.12.0</code> (<code>.nvmrc</code>) – neuer als der Boilerplate. Vor der Arbeit <code>nvm use</code> ausführen.</p>
</section>
<!-- 3 -->
<section class="sw">
<h2 class="section">3. Grundprinzipien</h2>
<ul>
<li><strong>Linting:</strong> Kompromisslos – <code>--max-warnings=0</code>. Eine Warning bricht den Build.</li>
<li><strong>Typsicherheit:</strong> TypeScript <code>strict</code>. <code>any</code>/Type-Escapes nur über eine gedeckelte Baseline.</li>
<li><strong>Tests:</strong> Sehr hohe Coverage auf kritischen Pfaden (Auth, Billing, Tenant-Isolation ~99 %), erzwungen pro Verzeichnis.</li>
<li><strong>End-to-End:</strong> Pflicht. Kritische Flows müssen als Playwright-Specs existieren; ein Gate prüft wörtliche Test-Inhalte.</li>
<li><strong>Accessibility:</strong> WCAG 2.2 AA als Gate (Farbkontrast, Mobile-Overflow).</li>
<li><strong>EU-Datenschutz by design:</strong> RLS, Envelope-Encryption und EU-only-Provider sind Architektur-Vorgaben, kein Add-on.</li>
<li><strong>Automatisierung statt Disziplin:</strong> ~80 <code>verify:*</code>-Gates erzwingen die Regeln in Pre-Commit-Hooks und CI.</li>
</ul>
<div class="callout"><span class="t">Leitsatz</span>Fehler nach links schieben: Was lokal in Sekunden gefangen werden kann, läuft im Pre-Commit-Hook. CI ist Sicherheitsnetz, nicht erster Prüfer.</div>
</section>
<!-- 4 -->
<section class="sw">
<h2 class="section">4. Code-Qualität &amp; Linting</h2>
<p>ESLint mit <code>@typescript-eslint</code>, <code>import</code>, <code>jsx-a11y</code>, <code>react-hooks</code> (Basis <code>plugin:jsx-a11y/recommended</code>). Konfiguration identisch zum Boilerplate.</p>
<div class="callout warn"><span class="t">Die wichtigste Regel</span><code>pnpm run lint</code> = <code>eslint --max-warnings=0 --ext .tsx,.ts .</code> — <strong>Null Warnings.</strong> Es gibt keine „gelben" Builds.</div>
<div class="group">
<h3>Harte Regeln (Error-Level)</h3>
<table>
<tr><th>Regel</th><th>Wirkung</th></tr>
<tr><td><code>no-debugger</code></td><td>Kein <code>debugger</code>.</td></tr>
<tr><td><code>no-console</code> (Server)</td><td>In Server-Code (<code>*.server.ts</code>, <code>apps/api</code>, <code>apps/worker</code>, <code>packages/*/src</code>, Service-/DB-Layer) verboten; in UI/Tests/Skripten erlaubt.</td></tr>
<tr><td><code>no-restricted-imports</code></td><td>Kein direkter <code>@prisma/client</code> (nur über <code>@toolctrl/db</code>); keine direkten <code>src/*</code>-Imports zwischen Packages; <code>foundation</code>/<code>view-models</code> ohne React/UI; UI-Packages ohne DB.</td></tr>
<tr><td><code>no-restricted-syntax</code> (Phase-C-UI)</td><td>Keine hartkodierten nutzersichtbaren Strings in <code>ui-app</code>/<code>ui-subframe</code>/<code>ui-mock</code> – alles über i18n-Keys.</td></tr>
</table>
</div>
<div class="group">
<h3>Accessibility-Lint &amp; Escape-Hatch-Baseline</h3>
<p>jsx-a11y-Regeln „monoton fallend": bei 0 Verstößen werden sie auf <code>error</code> hochgestuft und können nie wieder regredieren. Type-/Lint-Ausnahmen sind gedeckelt (<code>verify:eslint-baseline</code>): max. <strong>262</strong> <code>eslint-disable</code>, <strong>35</strong> <code>@ts-ignore</code>, <strong>43</strong> <code>@ts-expect-error</code>. Neue brauchen Baseline-Erhöhung + Review.</p>
<p class="small">Datei-Limits: <code>apps/api/src/app.ts</code> ≤ 450 Zeilen, API-Route-Module ≤ 500 Zeilen (<code>verify:source-health</code>).</p>
</div>
</section>
<!-- 5 -->
<section class="sw">
<h2 class="section">5. TypeScript &amp; Typsicherheit</h2>
<ul>
<li><code>strict: true</code> (inkl. <code>noImplicitAny</code>, <code>strictNullChecks</code> …). <code>any</code> praktisch verboten.</li>
<li>Typecheck ist Pflicht nach Routen-Änderungen und läuft als Pre-Push-Hook.</li>
</ul>
<div class="callout"><span class="t">React-Router-v7-Regel</span>Routen-Typen <strong>immer</strong> aus <code>./+types/[routeName]</code> (nie relativ). Fehlen Typen → <code>pnpm run typecheck</code>, nie den Importpfad „reparieren".</div>
</section>
<!-- 6 -->
<section class="sw">
<h2 class="section">6. Formatierung (Prettier)</h2>
<table>
<tr><th>Einstellung</th><th>Wert</th><th>Einstellung</th><th>Wert</th></tr>
<tr><td>printWidth</td><td>160</td><td>semi</td><td>true</td></tr>
<tr><td>singleQuote</td><td>false (Double Quotes)</td><td>tabWidth</td><td>2 (Spaces)</td></tr>
<tr><td>trailingComma</td><td>es5</td><td>arrowParens</td><td>always</td></tr>
<tr><td>endOfLine</td><td>lf</td><td>Plugin</td><td>prettier-plugin-tailwindcss</td></tr>
</table>
</section>
<!-- 7 -->
<section class="sw">
<h2 class="section">7. Unit- &amp; Integrationstests + Coverage</h2>
<p>Vitest + Testing Library (jsdom), Coverage-Provider v8. Schwellen identisch zum Boilerplate und <strong>pro Verzeichnis</strong> erzwungen (<code>verify:test-coverage</code> gegen <code>scripts/coverage-targets.json</code>).</p>
<table>
<tr><th>Kritischer Pfad</th><th>Statements</th><th>Branches</th></tr>
<tr><td>API Auth</td><td>99 %</td><td>92 %</td></tr>
<tr><td>Billing</td><td>99 %</td><td>97 %</td></tr>
<tr><td>Tenant-Isolation</td><td>99 %</td><td>99 %</td></tr>
<tr><td>Session / Permissions</td><td>99 %</td><td>98 %</td></tr>
<tr><td>Stripe</td><td>95 %</td><td>90 %</td></tr>
<tr><td>Basis-Targets (z. B. apps/api)</td><td>70 %</td><td>60 %</td></tr>
</table>
<p><strong>Test-Dichte-Gate</strong> (Pre-Commit): Mindestverhältnis Tests/Quelldateien je Workspace (z. B. <code>apps/api</code> ≥ 0,18; App-Routes ≥ 0,05; <code>packages/brand</code>/<code>config</code> ≥ 0,50). Neue HiveGrid-Pakete (z. B. <code>@hivegrid/activity</code>) brauchen eigene Tests.</p>
<p class="small">Konventionen: <code>*.test.ts(x)</code> / <code>*.spec.ts</code>; Boundary <code>*boundary.test.ts</code>; a11y <code>*.a11y.test.tsx</code> / <code>*.axe.test.tsx</code>.</p>
</section>
<!-- 8 -->
<section class="sw">
<h2 class="section">8. End-to-End-Tests (Playwright)</h2>
<p>Verzeichnis <code>tests/e2e</code> (~39 Specs). Chromium, optional Mobile-Matrix (Pixel 5 + iPhone 13). Seriell, Timeout 120 s, Traces bei Fehlern. E2E laufen nächtlich gegen die Live-Staging-Umgebung; Basis ist ein deterministischer Readiness-Tenant (<code>seed:e2e-readiness</code>).</p>
<div class="group">
<h3>a11y-Coverage-Gate (pinnt Tests wörtlich)</h3>
<p><code>verify:a11y-coverage</code> verlangt ≥ 12 Component-a11y-Tests und ≥ 7 E2E-a11y-Specs und prüft wörtliche Snippets kritischer Flows (z. B. <code>expectNoCriticalA11yViolations(</code>, <code>expectNoHorizontalOverflow(</code>).</p>
<div class="callout warn"><span class="t">Nach E2E-Refactors</span>Das Gate matcht <strong>literale Strings</strong>. Tests umbenennen/umbauen → <code>verify:a11y-coverage</code> erneut laufen lassen.</div>
</div>
<div class="group">
<h3>RR7 Single-Fetch-Interceptor-Contract</h3>
<p>Form-POST-Interceptoren müssen das <code>.data</code>-Ziel matchen und <strong>turbo-stream</strong> zurückgeben (kein rohes JSON), inkl. <code>X-Remix-Response: yes</code>. Helfer in <code>tests/e2e/utils/navigation.ts</code>.</p>
<p class="small">Pflicht-Flows (Auszug): Registration/Onboarding, Golden-Path, Subscription-Smoke, Plugin-Settings/-Lifecycle, Workflow-Builder+Run, Members, Audit, Profile-Security, Privacy, API-Keys, Mobile-Shell.</p>
</div>
</section>
<!-- 9 -->
<section class="sw">
<h2 class="section">9. Accessibility</h2>
<p>jsx-a11y-Lint · <code>verify:color-contrast</code> (WCAG 2.2 AA) · axe-core-Suite (<code>test:a11y</code>) · Mobile-Overflow-/Violation-Checks in E2E · Lighthouse-a11y als Warnung.</p>
</section>
<!-- 10 -->
<section class="sw">
<h2 class="section">10. Quality-Gates: Commit → Push → Merge → Deploy</h2>
<div class="group">
<h3>Beim Commit <span class="pill local">lefthook pre-commit · parallel</span></h3>
<p>lint-staged (ESLint auf geänderten Dateien) · <code>verify-secrets</code> · <code>verify-eslint-baseline</code> · <code>verify-test-density</code>.</p>
</div>
<div class="group">
<h3>Beim Push <span class="pill local">lefthook pre-push · seriell</span></h3>
<p><code>typecheck</code> · <code>verify-hardcoded-strings</code> (i18n). Notfall-Bypass nur mit <code>LEFTHOOK=0</code>, nie auf <code>main</code>.</p>
</div>
<div class="group">
<h3>Beim Merge (CI) <span class="pill block">blockiert</span></h3>
<p>Jobs <code>quality</code> + <code>api</code> + <code>worker</code> + <code>database</code> + <code>docker-smoke</code> müssen grün sein (Typecheck, Lint, Tests, Build, Bundle-Size, a11y, ~18+ Baseline-Gates).</p>
</div>
<div class="group">
<h3>Beim Deploy <span class="pill block">blockiert</span></h3>
<p>Preflight-Gates (<code>deploy-readiness</code>, <code>docker-baseline</code>, <code>docker-non-root</code>). Produktions-Promote erfordert <code>backup_confirmed=true</code>.</p>
</div>
</section>
<!-- 11 -->
<section class="sw">
<h2 class="section">11. CI/CD-Pipeline</h2>
<p>Zwei parallele Welten mit gleichen Gates: GitHub Actions und <strong>Gitea</strong> (self-hosted Runner). Eigene Gates sichern die Parität. <code>ci.yml</code>-Jobs: <code>quality</code>, <code>api</code>, <code>worker</code>, <code>database</code>, <code>docker-smoke</code>.</p>
<p>Die ~80 <code>verify:*</code>-Gates sind <strong>Baseline-/Anti-Regressions-Gates</strong>. Gruppen: Security/Auth · Mandantentrennung/Boundaries · Docker/Deploy · i18n/a11y · API/Specs · CI-Struktur · Docs.</p>
<div class="callout"><span class="t">Lighthouse &amp; SEO</span>Performance ≥ 95 % ist hart gegated; a11y/Best-Practices/PWA nur Warnung. <strong>SEO ist bewusst nicht gegated</strong> (B2B-SaaS).</div>
</section>
<!-- 12 -->
<section class="sw">
<h2 class="section">12. Infrastruktur &amp; Hosting</h2>
<p>HiveGrid Cloud läuft im gemeinsamen Hetzner-Estate (EU/Deutschland), zentral über Coolify gesteuert. Detail-Inventar im separaten Dokument „Server-Struktur". Das Wesentliche:</p>
<ul>
<li><strong>Control-Plane:</strong> Coolify v4 auf der <code>control</code>-VM (Dashboard <code>saas-ctrl.com</code>) steuert alle Hosts per SSH.</li>
<li><strong>Staging:</strong> Ein gemeinsamer Host <code>staging-1</code> trägt mehrere Produkt-Stagings nebeneinander – nicht eine VM pro Produkt.</li>
<li><strong>Produktion:</strong> Laut ADR-009 erhält jedes SaaS-Produkt ein <strong>eigenes Hetzner-Projekt</strong> für Prod (eigene DB/App). Das HiveGrid-Cloud-Prod-Projekt wird bei Reife angelegt; die SaaS-App ist aktuell in Entwicklung und noch nicht deployt.</li>
<li><strong>Zentrale Dienste (geteilt über WireGuard-Mesh, auf <code>node1</code>):</strong> Sentry (Errors), Loki (Logs), Meilisearch (Suche, per-Produkt-Scoped-Key), ClamAV (Virenscan), mCaptcha; Prometheus + Grafana auf <code>control</code>.</li>
<li><strong>Services pro App-Stack:</strong> web (8080), api (8080), worker ×5 Queue-Gruppen (general/mail/import/webhook/plugin), Postgres 16, PgBouncer (Transaction-Pooling), Redis 7, Authentik (Admin-SSO).</li>
<li><strong>Deploy:</strong> build-once-deploy-many über die Gitea-Registry; Coolify zieht das fertige Image und startet die Container neu (~30 s). Rollback = altes SHA-Image neu deployen.</li>
</ul>
<div class="callout ok"><span class="t">EU-Hosting</span>Compute, Daten und Object-Storage (S3) liegen in der EU – auf Hetzner (Deutschland), orchestriert über Coolify. Das stützt die EU-Datenschutz-Vorgaben aus Abschnitt 13.</div>
</section>
<!-- 13 -->
<section class="sw">
<h2 class="section">13. Architektur-Konventionen</h2>
<ul>
<li><strong>Upstream-Disziplin:</strong> Geteilten Code so lassen, dass <code>git merge upstream/main</code> sauber bleibt. Neues HiveGrid-Verhalten in <code>@hivegrid/*</code>-Paketen oder klar abgegrenzten Modulen.</li>
<li><strong>Package-Grenzen:</strong> Immer öffentliche Paketnamen importieren, nie fremdes <code>src/*</code>. Server-DB nur über <code>@toolctrl/db/client.server</code>, Env über <code>@toolctrl/config</code>, Queues über <code>@toolctrl/queue</code>.</li>
<li><strong>EU-Stack erzwingen:</strong> Keine US-AI-Provider (nur EU-Provider: Scaleway, Mistral, Aleph Alpha); kein Social-OAuth (nur E-Mail/Passwort, Magic-Link, TOTP); Per-Tenant-Envelope-Encryption; Postgres-RLS als zusätzliche Mandanten-Grenze.</li>
<li><strong>URLs:</strong> host-aware Helfer (<code>href()</code>, <code>appPath</code>/<code>adminPath</code>/<code>apiPath</code>), keine handgebauten Pfade.</li>
<li><strong>Secrets:</strong> nie in UI-Code; nur über <code>@toolctrl/config</code> / Server-Boundaries. Tenant-Filter bei jedem Datenzugriff erhalten.</li>
<li><strong>RR7:</strong> Typen aus <code>./+types/...</code>; <code>&lt;Outlet/&gt;</code> in Layouts; <code>typecheck</code> nach Routen-Änderungen.</li>
<li><strong>Doku mitführen:</strong> bei Feature-/Provider-/Worker-Änderungen <code>docs/feature-manifests/*.json</code>; bei mutierenden Routen <code>docs/security/csrf-inventory.md</code>; bei Rollen/Permissions <code>docs/roles-matrix.md</code>. HiveGrid-Fortschritt in <code>docs/PROGRESS-TRACKER.md</code>, Entscheidungen in <code>docs/ARCHITECTURE-DECISIONS.md</code>.</li>
</ul>
</section>
<!-- 14 -->
<section class="sw">
<h2 class="section">14. Onboarding-Checkliste</h2>
<div class="callout ok">
<span class="t">Vor dem ersten Commit</span>
<ol style="margin:4px 0 0 0;">
<li><code>nvm use</code> (Node <code>v22.12.0</code>), dann <code>pnpm install</code> – installiert die Hooks automatisch.</li>
<li><code>AGENTS.md</code> + <code>docs/ARCHITECTURE-DECISIONS.md</code> lesen (Package-Modell, EU-Stack, Milestone-Struktur).</li>
<li><code>pnpm run dev</code> starten – generiert die RR7-Routen-Typen.</li>
<li><code>pnpm run typecheck</code> &amp; <code>pnpm run lint</code> sauber (0 Warnings); vor Push <code>pnpm test</code> grün.</li>
<li>Für E2E: <code>seed:e2e-readiness</code>, <code>.env.e2e-readiness</code> sourcen, dann <code>verify:a11y-coverage</code> + Playwright.</li>
</ol>
</div>
<p class="small">Goldene Regel: Wenn ein Gate rot ist, ist die Lösung der Code – nicht das Umgehen des Gates.</p>
<hr>
<p class="small">Stand 14. Juni 2026. Quellen: HiveGrid-Cloud <code>package.json</code>, <code>.eslintrc</code>, <code>.prettierrc</code>, <code>tsconfig.json</code>, <code>vitest.config.ts</code>, <code>playwright.config.ts</code>, <code>lefthook.yml</code>, <code>scripts/coverage-targets.json</code>, <code>AGENTS.md</code>, <code>docs/ARCHITECTURE-DECISIONS.md</code> sowie das <code>infra</code>-Repository. Konfiguration ist – bis auf Node-Version und HiveGrid-spezifische Pakete/Docs – identisch zum ToolCTRL-Boilerplate.</p>
</section>
</body>
</html>
+162
View File
@@ -0,0 +1,162 @@
# Environment Access Inventory
This document is the environment-access baseline human-readable source of truth for direct environment access migration. It records key names, source locations, target runtimes, catalog classification, and follow-up owners only; it intentionally excludes values from `.env*` files and test fixtures.
## Scan command
Evidence was gathered with targeted source scans equivalent to:
```sh
grep -R "process\.env\|env(" apps/web/app apps/api apps/worker packages server prisma.config.ts .env.example .env.local.example .env.production.example .env.fly.example
```
Follow-up scans should prefer exact key extraction from source files and examples, then compare results against `packages/config/src/catalog.ts`.
## env drift gate contract
S02 wires the env catalog contract into CI with `pnpm run verify:env-drift`, implemented by `scripts/check-env-drift.mjs`. The gate scans the five versioned fixture/example surfaces `.env.example`, `.env.local.example`, `.env.production.example`, `.env.fly.example`, and `.env.test`, then compares their key names against `packages/config/src` catalog definitions, including dynamic-prefix handling such as `SITE_KEY_*`. In `.gitea/workflows/ci.yml`, the quality job runs `pnpm run verify:env-drift` immediately after `pnpm run verify:ci-baseline` and before `pnpm typecheck`, `pnpm lint`, `pnpm test`, and `pnpm build` so env-contract drift fails before more expensive checks.
`.env.test` is a committed, non-secret test fixture for deterministic local and CI validation; it must remain safe to read and version, even though real `.env*` files stay ignored. Drift diagnostics may print only file paths, line numbers, diagnostic categories, and key names. No secret values, placeholder values, or raw `.env` assignments may be emitted by `scripts/check-env-drift.mjs`, CI logs, or this inventory.
## Classification legend
| Classification | Meaning | Migration expectation |
| ------------------------ | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `required-core` | Required platform/core configuration used across runtimes. | Load through the shared config catalog before runtime startup. |
| `required-per-app` | Required for specific apps/web/app/API capabilities. | Migrate to the owning runtime loader and fail clearly when active feature paths require it. |
| `production-only` | Supplied by Node/platform or only needed in deployed environments. | Keep normalized behind runtime loaders; local defaults are acceptable where cataloged. |
| `optional-provider` | Optional third-party provider/integration settings. | Runtime loader should expose capability state and avoid hard failures until the provider feature is used. |
| `local-development-only` | Local/demo convenience only. | Keep out of production required groups. |
| `deprecated-remove` | Present in examples/catalog for audit history but no active source usage. | Remove from examples/docs unless a later slice finds active usage. |
| `dynamic` | Pattern-based key family rather than a single literal key. | Loader/enforcement must support prefix matching. |
| `test-only` | Test fixture mutation/read. | Not a runtime migration target. |
## Source-access table
| Key or family | Active source locations | Target runtime(s) | Catalog classification | Follow-up owner | Notes |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APP_NAME` | `apps/web/app/routes/**/*.tsx`, `apps/web/app/modules/**/*.ts`, `apps/api/src/app.ts`; core web loader usage adopted in `apps/web/app/utils/data/.server/rootData.ts` and `apps/web/app/utils/db/appConfiguration.db.server.ts` | web, api, worker/shared | `required-core` | S03 api, S05 docs, S06 enforcement | adopted root data/app-configuration reads through `loadWebConfig`; broad web route/module metadata reads remain direct and should be handled as later docs/enforcement cleanup rather than claimed complete here. |
| `DATABASE_URL` | `packages/db/src/client.server.ts`, `prisma.config.ts` | shared, web, api, worker | `required-core` | web, S03 api, S04 worker | Prisma runtime and Prisma config both need catalog-backed loading. |
| `SESSION_SECRET` | `apps/web/app/locale/i18next.server.ts`, `apps/web/app/utils/session.server.ts`, `apps/web/app/utils/analyticsCookie.server.ts` | web, api | `required-core` | web | Cookie/session signing secret; current fallback strings should be reviewed during loader migration. |
| `APP_BASE_DOMAIN` | `packages/config/src/routing.ts`, `apps/api/src/app.test.ts` | shared, web, api, worker | `required-core` | web, S03 api | Runtime routing helper currently accepts env object and defaults to `process.env`. Test mutation is fixture-only. |
| `WEB_APP_HOST` | `packages/config/src/routing.ts` via env object | web, api | `required-per-app` | web, S03 api | Canonical tenant app host. |
| `APP_URL` | `apps/web/app/routes/app.$tenant/forms*.tsx`, `apps/worker/src/jobs/email-sequence-runtime.ts`, `apps/worker/src/jobs/workflow-trigger-http-executor.ts` | web, worker | `optional-provider` | S02 web, S04 worker | Public app origin used for embed fallback URLs, email sequence links, and the workflow-trigger execution bridge fallback. |
| `ADMIN_APP_HOST` | `packages/config/src/routing.ts` via env object | web, api | `required-per-app` | web, S03 api | Canonical admin host. |
| `API_HOST` | `packages/config/src/routing.ts` via env object | api, web | `required-per-app` | web, S03 api | Canonical API host. |
| `APP_PROTOCOL` | `packages/config/src/routing.ts` via env object | shared, web, api, worker | `production-only` | web, S03 api | Defaults to `http` locally; production should provide `https`. |
| `CRYPTO_SECRET` | `apps/web/app/utils/api/.server/CryptoApi.ts` | web, api | `required-per-app` | web, S03 api | Used for credential encryption/decryption. |
| `API_ACCESS_TOKEN` | `apps/web/app/utils/services/apiService.ts`, `apps/api/src/app.ts`, `apps/api/src/app.test.ts`, `apps/worker/src/jobs/workflow-trigger-http-executor.ts` | web, api, worker | `required-per-app` | web, S03 api, S04 worker | Service token for authenticated API calls and the worker-to-web workflow execution bridge; API tests mutate it as fixtures. |
| `JWT_SECRET` | `apps/web/app/utils/services/apiService.ts`, `apps/api/src/app.ts`, `apps/api/src/app.test.ts` | web, api | `required-per-app` | web, S03 api | Legacy JWT signing/verification secret; API test mutations are fixtures. |
| `NODE_ENV` | `apps/web/app/locale/i18next.server.ts`, `apps/web/app/utils/url.server.ts`, `apps/web/app/utils/loaders.middleware.ts`, `apps/web/app/utils/stream/emitter.server.ts`, `apps/web/app/utils/session.server.ts`, `apps/web/app/utils/analyticsCookie.server.ts`, `apps/web/app/utils/db/seed/SeedService.ts`, `apps/web/app/utils/data/.server/rootData.ts`, `apps/web/app/routes/admin/**/*.tsx`, `packages/db/src/client.server.ts`, `server/server.ts` | shared, web, api, worker | `production-only` | web, S03 api, S04 worker | Supplied by runtime/platform. `server/server.ts` sets a production default before creating the React Router handler. |
| `APP_REVISION` | `packages/config/src/api.ts`, web/worker health loaders via runtime env | shared, web, api, worker | `production-only` | S03 api, S04 worker, S06 enforcement | Immutable deploy revision injected by image build or runtime platform; defaults to `unknown` locally. |
| `WEB_BUILD_DIRECTORY` | `server/server.ts` | web | `production-only` | web, S06 enforcement | Optional server-runtime override for smoke tests and non-standard runtime layouts; Docker images normally use `apps/web/build`. |
| `PORT` | `server/server.ts`, `apps/api/src/server.ts` | web, api | `production-only` | web, S03 api, S06 enforcement | Platform listener port; web usage is catalog-backed and API usage remains a catalog/exemption follow-up. |
| `HOST` | web listen usage adopted in `server/server.ts` | web | `local-development-only` | S06 enforcement | cataloged and adopted optional Node listen host through `loadWebConfig`; enforcement should keep it platform-owned and non-secret. |
| `DIRECT_URL` | `packages/config/src/catalog.ts` plus commented env examples such as `.env.example` and `.env.production.example` | shared, web, api, worker | `production-only` | S05 docs | No active runtime source read found in scanned TS/TSX; keep as optional Prisma future candidate. |
| `SERVER_URL` | `apps/web/app/utils/url.server.ts` commented code; examples list it commented | web, api | `production-only` | S05 docs | Commented-out URL helper candidate, not active runtime access. |
| `VERCEL_URL` | `apps/web/app/locale/i18next.server.ts` comment, `apps/web/app/utils/url.server.ts` commented code | web | `production-only` | S05 docs | Platform candidate only; no active runtime read. |
| `WEBHOOKS_URL` | `apps/web/app/utils/url.server.ts` | web, api | `optional-provider` | web, S03 api | Public webhook callback override, commonly local tunnel URL. |
| `STRIPE_SK` | `apps/web/app/utils/stripe.server.ts`, `apps/api/src/app.ts`, `apps/web/app/utils/services/authService.ts`, `apps/web/app/utils/api/.server/TenantsApi.ts`, `apps/web/app/utils/data/.server/rootData.ts`, `apps/worker/src/jobs/ai-cost-stripe-submitter.ts`, `apps/worker/src/jobs/ai-cost-reconciliation-bootstrap.ts` | web, api, worker | `optional-provider` | web, S03 api, S04 worker | Server secret; root data only checks test-mode prefix for UI state. Worker uses it for AI-overage meter event submission and reconciliation. |
| `STRIPE_WEBHOOK_SIGNING_SECRET` | `apps/api/src/app.ts` | api | `optional-provider` | S03 api | Stripe webhook signature verification secret. |
| `MAILJET_API_KEY` | `packages/mail/src/mailjet.ts` via env object | api, worker | `optional-provider` | S03 api, S04 worker | Default transactional provider. |
| `MAILJET_FROM_EMAIL` | `packages/mail/src/mailjet.ts` via env object | api, worker | `optional-provider` | S03 api, S04 worker | Default Mailjet sender address. |
| `MAILJET_FROM_NAME` | examples/catalog only | api, worker | `optional-provider` | S05 docs | Example-only optional display name; no active direct source read found. |
| `INTEGRATIONS_CONTACT_FORMSPREE` | `apps/web/app/modules/crm/services/CrmService.ts`, examples/catalog | web | `optional-provider` | S05 docs | Optional CRM contact form endpoint. |
| `CONVERTKIT_APIKEY` | `apps/web/app/modules/crm/services/CrmService.ts`, examples/catalog | web | `optional-provider` | S05 docs | Optional CRM/newsletter provider key; active web CRM usage. |
| `CONVERTKIT_FORM` | `apps/web/app/modules/crm/services/CrmService.ts`, examples/catalog | web | `optional-provider` | S05 docs | Optional CRM/newsletter form id; active web CRM usage. |
| `CONVERTKIT_API_SECRET` | `apps/web/app/modules/crm/services/CrmService.ts`, examples/catalog | web | `optional-provider` | S05 docs | Optional CRM/newsletter subscriber lookup/update secret; active web CRM usage. |
| `ANALYTICS_GA_TRACKING_ID` | `apps/web/app/utils/db/appConfiguration.db.server.ts` | web | `optional-provider` | web | Exposed through app configuration for Google Analytics. |
| `ANALYTICS_TOKEN` | `apps/api/src/app.ts`, `apps/api/src/app.test.ts` | api | `required-per-app` | S03 api | Public analytics endpoint rejects missing/default token. Test mutations are fixtures. |
| `AUTH_RECAPTCHA_SITE_KEY` | `apps/web/app/utils/db/appConfiguration.db.server.ts` | web | `optional-provider` | web | Public reCAPTCHA site key exposed through app configuration. |
| `NOTIFICATIONS_NOVU_API_KEY` | `apps/web/app/modules/notifications/services/.server/NotificationService.ts` | web | `optional-provider` | web | Optional admin diagnostics for the external notification provider; product sends use the internal queue. |
| `AI_GENERATION_MAX_ESTIMATED_INPUT_COST_CENTS` | `apps/web/app/modules/ai-gen/services/ai-generation-guards.server.ts` | web | `optional-provider` | web | Optional guardrail for denying generation requests above the configured estimated input-cost budget. |
| `AI_GENERATION_RATE_LIMIT` | `apps/web/app/modules/ai-gen/services/ai-generation-guards.server.ts` | web | `optional-provider` | web | Optional per-user generation request limit for the in-memory guard window. |
| `AI_GENERATION_RATE_LIMIT_WINDOW_MS` | `apps/web/app/modules/ai-gen/services/ai-generation-guards.server.ts` | web | `optional-provider` | web | Optional generation rate-limit window in milliseconds. |
| `MISTRAL_API_KEY` | `packages/ai/src/providers/mistral.ts`, examples/catalog; web AI generation imports `@toolctrl/ai` server-side | web, api, worker | `optional-provider` | web, S03 api, S04 worker | Primary AI provider key for deployments that select this provider. |
| `MISTRAL_MODEL` | `packages/ai/src/providers/mistral.ts`, examples/catalog; web AI generation imports `@toolctrl/ai` server-side | web, api, worker | `optional-provider` | web, S03 api, S04 worker | Primary Mistral model override; defaults to `mistral-large-latest`. |
| `SCALEWAY_API_KEY` | `packages/ai/src/providers/scaleway.ts`, examples/catalog; web AI generation imports `@toolctrl/ai` server-side | web, api, worker | `optional-provider` | web, S03 api, S04 worker | AI fallback provider key for deployments that select this provider. |
| `SCALEWAY_MODEL` | `packages/ai/src/providers/scaleway.ts`, examples/catalog; web AI generation imports `@toolctrl/ai` server-side | web, api, worker | `optional-provider` | web, S03 api, S04 worker | Scaleway fallback model override. |
| `SCALEWAY_BASE_URL` | `packages/ai/src/providers/scaleway.ts`, examples/catalog; web AI generation imports `@toolctrl/ai` server-side | web, api, worker | `optional-provider` | web, S03 api, S04 worker | Scaleway Generative APIs base URL; defaults to `https://api.scaleway.ai/v1`. |
| `ALEPH_ALPHA_API_KEY` | `packages/ai/src/providers/aleph-alpha.ts`, examples/catalog; web AI generation imports `@toolctrl/ai` server-side | web, api, worker | `optional-provider` | web, S03 api, S04 worker | Additional AI fallback provider key for deployments that select this provider. |
| `ALEPH_ALPHA_MODEL` | `packages/ai/src/providers/aleph-alpha.ts`, examples/catalog; web AI generation imports `@toolctrl/ai` server-side | web, api, worker | `optional-provider` | web, S03 api, S04 worker | Aleph Alpha fallback model override. |
| `ALEPH_ALPHA_BASE_URL` | `packages/ai/src/providers/aleph-alpha.ts`, examples/catalog; web AI generation imports `@toolctrl/ai` server-side | web, api, worker | `optional-provider` | web, S03 api, S04 worker | Aleph Alpha compatible base URL. |
| `WORKER_ENABLED` | `apps/worker/src/runtime.ts`, `apps/worker/src/health.ts` | worker | `optional-provider` | S04 worker | Worker runtime starts queues only when exactly `true`. |
| `EMAIL_QUEUE_NAME` | `packages/config/src/worker.ts` | worker | `optional-provider` | S04 worker | Optional transactional-email queue-name override; defaults to `toolctrl.email`. |
| `WORKFLOW_QUEUE_NAME` | `packages/config/src/worker.ts` | worker | `optional-provider` | S04 worker | Optional workflow queue-name override; defaults to `toolctrl.workflows`. |
| `WORKFLOW_TRIGGER_EXECUTOR_URL` | `apps/worker/src/jobs/workflow-trigger-http-executor.ts` | worker | `optional-provider` | S04 worker | Optional internal web-runtime origin for `outbox.workflow`; falls back to `APP_URL` or `WEB_ORIGIN`. |
| `WORKFLOW_TRIGGER_EXECUTOR_MODE` | `apps/worker/src/jobs/workflow-trigger-executor-factory.ts`, `apps/worker/src/jobs/workflow-trigger-native-executor.ts` | worker | `optional-provider` | S04 worker | Selects the `outbox.workflow` executor adapter; default `http`, `hybrid` canaries native safe-core execution with HTTP fallback, and `native` is an opt-in safe-core MVP for `event`, `manual`, `if`, `switch`, `doNothing`, `log`, `variable`, `alertUser`, `httpRequest`, plus tenant-scoped `rowGet` and `email` blocks. |
| `WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS` | `apps/worker/src/jobs/workflow-trigger-http-executor.ts` | worker | `optional-provider` | S04 worker | HTTP timeout for the worker-to-web workflow execution bridge. |
| `WORKFLOW_TRIGGER_OUTBOX_ENABLED` | `apps/worker/src/jobs/workflow-trigger-bootstrap.ts` | worker | `optional-provider` | S04 worker | Escape hatch for the general worker's `outbox.workflow` subscriber; set to `false` only for controlled maintenance. |
| `WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY` | `apps/worker/src/jobs/workflow-trigger-bootstrap.ts` | worker | `optional-provider` | S04 worker | BullMQ concurrency for the general worker's `outbox.workflow` subscriber. |
| `WORKFLOW_TRIGGER_OUTBOX_REQUIRED` | `apps/worker/src/jobs/workflow-trigger-bootstrap.ts`, `apps/worker/src/index.ts` | worker | `optional-provider` | S04 worker | Optional fail-loud guard for production general workers; set to `true` once the workflow trigger bridge and Redis must be present. |
| `OUTBOX_DISPATCHER_REQUIRED` | `apps/worker/src/outbox/bootstrap.ts`, `apps/worker/src/outbox/dispatcher.ts`, `apps/worker/src/index.ts` | worker | `optional-provider` | S04 worker | Optional fail-loud guard for production general workers; set to `true` once the outbox dispatcher must be active so queued outbox rows cannot silently pile up. |
| `IMPORT_QUEUE_ENABLED` | `apps/web/app/routes/app.$tenant/internal/imports/submit.tsx`, `apps/worker/src/queues.ts`, `apps/worker/src/health.ts` | web, worker | `optional-provider` | S03 api, S04 worker | Queued imports remain disabled unless import and scan services are wired for the worker. |
| `BULLMQ_REDIS_URL` | `packages/queue/src/connection.ts`, `apps/web/app/routes/app.$tenant/internal/imports/submit.tsx`, `apps/worker/src/health.ts` via env object | web, api, worker | `optional-provider` | web, S03 api, S04 worker, S05 docs | Primary BullMQ Redis URL for web/API enqueue and worker queues; optional unless the worker/queue path is enabled. |
| `REDIS_URL` | `packages/queue/src/connection.ts`, `apps/web/app/routes/app.$tenant/internal/imports/submit.tsx`, `apps/worker/src/health.ts` via env object | web, api, worker | `optional-provider` | web, S03 api, S04 worker, S05 docs | BullMQ fallback only when `BULLMQ_REDIS_URL` is absent; do not document as an independent required key. |
| `BROWSER_RUNTIME_URL` | `packages/config/src/worker.ts`, examples/catalog | api, worker | `optional-provider` | S04 worker, S05 docs | Optional browser-backed worker runtime endpoint. |
| `BROWSER_RUNTIME_API_KEY` | `packages/config/src/worker.ts`, examples/catalog | api, worker | `optional-provider` | S04 worker, S05 docs | Optional browser runtime secret for server-side worker use. |
| `BROWSER_SESSION_TIMEOUT_MS` | `packages/config/src/worker.ts`, examples/catalog | api, worker | `optional-provider` | S04 worker, S05 docs | Optional browser runtime timeout policy. |
| `TOOLCTRL_API_MAINTENANCE_MODE` | `apps/api/src/platform/maintenance/maintenance.ts` | api | `production-only` | S03 api | Set to `1` to serve maintenance responses from non-health API routes. |
| `TOOLCTRL_API_MAINTENANCE_RETRY_AFTER_SECONDS` | `apps/api/src/platform/maintenance/maintenance.ts` | api | `production-only` | S03 api | Optional `Retry-After` seconds value for API maintenance responses. |
| `IPSTACK_API_KEY` | `apps/web/app/utils/helpers/.server/IpService.ts` | api, worker | `optional-provider` | S03 api | IP enrichment provider key. |
| `IPAPIIS_API_KEY` | examples/catalog only in targeted scan | api, worker | `optional-provider` | S05 docs | IP enrichment provider candidate. |
| `PORTAL_SERVER_URL` | `apps/web/app/routes/app.$tenant/__portals/portals.tsx`, examples/catalog | web, api | `optional-provider` | web, S03 api | External portal server URL; current route throws when absent. |
| `PORTAL_SERVER_FLY_TOKEN` | `apps/web/app/modules/domains/services/Domains.server.ts`, examples/catalog | web, api, worker | `optional-provider` | web, S05 docs | Portal automation token candidate. |
| `DEMO_USER` | `apps/web/app/routes/login.tsx`, `packages/config/src/web.ts` | web | `local-development-only` | web | Local login hint in `email:password` shape; ignored outside `NODE_ENV=development`. |
| `SEED_ADMIN_EMAIL` | `apps/web/app/utils/db/seed/SeedService.ts`, examples/catalog | web | `production-only` | web | Production bootstrap admin email for guarded seed runs; local defaults are refused in production. |
| `SEED_ADMIN_PASSWORD` | `apps/web/app/utils/db/seed/SeedService.ts`, examples/catalog | web | `production-only` | web | Production bootstrap admin password for guarded seed runs; local default password is refused in production. |
| `SEED_DEMO_DATA` | `apps/web/app/utils/db/seed/SeedService.ts`, `apps/web/app/modules/users/services/AuthService.ts`, examples/catalog | web | `local-development-only` | web | Local demo seed opt-in; staging may enable it only with `ALLOW_STAGING_DEMO_SEED=true` for disposable CRM SaaS demo data and login. |
| `ALLOW_STAGING_DEMO_SEED` | `apps/web/app/utils/db/seed/SeedService.ts`, `apps/web/app/modules/users/services/AuthService.ts`, examples/catalog | web | `production-only` | web | Guard for disposable staging CRM SaaS demo seed and login; never set on production resources. |
| `SITE_KEY_*` | dynamic API-owned key family; catalog prefix `SITE_KEY_`; examples include `SITE_KEY_EXAMPLE_SITE` | api | `optional-provider` / `dynamic` | S03 api, S06 enforcement | Must be matched by prefix, not enumerated as one literal key. |
| `WEB_ORIGIN` | `apps/api/src/app.ts`, `apps/worker/src/jobs/workflow-trigger-http-executor.ts`, `apps/worker/src/health.ts`, `packages/config/src/catalog.ts` | api, worker | `optional-provider` | S03 api, S04 worker | Cataloged API CORS allow-list override and worker workflow HTTP bridge fallback when `WORKFLOW_TRIGGER_EXECUTOR_URL`/`APP_URL` are absent. |
| `ADMIN_ORIGIN` | `apps/api/src/app.ts`, `packages/config/src/catalog.ts` | api | `optional-provider` | S03 api | Cataloged API-only optional CORS allow-list override. |
<!-- Stable inventory tokens for env-access tests: | `BULLMQ_REDIS_URL` | | `REDIS_URL` | | `SITE_KEY_*` | | `WEB_ORIGIN` | | `ADMIN_ORIGIN` | -->
## Example-file-only keys
The following keys appear in `.env.example`, `.env.local.example`, `.env.production.example`, or `.env.fly.example` but no active direct runtime read was found in the targeted source scan:
| Key | Example files | Catalog status | Follow-up owner | Notes |
| -------------------------------- | ----------------------------------------------------------------------------------- | ----------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `MAILJET_FROM_NAME` | `.env.example`, `.env.local.example`, `.env.production.example`, `.env.fly.example` | `optional-provider` | S05 docs | Optional display name; package mail provider should confirm whether it reads through typed env object only. |
| `INTEGRATIONS_CONTACT_FORMSPREE` | `.env.example`, `.env.production.example`, `.env.fly.example` | `optional-provider` | S05 docs | Keep as optional docs entry unless contact-form source is restored. |
| `IPAPIIS_API_KEY` | `.env.example`, `.env.production.example` | `optional-provider` | S05 docs | Alternative IP enrichment provider candidate. |
| `PORTAL_SERVER_FLY_TOKEN` | `.env.example`, `.env.production.example`, `.env.fly.example` | `optional-provider` | S05 docs | Portal automation token candidate. |
| `SITE_KEY_EXAMPLE_SITE` | commented examples | covered by `SITE_KEY_*` | S03 api, S06 enforcement | Example literal for dynamic prefix matching only. |
| `DIRECT_URL` | commented examples | `production-only` | S05 docs | Prisma direct URL candidate; no active TS/TSX read found. |
| `SERVER_URL` | commented examples | `production-only` | S05 docs | Commented URL helper candidate. |
| `VERCEL_URL` | commented examples | `production-only` | S05 docs | Platform candidate. |
## Deprecated/remove candidates
| Key | Evidence | Recommendation |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_RECAPTCHA_SECRET_KEY` | Historical catalog tombstone with `deprecated-remove`; no current active source or example usage found. | Keep out of examples/docs unless a later migration reintroduces server-side reCAPTCHA verification. |
| `CRISP_CHAT_WEBSITE_ID` | Active read exists in `apps/web/app/utils/data/.server/rootData.ts` and legacy Fly examples list it. | Not a removal candidate while active source usage remains; either keep cataloged as web optional provider or remove the root-data integration first. |
| `CONVERTKIT_*` example keys | Active web reads remain in `apps/web/app/modules/crm/services/CrmService.ts`. | Keep catalog/examples as web-scoped optional provider config until CRM/newsletter extraction removes or replaces ConvertKit usage. |
## Tests and test-only mutations
`apps/api/src/app.test.ts` mutates `process.env` extensively to exercise API auth, analytics, CORS, and redaction behavior. These keys are test fixtures, not runtime migration targets: `APP_NAME`, `APP_BASE_DOMAIN`, `API_ACCESS_TOKEN`, `JWT_SECRET`, and `ANALYTICS_TOKEN`.
`packages/db/src/index.test.ts` sets a fallback `DATABASE_URL` before importing `prisma.config.ts`; this is also test-only. Later enforcement should allow isolated test mutation helpers while blocking new production runtime direct reads.
## Assumptions and safety notes
- No secret values were copied from `.env*` files; this inventory includes key names and paths only.
- `REDIS_URL` is a BullMQ fallback when `BULLMQ_REDIS_URL` is absent, not an independent required key.
- `SITE_KEY_*` is a dynamic API-owned key family and must be cataloged/enforced by prefix.
- `PORT`, `HOST`, `WEB_ORIGIN`, and `ADMIN_ORIGIN` are cataloged. `PORT` and `HOST` are adopted for the web listener through `loadWebConfig`; `WEB_ORIGIN` and `ADMIN_ORIGIN` are API CORS overrides, and `WEB_ORIGIN` is also the worker workflow bridge fallback after `WORKFLOW_TRIGGER_EXECUTOR_URL` and `APP_URL`.
- Commented source references are recorded as examples/docs follow-up, not active runtime dependencies.
- Where package helpers accept an `env` parameter defaulting to `process.env`, the package remains a migration target even if individual key reads happen through the passed object.
## Follow-up mapping
| Slice | Owner scope | Inventory items |
| --------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| S02 web | React Router/web loaders, sessions, root data, web route direct reads, Node server listen config | `APP_NAME`, `SESSION_SECRET`, routing host/protocol keys, `CRYPTO_SECRET`, `API_ACCESS_TOKEN`, `JWT_SECRET`, `NODE_ENV`, `HOST`, `PORT`, `WEBHOOKS_URL`, Supabase web keys, Stripe keys, OAuth keys, reCAPTCHA site key, Novu web state, `PORTAL_SERVER_URL`, `DEMO_USER`, `SEED_ADMIN_EMAIL`, `SEED_ADMIN_PASSWORD` |
| S03 api | Hono API runtime, API auth/JWT/analytics/CORS, API storage, enqueue/provider settings | `APP_NAME`, routing host/protocol keys, `APP_REVISION`, `API_ACCESS_TOKEN`, `JWT_SECRET`, `ANALYTICS_TOKEN`, `WEB_ORIGIN`, `ADMIN_ORIGIN`, Supabase server keys, Stripe webhook secret, queue Redis keys, `MISTRAL_API_KEY`, `MISTRAL_MODEL`, `SCALEWAY_*`, `ALEPH_ALPHA_*`, `SITE_KEY_*`, API provider keys |
| S04 worker | Standalone worker runtime, worker health, BullMQ Redis, browser runtime settings | `WORKER_ENABLED`, `APP_REVISION`, `APP_URL`, `WEB_ORIGIN`, `API_ACCESS_TOKEN`, `EMAIL_QUEUE_NAME`, `WORKFLOW_QUEUE_NAME`, `WORKFLOW_TRIGGER_EXECUTOR_MODE`, `WORKFLOW_TRIGGER_EXECUTOR_URL`, `WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS`, `WORKFLOW_TRIGGER_OUTBOX_ENABLED`, `WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY`, `WORKFLOW_TRIGGER_OUTBOX_REQUIRED`, `OUTBOX_DISPATCHER_REQUIRED`, `BULLMQ_REDIS_URL`, `REDIS_URL`, `BROWSER_RUNTIME_URL`, `BROWSER_RUNTIME_API_KEY`, `BROWSER_SESSION_TIMEOUT_MS`, `MISTRAL_API_KEY`, `MISTRAL_MODEL`, `SCALEWAY_*`, `ALEPH_ALPHA_API_KEY`, worker-owned provider keys |
| S05 docs | Example and documentation alignment, fallback wording, deprecated/example-only cleanup | Env examples and docs for `BULLMQ_REDIS_URL` as primary, `REDIS_URL` as fallback only when `BULLMQ_REDIS_URL` is absent, browser-runtime docs alignment, web-scoped ConvertKit CRM/newsletter keys, `DIRECT_URL`, `SERVER_URL`, `VERCEL_URL`, `AUTH_RECAPTCHA_SECRET_KEY`, dormant GitHub candidates |
| S06 enforcement | Future direct-read guardrails and drift tests | Prefix support for `SITE_KEY_*`, allow-list for test-only mutations, regression checks for S02-adopted web core keys (`PORT`, `HOST`, core URL/root/session/middleware reads), CORS-origin catalog drift checks for `WEB_ORIGIN` and `ADMIN_ORIGIN`, APP_REVISION catalog checks, no-value leakage checks for inventory docs |
Loaded 100 of 5784 files, more files were not shown because too many files have changed in this diff. Show more