Files
craftvia/docs/craftvia/lanes/pakete.md
T
msolarczekandClaude Opus 5 aca571b2e7 L17 Pakete: Smoke-Prüfungen, Audit-Labels, Betriebsdoku und Lane-Bericht
- smoke-auth: Admin-Karte, Teamleiterin ohne Chat-Platz, Monteur mit Platz,
  demo2 als Basis (Hinweis statt Profi-Seiten, API 403)
- Audit-Labels lotse_chat_seat / tenant_plan
- DEPLOY.md §7.5 (Pakete/Plätze setzen, Mehrverbrauch ablesen), API.md
- docs/craftvia/lanes/pakete.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:21:23 +02:00

16 KiB
Raw Blame History

Lane L17 – Pakete (Basis/Profi) & Lotse-Chat pro Nutzer (lane/pakete)

Stand: 2026-09-21 · Basis 4d5be72 (feature/craftvia-mvp) · Betriebsdoku: ../DEPLOY.md §7.5

Ziel: Zwei Pakete durchsetzen und den Lotse-Chat als Zusatzmodul pro Nutzer mit gemeinsamem Kontingent führen. Keine Abrechnung in der App – die App setzt Pakete durch, verwaltet Plätze und zählt Nutzung; der Betreiber stellt die Rechnung außerhalb.

1. Umfang / umgesetzte Entscheidungen

Punkt Umsetzung
1 Datenmodell Migration 20260921100000_pakete (additiv): enum TenantTier { BASIS PROFI }, am Tenant tier (Default PROFI → alle Bestandsmandanten und Testphasen unverändert), lotseChatSeats Int @default(0), lotseChatHardLimit Boolean @default(false) (Plattform-Daten, keine RLS). Neue Mandanten-Tabelle lotse_chat_seats (LotseChatSeat: tenantId, userId, assignedById, createdAt; unique tenantId+userId) mit enable_tenant_rls, in beiden TENANT_MODELS, userId/assignedById in pii-fields.ts. Index lotse_messages(tenant_id, role, created_at) für die Monatszählung.
2 Zentrale Regeln src/lib/plans.ts (client-sicher, rein) Stufen, PROFI_ONLY_MODULES = billing, emergency, imports, lotse, Features ohne eigenes Modul planning, lotse_chat; effectiveTier (Testphase → Profi), isModuleInTier, isFeatureInTier, LOTSE_CHAT_CHATS_PER_SEAT = 150, LOTSE_CHAT_OVERAGE_PACK = 100, chatQuota, chatOverage, overagePacks (angefangene Pakete zählen), usageLevel (ok / warn ab 80 % / exhausted / over), usagePercent, isQuotaBlocked, Monatsschlüssel.
3 Durchsetzung an einer Stelle src/server/plan.ts moduleState(tenantId, key) = not_in_plan (Stufe) → disabled (TenantModule) → enabled; effectiveModules (Navigation/Übersichten), isModuleActive, assertPlanFeature/requirePlanFeature (Planung). Genutzt von requireModule (Layouts: nicht im Paket → /dashboard?module=profi mit ruhigem Hinweis „Im Paket Profi enthalten“), assertModuleEnabled (moduleGuard + requireApiContext; ModuleDisabledError.reason = "not_in_plan", Audit denied, API → 403 mit details.reason = "not_in_plan" + Klartext), Sync (services/sync/apply.ts#OP_MODULES: emergency.create → emergency, milestone.reach → billing; gesperrt → gespeichert rejected/forbidden mit not_in_plan: <Klartext> bzw. module_disabled: …, Gerät zeigt eigenen Text `offline.problem.notInPlan
4 Lotse-Chat-Plätze services/lotse/chat/access.ts ist die eine Prüfstelle: Rechte → Stufe (blocked not_in_plan) → Lotse an (disabled) → Chat-Schalter (chat_disabled) → Platz (blocked no_seat, Testphase ohne Platzprüfung) → beim Senden Kontingent mit hartem Limit (blocked quota_exhausted). canUseLotseChat (Navigation, „Lotse fragen“) schließt den Platz ein. seats.ts: berechtigt = aktive Nutzer mit lotse:use + field:execute (DB-Rollen), Vergabe/Entzug setChatSeat (tenant:manage, Zod, Audit lotse_chat_seat before/after, idempotent, serialisiert über eine Zeilensperre der Mandanten-Einstellungen → parallele Vergaben überschreiten die Grenze nicht). Gültig sind die ältesten lotseChatSeats Zuweisungen (Überbelegung nach Senkung durch den Betreiber sichtbar). usage.ts: Chats im Monat gesamt und je Nutzer (Kalendermonat in der Mandanten-Zeitzone, DST-sicher), Kontingent = gezählte Plätze × 150 (Testphase: Berechtigte × 150, kein hartes Limit), Mehrverbrauch, 100er-Pakete, Stufe, Sperre. Die Token-Obergrenze (budget.ts) bleibt unverändert als Sicherheitsnetz.
5 Mandanten-Admin /settings/lotse Neue Karte „Paket & Lotse-Chat“: Paket nur lesend (Badge, Testphase-Hinweis), „x von y Plätzen vergeben“, Liste aller Nutzer mit Feldrolle mit Schalter je Nutzer (Button role="switch", ≥ 44 px, Status als Text + Icon, deaktiviert ohne freien Platz – serverseitig ohnehin geprüft), Monatsverbrauch als Balken (role="meter") mit Text „212 von 450 Chats“, Hinweis ab 80 %, bei ausgeschöpftem Kontingent / Mehrverbrauch (inkl. Paketzahl) / hartem Limit, Aufschlüsselung je Nutzer. Basis: Hinweis, Plätze bleiben gespeichert. Action actions/lotse/seats.ts (moduleGuard("lotse") + guard("tenant:manage")).
6 Mobil Ohne Platz kein Eintrag „Lotse“ in der Bottom-Navigation und kein „Lotse fragen“; /m/lotse zeigt „Der Lotse-Chat ist für dich noch nicht freigeschaltet – frag im Büro nach.“ Basis: Hinweis statt Weiterleitung (Lotse-Layout). Hartes Limit erreicht: Hinweis statt Eingabefeld (Verlauf bleibt lesbar, Chips deaktiviert); auch nach einer abgewiesenen Nachricht.
7 Betreiber /admin/[id] Karte „Paket & Lotse-Chat“: Stufe, Plätze vergeben/gebucht, hartes Limit, Chats laufender Monat und Vormonat („x von y Chats“), Mehrverbrauch → 100er-Pakete (Grundlage der Rechnung). „Paket ändern …“ (nur Voll-Admins) → Popup mit Stufe, Platzanzahl (0–1000), hartem Limit und Pflicht-Bestätigung; services/plans/platform.ts#updateTenantPlan prüft den Akteur gegen den PlatformAdmin-Store, Zod, Plattform-Audit tenant_plan (before/after). Mandantenliste: Spalte „Paket“ als Badge; Modul-Popup markiert Module außerhalb der Stufe („Nur im Paket Profi“).
8 Demo-Seed demo = Profi mit 3 Plätzen für Max, Nora, Paul (Monteure; Chat funktioniert weiter), demo2 = Basis (zeigt die Sperren). Idempotent (seedDemoPlan).
9 i18n/Doku Namespace plans (de/en), Ergänzungen in lotse.json (chat.errors.not_in_plan/no_seat/quota_exhausted, chat.quotaExhausted) und offline.json (problem.notInPlan/moduleDisabled); DEPLOY.md §7.5 (wie der Betreiber Pakete/Plätze setzt, wie Mehrverbrauch abgelesen wird), API.md (403-Details, Sync-Abweisung).

Entscheidungen

  • Admin als Platzinhaber: Berechtigung = Rechte lotse:use + field:execute (nicht der Rollenname). Der Mandantenadministrator hat alle Rechte und kann sich daher selbst einen Platz geben; Backoffice nicht.
  • Kontingent „je vergebenem Platz“: zählt vergebene Plätze, höchstens die gebuchte Anzahl.
  • Sync-Abweisung ist endgültig (gespeichert, wie andere deterministische Ablehnungen). Nach einem Upgrade muss der Monteur den Notdienst erneut erfassen.
  • Hartes Limit gilt nicht in der Testphase (keine gebuchten Plätze; Token-Budget bleibt).
  • Basis sperrt auch die Platzvergabe (moduleGuard lotse), Plätze bleiben gespeichert.

2. Dateien

Neu

  • prisma/migrations/20260921100000_pakete/migration.sql
  • src/lib/plans.ts, src/server/plan.ts
  • src/server/services/lotse/chat/{seats,usage}.ts, src/server/services/plans/platform.ts
  • src/server/actions/lotse/seats.ts, src/server/actions/plans-platform.ts
  • src/components/plans/{profi-hint,plan-admin-card,plan-form}.tsx, src/components/lotse/chat/{seat-admin,unavailable}.tsx
  • messages/{de,en}/plans.json, dieser Bericht
  • Tests: scripts/test-pakete-{rules,gates,seats}.ts

Eingriffe in Fundament-/Fremddateien (klein, markiert „L17“)

Datei Eingriff Grund
prisma/schema.prisma 3 Tenant-Felder, Enum, Modell LotseChatSeat, Index an LotseMessage Punkt 1
src/server/db.ts, src/server/backup/topology.ts, src/server/dsgvo/pii-fields.ts LotseChatSeat Pflicht neue Tenant-Tabelle
src/server/modules.ts requireModule/assertModuleEnabled über moduleState; ModuleDisabledError.reason zentrale Durchsetzung
src/server/api/respond.ts 403 mit details.reason = not_in_plan Klartext für Clients
src/lib/nav.ts, src/app/(app)/layout.tsx NavItem.feature, lockedFeatures; effektive Module Navigation
src/app/(app)/dashboard/page.tsx (L2) Hinweis ?module=profi, Kacheln Abrechnung/Notdienst nur bei aktivem Modul ruhiger Hinweis
src/app/(app)/settings/page.tsx Modulübersicht über effektive Module Anzeige
src/app/(app)/work-orders/[id]/page.tsx, src/server/services/work-orders/transition.ts (L2/L14) billingModuleActive über plan.ts, Tab „Meilensteine & Abrechnung“ nur bei aktiver Abrechnung Basis = Abrechnung aus
src/server/services/planning/{access,schedule,team-settings,watch}.ts, src/app/(app)/planning/layout.tsx (L13) Feature-Prüfung planning Planung nur Profi
src/server/services/sync/apply.ts (L4), src/lib/offline/outbox-core.ts (L7), messages/{de,en}/offline.json OP_MODULES + Klartext; problemKey Sync-Sperre
src/app/(field)/m/layout.tsx, src/components/field/bottom-nav.tsx (L4) Notdienst-Eintrag nur bei aktivem Modul, Spaltenzahl dynamisch mobile Navigation
src/server/services/lotse/{settings.ts,chat/access.ts,chat/engine.ts,chat/transcribe.ts}, src/lib/lotse/chat.ts, src/server/actions/lotse/_chat-state.ts, src/components/lotse/chat/lotse-chat.tsx, src/app/(field)/m/(core)/lotse/{layout,page}.tsx, src/app/(app)/settings/lotse/page.tsx, messages/{de,en}/lotse.json (L9/L16) Stufe in isLotseEnabled, Platz/Kontingent im Chat-Gate, Hinweise, Admin-Karte Punkte 4–6
src/app/(platform)/admin/page.tsx, admin/[id]/page.tsx Badge-Spalte, Karte, Modul-Popup-Markierung Punkt 7
src/components/audit-trail.tsx Labels lotse_chat_seat, tenant_plan Audit-Viewer
scripts/check-module-guards.ts plans-platform.ts EXEMPT Top-Level-Action
scripts/lib/demo-seed.ts seedDemoPlan, demo Profi + 3 Plätze, demo2 Basis Punkt 8
scripts/lib/e2e-fixture.ts, scripts/test-e2e-tenant-isolation.ts Cleanup + Fixture-Zeile LotseChatSeat Test verlangt alle Tenant-Modelle
scripts/lib/lotse-chat-fixture.ts (L16) 10 Plätze + Sitz je Testnutzer bestehende Chat-Tests brauchen jetzt einen Platz
scripts/smoke-auth.ts L17-Prüfungen (Admin-Karte, Teamleiterin ohne Platz, Monteur mit Platz, demo2 Basis) Smoke
docs/craftvia/{DEPLOY,API}.md §7.5, 403/Sync Betrieb

Keine neuen npm-Abhängigkeiten, keine Änderung an rbac.ts, keine neuen env-Variablen.

3. Tests

Skript Prüfungen Inhalt
test-pakete-rules.ts 51 Stufen, Testphase = Profi, Module/Features je Stufe, Kontingent/Mehrverbrauch/100er-Pakete, Warnstufen, hartes Limit, Monatsschlüssel, Navigation Basis/Profi
test-pakete-gates.ts 74 Default Profi; Layout-Guard-Funktionen (moduleState, requirePlanFeature → Redirect ?module=profi, Quelltextnachweis requireModule/Planungs-Layout); Action (assertModuleEnabled → not_in_plan + Audit) für billing/emergency/imports/lotse, Basis-Module frei; API 403 mit Klartext, Planungs-API 403; Planung (Plantafel, Live-Lage, Einplanen, Empfehlungen, Kapazität, Dashboard-Kachel, Watch) gesperrt / Profi erlaubt; isLotseEnabled, Abrechnungsschalter, Navigation; Sync: emergency.create/milestone.reach → rejected mit Klartext, gespeichert, kein Auftrag, Duplikat, Gerätetext; Profi → angewendet; TenantModule aus + Profi → gesperrt (Action, Sync); nur Betreiber ändert die Stufe (Mandanten-Admin, Nur-Lesen-Admin → forbidden, ungültige Werte → invalid, Mandanten-Einstellungen ändern keine Stufe); Wechsel Profi → Basis → Profi ohne Datenverlust mit Plattform-Audit; Mandantentrennung
test-pakete-seats.ts 68 berechtigte Nutzer; ohne Platz → no_seat (View, Senden, nichts gespeichert); Vergabe bis zur Grenze, darüber invalid no_seats_left, parallele Vergabe (genau eine), idempotent, Audit create/delete; mit Platz → ok/Senden; Entzug; Überbelegung nach Senkung; Feldrollen/Backoffice → forbidden (Vergabe, Übersicht, Verbrauch); ohne Feldrolle / deaktiviert → not_eligible; Mandantentrennung (B vergibt/entzieht nichts von A, sieht keine Plätze/Zählung); Monatsgrenze in Europe/Berlin inkl. Sommerzeit, Antworten zählen nicht, je Nutzer; Mehrverbrauch weiterzählen (305/300 → 1 Paket), hartes Limit → quota_exhausted ohne Speichern, Verlauf lesbar, B unberührt; Testphase ohne Platzprüfung (Kontingent Berechtigte × 150, kein hartes Limit); Basis sperrt Chat und Vergabe, Plätze bleiben; Betreiber-Übersicht laufender Monat/Vormonat

Gate (npm run gate) grün: prisma generate, tsc, lint (0 Fehler; 3 bestehende Warnungen in fremden Dateien), build inkl. Guard-Check (42 Action-Dateien), 87/87 Testskripte (vorher 84 + 3 neue). Bestehende Tests unverändert grün; angepasst wurde nur die L16-Chat-Fixture (Plätze).

RLS-Lauf RLS_ENFORCED=true (Lane-DB craftvia_pakete, Rolle craftvia_app): test-pakete-gates, test-pakete-seats, test-e2e-tenant-isolation, test-rls-enforcement, test-lotse-chat-engine – 5/5 grün.

HTTP-Smoke (next build && next start -p 3117, scripts/smoke-auth.ts ohne Passworteingabe): 152/155 – alle neuen L17-Prüfungen grün (Admin: Karte „Paket & Lotse-Chat“, „3 von 3 Plätzen vergeben“; Teamleiterin ohne Platz: kein Nav-Eintrag, Hinweis „frag im Büro nach“, kein „Lotse fragen“; Monteur mit Platz: Chat, Nav-Einträge Lotse/Notdienst; demo2 Basis: Planung/Abrechnung/Import/Notdienst-Prüfung → /dashboard?module=profi mit Hinweis und ohne gesperrte Links, /api/v1/planning/board und /api/v1/billing → 403). Die 3 Fehlschläge sind vorbestehend: demo2-Seiten /dashboard, /work-orders, /customers enthalten „Elbblick“, weil messages/*/marketing.json (Produktseite, Commits 31343d3/4815a95) den Beispieltext „Wohnanlage Elbblick“ enthält und die Messages an den Client serialisiert werden – kein Datenleck, aber die Isolationsprüfung schlägt an. Server danach beendet.

Visuell: nicht im Browser geprüft (Anmeldung hätte Passwort- oder Token-Eingabe erfordert). Bitte /settings/lotse (1024/768 px), /admin/<id> (Karte + Popup) und /m/lotse ohne Platz / bei hartem Limit (375 px) ansehen.

4. Offene Punkte

  1. Smoke-Isolationsprüfung „Elbblick“ (vorbestehend, s. o.): Beispieltext in marketing.json umbenennen oder die Prüfung auf Daten-Marker umstellen.
  2. Vormonat mit heutiger Platzzahl: Die Betreiber-Karte rechnet das Kontingent des Vormonats mit den aktuell vergebenen Plätzen (keine Historie der Platzanzahl). Ändert sich die Platzzahl im Monat, ist der Vormonatswert eine Näherung → ggf. monatlichen Snapshot (Tabelle) nachziehen.
  3. Aufbewahrung: Die Zählung basiert auf LotseMessage; AI_GENERATION_RETENTION_DAYS muss > ~62 Tage bleiben (Default 180), sonst fehlen Vormonats-Chats. Eine DSGVO-Löschung eines Nutzers entfernt auch seine Chats aus der Zählung.
  4. Hartes Limit unter Parallelität: geprüft vor dem Speichern; gleichzeitige Nachrichten können das Kontingent um wenige Chats überschreiten.
  5. Worker: import-extraction prüft die Stufe nicht (nur die Testphasen-Sperre). Nach einem Wechsel auf Basis laufen bereits eingereihte Importe noch durch; neue Importe sind gesperrt. Transkription ist über isLotseEnabled gesperrt.
  6. Abrechnungseinträge im Hintergrund: Der L14-Event-Hook legt auch bei Basis weiter Abrechnungskandidaten an (unsichtbar, nach Upgrade vorhanden) – bewusst belassen („Daten bleiben erhalten“).
  7. Sync-Abweisung endgültig: Eine im Basis-Paket offline erfasste Notdienst-Operation wird nach einem Upgrade nicht automatisch nachgeholt.
  8. services/emergency/sync-ops.ts prüft den Modulschalter weiterhin selbst (redundant zur zentralen Sync-Prüfung, unverändert).

5. Screens / Routen

Route Zugriff Inhalt
/dashboard?module=profi Backoffice Hinweis „Im Paket Profi enthalten“ (Ziel aller gesperrten Profi-Seiten)
/settings/lotse tenant:manage Karte „Paket & Lotse-Chat“: Paket, Plätze, Schalter je Nutzer, Monatsverbrauch
/m/lotse Feldrollen ohne Platz / Basis: Hinweis; hartes Limit: Hinweis statt Eingabefeld
/admin Plattform Spalte „Paket“
/admin/<id>?plan=edit Plattform-Voll-Admin Karte „Paket & Lotse-Chat“ + Popup „Paket & Chat-Plätze ändern“