Compare commits

121 Commits
Author SHA1 Message Date
msolarczekandClaude Opus 5 a4a61dc31f Ereignisse erst nach dem Commit ausliefern
Benachrichtigungs- und Mailversand lief im Handler noch in der offenen Transaktion und
zählte gegen deren Zeitlimit; unter Volllast brachen dadurch wechselnde Tests ab
(test-einsatz-field 31 s, test-planung-recommend). withDeferredEvents puffert Ereignisse
innerhalb von inTransaction und stellt sie nach dem Commit zu, bei Rollback gar nicht —
analog zu withDeferredAudit und passend zum dokumentierten Vertrag in events.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:05:49 +02:00
msolarczekandClaude Opus 5 1e1c154a8a Plantafel: Beispielwoche relativ zu heute; Live-Lage mit Kachelansicht
Neues Skript scripts/planning-demo.ts plant über createWorkOrder + scheduleWorkOrder eine
Woche für beide Kolonnen (Auslastung, Überbuchung, Überschneidung, Mehrtagesauftrag,
ungeplante Aufträge); die Demo-Termine aus dem Seed liegen relativ zum Seed-Tag und wandern
sonst aus dem Standardzeitraum.

Live-Lage: Umschalter Karte · Kacheln · Liste, Kachelansicht mit Status, Auftrag und Ort;
LiveMap meldet nicht erreichbare Kartenkacheln und die Ansicht wechselt automatisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 09:44:24 +02:00
msolarczekandClaude Opus 5 dd6a67d329 Offline: Fotos bei abgelaufener Testphase nicht mehr verwerfen
Upload-Antwort 422 trial_expired gilt als vorübergehend: Datei bleibt auf dem Gerät,
wartende Op bleibt in der Warteschlange, der Sync-Durchlauf stoppt mit Backoff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:23:14 +02:00
msolarczek a8fedc390f Merge lane/testphase in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	prisma/schema.prisma
#	scripts/test-e2e-tenant-isolation.ts
#	src/server/backup/topology.ts
#	src/server/db.ts
#	src/server/dsgvo/pii-fields.ts
2026-09-15 19:16:19 +02:00
msolarczekandClaude Opus 5 c3712598c1 Transaktionen: Wartezeit 10 s und Zeitlimit 20 s statt Prisma-Standard
Unter paralleler Last scheiterten Tests sporadisch mit „Unable to start a transaction in the given time“.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:14:31 +02:00
msolarczekandClaude Opus 5 df18ff9077 L15 Testphase & Onboarding: Lane-Bericht, Smoke prüft mobilen Scope
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:13:49 +02:00
msolarczekandClaude Opus 5 273a4535e3 L15 Testphase & Onboarding: Lese-Modus am moduleGuard für Seitenkontexte, Sperre für Auftragsdokument-Upload
Der HTTP-Smoke zeigte 500 auf /m für abgelaufene Testmandanten: mobile Seitenkontexte (field,
emergency) und der Import-Datei-Download nutzen moduleGuard zum Lesen. moduleGuard(key, { read: true })
überspringt dort die Schreibsperre; der Guard-Check verbietet den Lese-Modus in Server-Actions.
POST /api/v1/work-orders/[id]/documents läuft nicht über withApi und prüft die Sperre jetzt explizit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:07:20 +02:00
msolarczekandClaude Opus 5 969bc4e12c Merge lane/lotse-chat in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:04:40 +02:00
msolarczekandClaude Opus 5 eec23efa7e L16 Lotse-Chat für Monteure: optionaler Live-Test und Lane-Bericht
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:03:59 +02:00
msolarczekandClaude Opus 5 d9290a187c L15 Testphase & Onboarding: Selbstanmeldung mit Double-Opt-in, Plattform-Wizard, Nur-Lesen-Sperre, Export, Lebenszyklus-Job
- Datenmodell: Testphasen-Lebenszyklus am Mandanten (plan, trialEndsAt, readOnlySince, deletionDueAt,
  Versandmarker), TrialSignup (Plattform, Hashes statt Klartext), TenantExport (RLS), Onboarding-Status
- /testen: 5-Schritte-Wizard (Betrieb, Admin-Konto, Enddatum, Einrichtung, Zusammenfassung),
  Bestätigung per POST, direkte Anmeldung über login-ticket; Rate-Limit je IP/E-Mail, Honeypot,
  Enumeration-Schutz, Slug-Kollisionen
- Plattform: Wizard „Testmandant anlegen“ mit Einladung, Badges/Filter, Enddatum ändern,
  umwandeln, beenden, Löschung vormerken/abbrechen (Bestätigung + Audit)
- Schreibsperre nach Ablauf zentral in moduleGuard und requireApiContext (non-GET über withApi),
  Upload-Routen, Einstellungen/Nutzerverwaltung, Worker-Jobs; Banner Backoffice + mobil
- Datenexport (ZIP mit CSV/JSON + Dateien) als Worker-Job, auch im Nur-Lesen-Zustand
- Täglicher Job trial-lifecycle: Erinnerungen 7/3/1, Ablauf, Löschhinweis, Löschung über das Offboarding
- Erste-Schritte-Checkliste im Dashboard, Mail-Vorlagen de/en, Tests + Smoke, Betriebsdoku

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:01:47 +02:00
msolarczekandClaude Opus 5 2f46552970 L16 Lotse-Chat für Monteure: Tests für Chat-Schleife und Bestätigen, Isolation und Smoke
Fake-Provider mit geskripteten Tool-Aufrufen: Zuordnung, Vorschlag ohne Schreibzugriff,
Ausführung über Services, Blocker, Einmaligkeit/Ablauf/fremder Nutzer, Scope,
Mandantentrennung, Datenminimierung, Einstellung aus, Budget- und Rundengrenze.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:57:20 +02:00
msolarczekandClaude Opus 5 63baaf29df L16 Lotse-Chat für Monteure: mobile Chat-Seite, Aktionskarten, Mikrofon, Einstieg und Einstellung
/m/lotse mit Verlauf, Chips, Sprungzielen, Aktionskarten (Bestätigen/Bearbeiten/Verwerfen),
Spracheingabe mit editierbarem Transkript und Offline-Hinweis; Navigationseintrag, Button
„Lotse fragen“ im Auftragsdetail, Schalter und Datenfluss in /settings/lotse, Audit-Labels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:57:20 +02:00
msolarczekandClaude Opus 5 68f4eb32dc L16 Lotse-Chat für Monteure: Datenmodell, Provider mit Tool-Schleife, Vorschläge und Bestätigen über bestehende Services
Chatverlauf (LotseConversation/-Message) und Aktionskarten (LotseActionProposal) als
Mandantentabellen mit RLS; Schalter lotseChatEnabled. Tool-Use-Schleife mit Runden- und
Tokengrenze, Datenminimierung per Platzhalter, Auftragszuordnung im Sichtbarkeits-Scope,
Bestätigen/Verwerfen/Alle bestätigen mit Hash, Ablauf und Idempotenz, Transkriptions-API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:57:20 +02:00
msolarczekandClaude Opus 5 6b8cdf543b Abrechnen nur über die Abrechnungsübersicht, solange das Modul aktiv ist
Direkter Übergang nach billed → invalid use_billing_overview (nach der Rechteprüfung),
Button im Auftragsdetail ausgeblendet, markBilled läuft über den Abrechnungseintrag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:53:16 +02:00
msolarczekandClaude Opus 5 8324c48178 Merge lane/abrechnung in feature/craftvia-mvp
Konflikte gelöst (additiv): Benachrichtigungstexte (Planung + Meilensteine), Event-Typen,
Navigation, Job-Queues/Prozessoren (geocode-site, planning-watch, billing-pdf), OpenAPI-Tags,
Smoke-Prüfungen (zweiter Mandant: Planung + Abrechnung).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:37:52 +02:00
msolarczekandClaude Opus 5 2df785dd14 L14 Abrechnungsübersicht: Tests, Backfill, Smoke und Lane-Bericht
test-abrechnung-service (118 Prüfungen inkl. Mandantentrennung und PDF-Render-Smoke), test-abrechnung-sync (21), Fixture-Zeilen für die neuen Tenant-Modelle, Rollen-Matrix billing:*, smoke-auth um /billing, Detail, Druckansicht, Auftrags-Tab und mobile Meilensteine erweitert, scripts/billing-backfill.ts, docs/craftvia/lanes/abrechnung.md, ABNAHME §3 „Abrechnungsübersicht“ (keine Buchhaltung). Gate 69/69, RLS 69/69.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:36:10 +02:00
msolarczekandClaude Opus 5 b76c1bdbfd L14 Abrechnungsübersicht: Backoffice-Übersicht, Detail, Druckansicht, Meilensteine im Auftrag und mobil, API
/billing mit Tabs Offen/Abgerechnet/Storniert, Filtern und Mehrfachdruck; /billing/[id] mit Abrechnungsblatt und Aktionen (abgerechnet mit Rechnungsnummer, PDF, Rechnungsnummer ändern, stornieren); /billing/print; Auftrags-Tab „Meilensteine & Abrechnung“; mobiler Abschnitt „Erreicht melden“ (offline); Dashboard-Kachel „Bereit zur Abrechnung“; /api/v1/billing*, Meilenstein-Endpunkte und OpenAPI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:35:56 +02:00
msolarczekandClaude Opus 5 ac2d857e26 L14 Abrechnungsübersicht: Services für Einträge, Aufstellung, Abrechnen/Stornieren, Meilensteine und PDF
syncBillingCandidates (idempotent, Event-Hook für work_order.released_for_billing und report.approved), Aufstellung ohne Preise (nur freigegebene Zeiten, Fahrzeit getrennt, Pausen informativ, Anfahrten-Zählung, Material), markBilled mit eingefrorenem Snapshot und Positionszuordnung, voidBilling mit Grund und neuem offenen Eintrag (billed → released_for_billing nur über applyTransition-Option billingVoid), Meilenstein-Flow mit Events, Job billing-pdf (Dokument other/backoffice_only), Sync-Op milestone.reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:35:38 +02:00
msolarczekandClaude Opus 5 6358d5762c L14 Abrechnungsübersicht: Datenmodell, Migration, Rechte und Modul
Neue Tenant-Tabellen work_order_milestones und billing_records (RLS, TENANT_MODELS, PII-Felder), Partial-Unique-Indizes je Quelle, billing_record_id an time_entries/material_usages. Rechte billing:read/billing:write (Backoffice, Mandantenadmin), Modul billing, Events milestone.*.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:35:20 +02:00
msolarczekandClaude Opus 5 9b087168d1 L14 Abrechnungsübersicht: Demo-Seed auf frischer Datenbank lauffähig
Seit L12 darf ein Nutzer nur eine laufende Uhr haben. DEMO-06/DEMO-07 (laufende Uhren) werden deshalb nach den vergangenen Einsätzen derselben Monteure angelegt; Inhalte unverändert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:35:07 +02:00
msolarczekandClaude Opus 5 bf92100661 Merge lane/planung in feature/craftvia-mvp
Konflikte gelöst (additiv): Benachrichtigungstexte (Zeiterfassung + Planung, JSON zusammengeführt),
Event-Typen, Navigation, Empfänger-Felder, handle-event (reason inkl. rejectionReason + Planungsfelder),
Smoke-Prüfungen (Teamleiter: Zeiten + Planung).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:15:47 +02:00
msolarczekandClaude Opus 5 edee395b2e L13 Planung: Lane-Bericht und Abnahme-Matrix (Kalender, Live-Lage, Verzug, Standortdaten)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:13:57 +02:00
msolarczekandClaude Opus 5 ee17134863 L13 Planung: Menüpunkt Planung mit Plantafel und Live-Lage
Plantafel: Standard heute + nächste 4 Werktage (Kolonnen als Zeilen, heutige Spalte mit Live-Status), Heute mit Kolonnen als parallelen Spalten (6–20 Uhr), Woche/nächste Woche, Auslastung, Konflikte/Hinweise, Verzugs- und Gefährdungs-Badges, Drag & Drop (@dnd-kit/core) mit Bestätigungs-Popover und Tastatur-Alternative, ungeplante Aufträge mit Vorschlägen, früher fertig mit Vorziehen, Kolonnenkapazität-Popup. Live-Lage: Leaflet/OSM-Karte mit einem Marker je Kolonne, Liste, Polling 30 s, Hinweis keine GPS-Ortung; CSP img-src für Kacheln. Dashboard-Kacheln Planung heute und Konflikte diese Woche, Einsatz-Vorschläge im Auftragsdetail, Navigation mit Unterpunkten, Smoke-Prüfungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:13:50 +02:00
msolarczekandClaude Opus 5 5e10523df6 L13 Planung: Plantafel-, Einplan-, Empfehlungs-, Live- und Verzugsdienste mit API und Meldungen
Services board/schedule (L2 assign+update in inTransaction, Versionskonflikt, Audit), recommend (Luftlinie + freie Kolonnenzeit), live (Kolonnen, ohne Geräte-Koordinaten), watch (Verzug ab 80 %, Überschreitung, gefährdete Folgeaufträge, früher fertig mit Vorschlägen), Kolonnenkapazität. Job planning-watch alle 5 min, Events planning.capacity_freed/overrun/followup_at_risk (In-App an Backoffice + Teamleiter, Dedupe im Audit-Log), L12-Adapter für source/approvalStatus/manual. API /api/v1/planning/{board,schedule,recommendations,live} + OpenAPI. Tests core, recommend, live, watch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:13:39 +02:00
msolarczekandClaude Opus 5 213bcca3a1 L13 Planung: Geocoding der Objekte über OpenStreetMap Nominatim
Provider-Interface (nominatim|none), strukturierte Suche mit User-Agent und Accept-Language, Drosselung 1/s je Prozess, Job geocode-site (Worker: Concurrency 1 + Limiter), Cache am Objekt, manuelle Koordinaten bleiben, Auslöser Anlage/Adressänderung/Import-Bestätigung nur per Queue, Backfill-Skript, Env-Beispiele. Tests mit Fake-Provider, Nominatim nie aufgerufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:13:26 +02:00
msolarczekandClaude Opus 5 604f2bfdc5 L13 Planung: Datenmodell (Kolonnenkapazität, Dauer, Geocoding-Cache) und Planungsregeln
Migration 20260915120000_planung (nur Spalten): Team.dailyCapacityMinutes/workingDays, OrderType.defaultDurationMinutes (+ Backfill Standardarten), WorkOrder.plannedDurationMinutes, Site.geocodedAt/geocodeStatus/geocodeQuery. Client-sichere Regeln: Haversine, Tage/Werktage, Dauer, Kolonnenkapazität, Konflikte inkl. Hinweis crew_incomplete, Vereinigung von Arbeitssegmenten, Fahrzeitschätzung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:13:18 +02:00
msolarczekandClaude Opus 5 2f312a7835 Zeiterfassung: Backoffice erfasst Zeiten für Mitarbeiter
- Backoffice-Rolle erhält field:correct_time (User-Entscheidung): Zeiten für
  Monteure/Teamleiter direkt freigegeben anlegen und korrigieren
- Eigene Zeiten erfordern weiterhin field:record_own_time (Backoffice erfasst
  keine eigenen Zeiten)
- Auftragsdetail › Zeiten: Formular „Zeit für Mitarbeiter erfassen“ (Mitarbeiter,
  Art, Datum, von–bis oder Dauer, Begründung; Mandanten-Zeitzone)
- Server Action recordTimeForUserAction (moduleGuard work_orders)
- Test test-backoffice-time (Backoffice, fremdes Team, Monteur, Mandantentrennung)

Demo-Daten lokal bereinigt: je Monteur nur noch eine aktive Uhr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:54:06 +02:00
msolarczekandClaude Opus 5 5ea23a179d Merge lane/zeiterfassung in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:35:16 +02:00
msolarczekandClaude Opus 5 ceab79b7f8 L12 Zeiterfassung: Lane-Bericht und Abnahme-Entscheidung Arbeitszeitkorrektur
docs/craftvia/lanes/zeiterfassung.md (Umfang, Dateien, Tests, Gate/RLS, Lücken),
ABNAHME §4 „Arbeitszeitkorrektur“ auf die Freigaberegel aktualisiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:31:29 +02:00
msolarczekandClaude Opus 5 9f4eecf6e2 L12 Zeiterfassung: Tests und authentifizierter Smoke
test-zeiterfassung-service (78 Prüfungen) und test-zeiterfassung-sync (29 Prüfungen),
Rollen-Matrix um field:record_own_time und time:approve ergänzt, smoke-auth um
/m/time, /m/time/new, /m/approvals, Uhr-Leiste und /work-orders/time-approvals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:29:46 +02:00
msolarczekandClaude Opus 5 fe3eba6e44 L12 Zeiterfassung: Mobile Uhr-Leiste, Meine Zeiten, Freigaben und Backoffice-Liste
Laufende-Uhr-Leiste auf allen /m-Seiten, Start/Pause direkt auf den Auftragskarten,
Auto-Wechsel-Dialog, Auftragsdetail mit Pause / Für heute beenden / Abschließen-Link
und Segmentwechseln, /m/time (Tagesübersicht, Nachtragen, Korrektur vorschlagen),
/m/approvals für Teamleiter, /work-orders/time-approvals mit Sammelfreigabe,
Zeiten-Tab mit Badges und Inline-Freigabe, Dashboard-Kachel, Texte de/en.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:29:38 +02:00
msolarczekandClaude Opus 5 0e94eb0d9d L12 Zeiterfassung: Services für Nachtrag, Freigabe, Auto-Wechsel und Für heute beenden
time-entries.ts (manuelle Einträge, Korrekturvorschläge, Freigabe/Ablehnung mit
Team-Scope), Sessions in inTransaction mit switchFromOther, stopForToday,
Segmentwechsel und getMyActiveSession. Sync-Ops time.add_manual,
time.propose_correction, session.stop_day, session.segment inkl. optimistischer
Offline-Ansicht. Bericht, Zeiten-Tab, Dashboard und Lotse zählen nur freigegebene
Zeiten; Abrechnungsfreigabe blockiert bei offenen Zeiten. Empfänger und Links der
Zeit-Events. Notdienst-Start pausiert eine laufende Uhr automatisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:29:30 +02:00
msolarczekandClaude Opus 5 03a2ccab4f L12 Zeiterfassung: Datenmodell, Rechte und Events für die Zeitfreigabe
Migration zeiterfassung_freigabe (TimeEntry source/approvalStatus/pendingChange,
WorkSession.manual), Rechte field:record_own_time und time:approve,
PII approvedById, Events time.approval_requested/approved/rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:29:20 +02:00
msolarczekandClaude Opus 5 d3bc7f2d29 Doku: Abnahme-Matrix Craftvia MVP (Spec §40, US-001–012, §44)
17 Abnahmekriterien und 12 User Stories mit Status und Nachweis (Testskript, Route,
Lane-Bericht); Soll-/Kann-Funktionen; getroffene MVP-Annahmen zu den offenen
fachlichen Entscheidungen; bekannte Einschränkungen und nicht verifizierte Punkte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:59:47 +02:00
msolarczekandClaude Opus 5 ef2f3f16c6 Härtung: Login-Drosselung je IP/Konto, Mail-Anhangfehler ohne Retry, doppeltes Upload-Audit
- rate-limit: neuer Scope login (20/15 min, LOGIN_RATE_LIMIT_PER_15_MIN) je IP und
  je Konto; geprüft in verifyIdentityPassword (Login-Seite + Credentials-Provider),
  gedrosselt verhält sich wie Fehlanmeldung (generisch, konstante Laufzeit) – L10a
- mail/worker: MailAttachmentError wie MailNotConfiguredError unrecoverable
  (fremder Mandant/Prüfsumme/Größe ändern sich nicht durch Warten) – L11
- field/uploads: Dokument-Audit nur noch in storeFile (vorher doppelt) – L10a
- Test test-login-rate-limit

Gate 65/65 grün; komplette Suite mit RLS_ENFORCED=true 65/65 grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:59:47 +02:00
msolarczekandClaude Opus 5 40c3e8e46b Merge lane/qualitaet in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:52:19 +02:00
msolarczekandClaude Opus 5 3769312bfb L10a Qualität & Abnahmetests: Lane-Bericht
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:49:54 +02:00
msolarczekandClaude Opus 5 fcd1dca971 Merge lane/kundenversand in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:49:42 +02:00
msolarczekandClaude Opus 5 fe891b1b79 L11 Kundenversand: Tests und Lane-Bericht
- scripts/test-report-customer-mail.ts: 41 Prüfungen (Rechte/Scope, Mandantentrennung
  beim Zustellen, Prüfsumme, Größenlimit, Dedupe, Fake-Provider, Mailhog-Durchstich)
- docs/craftvia/lanes/kundenversand.md: Umfang, Gate 53/53, HTTP-Smoke 13/13, offene Punkte

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:49:06 +02:00
msolarczekandClaude Opus 5 29c80a0e73 L11 Kundenversand: Bericht-PDF an Kunden senden (Service, Action, API, Berichtsseite)
- sendReportToCustomer: report:approve, Scope, nur approved mit PDF, Empfänger
  Ansprechpartner -> Kunde, Dedupe je Adresse+Version, Audit export
- Server Action + POST /api/v1/reports/{id}/send inkl. OpenAPI-Eintrag
- /reports/[id]: Abschnitt „An Kunden senden" mit bisherigen Versänden, Texte de/en

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:48:57 +02:00
msolarczekandClaude Opus 5 e0106b8f8e L11 Kundenversand: Mail-Anhänge per Dokument-Referenz und Template craftvia_report_customer
- MailJob/EnqueueInput: attachments als { documentId } (keine Bytes in Redis, nur mit tenantId)
- deliverMail: Anhänge mandantengebunden aus MailLog.tenantId laden, SHA-256 prüfen,
  Größenlimit MAIL_MAX_ATTACHMENT_BYTES (Default 10 MB); Fehler -> failed ohne Versand
- SMTP-Provider reicht Anhänge an nodemailer durch; optionaler Provider für Tests
- Template craftvia_report_customer (de/en) ohne App-Link, eigene CUSTOMER_TEMPLATE_KEYS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:48:45 +02:00
msolarczekandClaude Opus 5 565cd4ef5c L10a Qualität & Abnahmetests: Authentifizierter Durchstich aller Kernseiten je Rolle, Last-Seed (5 000 Aufträge) mit Performance-Messung, Demo-Import für die Prüfmaske
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:43:47 +02:00
msolarczekandClaude Opus 5 388ca85f54 L10a Qualität & Abnahmetests: HTTP-Sicherheitstest (manipulierte IDs aller /api/v1-Routen, Datei-Routen, Uploads, CSRF, Sessions, Login-Sperre, Header) gegen next start
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:34:29 +02:00
msolarczekandClaude Opus 5 6687ef2e03 Tests: vollständige Suite auch mit RLS_ENFORCED=true grün
- test-tenant-isolation: Compound-Key mit fremdem Mandanten – im Owner-Betrieb Throw
  (Tenant-Guard), unter scharfer RLS liefert die DB null; beides = kein Datenabfluss
- run-tests.ts: lädt .env und leitet RLS_DATABASE_URL (Rolle craftvia_app) aus
  DATABASE_URL ab, wenn RLS_ENFORCED=true und keine URL gesetzt ist

Nachweis: RLS_ENFORCED=true npm run test → 52/52; npm run gate → 52/52.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:33:22 +02:00
msolarczekandClaude Opus 5 bc58738129 Merge lane/betrieb in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:26:36 +02:00
msolarczekandClaude Opus 5 c297cfbe83 L10a Qualität & Abnahmetests: Sicherheitstests (Rollen-Matrix, schädliche Uploads, Login-Sperre/Rate-Limit/Sessions) und Audit-Log append-only für craftvia_app
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:25:55 +02:00
msolarczekandClaude Opus 5 5343174136 L10b Betrieb & Aufräumen: Lane-Bericht und HTTP-Smoke
- docs/craftvia/lanes/betrieb.md: Umfang a–k, Entscheidungen (b: report.submit übernehmbar, h: Audit nach Commit umgesetzt), Verhaltensänderungen (Fehlerformat, 422), Dateien, Tests (api 150, sync 36, audit 48), Gate 52/52, RLS_ENFORCED-Lauf 50/52 (zwei Owner-Modus-Tests unverändert), Docker-Build, offene Punkte.
- scripts/smoke-betrieb.ts: HTTP-Smoke gegen den Dev-Server (Session-Cookies ohne Passworteingabe) für Fehlerformat, OpenAPI, Sync-Ops, Bundle, Lotse-Kontingent, Sidebar; 18/18 grün auf :3111.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:25:36 +02:00
msolarczekandClaude Opus 5 cadaedc6cc L10b Betrieb & Aufräumen: Deploy – craftvia-worker, CI-Testjob, DEPLOY.md, Certvia-Doku archiviert
- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium,
  shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker.
- Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler
  docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft.
- .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS,
  KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS,
  AI_MONTHLY_TOKEN_LIMIT.
- CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy,
  seed, Passwort für craftvia_app, tsc, lint, build, npm run test.
- docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets,
  Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke,
  Update/Rollback. build-and-push-images.sh baut craftvia-worker.
- Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md
  und Skript-Kommentaren angepasst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00
msolarczekandClaude Opus 5 21d6dc016a L10b Betrieb & Aufräumen: einklappbare Backoffice-Sidebar, Berichtseditor mit Offline-Entwurf
- Aufräumpunkt g (L1 offener Punkt 10): components/backoffice-frame.tsx – unter 1024 px ist die
  Sidebar ein Drawer hinter einem Menü-Button (44 px, aria-expanded, schließt bei Navigation,
  Hintergrund, Escape); ab 1024 px statisch wie bisher. Header kompakter auf schmalen Screens.
- Aufräumpunkt i (L7 offener Punkt 7): mobiler Berichtseditor speichert ungesicherte Eingaben
  über useOfflineDraft (IndexedDB je Mandant/Nutzer). Ein Entwurf wird nur wiederhergestellt,
  solange die Servertexte unverändert sind (sonst gewinnt der Server, z. B. nach Übernahme eines
  Lotse-Vorschlags); nach Speichern/Absenden gelöscht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00
msolarczekandClaude Opus 5 b0aedb5d23 L10b Betrieb & Aufräumen: Lotse-Betrieb – Aufbewahrung KI-Protokoll und Token-Kontingent
Aufräumpunkt k (Spec §31):
- Aufbewahrung: services/lotse/retention.ts leert input/output und createdById von
  AiGeneration-Einträgen älter als AI_GENERATION_RETENTION_DAYS (Default 180), Metadaten bleiben,
  Audit je Mandant. Queue/Processor ai-retention, täglicher BullMQ-Job-Scheduler beim Start des
  craftvia-worker.
- Kontingent: services/lotse/budget.ts (Tokens ein+aus je Kalendermonat, TenantSettings-Wert vor
  Env AI_MONTHLY_TOKEN_LIMIT, 0 = unbegrenzt). Lotse-Entwurf und Sprachnotiz-Zusammenfassung
  → blocked budget_exceeded mit Klartext; Import-Extraktion fällt auf manuelle Erfassung zurück
  (Hinweis ai_budget_exceeded). /settings/lotse: Kontingent setzen, Verbrauch anzeigen.
- scripts/test-betrieb-audit.ts: Audit nach Commit/Rollback/verschachtelt, Merge atomar und in
  äußerer Transaktion, Audit „read", Aufbewahrung (Frist, Metadaten, Idempotenz, Mandant B),
  Kontingent (Mandant/Env/Vormonat/unbegrenzt, Rollen, Audit).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00
msolarczekandClaude Opus 5 8aedc642ca L10b Betrieb & Aufräumen: Audit nach Commit, mergeCustomers atomar, Audit-Aktion read
- Aufräumpunkt h: writeAuditLog puffert innerhalb von inTransaction (AsyncLocalStorage) und
  schreibt nach dem Commit; bei Rollback werden die Einträge verworfen, nur „denied" bleibt.
  Verschachtelte Transaktionen nutzen den äußeren Puffer.
- Aufräumpunkt d: mergeCustomers läuft über inTransaction (sequenziell, geschützter Statuswechsel)
  statt ctx.db.$transaction([...]) und ist damit auch bei RLS_ENFORCED=true atomar und in äußere
  Transaktionen einbettbar.
- Aufräumpunkt e: AuditAction „read" (+ Label im Audit-Viewer de/en); Notdienst-Kunden- und
  Objektsuche protokollieren als „read" statt „export".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00
msolarczekandClaude Opus 5 85bae832d0 L10b Betrieb & Aufräumen: Sync – Berichts-Ops, Konflikt-Übernahme, eigene Session im Bundle
- Aufräumpunkt j: report.save_draft und report.submit mit Zod-Schemas (lib/sync/ops.ts) und
  Registry-Einträgen → services/reports/sync-ops.ts. report.submit reicht baseVersion als
  expectedWorkOrderVersion und aiReviewed an submitReport durch; Lotse-Entwürfe ohne Bestätigung →
  rejected invalid. signature.capture bleibt unregistriert (Upload-Art für Unterschriftsbild fehlt).
- Aufräumpunkt b: „Übernehmen" in der Konfliktliste delegiert an den Sync-Dispatcher
  (apply.ts#reapplyOperation, ohne baseVersion) statt des L2-Stubs; unterstützt
  work_order.transition und report.submit. Hinweistext der Konfliktliste angepasst.
- Aufräumpunkt c: getFieldBundle liefert je Auftrag mySession (eigene aktive WorkSession); die
  Offline-Ansicht leitet den Zeitstatus daraus ab (alte Bundles: Näherung über Auftragsstatus).
- scripts/test-betrieb-sync.ts (Bundle, clientId je Mandant, Berichts-Ops, Konflikt-Übernahme,
  Mandant B, Monteur ohne Zuweisung); test-einsatz-sync.ts prüft „nicht verfügbare Op" jetzt mit
  signature.capture, weil report.save_draft registriert ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00
msolarczekandClaude Opus 5 5f08df324f L10b Betrieb & Aufräumen: Migrationen clientId je Mandant und KI-Token-Kontingent
- 20260915090000_betrieb_client_id_per_tenant (Aufräumpunkt f, L8 offener Punkt 6): globale
  Unique-Indizes auf client_id (material_usages, work_sessions, time_entries, activity_notes,
  photos, voice_notes, reports, signatures) → @@unique([tenantId, clientId]). Offline-IDs sind nur
  je Mandant eindeutig; ein Replay derselben ID in einem anderen Mandanten scheiterte bisher mit
  internem Fehler. Keine neuen Tabellen.
- 20260915091000_betrieb_ai_token_limit (Aufräumpunkt k): tenant_settings.ai_monthly_token_limit
  (NULL = Plattform-Vorgabe AI_MONTHLY_TOKEN_LIMIT, 0 = unbegrenzt). Keine neue Tabelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00
msolarczekandClaude Opus 5 49f05c8db3 L10b Betrieb & Aufräumen: OpenAPI 3.1 unter /api/v1/openapi.json, API-Doku und API-Test
- src/lib/api/openapi.ts: statisch gepflegte Spezifikation aller v1-Routen inkl. Fehlerformat,
  Pagination, Idempotenz (clientOpId/clientId), Konflikte, Rate Limits, Rechte je Operation.
- GET /api/v1/openapi.json liefert das Dokument (angemeldete Nutzer).
- docs/craftvia/API.md: Kurzdoku mit Endpunkt-Tabelle.
- scripts/test-betrieb-api.ts: jede Route nutzt requireApiContext/respond.ts, 401 ohne Sitzung
  im einheitlichen Format, 403 bei fremdem Origin/Sec-Fetch-Site, Fehler-Mapping, Rate Limit je
  Nutzer (Standard/Einsatz getrennt), OpenAPI deckt jede route.ts ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00
msolarczekandClaude Opus 5 fb993a7730 L10b Betrieb & Aufräumen: /api/v1 über gemeinsamen Adapter, einheitliches Fehlerformat, Rate Limiting
Aufräumpunkt a: Die lane-lokalen API-Kontexte (imports/_context.ts, sync/api-context.ts,
reports/http.ts, work-orders/_http.ts mit moduleGuard) sind entfernt. Alle v1-Routen laufen über
requireApiContext (DB-autoritative Rechte, 401/403) und withApi/toErrorResponse (respond.ts):
- Fehlerformat überall { error: { code, message, details? } }; invalid und blocked → 422,
  conflict → 409, payload_too_large → 413, rate_limited → 429 + Retry-After.
- Same-Origin-Prüfung in withApi für jede Mutation vor der Anmeldung (vorher fehlte sie bei
  imports, reports und work-orders).
- Rate Limiting je Nutzer mit rate-limit.ts: api (API_RATE_LIMIT_PER_MINUTE, 300/min) und
  apiField für sync/uploads/field (API_FIELD_RATE_LIMIT_PER_MINUTE, 1200/min).
- Clients angepasst: Import-Uploader liest das neue Fehlerformat, Upload/Outbox werten 422 als
  endgültig ungültig (429 bleibt transient mit Backoff).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00
msolarczekandClaude Opus 5 4507862375 L10a Qualität & Abnahmetests: E2E-Prozesstests (regulärer Auftrag, mehrtägig, Notdienst, Offline-Sync, Pflichtfotos/Unterschrift, Dubletten, Mandantentrennung systematisch)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:16:11 +02:00
msolarczekandClaude Opus 5 1a75c28bb4 L10a Qualität & Abnahmetests: Demo-Seed (Teams, Kunden, Objekte, 20 Aufträge, Berichte, Notdienst, demo2)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:58:22 +02:00
msolarczekandClaude Opus 5 a7d4b02a13 Merge lane/lotse in feature/craftvia-mvp
Konflikt gelöst: nav.ts Icon-Imports (Siren aus L8, Compass aus L9).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:44:06 +02:00
msolarczekandClaude Opus 5 1796869e05 PWA: Service Worker ohne Session erreichbar, nie HTTP-gecacht; authentifizierter Smoke
- proxy: /sw.js vom Session-Gate ausgenommen (Update-Prüfung auch bei abgelaufener
  Sitzung; enthält keine Mandantendaten) – gemeldet von L7
- next.config: /sw.js mit Cache-Control no-cache/no-store, Service-Worker-Allowed /
- scripts/smoke-auth.ts: Session-Cookie über finalizeIdentityLogin + next-auth/jwt
  encode (ohne Passworteingabe), prüft Backoffice- und Monteur-Seiten

Nachweis: /sw.js anonym 200 + no-cache; Smoke 19/19 Seiten grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:43:29 +02:00
msolarczekandClaude Opus 5 ffd2f02632 L9 Lotse – KI-Assistent: Lane-Bericht
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:31:16 +02:00
msolarczekandClaude Opus 5 2a30ee1919 Merge lane/offline in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:28:04 +02:00
msolarczekandClaude Opus 5 7825245901 Fix: /dashboard 500 – buttonCls aus Client-Modul herausgelöst
Server-Komponenten (Dashboard, Aufträge, Suche, Checklisten, Auftragsdetail) riefen
buttonCls() aus einem "use client"-Modul auf; das bricht zur Laufzeit (gemeldet von L8).
Neu: src/components/work-orders/button-cls.ts (server-sicher), action-form re-exportiert.

Nachweis: authentifizierter HTTP-Smoke (Backoffice 13 Seiten, Monteur 6 Seiten) alle 200,
/dashboard vorher 500. tsc/lint grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:27:46 +02:00
msolarczekandClaude Opus 5 e5d5ccad6f L7 Offline & PWA: Lane-Bericht
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:26:20 +02:00
msolarczekandClaude Opus 5 5f6e70fdf3 L9 Lotse – KI-Assistent: Gate-Fixes (EXEMPT-Auth-Prüfung, Sprachnotiz ohne Transkriptionsanbieter nicht einreihen)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:25:50 +02:00
msolarczekandClaude Opus 5 0757f212f5 Merge lane/notdienst in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:23:13 +02:00
msolarczekandClaude Opus 5 519bd2a303 L8 Notdienst: Lane-Bericht
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:22:26 +02:00
msolarczekandClaude Opus 5 8ebf3c0685 L7 Offline & PWA: E2E-Test (20 Ops offline → ein Batch → applied/duplicate, Mandanten-/Scope-Trennung), Message-Keys ohne Punkt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:21:41 +02:00
msolarczekandClaude Opus 5 9b3c50923f L7 Offline & PWA: submitOp über Outbox, Foto/Sprachnotiz über Upload-Warteschlange, Entwürfe in IndexedDB
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:21:14 +02:00
msolarczekandClaude Opus 5 f0620f9c3b L7 Offline & PWA: Sync-Seite, Offline-Ansicht, Sync-Badge, Installationshinweis, Logout-Schutz
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:20:44 +02:00
msolarczekandClaude Opus 5 4dd330b9fd L7 Offline & PWA: Service Worker (Static/Seiten/Dokument-Cache) und Manifest start_url /m
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:20:20 +02:00
msolarczekandClaude Opus 5 9f73cdae49 L7 Offline & PWA: Outbox-Kern, IndexedDB-Store, Sync-Engine und Bundle-Logik
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:19:43 +02:00
msolarczekandClaude Opus 5 f42ead3ced L9 Lotse – KI-Assistent: Tests (Entwurf, Datenminimierung, Rechte/Mandant, Transkription, Live)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:19:38 +02:00
msolarczekandClaude Opus 5 9a3d472682 L9 Lotse – KI-Assistent: UI im Berichtseditor, Auftragsdetail, Sprachnotizen und Einstellungen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:19:38 +02:00
msolarczekandClaude Opus 5 ff5c57f276 L9 Lotse – KI-Assistent: Transkription, Berichtsentwurf, Vollständigkeitsprüfung, Freigabeprinzip (Services)
- OpenAI-kompatible Transkription + Processor transcription (done/failed/disabled, AiGeneration, Notiz aus Sprachnotiz)
- Claude-Lotse (strukturierte Ausgabe, Refusal/Fallback), Datenminimierung, Vorschläge in content.lotse
- Vollständigkeitsprüfung (Regeln + KI-Hinweise mit Deep-Link), Einstellungen, KI-Protokoll
- Freigabeprinzip: Submit eines Lotse-Entwurfs nur mit Prüfbestätigung (serverseitig)
- Migration lotse_address_form (TenantSettings)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:19:38 +02:00
msolarczekandClaude Opus 5 8e08156e55 L8 Notdienst: Tests für Erfassung, Mandantentrennung, Sync und Prüfung
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:12:33 +02:00
msolarczekandClaude Opus 5 dece70c129 L8 Notdienst: Backoffice-Nachbearbeitung /work-orders/emergency-review
Liste und Detail mit fünf Prüfschritten (Kunde, Objekt, Auftrag, Bericht,
Abrechnung), Navigationseintrag, Dashboard-Kachel verlinkt auf die Prüfung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:12:33 +02:00
msolarczekandClaude Opus 5 ef61754a0a L8 Notdienst: mobile Erfassung /m/emergency in drei Schritten
Kunde suchen oder vorläufig anlegen, Einsatzort mit Ansprechpartner,
Grund mit optionaler Sprachnotiz, Beginn und Team; Start über die Sync-Op.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:12:33 +02:00
msolarczekandClaude Opus 5 f173563424 L8 Notdienst: Services für Erfassung, Suche, Abschluss-Event und Sync-Op
createEmergencyOrder in einer Transaktion (vorläufiger Kunde/Objekt, Auftrag N-…
in_progress, Team/Zuweisung, WorkSession), searchCustomersForEmergency mit
minimalem Feldumfang + Audit, emergency.completed nach Abschlussbericht,
Sync-Op emergency.create (Registry + Zod-Schema), Review-Services.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:12:33 +02:00
msolarczekandClaude Opus 5 d5c1221ab5 UI: veraltete Stub-Hinweise in Auftragsdetail und Konfliktliste bereinigt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:49:46 +02:00
msolarczekandClaude Opus 5 3883296300 Integration: Stubs von L2/L3/L4/L5 gegen echte Services getauscht
- Aufträge: createWorkOrder/transitionWorkOrder/getCompletionBlockers aus L2
- Dokumente: storeFile aus L1; neu services/documents/read.ts (readStoredBytes,
  readDocumentBytes mit Prüfsummenprüfung); L2-Upload nutzt zentrale Ablage
- Dubletten aus L1 (findDuplicateCustomers(ctx)); Objekt-Kandidaten als
  imports/site-candidates.ts; Dateityp-Erkennung mobil als field/mime.ts
- Objekt-Historie mobil als Adapter auf L1 getSiteHistory, PDF über /api/v1/reports/:id/pdf
- L4-Upload-Idempotenz: fester Upload-Lineage nach storeFile, Race → Soft-Delete + Replay
- PDF-Worker-Kontext: document:write zum Ablegen des Berichts-PDF
- Import: doppeltes Work-Order-Audit entfernt; bestätigte Aufträge starten planned
- Dateilinks in Auftragsdetail auf /files/[documentId]
- proxy: /api/v1 ohne Session → 401 JSON statt Login-Redirect
- ARCHITEKTUR §2: Objekt-Historie für Feldrollen (freigegebene Einsätze aller Teams)

Gate: tsc, lint, build, 42/42 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:49:19 +02:00
msolarczekandClaude Opus 5 2984cc18d3 Merge lane/stammdaten in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:38:34 +02:00
msolarczekandClaude Opus 5 f17eeaf47b L1 Stammdaten: Lane-Bericht und mobile Suchfelder
Bericht docs/craftvia/lanes/stammdaten.md (Umfang, Dateien, Tests, Smoke,
offene Punkte) sowie umbrechende Suchfelder der Kunden- und Objektliste bei
schmalen Viewports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:37:07 +02:00
msolarczekandClaude Opus 5 d9476d324b Merge lane/auftraege in feature/craftvia-mvp
Konflikte gelöst: Header mit Suche (L2) und Glocke (L6), Audit-Labels vereinigt
(ohne doppeltes sync_operation), Navigation mit Benachrichtigungen und Auftragsvorlagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:36:21 +02:00
msolarczekandClaude Opus 5 da2afb79e3 Fix: BullMQ-Job-IDs ohne Doppelpunkt
BullMQ lehnt benutzerdefinierte Job-IDs mit ':' ab; dispatchJob fiel dadurch
mit Redis immer auf die Inline-Verarbeitung zurück (gemeldet von L4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:35:24 +02:00
msolarczekandClaude Opus 5 e0f5e13e1c Fix: doppelte Audit-Entity-Labels nach Merge lane/einsatz
Gate: tsc, lint, build, 33/33 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:35:14 +02:00
msolarczekandClaude Opus 5 024ec5e3ab Merge lane/einsatz in feature/craftvia-mvp
Konflikte gelöst: processors/index.ts (report-pdf + image-derivatives),
(app)/layout.tsx (Logo, Glocke, AccountInactiveNotice). L5-Mobilseiten
report/sign nach src/app/(field)/m/(core)/orders/[id]/ verschoben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:32:35 +02:00
msolarczekandClaude Opus 5 a9530f2513 L2 Aufträge & Backoffice: Lane-Bericht docs/craftvia/lanes/auftraege.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:30:54 +02:00
msolarczekandClaude Opus 5 b49d14bab3 L2 Aufträge & Backoffice: Einzeiler Navigation, Header-Suche, Audit-Entity-Labels
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:30:44 +02:00
msolarczekandClaude Opus 5 b96016d593 L2 Aufträge & Backoffice: Backoffice-UI (Liste, Detail, Konflikte, Dashboard, Suche, Einstellungen) + Texte de/en
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:30:32 +02:00
msolarczekandClaude Opus 5 edded26b21 L4 Einsatz mobil: Lane-Bericht
docs/craftvia/lanes/einsatz.md: Umfang, Routen, Dateien, Tests, Stubs und
Abhängigkeiten, bekannte Lücken, Gate- und Smoke-Ergebnis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:30:03 +02:00
msolarczekandClaude Opus 5 95f41b29f5 L4 Einsatz mobil: Mobile Shell und Einsatz-Oberfläche
- /m in eigene Route-Group (field)/m verschoben (emergency-Platzhalter mit),
  Zugriffsprüfungen aus (app)/layout.tsx nach server/app-access.ts extrahiert
  und von Backoffice- und Mobile-Shell gemeinsam genutzt
- Mobile Shell mit Bottom-Nav (Heute · Aufträge · Notdienst · Sync · Profil)
  und Online/Offline-Badge; Startseite rollenabhängig (Feldrollen → /m),
  Login-Default-Redirect auf /
- Heute, Auftragsliste mit Tabs, Auftragsdetail mit einer Primäraktion je
  Zustand, Unterseiten Fotos (Kamera, Kompression, Upload-Fortschritt),
  Notizen + Sprachaufnahme, Material mit Stepper, Checkliste, Zeiten mit
  Korrektur, Profil; Sync-Platzhalter für L7
- Client-Wrapper submitOp (lib/field/client-ops.ts), Upload mit Fortschritt,
  Bildkompression, Formatierung; Texte in messages/{de,en}/field.json

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:30:03 +02:00
msolarczekandClaude Opus 5 4a25f2cc3b Fundament: atomare Mandanten-Transaktionen, iframe-Vorschau, Uploads bis 25 MB, DSGVO-Felder
- db.ts: tenantTransaction() – atomar auch bei RLS_ENFORCED=true (AsyncLocalStorage
  bindet Operationen an eine craftvia_app-Transaktion, Kontext einmal gesetzt,
  verschachtelte Aufrufe treten bei, fremder Mandant wird abgewiesen)
- services/context.ts: inTransaction(ctx, fn); imports/confirm.ts umgestellt
- next.config.ts: EMBEDDABLE_FILE_ROUTES mit frame-ancestors 'self'/SAMEORIGIN
  (PDF-Vorschau Prüfmaske), proxyClientMaxBodySize 26mb (Import bis 25 MB)
- test-rls-enforcement: RLS-URL-Default aus DATABASE_URL (Lane-DBs)
- dsgvo/pii-fields: 26 Personenreferenzen des Craftvia-Domänenmodells
- ARCHITEKTUR §4.8: Transaktions-, Header-, Upload-, Versions- und PII-Regeln
- Test test-tenant-transaction (Commit/Rollback/Fremdmandant/Verschachtelung),
  grün im Owner- und im RLS-Modus

Gate: tsc, lint, build, 31/31 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:29:57 +02:00
msolarczekandClaude Opus 5 952e74cddd L4 Einsatz mobil: Field-Services, Sync-API, Uploads und Tests
- services/field: Einsatz-Sessions (Anfahrt/Arbeit/Pause als TimeEntry-Segmente,
  eine aktive Session je User+Auftrag), Zeitkorrektur mit Recht + Grund + Audit,
  Checkliste, Material (Abweichung nur mit Begründung, Zusatzmaterial), Notizen,
  Fotos, Sprachnotizen (ohne Transkriptions-Processor Status disabled), Uploads
  (idempotent je Mandant), autorisierte Dokument-Auslieferung, Lesemodelle + Bundle
- services/sync: applyOperations mit Idempotenz, baseVersion-Konfliktprüfung,
  Registry für Ops anderer Lanes, lane-lokaler requireApiContext
- /api/v1/sync, /api/v1/uploads, /api/v1/field/bundle, /api/v1/field/documents/[id]
- lib/sync/ops.ts (Zod-Payloads je opType), lib/field/material-rules.ts
- Stubs mit Vertragssignatur: transitionWorkOrder (L2), storeFile (§4.3),
  getSiteHistory (L1)
- Processor image-derivatives + Registrierung, Audit-Entity-Labels
- Tests: test-einsatz-field (48 Prüfungen), test-einsatz-sync (38 Prüfungen)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:29:48 +02:00
msolarczekandClaude Opus 5 a962fa8be9 L2 Aufträge & Backoffice: Server Actions und /api/v1/work-orders
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:29:42 +02:00
msolarczekandClaude Opus 5 2311b35d8c L2 Aufträge & Backoffice: Service-Schicht Aufträge + Tests
Statusmaschine (transitionWorkOrder inkl. eventData), Zuweisung, Completion-Guards,
Materialvorgabe, Checklisten/Pflichtfotos, Liste/Dashboard-Presets, Suche,
Sync-Konflikte, Einstellungen (Auftragsarten, Vorlagen, Nummernkreise).
Tests: Übergangsmatrix je Rolle, Kernlogik, Scope/Mandantentrennung, Nummernkreis-Parallelität.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:29:15 +02:00
msolarczekandClaude Opus 5 6423351035 L1 Stammdaten: Tests für Dubletten, Kunden, Objekte, Teams und Dokumente
Kernlogik, Mandantentrennung (Mandant B liest/ändert nichts von A) und
Rollen/Scope (Monteur ohne Zuweisung → not_found/forbidden).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:26:27 +02:00
msolarczekandClaude Opus 5 d18f4fe431 L1 Stammdaten: Backoffice-Seiten Kunden, Objekte, Teams und Dokumente
Listen mit Suche/Filter/Paginierung, Popups für Anlage und Bearbeitung,
Kundendetail mit Tabs, Dublettenhinweis und Zusammenführen, Objektdetail mit
Kartenlink, Dokumenten-Tab und Historie, Teamverwaltung mit Mitgliedern,
Dokumentenübersicht. Texte in messages de/en, Audit-Label Ansprechpartner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:26:27 +02:00
msolarczekandClaude Opus 5 49c5ad0e33 L1 Stammdaten: Dokumentenablage-Service und Download per documentId
storeFile (Allowlist, Magic Bytes, Größenlimits, Dateinamen-Normalisierung,
SHA-256, Versionierung über lineageId), FileScanner mit optionalem ClamAV-Hook,
Sichtbarkeits-/Scope-Autorisierung, Upload-Route und Umbau der Download-Route
von files/[...key] auf files/[documentId].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:26:27 +02:00
msolarczekandClaude Opus 5 1f8e6413fe L1 Stammdaten: Services, Dublettenprüfung, Server Actions und API v1
Kunden (Nummernkreis, Ansprechpartner, vorläufig bestätigen, Zusammenführen mit
Bestätigung), Objekte inkl. Historie, Teams mit Mitgliedschaften, Dublettenlogik
(lib + Service), API-Kontext/Antwortformat unter src/server/api und die Endpunkte
/api/v1/customers, /api/v1/sites, /api/v1/sites/[id]/history.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:26:07 +02:00
msolarczekandClaude Opus 5 3e16689b2f Merge lane/berichte in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:23:34 +02:00
msolarczekandClaude Opus 5 9e35bb4e47 L5 Berichte & Unterschrift: Tests und Lane-Bericht
Flow-Tests (Content, Status, Unterschrift, Versionierung, Mandantentrennung, Scope) und
PDF-Render-Smoke; Lane-Bericht docs/craftvia/lanes/berichte.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:22:40 +02:00
msolarczekandClaude Opus 5 94092ade3f L5 Berichte & Unterschrift: Backoffice- und Mobil-Oberflächen
/reports mit Filtern (zur Prüfung zuerst), /reports/[id] mit Aktionen, Versionen und PDF-Link;
mobile Komponenten für Bericht, Prüfung und Unterschrift inkl. Signature-Pad; Texte de/en.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:22:40 +02:00
msolarczekandClaude Opus 5 8f53df6208 L5 Berichte & Unterschrift: Server Actions und API v1
Actions mit moduleGuard("reports"); POST daily-report/completion-report, POST approve,
GET pdf sowie Dateiauslieferung für im Bericht referenzierte Dokumente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:22:40 +02:00
msolarczekandClaude Opus 5 8a6fdd8f7a L5 Berichte & Unterschrift: Berichtsinhalt, Services, Unterschrift und PDF
ReportContent-Vertrag, Content-Builder mit Tagesfilter, Services für Tages-/Abschlussbericht,
Bearbeiten, Absenden, Freigabe, Zurückweisen, neue Version, Unterschrift und PDF-Erzeugung
(playwright-core, Worker-Processor, Dockerfile-Stage worker). Stubs für L2-Transition/Blocker
und Dokumenten-Store.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:22:40 +02:00
msolarczekandClaude Opus 5 f532ba9a96 Merge lane/import in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:22:07 +02:00
msolarczekandClaude Opus 5 ee27af7cd4 Test: Audit-Request-Kontext und Mail-Absender je Mandant
10 Prüfungen: IP/User-Agent null außerhalb eines Requests, Anzeigename und
Reply-To des Mandanten, Plattform-Adresse bleibt, Header-Injection bereinigt,
Rückfall auf Plattform-Defaults für Plattform-Mails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:21:34 +02:00
msolarczekandClaude Opus 5 cf012d40c2 L3 Auftragsimport: Prüfmaske, Upload, API und Lane-Bericht
- /imports: Upload per Drag & Drop/Dateiauswahl mit Fortschritt, Liste mit Status, Neu verarbeiten
- /imports/[id]: Originaldokument + Prüfmaske (Kunde, Objekt, Ansprechpartner, Auftrag, Positionen),
  unsichere Felder markiert, Kunden-/Objektentscheidung, Bestätigen/Verwerfen
- Datei-Route für die Vorschau, Server Actions (moduleGuard("imports"))
- API: POST /api/v1/work-orders/import, GET /api/v1/imports/[id], POST /api/v1/imports/[id]/confirm
- Texte messages/{de,en}/imports.json, Bericht docs/craftvia/lanes/import.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:21:15 +02:00
msolarczekandClaude Opus 5 b8b8bddefe Fundament: Audit mit IP/User-Agent, Mail-Absender je Mandant
- Migration audit_request_context: ip_address, user_agent an audit_logs (Spec §26)
- writeAuditLog/writePlatformAudit erfassen IP (X-Forwarded-For) und User-Agent
  aus dem Request; außerhalb eines Requests (Worker/Skripte) null
- Audit-Viewer liest die neuen Spalten statt Heuristik aus before/after
- deliverMail nutzt Anzeigename und Reply-To aus TenantSettings (Spec §33.2);
  Absenderadresse bleibt Plattform-Domain (SPF/DKIM), Header-Injection bereinigt

Gate: tsc, lint, build, 24/24 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:20:41 +02:00
msolarczekandClaude Opus 5 2d1c07cf74 Fix: Agent-Worktrees (.claude/) von ESLint und tsc ausschließen
Die parallelen Lane-Worktrees liegen unter .claude/worktrees im Repo; ESLint und
tsc erfassten deren .next-Artefakte und brachen das Gate ab.

Gate nach Merge lane/benachrichtigungen: tsc, lint, build, 24/24 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:18:35 +02:00
msolarczekandClaude Opus 5 12a764786a L3 Auftragsimport: Extraktion, Plausibilität, Dubletten, Bestätigung (Services + Tests)
- Claude-Extraktion (PDF nativ/Bild, Structured Output, Konfidenzen, Volltext), FakeProvider
- Plausibilitätsprüfung, Mapping Extraktion → Formular, Korrektur-Diff
- Services Upload/Verarbeitung/Bestätigung/Verwerfen/Neu verarbeiten, Processor import-extraction
- Stubs: storeFile (§4.3), findDuplicateCustomers (L1), createWorkOrder (L2)
- Tests test-import-rules/-flow/-live, Beispiel-PDFs + Generator

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:17:13 +02:00
msolarczekandClaude Opus 5 2fb2978c71 Merge lane/benachrichtigungen in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:15:20 +02:00
msolarczekandClaude Opus 5 c41d834b9c L6 Benachrichtigungen & Audit: Lane-Bericht
Umfang, Empfängerregeln, Verträge für andere Lanes (occurrenceId, approvalStage,
startedAt/endedAt), Dateien, Migration, Tests, Gate und Smoke, Fundament-Bedarf
(IP/User-Agent im Audit-Log, Absender/Reply-To je Mandant im Mail-Kern), bekannte Lücken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:14:31 +02:00
msolarczekandClaude Opus 5 dfbc92bb4d L6 Benachrichtigungen & Audit: Audit-Viewer mit Filter und Diff
- /settings/audit (audit:read): Filter Zeitraum, Benutzer, Aktion, Objektart,
  Objekt-ID; Pagination; Detail-Popup mit before/after-Diff, Ergebnis, IP/User-Agent
  (derzeit nicht erfasst, Fundament-Bedarf).
- Service services/audit/viewer.ts, strikt mandantengebunden über ctx.db.
- Audit-Entity-Labels für alle Craftvia-Entitäten.
- Test scripts/test-benachrichtigungen-inbox-audit.ts (46 Prüfungen): Posteingang,
  Mandantentrennung, Rollen/Scope, Mailkonfiguration, Audit-Viewer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:12:51 +02:00
msolarczekandClaude Opus 5 879012415e L6 Benachrichtigungen & Audit: Glocke, Posteingang, Nutzer-Einstellungen, Mailkonfiguration
- Glocke im Backoffice-Header (ungelesen-Zähler, letzte 10, alle gelesen), mobil
  einbindbar über variant="mobile".
- /notifications mit Filter gelesen/ungelesen/Art, Öffnen markiert gelesen (nur
  relative Links), Pagination.
- /account Abschnitt Benachrichtigungen: E-Mail-Opt-out je Typ, Notdienst Pflicht.
- /settings/email (tenant:manage): Absendername, Antwortadresse, Empfänger Notdienst
  und Abrechnung; Validierung gegen Header-Injection, max. 20 Adressen, Audit.
- Actions unter actions/notifications mit moduleGuard + guard; Navigation ergänzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:12:21 +02:00
msolarczekandClaude Opus 5 7b37c41a83 L6 Benachrichtigungen & Audit: Empfängerregeln, In-App-Benachrichtigungen und Craftvia-Mails
- handleEvent (Signatur unverändert) löst alle 17 Domain-Events auf: Team/Assignees,
  Backoffice (read_all + report:approve), Teamleiter-Freigaben, Ersteller, Abrechnung,
  Notdienst, Import, Sync; Akteur ausgenommen, alle IDs über ctx.db neu aufgelöst.
- In-App-Notification über ctx.db (ungelesene gleiche Meldung wird aufgefrischt),
  E-Mail über enqueueMail mit dedupeKey event:entity:user (+ optional occurrenceId),
  Opt-out je Typ, Notdienst als Pflichtmail, feste Empfänger ohne Doppelmail.
- Mail-Templates craftvia_* (de/en) inkl. Notdienst-Format Spec §19.4 und Craftvia-Fußzeile.
- Migration tenant_mail_settings: mailFromName, mailReplyTo, emergencyRecipients,
  billingRecipients an tenant_settings (keine neue Tabelle).
- Texte aus messages/{de,en}/notifications.json.
- Test scripts/test-benachrichtigungen-events.ts (63 Prüfungen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:11:53 +02:00
msolarczekandClaude Opus 5 bf4456718e Architektur: Craftvia-Domänenmodell, Verträge und Team-Schnitte
- Migration 0002_craftvia_domain: 27 Fachtabellen inkl. RLS (enable_tenant_rls)
- TENANT_MODELS (db.ts, backup/topology.ts) um alle Fachmodelle ergänzt
- moduleGuard liefert DB-autoritative Rechte; ServiceCtx für Domänen-Services
- Verträge: Statusmaschine, Events, Nummernkreise, Sichtbarkeits-Scopes,
  Job-Queues + Worker, KI-Provider-Interfaces, Sync-Envelope
- docs/craftvia/ARCHITEKTUR.md mit Lanes, Ownership und DoD

Gate: tsc, lint, build, 22/22 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:49:21 +02:00
msolarczekandClaude Opus 5 1701db0a62 Fundament: Doku, Testanpassungen, Restbereinigung
- AGENTS.md und README.md auf Craftvia umgeschrieben (Regeln, Andockpunkte,
  Stack mit Garage, tsx-Tests, Gate, Demo-Logins)
- ISMS-Dokumente entfernt (SPEC, Prototypen, Lane-Prompts, Übergaben, Konzepte
  Incidents/Framework); Fundament-Doku (Deploy, Sicherheit, Backup, Identity) bleibt
- Fundament-Tests an Craftvia-Rollen/Branding angepasst, Resttreffer
  certvia/isms in Skripten und Kommentaren bereinigt

Gate: prisma generate, migrate status, tsc, lint, build, 22/22 Testskripte grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:44:58 +02:00
msolarczekandClaude Opus 5 ba7d6ac3ed Infra: Worker/Garage/Test-Runner
- docker-compose.yml: explizite Build-Targets (runner/migrate) — Ursache für
  "npx not found" bei garage-provision war das Default-Target (letzte Stage =
  Garage-Image ohne Node); worker startet npm run worker:mail; Defaults craftvia
- Coolify-Compose: incident-inbound-worker und sync-policy-templates entfernt,
  Rollen-/Bucket-/Image-Namen auf craftvia
- Dockerfile: seed/ und docs/wizard-uebergabe entfernt, messages/ im Runner
- npm: Name craftvia, Scripts test (scripts/run-tests.ts) und gate, ISMS-Pakete
  entfernt (handlebars, marked, sanitize-html, @xyflow/react, @dagrejs/dagre,
  html-to-image, imapflow, mailparser, exceljs, @dnd-kit/core)
- .env-Beispiele, launch.json (craftvia-dev), CI-Kommentare umbenannt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:44:58 +02:00
msolarczekandClaude Opus 5 6f3f5a5947 Fundament: Baseline-Migration mit RLS-Funktion
- 67 Certvia-Migrationen zu prisma/migrations/0001_baseline gesquasht
  (prisma migrate diff --from-empty --to-schema)
- RLS-Block: Rolle craftvia_app (NOLOGIN NOBYPASSRLS, idempotent), GRANTs +
  ALTER DEFAULT PRIVILEGES, wiederverwendbare Funktion enable_tenant_rls(tbl)
  (ENABLE + Policy tenant_isolation USING/WITH CHECK + FORCE + GRANT),
  angewendet auf alle Tenant-Tabellen (inkl. nullable tenant_id wie bisher)
- docs/craftvia/MIGRATIONS.md: Pflichten für neue Tenant-Tabellen
  (enable_tenant_rls + beide TENANT_MODELS-Listen + PII-Felder)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:40:34 +02:00
msolarczekandClaude Opus 5 9ec7fa5356 Fundament: Branding Craftvia
- Farb-Tokens laut Brandbook §11.4 (Lotsenblau, Signalorange, Graphit, Hafengrau,
  Stahlgrau, Zink, Funktionsfarben) in globals.css + src/lib/brand.ts; helles Theme
- Inline-SVG-Logo src/components/brand/craftvia-logo.tsx (horizontal/signet,
  color/mono/reversed, App-Icon-Kachel, optionale Tagline); Certvia-/GEFIM-Logos entfernt
- Favicon/PWA-Icons aus dem Signet erzeugt (scripts/generate-brand-icons.ts),
  site.webmanifest (Craftvia, theme_color #082E5B)
- Inter selbst gehostet (next/font/local, lokale OFL-Datei), Open Sans/Poppins entfernt
- Metadaten, WebAuthn-RP-Name, Mail-/Dokument-CD auf Craftvia umgestellt
- docs/craftvia/BRANDING.md ersetzt docs/BRANDING-CERTVIA.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:39:52 +02:00
msolarczekandClaude Opus 5 8491c7f173 Fundament: ISMS-Module entfernt; Craftvia-Rollen, Module, Navigation, i18n-Split
- ISMS-Routen, Actions, Server-/Lib-Code, Komponenten, Prisma-Modelle, Seeds,
  Importer, Skripte und ISMS-Tests entfernt (Fundament bleibt: Auth, Identity,
  MFA/WebAuthn, RBAC, Audit, Mail, Storage, Backup/DSGVO, Plattform-Admin)
- Schema auf Fundament-Modelle reduziert; TenantSettings generisch (+phone/email)
- TENANT_MODELS (db.ts, backup/topology.ts) und PII-Felder ausgedünnt
- RBAC: Rollen tenant-admin/backoffice/team-lead/technician + Craftvia-Permissions
- Modul-Katalog (customers, sites, teams, work_orders, imports, field, reports,
  emergency, documents, notifications, lotse) + Navigation aus src/lib/nav.ts
- Modul-Routen mit requireModule-Layout und Platzhalterseite
- Message-Katalog je Namespace (messages/<locale>/<namespace>.json), fs-Loader
- check-module-guards: Modul-Key aus src/server/actions/<moduleKey>/
- Provisionierung, Admin-Konsole, Einstellungen, Files-Route, Mail entkoppelt
- Seed minimal (demo/demo2, Nutzer je Rolle); Fundament-Tests auf Role/
  NotificationPreference-Fixtures umgestellt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:35:44 +02:00
1295 changed files with 79607 additions and 100863 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"version": "0.0.1",
"configurations": [
{
"name": "isms-dev",
"name": "craftvia-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3000
+61 -17
View File
@@ -1,13 +1,20 @@
# Referenz für die Coolify-Environment-Variablen (Testserver, intern).
# ECHTE Secrets NUR in Coolify eintragen — diese Datei enthält nur Platzhalter.
# In Coolify: Ressource -> Environment Variables (Bulk-Paste möglich).
# Referenz für die Coolify-Environment-Variablen (Testserver, intern).
# ECHTE Secrets NUR in Coolify eintragen – diese Datei enthält nur Platzhalter.
# In Coolify: Ressource -> Environment Variables (Bulk-Paste möglich).
# Hostnamen sind die Compose-Service-Namen (postgres/redis/garage), NICHT localhost.
# Betriebsdoku: docs/craftvia/DEPLOY.md
# --- Datenbank (Service "postgres") ---
POSTGRES_USER=isms
POSTGRES_USER=craftvia
POSTGRES_PASSWORD=CHANGE_ME_db_password
POSTGRES_DB=isms
DATABASE_URL=postgresql://isms:CHANGE_ME_db_password@postgres:5432/isms?schema=public
POSTGRES_DB=craftvia
DATABASE_URL=postgresql://craftvia:CHANGE_ME_db_password@postgres:5432/craftvia?schema=public
# --- Row Level Security (F-04) ---
# Auf dem Testserver zunächst false (Owner-Betrieb). Zum Scharfschalten craftvia_app mit
# LOGIN + Passwort versehen (ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<pw>';), dann:
RLS_ENFORCED=false
# RLS_DATABASE_URL=postgresql://craftvia_app:CHANGE_ME_app_password@postgres:5432/craftvia?schema=public
# --- Redis (Service "redis") ---
# F-18: Redis läuft mit requirepass. NUR REDIS_PASSWORD setzen — REDIS_URL wird in der
@@ -24,7 +31,7 @@ REDIS_PASSWORD=CHANGE_ME_redis_password
S3_ENDPOINT=http://garage:3900
S3_ACCESS_KEY=GK000000000000000000000000
S3_SECRET_KEY=CHANGE_ME_openssl_rand_hex_32
S3_BUCKET=isms-documents
S3_BUCKET=craftvia-documents
S3_REGION=us-east-1
# Garage-Secrets: der Daemon liest sie aus der Env (NICHT in deploy/garage.toml).
# In Coolify LITERAL setzen, NICHT via ${...} referenzieren (Interpolationsfalle).
@@ -44,29 +51,66 @@ GARAGE_ADMIN_TOKEN=CHANGE_ME_openssl_rand_hex_32
# `backups` auf /app/.backups (app + backup-worker) — Pfad hier NICHT aendern, ausser
# der Mount wird angepasst.
BACKUP_LOCAL_DIR=/app/.backups
# BACKUP_ENC_KEY= (leer = AUTH_SECRET)
# --- Auth (NextAuth) ---
# --- Auth (Auth.js v5) ---
# AUTH_SECRET: openssl rand -base64 32
# AUTH_URL: exakt die Coolify-Domain des app-Service (http:// für intern)
# AUTH_URL: exakt die Coolify-Domain des app-Service (http:// für intern)
AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32
# PASSWORD_PEPPER (Härtung §1): openssl rand -hex 32 — frisch je Umgebung, NICHT rotierbar, nie ins Artefakt.
PASSWORD_PEPPER=CHANGE_ME_openssl_rand_hex_32
# MFA_ENC_KEY= (leer = aus AUTH_SECRET abgeleitet; nach dem Setzen nicht mehr ändern)
AUTH_URL=http://REPLACE-WITH-COOLIFY-SSLIP-DOMAIN
# Hinter Reverse-Proxy (Coolify/Traefik) für Auth.js v5 zwingend, sonst UntrustedHost:
# Hinter Reverse-Proxy (Coolify/Traefik) für Auth.js v5 zwingend, sonst UntrustedHost:
AUTH_TRUST_HOST=true
# --- Demo-Seed (NUR Testserver!) ---
# true => migrate-Job legt nach der Migration den Demo-Mandanten + Nutzer an
# true => migrate-Job legt nach der Migration die Demo-Mandanten + Nutzer an
# (admin@demo.example / Demo1234!). In Produktion NICHT setzen / auf false lassen.
RUN_DEMO_SEED=true
# --- KI-Provider (optional, aktuell ungenutzt) ---
AI_PROVIDER=anthropic
AI_API_KEY=
# --- E-Mail (optional, im Test ungenutzt) ---
# --- E-Mail (optional; ohne SMTP bleiben Mails "pending") ---
SMTP_HOST=
SMTP_PORT=1025
SMTP_SECURE=
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=isms@example.com
SMTP_FROM=craftvia@example.com
MAIL_FROM_NAME=Craftvia
MAIL_REPLY_TO=
# Basis für absolute Links in Mails/PDFs; leer = AUTH_URL.
APP_BASE_URL=
# --- KI: Auftragsimport-Extraktion & Lotse (Anthropic, optional) ---
# Ohne ANTHROPIC_API_KEY: manuelle Erfassung, kein Lotse-Entwurf.
AI_EXTRACTION_PROVIDER=anthropic
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=
# --- KI: Transkription (Whisper-kompatibel, optional) ---
TRANSCRIPTION_PROVIDER=openai-compatible
TRANSCRIPTION_API_URL=https://api.openai.com/v1/audio/transcriptions
TRANSCRIPTION_API_KEY=
TRANSCRIPTION_MODEL=whisper-1
# --- KI: Kostenbremse & Aufbewahrung KI-Protokoll ---
# Tokens je Mandant je Kalendermonat (ein+aus), 0 = unbegrenzt.
AI_MONTHLY_TOKEN_LIMIT=0
# --- Craftvia: Testphase (L15, docs/craftvia/TESTPHASE.md) ---
# Längste selbst gewählte Testphase in Tagen (1–365); Vorbelegung im Wizard heute + 14.
TRIAL_MAX_DAYS=30
# Kontakt in Banner und Testphasen-Mails (Vollversion/Verlängerung); leer = allgemeiner Hinweis.
TRIAL_CONTACT_EMAIL=
# Ein-/Ausgaben im KI-Protokoll (AiGeneration) nach N Tagen leeren/pseudonymisieren.
AI_GENERATION_RETENTION_DAYS=180
# --- Craftvia: API-Rate-Limits (je Nutzer/Minute) ---
API_RATE_LIMIT_PER_MINUTE=300
# /api/v1/sync, /api/v1/uploads, /api/v1/field/**
API_FIELD_RATE_LIMIT_PER_MINUTE=1200
# --- Craftvia: Offline/PWA & Malware-Scan ---
OFFLINE_MAX_DAYS=7
CLAMAV_HOST=
CLAMAV_PORT=3310
+90 -18
View File
@@ -1,21 +1,21 @@
# --- Datenbank ---
POSTGRES_USER=isms
POSTGRES_PASSWORD=isms
POSTGRES_DB=isms
DATABASE_URL=postgresql://isms:isms@localhost:5432/isms?schema=public
POSTGRES_USER=craftvia
POSTGRES_PASSWORD=craftvia
POSTGRES_DB=craftvia
DATABASE_URL=postgresql://craftvia:craftvia@localhost:5432/craftvia?schema=public
# --- Row Level Security (F-04) — lokal AUS (Owner-Betrieb) ---
# Lokal bleibt RLS aus: Der Owner isms ist Superuser/BYPASSRLS und sieht trotz
# --- Row Level Security (F-04) –€” lokal AUS (Owner-Betrieb) ---
# Lokal bleibt RLS aus: Der Owner craftvia ist Superuser/BYPASSRLS und sieht trotz
# FORCE alle Daten. Zum scharfen Testen die beiden Zeilen aktivieren (die Rolle
# isms_app zuvor mit LOGIN+Passwort versehen, s. scripts/test-rls-enforcement.ts):
# craftvia_app zuvor mit LOGIN+Passwort versehen, s. scripts/test-rls-enforcement.ts):
# RLS_ENFORCED=true
# RLS_DATABASE_URL=postgresql://isms_app:isms_app_local@localhost:5432/isms?schema=public
# RLS_DATABASE_URL=postgresql://craftvia_app:craftvia_app_local@localhost:5432/craftvia?schema=public
# --- Redis (BullMQ) ---
# F-18: Redis läuft mit requirepass. REDIS_PASSWORD muss zum docker-compose-Default
# passen; das Passwort steht zusätzlich in der REDIS_URL (redis://:<pw>@host:port).
REDIS_PASSWORD=isms-redis
REDIS_URL=redis://:isms-redis@localhost:6379
# F-18: Redis läuft mit requirepass. REDIS_PASSWORD muss zum docker-compose-Default
# passen; das Passwort steht zusätzlich in der REDIS_URL (redis://:<pw>@host:port).
REDIS_PASSWORD=craftvia-redis
REDIS_URL=redis://:craftvia-redis@localhost:6379
# --- Objektspeicher (Garage / S3-kompatibel) ---
# Sind alle vier S3_*-Pflichtvariablen gesetzt, nutzt der Storage-Adapter das echte
@@ -28,7 +28,7 @@ REDIS_URL=redis://:isms-redis@localhost:6379
S3_ENDPOINT=http://localhost:3900
S3_ACCESS_KEY=GKdead0000beef0000cafe0000
S3_SECRET_KEY=0000000000000000000000000000000000000000000000000000000000000001
S3_BUCKET=isms-documents
S3_BUCKET=craftvia-documents
# Region beidseitig us-east-1 (deckt sich mit garage.toml s3_region). forcePathStyle ist
# im Code fest gesetzt - Garage bedient ausschliesslich Path-Style.
S3_REGION=us-east-1
@@ -61,9 +61,9 @@ AUTH_URL=http://localhost:3000
AI_PROVIDER=anthropic
AI_API_KEY=
# --- Anthropic (KI-Entwurf Control-Beschreibungen, Audit-Vorbereitung) ---
# --- Anthropic (Lotse KI-Assistent, Auftragsimport) ---
# Ohne ANTHROPIC_API_KEY ist die KI-Anbindung deaktiviert (graceful degradation):
# kein Entwurf, Status bleibt „manuell zu erfassen", die UI funktioniert weiter.
# kein Entwurf, Status bleibt –€žmanuell zu erfassen", die UI funktioniert weiter.
ANTHROPIC_API_KEY=
# Optionale Modell-ID; Default: claude-opus-5
ANTHROPIC_MODEL=claude-opus-5
@@ -71,7 +71,7 @@ ANTHROPIC_MODEL=claude-opus-5
# --- E-Mail / SMTP (SEC1) ---
# Dev: Mailpit oder Mailhog (docker-compose Service `mailhog`, SMTP auf 1025,
# Weboberflaeche auf 8025). Gegen localhost wird TLS bewusst nicht erzwungen.
# Prod: dedizierte Absenderdomain mit SPF, DKIM und DMARC — Vorbedingung vor
# Prod: dedizierte Absenderdomain mit SPF, DKIM und DMARC –€” Vorbedingung vor
# dem ersten Produktivversand. Secrets ausschliesslich aus dem Secret-Store.
SMTP_HOST=localhost
SMTP_PORT=1025
@@ -82,9 +82,81 @@ SMTP_USER=
SMTP_PASSWORD=
# Absenderadresse und Anzeigename. MAIL_FROM/MAIL_FROM_NAME werden als Alias
# ebenfalls akzeptiert.
SMTP_FROM=no-reply@certvia.de
MAIL_FROM_NAME=Certvia
SMTP_FROM=no-reply@craftvia.de
MAIL_FROM_NAME=Craftvia
# Optionale Antwortadresse (sonst keine Reply-To-Kopfzeile).
MAIL_REPLY_TO=
# Basis fuer absolute Links in Mails. Faellt auf AUTH_URL zurueck.
APP_BASE_URL=http://localhost:3000
# --- Craftvia: KI-Extraktion (Auftragsimport) & Lotse ---
# Nutzt ANTHROPIC_API_KEY / ANTHROPIC_MODEL (s. oben). Ohne Key: manuelle Erfassung.
AI_EXTRACTION_PROVIDER=anthropic
# --- Craftvia: Transkription von Sprachnotizen (Whisper-kompatible API) ---
# Ohne TRANSCRIPTION_API_KEY bleibt die Transkription deaktiviert (Status "disabled").
TRANSCRIPTION_PROVIDER=openai-compatible
TRANSCRIPTION_API_URL=https://api.openai.com/v1/audio/transcriptions
TRANSCRIPTION_API_KEY=
TRANSCRIPTION_MODEL=whisper-1
# --- Craftvia: optionaler Malware-Scan für Uploads (ClamAV clamd) ---
# Leer = nur Allowlist/Magic-Byte-Prüfung; gesetzt = zusätzlich clamd INSTREAM.
CLAMAV_HOST=
CLAMAV_PORT=3310
# --- Craftvia: Berichts-PDF (Worker, playwright-core + Chromium) ---
# Leer = Playwright-Chromium bzw. lokal installiertes Google Chrome (Entwicklerrechner).
# Im Docker-Worker-Image fest /usr/bin/chromium.
PDF_CHROMIUM_PATH=
# --- Craftvia: Offline/PWA ---
# Ab wie vielen Tagen ein lokal gespeichertes Auftragsbundle als veraltet gilt (1–365).
OFFLINE_MAX_DAYS=7
# --- Craftvia: Rate Limits der REST-API (je Nutzer, Anfragen pro Minute) ---
# Allgemein für /api/v1/**.
API_RATE_LIMIT_PER_MINUTE=300
# Einsatz-/Sync-Endpunkte (/api/v1/sync, /api/v1/uploads, /api/v1/field/**) – höher, weil
# die PWA nach Offline-Phasen Outbox und Fotos in Schüben nachsendet.
API_FIELD_RATE_LIMIT_PER_MINUTE=1200
# --- Craftvia: KI-Protokoll & Kostenbremse ---
# Nach N Tagen leert/pseudonymisiert ein Worker-Job Ein-/Ausgaben im KI-Protokoll
# (AiGeneration); Metadaten (Art, Modell, Tokens, Zeitpunkt) bleiben erhalten.
AI_GENERATION_RETENTION_DAYS=180
# Tokens (ein + aus) je Mandant je Kalendermonat; darüber lehnen Lotse und
# Import-Extraktion ab. 0 = unbegrenzt.
AI_MONTHLY_TOKEN_LIMIT=0
# --- Craftvia: Testphase (L15, docs/craftvia/TESTPHASE.md) ---
# Längste selbst gewählte Testphase in Tagen (1–365); Vorbelegung im Wizard heute + 14.
TRIAL_MAX_DAYS=30
# Kontakt in Banner und Testphasen-Mails (Vollversion/Verlängerung); leer = allgemeiner Hinweis.
TRIAL_CONTACT_EMAIL=
# --- Craftvia: Planung – Karten & Geocoding (L13) ---
# Adresse → Koordinaten nur serverseitig im Worker (Job geocode-site, max. 1 Anfrage/s),
# Ergebnis wird am Objekt gespeichert. nominatim | none (none = keine Verortung, Objekte
# erscheinen „ohne Ortsangabe“). Für Produktion eigenen/vertraglichen Dienst verwenden.
GEOCODING_PROVIDER=nominatim
# GEOCODING_URL=https://nominatim.openstreetmap.org
# Eindeutiger User-Agent (Nominatim-Richtlinie); leer = "Craftvia/<version> (+APP_BASE_URL)".
# GEOCODING_USER_AGENT=
# Kartenkacheln der Live-Lage; der Host wird beim Build in die CSP (img-src) übernommen.
# MAP_TILE_URL=https://tile.openstreetmap.org/{z}/{x}/{y}.png
# MAP_ATTRIBUTION=© OpenStreetMap-Mitwirkende
# --- Optionale Fundament-Variablen (Default leer) ---
# MFA_ENC_KEY: Schlüssel für TOTP-Secrets at-rest (leer = aus AUTH_SECRET abgeleitet).
# ⚠ Nach dem Setzen nicht mehr ändern.
# MFA_ENC_KEY=
# BACKUP_ENC_KEY: Verschlüsselung der Backup-Artefakte (leer = AUTH_SECRET).
# BACKUP_ENC_KEY=
# WebAuthn/Passkeys: Origin und RP-ID (leer = aus AUTH_URL abgeleitet).
# WEBAUTHN_ORIGIN=http://localhost:3000
# WEBAUTHN_RP_ID=localhost
# Demo-Seed-Passwort (Default Demo1234!).
# SEED_PASSWORD=
# RLS-Test hart statt Skip, wenn craftvia_app kein LOGIN hat (CI).
# RLS_TEST_REQUIRED=true
+107 -44
View File
@@ -1,76 +1,139 @@
# Referenz für die PRODUKTIV-Env-Variablen (Contabo-VPS + Coolify).
# ECHTE Secrets NUR in Coolify eintragen — diese Datei enthält nur Platzhalter.
# Unterschiede zum Testserver: HTTPS-AUTH_URL, KEIN Demo-Seed, stattdessen Bootstrap-Admin.
# Referenz für die PRODUKTIV-Env-Variablen (Coolify, docker-compose.coolify[.prebuilt].yml).
# ECHTE Secrets NUR in Coolify eintragen – diese Datei enthält nur Platzhalter.
# Unterschiede zum Testserver: HTTPS-AUTH_URL, KEIN Demo-Seed, stattdessen Bootstrap-Admin,
# RLS scharf. Betriebsdoku: docs/craftvia/DEPLOY.md
# --- Datenbank (Service "postgres") ---
POSTGRES_USER=isms
POSTGRES_USER=craftvia
POSTGRES_PASSWORD=CHANGE_ME_starkes_db_passwort
POSTGRES_DB=isms
DATABASE_URL=postgresql://isms:CHANGE_ME_starkes_db_passwort@postgres:5432/isms?schema=public
POSTGRES_DB=craftvia
DATABASE_URL=postgresql://craftvia:CHANGE_ME_starkes_db_passwort@postgres:5432/craftvia?schema=public
# --- Row Level Security scharfschalten (F-04) ---
# RLS_ENFORCED=true → die App verbindet sich als eingeschränkte Rolle isms_app
# (NOBYPASSRLS) und setzt app.tenant_id pro Transaktion; FORCE ROW LEVEL SECURITY
# macht die Policies dann scharf. Ist der Kontext nicht gesetzt, sieht isms_app
# NULL Zeilen — daher NUR mit korrekt gesetztem RLS_DATABASE_URL einschalten.
# RLS_ENFORCED=true – app und craftvia-worker verbinden sich als eingeschränkte Rolle
# craftvia_app (NOBYPASSRLS) und setzen app.tenant_id pro Transaktion; FORCE ROW LEVEL
# SECURITY macht die Policies dann scharf. Ist der Kontext nicht gesetzt, sieht craftvia_app
# NULL Zeilen – daher NUR mit korrekt gesetztem RLS_DATABASE_URL einschalten (sonst
# bricht die App beim Start bewusst ab).
# WICHTIG: Die Owner-/Migrate-Rolle in DATABASE_URL MUSS BYPASSRLS/Superuser sein
# (Migrationen, Seed und der mandantenübergreifende Login-Lookup laufen darüber),
# sonst sähe der Login keine Nutzer. isms_app in Prod EINMALIG mit LOGIN + starkem
# Passwort versehen: ALTER ROLE isms_app WITH LOGIN PASSWORD '<stark>';
# (Migrationen, Seed, Mail-/Backup-Worker und der mandantenübergreifende Login-Lookup
# laufen darüber). craftvia_app wird von der Baseline-Migration NOLOGIN angelegt und in
# Prod EINMALIG mit LOGIN + starkem Passwort versehen:
# ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<stark>';
RLS_ENFORCED=true
RLS_DATABASE_URL=postgresql://isms_app:CHANGE_ME_starkes_isms_app_passwort@postgres:5432/isms?schema=public
RLS_DATABASE_URL=postgresql://craftvia_app:CHANGE_ME_starkes_craftvia_app_passwort@postgres:5432/craftvia?schema=public
# --- Redis ---
# F-18: Redis läuft mit requirepass. REDIS_PASSWORD setzen (stark!) und identisch
# in die REDIS_URL einsetzen (redis://:<pw>@redis:6379).
# --- Redis (Service "redis") ---
# F-18: Redis läuft mit requirepass. NUR REDIS_PASSWORD setzen – REDIS_URL wird in den
# Coolify-Compose-Dateien daraus abgeleitet (redis://:${REDIS_PASSWORD}@redis:6379).
REDIS_PASSWORD=CHANGE_ME_starkes_redis_passwort
REDIS_URL=redis://:CHANGE_ME_starkes_redis_passwort@redis:6379
# --- Objektspeicher (MinIO) — S3_* muss zu MINIO_ROOT_* passen ---
S3_ENDPOINT=http://minio:9000
S3_ACCESS_KEY=isms
S3_SECRET_KEY=CHANGE_ME_starkes_minio_passwort
S3_BUCKET=isms-documents
MINIO_ROOT_USER=isms
MINIO_ROOT_PASSWORD=CHANGE_ME_starkes_minio_passwort
# --- Objektspeicher (Service "garage", S3-kompatibel) ---
# Bucket/Key legt der Init-Job "garage-provision" an (Admin-API). Format erzwungen:
# S3_ACCESS_KEY = "GK" + 24 Hex -> echo "GK$(openssl rand -hex 12)"
# S3_SECRET_KEY = 64 Hex -> openssl rand -hex 32
S3_ENDPOINT=http://garage:3900
S3_ACCESS_KEY=GK000000000000000000000000
S3_SECRET_KEY=CHANGE_ME_openssl_rand_hex_32
S3_BUCKET=craftvia-documents
S3_REGION=us-east-1
# Garage-Daemon-Secrets (LITERAL in Coolify setzen, nicht via ${...}).
GARAGE_RPC_SECRET=CHANGE_ME_openssl_rand_hex_32
GARAGE_ADMIN_TOKEN=CHANGE_ME_openssl_rand_hex_32
# GARAGE_ZONE=dc1
# GARAGE_CAPACITY_BYTES=100000000000
# --- Backup-Zielspeicher (optional) ---
# Ziel der Backup-/DSGVO-Artefakte ist im Betreiber-Portal (/admin/backup) waehlbar
# (Lokal/S3) und wird verschluesselt in der DB gehalten. Praezedenz: DB-Config →
# Env (S3_*/BACKUP_LOCAL_DIR) → lokaler Default. Sobald im Portal gespeichert, hat
# die DB-Config Vorrang. Fuer „Lokal" auf ein gemountetes, persistentes Volume zeigen.
# Env (S3_*/BACKUP_LOCAL_DIR) → lokaler Default. Fuer „Lokal" mountet die Compose-Datei
# das persistente Volume `backups` auf /app/.backups (app + backup-worker).
BACKUP_LOCAL_DIR=/app/.backups
# Optionaler eigener Backup-Bucket (nur wenn der Backup-Store auf S3 laeuft).
# BACKUP_S3_BUCKET=
# Verschluesselung der Backup-Artefakte (AES-256-GCM); leer = AUTH_SECRET. Je Umgebung
# eigener Wert, alte Keys bis Retention-Ende aufbewahren.
BACKUP_ENC_KEY=CHANGE_ME_openssl_rand_hex_32
# --- Auth (NextAuth) — Produktiv über HTTPS ---
# --- Auth (Auth.js v5) – Produktiv über HTTPS ---
# AUTH_SECRET: openssl rand -base64 32 (frisch, NICHT der Testwert)
AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32
# PASSWORD_PEPPER (Härtung §1): openssl rand -hex 32 — frisch je Umgebung, NICHT rotierbar, nie ins Artefakt.
PASSWORD_PEPPER=CHANGE_ME_openssl_rand_hex_32
AUTH_URL=https://app.certvia.de
# AUTH_TRUST_HOST ist im Compose fest auf true (hinter dem Coolify-Proxy) — nicht nötig.
# MFA_ENC_KEY: TOTP-Secrets at-rest (leer = aus AUTH_SECRET). ⚠ Nach dem Setzen nicht mehr ändern.
MFA_ENC_KEY=CHANGE_ME_openssl_rand_hex_32
AUTH_URL=https://app.craftvia.example
# AUTH_TRUST_HOST ist im Compose fest auf true (hinter dem Coolify-Proxy) – nicht nötig.
# Passkeys/WebAuthn: leer = aus AUTH_URL abgeleitet.
# WEBAUTHN_ORIGIN=https://app.craftvia.example
# WEBAUTHN_RP_ID=app.craftvia.example
# --- KI-Provider (optional) ---
AI_PROVIDER=anthropic
AI_API_KEY=
# --- E-Mail (produktives SMTP-Relay, sobald Einladungs-/Mailflow aktiv) ---
# --- E-Mail (produktives SMTP-Relay; SPF/DKIM/DMARC der Absenderdomain vorher einrichten) ---
SMTP_HOST=
SMTP_PORT=587
# true = implizites TLS (465), false = STARTTLS (587); leer = aus Port abgeleitet.
SMTP_SECURE=
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=noreply@certvia.de
SMTP_FROM=no-reply@craftvia.example
MAIL_FROM_NAME=Craftvia
MAIL_REPLY_TO=
# Basis für absolute Links in Mails/PDFs; leer = AUTH_URL.
APP_BASE_URL=https://app.craftvia.example
# --- KI: Auftragsimport-Extraktion & Lotse (Anthropic) ---
# Ohne ANTHROPIC_API_KEY: graceful degradation (manuelle Erfassung, kein Lotse-Entwurf).
AI_EXTRACTION_PROVIDER=anthropic
ANTHROPIC_API_KEY=
# Leer = Default-Modell aus src/server/ai/client.ts
ANTHROPIC_MODEL=
# --- KI: Transkription von Sprachnotizen (Whisper-kompatible API) ---
# Ohne TRANSCRIPTION_API_KEY bleibt die Transkription deaktiviert (Status "disabled").
TRANSCRIPTION_PROVIDER=openai-compatible
TRANSCRIPTION_API_URL=https://api.openai.com/v1/audio/transcriptions
TRANSCRIPTION_API_KEY=
TRANSCRIPTION_MODEL=whisper-1
# --- KI: Kostenbremse & Aufbewahrung ---
# Tokens (ein + aus) je Mandant je Kalendermonat; darüber lehnen Lotse und
# Import-Extraktion ab. 0 = unbegrenzt.
AI_MONTHLY_TOKEN_LIMIT=0
# --- Craftvia: Testphase (L15, docs/craftvia/TESTPHASE.md) ---
# Längste selbst gewählte Testphase in Tagen (1–365); Vorbelegung im Wizard heute + 14.
TRIAL_MAX_DAYS=30
# Kontakt in Banner und Testphasen-Mails (Vollversion/Verlängerung); leer = allgemeiner Hinweis.
TRIAL_CONTACT_EMAIL=
# Nach N Tagen leert/pseudonymisiert ein Worker-Job Ein-/Ausgaben im KI-Protokoll
# (AiGeneration). Frist mit dem DSB abstimmen.
AI_GENERATION_RETENTION_DAYS=180
# --- Craftvia: API-Rate-Limits (je Nutzer, Anfragen pro Minute) ---
API_RATE_LIMIT_PER_MINUTE=300
# /api/v1/sync, /api/v1/uploads, /api/v1/field/**
API_FIELD_RATE_LIMIT_PER_MINUTE=1200
# --- Craftvia: Offline/PWA ---
OFFLINE_MAX_DAYS=7
# --- Craftvia: optionaler Malware-Scan (ClamAV clamd) ---
CLAMAV_HOST=
CLAMAV_PORT=3310
# PDF_CHROMIUM_PATH ist im Worker-Image/Compose fest /usr/bin/chromium – nicht setzen.
# --- Demo-Seed: in PROD AUS lassen! ---
RUN_DEMO_SEED=false
# --- Erst-Superadmin-Bootstrap (statt Demo-Seed) ---
# Beim ersten Deploy true setzen -> migrate-Job legt Admin + Mandant an (idempotent).
# Danach kann true bleiben (tut nichts, wenn der Admin existiert) oder auf false.
# --- Erst-Admin-Bootstrap (statt Demo-Seed) ---
# Beim ersten Deploy true setzen -> migrate-Job legt Plattform-Admin + ersten Mandanten an
# (idempotent). Danach auf false setzen oder stehen lassen (No-op, wenn vorhanden).
BOOTSTRAP_ADMIN=true
BOOTSTRAP_ADMIN_EMAIL=admin@certvia.de
BOOTSTRAP_ADMIN_EMAIL=admin@craftvia.example
BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_initiales_admin_passwort
BOOTSTRAP_ADMIN_NAME=Certvia Admin
BOOTSTRAP_TENANT_NAME=Certvia
BOOTSTRAP_TENANT_SLUG=certvia
BOOTSTRAP_TENANT_SHORT=Certvia
BOOTSTRAP_ADMIN_NAME=Craftvia Admin
BOOTSTRAP_TENANT_NAME=Musterbetrieb GmbH
BOOTSTRAP_TENANT_SLUG=musterbetrieb
BOOTSTRAP_TENANT_SHORT=Musterbetrieb
BOOTSTRAP_TENANT_SECTOR=
+60 -8
View File
@@ -1,6 +1,6 @@
# CI-Pipeline (Gitea Actions — GitHub-Actions-kompatibel).
#
# WICHTIG: Braucht einen aktivierten Gitea-Actions-Runner (git.certvia.de ->
# WICHTIG: Braucht einen aktivierten Gitea-Actions-Runner (Gitea ->
# Settings -> Actions -> Runners). Ohne registrierten Runner wird dieser Workflow
# NICHT ausgeführt (er schlägt nicht fehl, er läuft schlicht nicht an).
# Inhaltlich identisch zu .github/workflows/ci.yml (Spiegel für Nicht-Gitea-Remotes).
@@ -13,8 +13,50 @@ on:
branches: ["main", "dev", "dev-*"]
jobs:
build-and-check:
# Vollständiges Qualitäts-Gate (= `npm run gate`) gegen echte Infrastruktur:
# Postgres 16 mit pgvector + Redis als Service-Container. Garage/S3 wird bewusst
# weggelassen: der Storage-Adapter fällt ohne S3_* auf den Stub zurück, S3-abhängige
# Prüfungen (test-garage-storage, Byte-Abruf in test-einsatz-sync/test-berichte-pdf)
# überspringen sich. Ohne SMTP_HOST überspringt test-mail den echten Versand, ohne
# ANTHROPIC_API_KEY/TRANSCRIPTION_API_KEY die Live-KI-Tests. test-berichte-pdf
# überspringt sich, wenn im Runner-Image kein Chromium/Chrome startbar ist.
gate:
runs-on: ubuntu-latest
timeout-minutes: 60
services:
postgres:
image: pgvector/pgvector:0.8.0-pg16
env:
POSTGRES_USER: craftvia
POSTGRES_PASSWORD: craftvia
POSTGRES_DB: craftvia
options: >-
--health-cmd "pg_isready -U craftvia"
--health-interval 5s
--health-timeout 5s
--health-retries 20
redis:
image: redis:7.4.2-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 20
# Nur CI-Dummywerte (keine echten Secrets). Gitea act_runner führt den Job in einem
# Container im selben Netz wie die Services aus → Hostnamen = Service-Namen
# (postgres/redis), NICHT localhost. Läuft der Runner im Host-Modus
# (Label ubuntu-latest:host), Hosts auf localhost umstellen und ports: ergänzen.
env:
DATABASE_URL: "postgresql://craftvia:craftvia@postgres:5432/craftvia?schema=public"
# RLS-Test (scripts/test-rls-enforcement.ts): RLS_ENFORCED bleibt aus, der Test schaltet
# selbst scharf. RLS_TEST_REQUIRED=true macht aus dem Skip einen harten Fehler.
RLS_DATABASE_URL: "postgresql://craftvia_app:craftvia_app_ci@postgres:5432/craftvia?schema=public"
RLS_TEST_REQUIRED: "true"
REDIS_URL: "redis://redis:6379"
AUTH_SECRET: "ci-dummy-auth-secret-0000000000000000"
AUTH_URL: "http://localhost:3000"
APP_BASE_URL: "http://localhost:3000"
PASSWORD_PEPPER: "0000000000000000000000000000000000000000000000000000000000000abc"
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -34,11 +76,20 @@ jobs:
run: npm ci --include=optional --no-audit --no-fund
- name: Prisma Client generieren
# Platzhalter-URL nur fürs Laden von prisma.config.ts — keine echte DB-Verbindung.
env:
DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public"
run: npx prisma generate
- name: Migrationen anwenden
run: npx prisma migrate deploy
- name: Demo-Seed
run: npx prisma db seed
# Die Baseline-Migration legt craftvia_app NOLOGIN an; das Passwort ist ein Betriebs-
# Secret und wird nie migriert. Für den RLS-Test hier ein CI-Dummy-Passwort setzen
# (über die Prisma-Config-Datasource, damit kein psql-Client im Runner nötig ist).
- name: RLS-Rolle craftvia_app mit LOGIN versehen
run: echo "ALTER ROLE craftvia_app WITH LOGIN PASSWORD 'craftvia_app_ci';" | npx prisma db execute --stdin
- name: Typprüfung (tsc --noEmit)
run: npx tsc --noEmit
@@ -46,10 +97,11 @@ jobs:
run: npm run lint
- name: Build
env:
DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public"
run: npm run build
- name: Tests (scripts/test-*.ts)
run: npm run test
audit:
runs-on: ubuntu-latest
steps:
@@ -103,7 +155,7 @@ jobs:
# Lockfile die @swc/helpers-Inkonsistenz trägt (siehe Dockerfile / Folgeänderung
# der Dependency-Lane), bricht der Schritt mit ESBOMPROBLEMS ab — daher
# continue-on-error. Alternative ohne npm-Baum-Validierung: Syft gegen das
# gebaute Image (siehe docs/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM).
# gebaute Image (siehe docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM).
- name: SBOM erzeugen (CycloneDX)
continue-on-error: true
run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cyclonedx.json
+61 -7
View File
@@ -1,5 +1,5 @@
# CI-Pipeline — Spiegel von .gitea/workflows/ci.yml (identischer Job-Inhalt).
# Primäres Remote ist Gitea (git.certvia.de); diese Datei greift nur, falls das
# Primäres Remote ist Gitea (Gitea); diese Datei greift nur, falls das
# Repo (auch) auf einem GitHub-Actions-Remote gespiegelt wird.
#
# WICHTIG: Braucht einen aktivierten Actions-Runner. Ohne Runner läuft der
@@ -13,8 +13,51 @@ on:
branches: ["main", "dev", "dev-*"]
jobs:
build-and-check:
# Vollständiges Qualitäts-Gate (= `npm run gate`) gegen echte Infrastruktur:
# Postgres 16 mit pgvector + Redis als Service-Container. Garage/S3 wird bewusst
# weggelassen: der Storage-Adapter fällt ohne S3_* auf den Stub zurück, S3-abhängige
# Prüfungen (test-garage-storage, Byte-Abruf in test-einsatz-sync/test-berichte-pdf)
# überspringen sich. Ohne SMTP_HOST überspringt test-mail den echten Versand, ohne
# ANTHROPIC_API_KEY/TRANSCRIPTION_API_KEY die Live-KI-Tests. test-berichte-pdf nutzt
# Google Chrome des Runners (channel "chrome") oder überspringt sich ohne Browser.
gate:
runs-on: ubuntu-latest
timeout-minutes: 60
services:
postgres:
image: pgvector/pgvector:0.8.0-pg16
env:
POSTGRES_USER: craftvia
POSTGRES_PASSWORD: craftvia
POSTGRES_DB: craftvia
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U craftvia"
--health-interval 5s
--health-timeout 5s
--health-retries 20
redis:
image: redis:7.4.2-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 20
# Nur CI-Dummywerte (keine echten Secrets). GitHub-Hosted-Runner: Service-Ports auf localhost.
env:
DATABASE_URL: "postgresql://craftvia:craftvia@localhost:5432/craftvia?schema=public"
# RLS-Test (scripts/test-rls-enforcement.ts): RLS_ENFORCED bleibt aus, der Test schaltet
# selbst scharf. RLS_TEST_REQUIRED=true macht aus dem Skip einen harten Fehler.
RLS_DATABASE_URL: "postgresql://craftvia_app:craftvia_app_ci@localhost:5432/craftvia?schema=public"
RLS_TEST_REQUIRED: "true"
REDIS_URL: "redis://localhost:6379"
AUTH_SECRET: "ci-dummy-auth-secret-0000000000000000"
AUTH_URL: "http://localhost:3000"
APP_BASE_URL: "http://localhost:3000"
PASSWORD_PEPPER: "0000000000000000000000000000000000000000000000000000000000000abc"
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -34,10 +77,20 @@ jobs:
run: npm ci --include=optional --no-audit --no-fund
- name: Prisma Client generieren
env:
DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public"
run: npx prisma generate
- name: Migrationen anwenden
run: npx prisma migrate deploy
- name: Demo-Seed
run: npx prisma db seed
# Die Baseline-Migration legt craftvia_app NOLOGIN an; das Passwort ist ein Betriebs-
# Secret und wird nie migriert. Für den RLS-Test hier ein CI-Dummy-Passwort setzen
# (über die Prisma-Config-Datasource, damit kein psql-Client nötig ist).
- name: RLS-Rolle craftvia_app mit LOGIN versehen
run: echo "ALTER ROLE craftvia_app WITH LOGIN PASSWORD 'craftvia_app_ci';" | npx prisma db execute --stdin
- name: Typprüfung (tsc --noEmit)
run: npx tsc --noEmit
@@ -45,10 +98,11 @@ jobs:
run: npm run lint
- name: Build
env:
DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public"
run: npm run build
- name: Tests (scripts/test-*.ts)
run: npm run test
audit:
runs-on: ubuntu-latest
steps:
@@ -100,7 +154,7 @@ jobs:
# Lockfile die @swc/helpers-Inkonsistenz trägt (siehe Dockerfile / Folgeänderung
# der Dependency-Lane), bricht der Schritt mit ESBOMPROBLEMS ab — daher
# continue-on-error. Alternative ohne npm-Baum-Validierung: Syft gegen das
# gebaute Image (siehe docs/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM).
# gebaute Image (siehe docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM).
- name: SBOM erzeugen (CycloneDX)
continue-on-error: true
run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cyclonedx.json
+69 -18
View File
@@ -4,37 +4,88 @@
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
# ISMS-Tool — Projektregeln
# Craftvia — Projektregeln
Multi-Tenant-SaaS für Informationssicherheits-Managementsysteme (ISO/IEC 27001:2022, TISAX/VDA-ISA 6.0).
Mandantenfähige Einsatz-PWA für Handwerks- und Montagebetriebe: Kunden, Objekte, Teams,
Aufträge (inkl. PDF-Import), mobile Einsatzbearbeitung, Berichte, Notdienst, Dokumente,
KI-Assistent „Lotse".
**Maßgebliche Spezifikation: [docs/SPEC.md](docs/SPEC.md)** — Architektur, Datenmodell, Module, Rollen, Akzeptanzkriterien. UI-Referenz: [docs/ISMS-Prototyp-GEFIM.html](docs/ISMS-Prototyp-GEFIM.html) (Orientierung für Navigation/Layout; fachlich gilt die Spec, die über das Mockup hinausgeht).
**Maßgebliche Spezifikation: [docs/craftvia/SPEC-CRAFTVIA.md](docs/craftvia/SPEC-CRAFTVIA.md)**
(Rollen, Module, Abläufe, Akzeptanzkriterien) und
**[docs/craftvia/ARCHITEKTUR.md](docs/craftvia/ARCHITEKTUR.md)** (Datenmodell, Schnitte; wird
separat geliefert). Marke: [docs/craftvia/BRANDBOOK.md](docs/craftvia/BRANDBOOK.md), Umsetzung
im Code: [docs/craftvia/BRANDING.md](docs/craftvia/BRANDING.md).
## Eiserne Regeln
1. **Mandanten-Isolation:** Kein DB-Zugriff an Prisma vorbei, kein Query ohne Tenant-Kontext. Fachliche Tabellen tragen `tenant_id`; Zugriff nur über den Tenant-Guard (`src/server/db.ts`), zusätzlich Postgres RLS.
2. **RBAC serverseitig:** Jede Mutation/Query prüft Permissions am Server (`asset:read`, `risk:write`, …). UI-Ausblenden ist nur Komfort, nie Sicherheit.
3. **Audit-Log:** Jede schreibende Aktion erzeugt einen `AuditLog`-Eintrag (before/after).
4. **i18n:** Keine hartkodierten UI-Texte — alle Strings über den Message-Katalog (`de` default, `en` vorbereitet).
5. **Lizenz:** Keine ISO-Normtexte oder VDA-ISA-Kataloginhalte einchecken — nur Control-Referenzen/Titel; VDA-ISA-Inhalte kommen per Kunden-Import (`ISA6-EN.xlsx`).
1. **Mandanten-Isolation:** Kein DB-Zugriff an Prisma vorbei, kein Query ohne Tenant-Kontext.
Fachliche Tabellen tragen `tenant_id`; Zugriff nur über `dbForTenant(tenantId)`
(`src/server/db.ts`), zusätzlich Postgres RLS. Neue Tenant-Tabelle ⇒
`SELECT enable_tenant_rls('<tabelle>');` in der Migration **und** Eintrag in beiden
`TENANT_MODELS`-Listen (`src/server/db.ts`, `src/server/backup/topology.ts`) — siehe
[docs/craftvia/MIGRATIONS.md](docs/craftvia/MIGRATIONS.md).
2. **RBAC serverseitig:** Jede Mutation/Query prüft Permissions am Server
(`customer:write`, `work_order:assign`, …; Katalog in `src/server/rbac.ts`). UI-Ausblenden
ist nur Komfort, nie Sicherheit.
3. **Modul-Gating:** Routen eines Moduls haben ein `layout.tsx` mit
`await requireModule("<moduleKey>")`; Server-Actions liegen in
`src/server/actions/<moduleKey>/*.ts` und laufen über `moduleGuard("<moduleKey>")` +
`await guard(...)`. `scripts/check-module-guards.ts` erzwingt das (prebuild).
4. **Audit-Log:** Jede schreibende Aktion erzeugt einen `AuditLog`-Eintrag (before/after,
`writeAuditLog` in `src/server/audit.ts`).
5. **i18n:** Keine hartkodierten UI-Texte — alle Strings über den Message-Katalog
(`de` default, `en` gepflegt). Ein File je Namespace: `messages/<locale>/<namespace>.json`.
6. **DSGVO:** Felder, die Personen referenzieren (`User.id`), in
`src/server/dsgvo/pii-fields.ts` eintragen.
## Andockpunkte für Fachmodule
| Was | Wo |
|---|---|
| Modul-Keys | `src/lib/modules.ts` (`customers`, `sites`, `teams`, `work_orders`, `imports`, `field`, `reports`, `emergency`, `documents`, `notifications`, `lotse`) |
| Sidebar (Backoffice) | `src/lib/nav.ts` (Modul + Permission-Filter) |
| Routen-Platzhalter | `src/app/(app)/<route>/{layout,page}.tsx` — Lane ersetzt `page.tsx` |
| Server-Actions | `src/server/actions/<moduleKey>/*.ts` |
| Rollen/Permissions | `src/server/rbac.ts` (danach `scripts/sync-role-permissions.ts`) |
| Texte | `messages/de/<namespace>.json` + `messages/en/<namespace>.json` |
| Benachrichtigungen | `notifyUser()` in `src/server/mail/notifications.ts` |
| Dateien | `src/server/storage/*` (Keys mit Präfix `<tenantId>/`), Download `/files/<key>` |
| KI | `src/server/ai/client.ts` |
| Provisionierung neuer Mandanten | `src/server/provision.ts` |
## Stack & Konventionen
- Next.js (App Router) + TypeScript, Tailwind CSS 4, shadcn/ui, dnd-kit, Recharts.
- Prisma + PostgreSQL 16 (pgvector), BullMQ + Redis (Worker), MinIO (S3), Auth.js (Credentials, Argon2id).
- Tests: Vitest (Unit), Playwright (E2E). `npm run lint` und `npm run build` müssen vor jedem Commit grün sein.
- Sprache: UI und Fachbegriffe Deutsch; Code (Bezeichner, Kommentare) Englisch.
- Next.js 16 (App Router, `src/proxy.ts` statt Middleware) + TypeScript, Tailwind CSS 4,
shadcn/ui (Base UI), lucide-react, next-intl.
- Prisma 7 (`prisma.config.ts`, Adapter `@prisma/adapter-pg`) + PostgreSQL 16 (pgvector),
BullMQ + Redis (Mail-/Backup-Worker), **Garage** (S3-kompatibel) als Objektspeicher,
Auth.js v5 (Credentials, Argon2id + Pepper, TOTP/WebAuthn, getrennte Plattform-Auth).
- Tests: tsx-Skripte `scripts/test-*.ts` gegen die lokale Infra; Runner `npm run test`.
- Qualitäts-Gate vor jedem Commit: `npm run gate`
(= `prisma generate && tsc --noEmit && lint && build && test`).
- Sprache: UI und Fachbegriffe Deutsch; Code (Bezeichner) Englisch, Kommentare Deutsch
oder Englisch.
## Offene Härtungspunkte (Iteration 8)
## Rollen (Spec §4)
- RLS scharfschalten: App-Verbindung auf DB-Rolle `isms_app` umstellen und `app.tenant_id` pro Transaktion setzen (Policies existieren bereits, siehe Migration `row_level_security`).
- TOTP-2FA (Schema-Feld `mfa_secret` ist vorbereitet).
- Rollenänderungen wirken erst beim nächsten Login (Permissions liegen im JWT).
`tenant-admin` (Mandantenadministrator), `backoffice`, `team-lead` (Teamleiter),
`technician` (Monteur). Plattform-Administratoren sind ein getrennter Store
(`/platform/login`) ohne Zugriff auf Mandanten-Fachdaten. Rechte liegen im JWT und wirken
für Lesepfade nach erneutem Login; Mutationen prüfen autoritativ gegen die DB (`moduleGuard`).
## Entwicklung
```bash
docker compose up -d postgres redis minio # Infrastruktur
npx prisma migrate dev # Migrationen
docker compose up -d postgres redis garage # Infrastruktur (+ --profile dev für mailhog)
GARAGE_ADMIN_URL=http://localhost:3903 npx tsx scripts/garage-provision.ts # Bucket/Key (einmalig)
npx prisma migrate deploy && npx prisma db seed # Schema + Demo-Daten
npx tsx scripts/geocode-backfill.ts --tenant=demo # Koordinaten der Objekte (Karte, Empfehlungen)
npx tsx scripts/planning-demo.ts # Beispielbelegung der Plantafel (relativ zu heute)
npm run dev # App auf :3000
npm run gate # vollständiges Qualitäts-Gate
```
Demo-Logins (Passwort `Demo1234!` bzw. `SEED_PASSWORD`): `admin@demo.example`,
`backoffice@demo.example`, `teamleiter@demo.example`, `monteur@demo.example`
(Mandant „Musterbau Haustechnik GmbH"), `admin2@demo.example` (`demo2`),
`multi@demo.example` (beide Mandanten), Plattform: `platform@demo.example`.
+32 -20
View File
@@ -1,4 +1,4 @@
# ISMS-Tool — Next.js App (Multi-Stage-Build)
# Craftvia — Next.js App (Multi-Stage-Build)
#
# Base-Image: node:22-slim (Debian/glibc) statt node:22-alpine (musl).
# Begründung (F-11): Mit einem glibc-Base greifen die im Lockfile hinterlegten
@@ -57,19 +57,6 @@ COPY package.json package-lock.json prisma.config.ts tsconfig.json ./
COPY prisma ./prisma
COPY scripts ./scripts
COPY src ./src
# seed/ trägt das Richtlinien-Vorlagenpaket (mapping.json + Quelldateien), das der
# Demo-Seed (prisma/seed.ts) bzw. Bootstrap über importPolicies liest. Ohne diesen COPY
# bricht der migrate-Job mit ENOENT auf seed/isms-vorlagenpaket-v2/mapping.json ab.
# Einige Quelldateien liegen als 0600 vor → a+rX, damit der non-root app-User sie lesen kann.
COPY seed ./seed
RUN chmod -R a+rX ./seed
# docs/wizard-uebergabe/: Fachcontent-Quelldateien, aus denen der Demo-Seed den
# Risikokatalog (C4, import-risks.ts) und die Umsetzungshinweise (C6, import-hints.ts)
# liest. Nur im migrate-Image nötig — die Laufzeit/runner ruft diese Importer nicht auf
# (importManaged in provision.ts liest keine Dateien). Ohne diesen COPY bricht der
# Demo-Seed mit ENOENT auf docs/wizard-uebergabe/.../C6_Umsetzungshinweise.md ab.
COPY docs/wizard-uebergabe ./docs/wizard-uebergabe
RUN chmod -R a+rX ./docs
# Prisma-Client für Seed/Bootstrap generieren (migrate deploy selbst braucht nur schema+migrations).
ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public"
RUN npx prisma generate
@@ -94,12 +81,9 @@ RUN groupadd --system --gid 1001 app \
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
# Richtlinien-Vorlagenpaket für den Laufzeit-Import: Modul „policies" aktivieren
# (toggleTenantModule), Mandant mit Seed anlegen (createTenant), manueller Paket-Import
# (importPolicyPackageForTenant) lesen alle aus seed/isms-vorlagenpaket-v2/. Fehlt es,
# wirft die App zur Laufzeit ENOENT. a+rX wegen der 0600-Quelldateien (non-root app-User).
COPY seed ./seed
RUN chmod -R a+rX ./seed
# i18n-Kataloge (messages/<locale>/<namespace>.json) liegen über outputFileTracingIncludes
# bereits im standalone-Output; der explizite COPY hält sie auch bei Tracing-Änderungen vor.
COPY --from=builder /app/messages ./messages
# Backup-/Restore-/DSGVO-Topologie (src/server/backup/topology.ts) liest zur Laufzeit
# prisma/schema.prisma: Prisma 7 entfernt relationFromFields aus dem Laufzeit-DMMF,
# die FK-Relationen kommen daher aus der Schema-Datei. Der Inline-Export (Direkt-
@@ -119,3 +103,31 @@ CMD ["node", "server.js"]
# GARAGE_RPC_SECRET/GARAGE_ADMIN_TOKEN); nur die secret-freie Basiskonfig wird kopiert.
FROM dxflrs/garage:v1.2.0 AS garage
COPY deploy/garage.toml /etc/garage.toml
# --- Worker-Stage (Vorschlag Lane L5 Berichte): Craftvia-Job-Worker inkl. Chromium für PDF ---
# ARCHITEKTUR §1: HTML → PDF läuft über playwright-core + Chromium NUR im Worker, nie im App-Container.
# Debian-Chromium aus dem Paketspiegel statt Playwright-Download (reproduzierbar, Updates über das Base-Image);
# render.ts nutzt PDF_CHROMIUM_PATH. fonts-dejavu/-liberation als Fallback, Inter wird eingebettet (src/app/fonts).
# tsx + src/messages/prisma werden wie in der migrate-Stage zur Laufzeit gebraucht (Worker läuft über tsx).
FROM node:22.14.0-slim AS worker
WORKDIR /app
ENV NODE_ENV=production
ENV PDF_CHROMIUM_PATH=/usr/bin/chromium
RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates chromium fonts-dejavu-core fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
COPY --from=deps /app/node_modules ./node_modules
COPY package.json package-lock.json prisma.config.ts tsconfig.json ./
COPY prisma ./prisma
COPY scripts ./scripts
COPY src ./src
COPY messages ./messages
ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public"
RUN npx prisma generate
# Chromium legt beim Start ein Profil-/Crashpad-Verzeichnis unter $HOME an. /app gehört root
# → als User "app" bricht der Start mit "Failed to create headless user data directory" ab.
# Daher eigenes, beschreibbares Home-Verzeichnis für den non-root-User.
RUN groupadd --system --gid 1001 app \
&& useradd --system --uid 1001 --gid app --home-dir /home/app --create-home app
ENV HOME=/home/app
USER app
CMD ["npx", "tsx", "scripts/craftvia-worker.ts"]
+41 -28
View File
@@ -1,42 +1,55 @@
# ISMS-Tool
# Craftvia
Multi-Tenant-SaaS zum Betrieb eines Informationssicherheits-Managementsystems (ISMS), ausgerichtet auf **ISO/IEC 27001:2022** und **TISAX / VDA-ISA 6.0**.
**Handwerk. Digital auf Kurs.** — mandantenfähige Einsatz-PWA für Handwerks- und
Montagebetriebe.
📄 Vollständige Spezifikation: [docs/SPEC.md](docs/SPEC.md) · UI-Mockup: [docs/ISMS-Prototyp-GEFIM.html](docs/ISMS-Prototyp-GEFIM.html)
- Spezifikation: [docs/craftvia/SPEC-CRAFTVIA.md](docs/craftvia/SPEC-CRAFTVIA.md)
- Architektur/Datenmodell: [docs/craftvia/ARCHITEKTUR.md](docs/craftvia/ARCHITEKTUR.md) (folgt)
- Projektregeln für Entwickler und Agenten: [AGENTS.md](AGENTS.md)
- Branding: [docs/craftvia/BRANDBOOK.md](docs/craftvia/BRANDBOOK.md), [docs/craftvia/BRANDING.md](docs/craftvia/BRANDING.md)
- Migrationen & RLS: [docs/craftvia/MIGRATIONS.md](docs/craftvia/MIGRATIONS.md)
## Module
## Stand
- **Assets & BIA** — Asset-Inventar und Business Impact Analyse als gemeinsames Modul (Schutzbedarf, RTO/RPO/MTD, primäre/sekundäre Assets, zugeordnete Risiken)
- **Risikoanalyse** — 5×5-Heatmap mit Drag-and-Drop, Detailansicht mit Maßnahmen, betroffenen Assets und Rest-Risiko
- **SoA & Control-Kataloge** — ISO 27001:2022 Annex A (93 Controls), VDA-ISA-6.0-Struktur mit Reifegraden
- **Maßnahmen** — Kanban, wiederkehrende Aufgaben, Fristen und Eskalation
- **Vorfälle, Audits, Lieferanten, Nachweise, Richtlinien (KI-Assistent), RAG-Chat, Dashboards, Management-Review**
Fundament aus Certvia übernommen (Auth/Identity/MFA/WebAuthn, Mandanten, RBAC, Audit-Log,
Mail, Objektspeicher, Backup/DSGVO, Plattform-Administration, i18n). Die ISMS-Fachlichkeit
ist entfernt; die Craftvia-Fachmodule haben Routen-Platzhalter, Modul-Gates, Navigation und
Rollen/Permissions als Andockpunkte.
## Tech-Stack
## Schnellstart (lokal)
Next.js (App Router, TypeScript) · Prisma + PostgreSQL 16 (pgvector) · BullMQ + Redis · MinIO (S3) · Auth.js · Tailwind CSS + shadcn/ui · Docker Compose
## Quickstart (Entwicklung)
Voraussetzungen: Node 22, Docker.
```bash
cp .env.example .env # Werte anpassen
docker compose up -d postgres redis minio # Infrastruktur starten
npm install
npx prisma migrate dev # Datenbank migrieren
cp .env.example .env # AUTH_SECRET/PASSWORD_PEPPER erzeugen (siehe Kommentare)
npm ci
docker compose up -d postgres redis garage
GARAGE_ADMIN_URL=http://localhost:3903 npx tsx scripts/garage-provision.ts
npx prisma migrate deploy
npx prisma db seed
npx tsx scripts/geocode-backfill.ts --tenant=demo # Koordinaten für Karte/Empfehlungen (1 Anfrage/s)
npx tsx scripts/planning-demo.ts # Beispielbelegung der Plantafel (heute + 4 Werktage)
npm run dev # http://localhost:3000
```
Produktions-Stack (App + Worker + Infrastruktur):
Login: `admin@demo.example` / `Demo1234!` (weitere Demo-Nutzer siehe AGENTS.md).
```bash
docker compose up -d --build
```
## Befehle
## Projektstruktur
| Befehl | Zweck |
|---|---|
| `npm run dev` | Entwicklungsserver |
| `npm run lint` / `npx tsc --noEmit` | Lint / Typprüfung |
| `npm run build` | Produktions-Build (inkl. Modul-Guard-Check) |
| `npm run test` | alle `scripts/test-*.ts` gegen die lokale Infra |
| `npm run gate` | generate + tsc + lint + build + test |
| `npm run worker:mail` / `worker:backup` | Hintergrund-Worker |
| `npm run brand:icons` | Favicon/PWA-Icons aus dem Signet erzeugen |
```
docs/ Spezifikation (SPEC.md) und UI-Mockup
prisma/ Datenbankschema, Migrationen, Seeds
src/app/ Next.js App Router (UI + API)
src/server/ Server-Logik (Tenant-Guard, RBAC, Services)
```
## Betrieb
Container-Build über das Multi-Stage-`Dockerfile` (Targets `runner`, `migrate`, `garage`, `worker`),
Deployment mit `docker-compose.coolify.yml` bzw. `docker-compose.coolify.prebuilt.yml`.
Hinweise: [`docs/craftvia/DEPLOY.md`](docs/craftvia/DEPLOY.md) (Betrieb, Secrets, Worker, RLS, Backup),
[`docs/craftvia/API.md`](docs/craftvia/API.md) (`/api/v1`). Übernommene Certvia-Dokumente liegen in
`docs/_certvia-archiv/`.
+1 -1
View File
@@ -1,5 +1,5 @@
# Garage-Objektspeicher — Basiskonfiguration (Single-Node pro Environment).
# Gehört zur MinIO→Garage-Migration, siehe docs/KONZEPT-garage-migration.md (§5).
# Gehört zur MinIO→Garage-Migration, siehe docs/_certvia-archiv/KONZEPT-garage-migration.md (§5).
#
# WICHTIG — KEINE Secrets in dieser Datei (sie ist im Repo eingecheckt):
# rpc_secret ← wird zur Laufzeit aus GARAGE_RPC_SECRET gelesen
+94 -35
View File
@@ -1,14 +1,16 @@
# Coolify-Deployment (PREBUILT-Variante) — zieht fertige Images aus der Registry
# statt auf dem Host zu bauen (Plan B: langsamer/timeoutender Host-Build umgehen).
# Images vorher bauen+pushen: scripts/build-and-push-images.sh
# Registry/Tag via Coolify-Env: REGISTRY (Default git.certvia.de/msolarczek), IMAGE_TAG (Default main).
# Registry/Tag via Coolify-Env: REGISTRY (Default registry.example.com/craftvia), IMAGE_TAG (Default main).
# Abgeleitet von docker-compose.coolify.yml.
# Unterschiede zum lokalen Dev-Compose:
# - keine host "ports": Coolify-Proxy routet die Domain intern auf app:3000
# - Konfiguration über Coolify-Env-Variablen statt env_file: .env
# - Service "migrate": Init-Job (prisma migrate deploy + Rollen-Rechte-Sync), läuft einmalig VOR app
# - kein mailhog (Dev); Service "worker" = SEC1 Mail-Worker (BullMQ/Redis-Queue)
# In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen.
# - Service "craftvia-worker" = Craftvia-Job-Worker (Image craftvia-worker, Stage "worker")
# In Coolify als "Docker Compose Location" -> docker-compose.coolify.prebuilt.yml setzen.
# Betriebsdoku: docs/craftvia/DEPLOY.md
#
# Härtung (F-11/F-18):
# - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt.
@@ -24,8 +26,8 @@ services:
# Optionaler Demo-Seed nur, wenn RUN_DEMO_SEED=true (Testserver). Idempotent (upserts).
# In Produktion die Variable NICHT setzen; stattdessen BOOTSTRAP_ADMIN.
migrate:
image: ${REGISTRY:-git.certvia.de/msolarczek}/certvia-migrate:${IMAGE_TAG:-main}
command: sh -c "npx prisma migrate deploy && echo '>> Rollen-Rechte-Sync läuft…' && npx tsx scripts/sync-role-permissions.ts && echo '>> Vorlagen-Sync läuft…' && npx tsx scripts/sync-policy-templates.ts && if [ \"$$RUN_DEMO_SEED\" = \"true\" ]; then echo '>> Demo-Seed läuft…'; npx tsx prisma/seed.ts; fi && if [ \"$$BOOTSTRAP_ADMIN\" = \"true\" ]; then echo '>> Bootstrap-Admin läuft…'; npx tsx scripts/bootstrap-admin.ts; fi"
image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-migrate:${IMAGE_TAG:-main}
command: sh -c "npx prisma migrate deploy && echo '>> Rollen-Rechte-Sync läuft…' && npx tsx scripts/sync-role-permissions.ts && if [ \"$$RUN_DEMO_SEED\" = \"true\" ]; then echo '>> Demo-Seed läuft…'; npx tsx prisma/seed.ts; fi && if [ \"$$BOOTSTRAP_ADMIN\" = \"true\" ]; then echo '>> Bootstrap-Admin läuft…'; npx tsx scripts/bootstrap-admin.ts; fi"
environment:
DATABASE_URL: ${DATABASE_URL}
# Härtung §1: Passwort-Pepper (Argon2 `secret`). Seed/Bootstrap hashen Passwörter
@@ -59,12 +61,12 @@ services:
restart: "no"
app:
image: ${REGISTRY:-git.certvia.de/msolarczek}/certvia-app:${IMAGE_TAG:-main}
image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-app:${IMAGE_TAG:-main}
environment:
DATABASE_URL: ${DATABASE_URL}
# F-04: Row Level Security scharf. Der migrate-Job oben läuft bewusst
# weiter mit der Owner-DATABASE_URL (BYPASSRLS). Nur der App-Prozess
# verbindet als eingeschränkte Rolle isms_app (NOBYPASSRLS) über
# verbindet als eingeschränkte Rolle craftvia_app (NOBYPASSRLS) über
# RLS_DATABASE_URL und setzt app.tenant_id pro Transaktion.
RLS_ENFORCED: ${RLS_ENFORCED:-false}
RLS_DATABASE_URL: ${RLS_DATABASE_URL}
@@ -97,13 +99,36 @@ services:
# wählt bzw. als Env-Fallback. Muss auf das gemountete `backups`-Volume zeigen,
# sonst sind Sicherungen beim Redeploy flüchtig. DB-Config hat Vorrang vor dieser Var.
BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups}
AI_PROVIDER: ${AI_PROVIDER}
AI_API_KEY: ${AI_API_KEY}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_SECURE: ${SMTP_SECURE:-}
SMTP_USER: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_FROM: ${SMTP_FROM}
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-}
MAIL_REPLY_TO: ${MAIL_REPLY_TO:-}
# Basis für absolute Links (Mails, PDFs); leer = Fallback AUTH_URL.
APP_BASE_URL: ${APP_BASE_URL:-}
# Craftvia-KI (ARCHITEKTUR §4.5). Ohne ANTHROPIC_API_KEY bzw. TRANSCRIPTION_API_KEY
# graceful degradation (Status "disabled", manuelle Eingabe).
AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible}
TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-}
TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-}
TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-}
# Tokenbudget je Mandant je Kalendermonat (ein+aus); 0 = unbegrenzt.
AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0}
# Optionaler Malware-Scan der Uploads (clamd INSTREAM); leer = nur Typ-/Magic-Byte-Prüfung.
CLAMAV_HOST: ${CLAMAV_HOST:-}
CLAMAV_PORT: ${CLAMAV_PORT:-3310}
# PWA: ab wie vielen Tagen ein Offline-Bundle als veraltet gilt (1–365, Default 7).
OFFLINE_MAX_DAYS: ${OFFLINE_MAX_DAYS:-7}
# Rate Limits je Nutzer/Minute: /api/v1/** allgemein bzw. Einsatz-/Sync-Endpunkte
# (/api/v1/sync, /api/v1/uploads, /api/v1/field/**).
API_RATE_LIMIT_PER_MINUTE: ${API_RATE_LIMIT_PER_MINUTE:-300}
API_FIELD_RATE_LIMIT_PER_MINUTE: ${API_FIELD_RATE_LIMIT_PER_MINUTE:-1200}
# Persistenter lokaler Backup-Zielspeicher (überlebt Redeploys).
volumes:
- backups:/app/.backups
@@ -148,7 +173,7 @@ services:
# Owner-DATABASE_URL (kein RLS): der Worker arbeitet mandantenübergreifend.
# Im default-Netz (Egress), damit der externe SMTP-Server erreichbar ist.
worker:
image: ${REGISTRY:-git.certvia.de/msolarczek}/certvia-migrate:${IMAGE_TAG:-main}
image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-migrate:${IMAGE_TAG:-main}
command: ["npx", "tsx", "scripts/mail-worker.ts"]
environment:
DATABASE_URL: ${DATABASE_URL}
@@ -192,7 +217,7 @@ services:
# Braucht das S3/MinIO-Backup-Bucket (S3_*) und den Backup-Schlüssel (BACKUP_ENC_KEY,
# Fallback AUTH_SECRET). concurrency ist im Worker seriell (destruktiv).
backup-worker:
image: ${REGISTRY:-git.certvia.de/msolarczek}/certvia-migrate:${IMAGE_TAG:-main}
image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-migrate:${IMAGE_TAG:-main}
command: ["npx", "tsx", "scripts/backup-worker.ts"]
environment:
DATABASE_URL: ${DATABASE_URL}
@@ -238,29 +263,57 @@ services:
condition: service_completed_successfully
restart: unless-stopped
# IM-D — Inbound-Mail-Worker (E-Mail-to-Ticket). Holt Mails vom Catch-all-Postfach
# (vorfall-<token>@in.certvia.de) per IMAP ab und legt daraus Vorfälle an bzw. reiht
# unklare Mails in die Betreiber-Review. Kein Redis nötig (IMAP-Poller, keine Queue).
# Ohne INCIDENT_IMAP_* beendet sich der Prozess sauber („nicht konfiguriert").
incident-inbound-worker:
image: ${REGISTRY:-git.certvia.de/msolarczek}/certvia-migrate:${IMAGE_TAG:-main}
command: ["npx", "tsx", "scripts/incident-inbound-worker.ts"]
# Craftvia-Job-Worker (ARCHITEKTUR §4.4, scripts/craftvia-worker.ts): je BullMQ-Queue ein
# Worker für import-extraction, transcription, report-pdf, image-derivatives.
# OHNE diesen Dienst bleiben Import-Extraktion, Transkription, Berichts-PDFs und
# Bild-Derivate in der Queue liegen (die App reiht bei gesetztem REDIS_URL nur ein).
# Image craftvia-worker = Dockerfile-Stage "worker" (tsx + src + Prisma-Client +
# Debian-Chromium + Schriften) — HTML→PDF läuft NUR hier, nie in app.
# Processors greifen über dbForTenant zu → bei RLS_ENFORCED=true wie app über
# RLS_DATABASE_URL (Rolle craftvia_app). Egress (default-Netz) für Anthropic-/
# Transkriptions-API und SMTP. Kein Port, kein Traefik.
craftvia-worker:
image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-worker:${IMAGE_TAG:-main}
command: ["npx", "tsx", "scripts/craftvia-worker.ts"]
environment:
DATABASE_URL: ${DATABASE_URL}
RLS_ENFORCED: ${RLS_ENFORCED:-false}
RLS_DATABASE_URL: ${RLS_DATABASE_URL}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
AUTH_SECRET: ${AUTH_SECRET}
# Pepper wird von der Fail-Secure-Startprüfung erwartet (assertSecureEnv).
PASSWORD_PEPPER: ${PASSWORD_PEPPER}
# IMAP-Zugang des Catch-all-Postfachs. Fehlt es, beendet sich der Worker sauber.
INCIDENT_IMAP_HOST: ${INCIDENT_IMAP_HOST:-}
INCIDENT_IMAP_PORT: ${INCIDENT_IMAP_PORT:-993}
INCIDENT_IMAP_USER: ${INCIDENT_IMAP_USER:-}
INCIDENT_IMAP_PASSWORD: ${INCIDENT_IMAP_PASSWORD:-}
INCIDENT_IMAP_TLS: ${INCIDENT_IMAP_TLS:-true}
INCIDENT_IMAP_MAILBOX: ${INCIDENT_IMAP_MAILBOX:-INBOX}
INCIDENT_IMAP_POLL_MS: ${INCIDENT_IMAP_POLL_MS:-60000}
# Muss zur Catch-all-Subdomain passen (Ableitung der Intake-Adressen).
INCIDENT_INTAKE_DOMAIN: ${INCIDENT_INTAKE_DOMAIN:-in.certvia.de}
MFA_ENC_KEY: ${MFA_ENC_KEY:-}
AUTH_URL: ${AUTH_URL}
APP_BASE_URL: ${APP_BASE_URL:-}
# Objektspeicher (Garage): Import-PDFs/Sprachnotizen lesen, PDFs/Derivate schreiben.
S3_ENDPOINT: ${S3_ENDPOINT}
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET}
S3_REGION: ${S3_REGION:-us-east-1}
# KI-Provider (siehe app). Ohne Key: Jobs enden mit Status "disabled".
AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible}
TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-}
TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-}
TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-}
AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0}
# Aufbewahrung KI-Protokoll (AiGeneration): Ein-/Ausgaben älter als N Tage leeren.
AI_GENERATION_RETENTION_DAYS: ${AI_GENERATION_RETENTION_DAYS:-180}
# Chromium aus dem Debian-Paket (im Image bereits gesetzt, hier explizit).
PDF_CHROMIUM_PATH: /usr/bin/chromium
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_SECURE: ${SMTP_SECURE:-}
SMTP_USER: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_FROM: ${SMTP_FROM}
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-}
MAIL_REPLY_TO: ${MAIL_REPLY_TO:-}
# Chromium nutzt /dev/shm für Renderer-Speicher; render.ts setzt zusätzlich
# --disable-dev-shm-usage, 1 GB schützt dennoch vor Abstürzen bei Fotoberichten.
shm_size: "1gb"
networks:
- backend
- default
@@ -271,11 +324,17 @@ services:
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
cpus: "1.0"
memory: 1536M
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
garage:
condition: service_healthy
garage-provision:
condition: service_completed_successfully
migrate:
condition: service_completed_successfully
restart: unless-stopped
@@ -338,7 +397,7 @@ services:
restart: unless-stopped
# Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service
# (MinIO Community EOL/Maintenance-Mode). Konzept: docs/KONZEPT-garage-migration.md.
# (MinIO Community EOL/Maintenance-Mode). Konzept: docs/_certvia-archiv/KONZEPT-garage-migration.md.
# Buckets/Keys werden NICHT über die S3-API angelegt, sondern vom Init-Job
# "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) —
# rein clusterintern, wie minio zuvor. Version gepinnt (kein latest).
@@ -346,7 +405,7 @@ services:
# Image mit eingebackener deploy/garage.toml (Dockerfile-Stage „garage"). KEIN
# Bind-Mount der Config: Coolify legt relative Bind-Quellen sonst als Verzeichnis an
# → Garage bekäme /etc/garage.toml als Ordner („IO error: Is a directory").
image: ${REGISTRY:-git.certvia.de/msolarczek}/certvia-garage:${IMAGE_TAG:-main}
image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-garage:${IMAGE_TAG:-main}
# Secrets stehen NICHT in der eingebackenen deploy/garage.toml — der Daemon liest
# rpc_secret/admin_token aus der Env (in Coolify literal setzen, nicht via ${…}).
environment:
@@ -383,7 +442,7 @@ services:
# Läuft bei JEDEM Deploy; "already exists" = Erfolg. Ohne GARAGE_ADMIN_TOKEN No-op.
# Nutzt das schlanke "migrate"-Image (Node/tsx) — kein Garage-Binary nötig.
garage-provision:
image: ${REGISTRY:-git.certvia.de/msolarczek}/certvia-migrate:${IMAGE_TAG:-main}
image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-migrate:${IMAGE_TAG:-main}
command: ["npx", "tsx", "scripts/garage-provision.ts"]
environment:
GARAGE_ADMIN_URL: http://garage:3903
@@ -391,7 +450,7 @@ services:
# Der zu importierende Garage-Key = App-Key (App-Env und Garage synchron).
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET:-isms-documents}
S3_BUCKET: ${S3_BUCKET:-craftvia-documents}
# Optionaler separater Backup-Bucket (nur falls Backup-Store auf S3 statt lokal).
BACKUP_S3_BUCKET: ${BACKUP_S3_BUCKET:-}
# Single-Node-Layout (nominale Kapazität/Zone; für Prod ggf. anheben).
+87 -27
View File
@@ -4,7 +4,10 @@
# - Konfiguration über Coolify-Env-Variablen statt env_file: .env
# - Service "migrate": Init-Job (prisma migrate deploy + Rollen-Rechte-Sync), läuft einmalig VOR app
# - kein mailhog (Dev); Service "worker" = SEC1 Mail-Worker (BullMQ/Redis-Queue)
# - Service "craftvia-worker" = Craftvia-Job-Worker (Import-Extraktion, Transkription,
# Berichts-PDF mit Chromium, Bild-Derivate) aus der Dockerfile-Stage "worker"
# In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen.
# Betriebsdoku: docs/craftvia/DEPLOY.md
#
# Härtung (F-11/F-18):
# - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt.
@@ -23,7 +26,7 @@ services:
build:
context: .
target: migrate
command: sh -c "npx prisma migrate deploy && echo '>> Rollen-Rechte-Sync läuft…' && npx tsx scripts/sync-role-permissions.ts && echo '>> Vorlagen-Sync läuft…' && npx tsx scripts/sync-policy-templates.ts && if [ \"$$RUN_DEMO_SEED\" = \"true\" ]; then echo '>> Demo-Seed läuft…'; npx tsx prisma/seed.ts; fi && if [ \"$$BOOTSTRAP_ADMIN\" = \"true\" ]; then echo '>> Bootstrap-Admin läuft…'; npx tsx scripts/bootstrap-admin.ts; fi"
command: sh -c "npx prisma migrate deploy && echo '>> Rollen-Rechte-Sync läuft…' && npx tsx scripts/sync-role-permissions.ts && if [ \"$$RUN_DEMO_SEED\" = \"true\" ]; then echo '>> Demo-Seed läuft…'; npx tsx prisma/seed.ts; fi && if [ \"$$BOOTSTRAP_ADMIN\" = \"true\" ]; then echo '>> Bootstrap-Admin läuft…'; npx tsx scripts/bootstrap-admin.ts; fi"
environment:
DATABASE_URL: ${DATABASE_URL}
# Härtung §1: Passwort-Pepper (Argon2 `secret`). Seed/Bootstrap hashen Passwörter
@@ -64,7 +67,7 @@ services:
DATABASE_URL: ${DATABASE_URL}
# F-04: Row Level Security scharf. Der migrate-Job oben läuft bewusst
# weiter mit der Owner-DATABASE_URL (BYPASSRLS). Nur der App-Prozess
# verbindet als eingeschränkte Rolle isms_app (NOBYPASSRLS) über
# verbindet als eingeschränkte Rolle craftvia_app (NOBYPASSRLS) über
# RLS_DATABASE_URL und setzt app.tenant_id pro Transaktion.
RLS_ENFORCED: ${RLS_ENFORCED:-false}
RLS_DATABASE_URL: ${RLS_DATABASE_URL}
@@ -97,13 +100,36 @@ services:
# wählt bzw. als Env-Fallback. Muss auf das gemountete `backups`-Volume zeigen,
# sonst sind Sicherungen beim Redeploy flüchtig. DB-Config hat Vorrang vor dieser Var.
BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups}
AI_PROVIDER: ${AI_PROVIDER}
AI_API_KEY: ${AI_API_KEY}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_SECURE: ${SMTP_SECURE:-}
SMTP_USER: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_FROM: ${SMTP_FROM}
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-}
MAIL_REPLY_TO: ${MAIL_REPLY_TO:-}
# Basis für absolute Links (Mails, PDFs); leer = Fallback AUTH_URL.
APP_BASE_URL: ${APP_BASE_URL:-}
# Craftvia-KI (ARCHITEKTUR §4.5). Ohne ANTHROPIC_API_KEY bzw. TRANSCRIPTION_API_KEY
# graceful degradation (Status "disabled", manuelle Eingabe).
AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible}
TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-}
TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-}
TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-}
# Tokenbudget je Mandant je Kalendermonat (ein+aus); 0 = unbegrenzt.
AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0}
# Optionaler Malware-Scan der Uploads (clamd INSTREAM); leer = nur Typ-/Magic-Byte-Prüfung.
CLAMAV_HOST: ${CLAMAV_HOST:-}
CLAMAV_PORT: ${CLAMAV_PORT:-3310}
# PWA: ab wie vielen Tagen ein Offline-Bundle als veraltet gilt (1–365, Default 7).
OFFLINE_MAX_DAYS: ${OFFLINE_MAX_DAYS:-7}
# Rate Limits je Nutzer/Minute: /api/v1/** allgemein bzw. Einsatz-/Sync-Endpunkte
# (/api/v1/sync, /api/v1/uploads, /api/v1/field/**).
API_RATE_LIMIT_PER_MINUTE: ${API_RATE_LIMIT_PER_MINUTE:-300}
API_FIELD_RATE_LIMIT_PER_MINUTE: ${API_FIELD_RATE_LIMIT_PER_MINUTE:-1200}
# Persistenter lokaler Backup-Zielspeicher (überlebt Redeploys).
volumes:
- backups:/app/.backups
@@ -242,31 +268,59 @@ services:
condition: service_completed_successfully
restart: unless-stopped
# IM-D — Inbound-Mail-Worker (E-Mail-to-Ticket). Holt Mails vom Catch-all-Postfach
# (vorfall-<token>@in.certvia.de) per IMAP ab und legt daraus Vorfälle an bzw. reiht
# unklare Mails in die Betreiber-Review. Kein Redis nötig (IMAP-Poller, keine Queue).
# Ohne INCIDENT_IMAP_* beendet sich der Prozess sauber („nicht konfiguriert").
incident-inbound-worker:
# Craftvia-Job-Worker (ARCHITEKTUR §4.4, scripts/craftvia-worker.ts): je BullMQ-Queue ein
# Worker für import-extraction, transcription, report-pdf, image-derivatives.
# OHNE diesen Dienst bleiben Import-Extraktion, Transkription, Berichts-PDFs und
# Bild-Derivate in der Queue liegen (die App reiht bei gesetztem REDIS_URL nur ein).
# Eigene Dockerfile-Stage "worker": tsx + src + Prisma-Client + Debian-Chromium und
# Schriften (fonts-dejavu-core, fonts-liberation) — HTML→PDF läuft NUR hier, nie in app.
# Processors greifen über dbForTenant zu → bei RLS_ENFORCED=true wie app über
# RLS_DATABASE_URL (Rolle craftvia_app). Egress (default-Netz) für Anthropic-/
# Transkriptions-API und SMTP. Kein Port, kein Traefik.
craftvia-worker:
build:
context: .
target: migrate
command: ["npx", "tsx", "scripts/incident-inbound-worker.ts"]
target: worker
command: ["npx", "tsx", "scripts/craftvia-worker.ts"]
environment:
DATABASE_URL: ${DATABASE_URL}
RLS_ENFORCED: ${RLS_ENFORCED:-false}
RLS_DATABASE_URL: ${RLS_DATABASE_URL}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
AUTH_SECRET: ${AUTH_SECRET}
# Pepper wird von der Fail-Secure-Startprüfung erwartet (assertSecureEnv).
PASSWORD_PEPPER: ${PASSWORD_PEPPER}
# IMAP-Zugang des Catch-all-Postfachs. Fehlt es, beendet sich der Worker sauber.
INCIDENT_IMAP_HOST: ${INCIDENT_IMAP_HOST:-}
INCIDENT_IMAP_PORT: ${INCIDENT_IMAP_PORT:-993}
INCIDENT_IMAP_USER: ${INCIDENT_IMAP_USER:-}
INCIDENT_IMAP_PASSWORD: ${INCIDENT_IMAP_PASSWORD:-}
INCIDENT_IMAP_TLS: ${INCIDENT_IMAP_TLS:-true}
INCIDENT_IMAP_MAILBOX: ${INCIDENT_IMAP_MAILBOX:-INBOX}
INCIDENT_IMAP_POLL_MS: ${INCIDENT_IMAP_POLL_MS:-60000}
# Muss zur Catch-all-Subdomain passen (Ableitung der Intake-Adressen).
INCIDENT_INTAKE_DOMAIN: ${INCIDENT_INTAKE_DOMAIN:-in.certvia.de}
MFA_ENC_KEY: ${MFA_ENC_KEY:-}
AUTH_URL: ${AUTH_URL}
APP_BASE_URL: ${APP_BASE_URL:-}
# Objektspeicher (Garage): Import-PDFs/Sprachnotizen lesen, PDFs/Derivate schreiben.
S3_ENDPOINT: ${S3_ENDPOINT}
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET}
S3_REGION: ${S3_REGION:-us-east-1}
# KI-Provider (siehe app). Ohne Key: Jobs enden mit Status "disabled".
AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible}
TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-}
TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-}
TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-}
AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0}
# Aufbewahrung KI-Protokoll (AiGeneration): Ein-/Ausgaben älter als N Tage leeren.
AI_GENERATION_RETENTION_DAYS: ${AI_GENERATION_RETENTION_DAYS:-180}
# Chromium aus dem Debian-Paket (im Image bereits gesetzt, hier explizit).
PDF_CHROMIUM_PATH: /usr/bin/chromium
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_SECURE: ${SMTP_SECURE:-}
SMTP_USER: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_FROM: ${SMTP_FROM}
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-}
MAIL_REPLY_TO: ${MAIL_REPLY_TO:-}
# Chromium nutzt /dev/shm für Renderer-Speicher; render.ts setzt zusätzlich
# --disable-dev-shm-usage, 1 GB schützt dennoch vor Abstürzen bei Fotoberichten.
shm_size: "1gb"
networks:
- backend
- default
@@ -277,11 +331,17 @@ services:
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
cpus: "1.0"
memory: 1536M
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
garage:
condition: service_healthy
garage-provision:
condition: service_completed_successfully
migrate:
condition: service_completed_successfully
restart: unless-stopped
@@ -344,7 +404,7 @@ services:
restart: unless-stopped
# Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service
# (MinIO Community EOL/Maintenance-Mode). Konzept: docs/KONZEPT-garage-migration.md.
# (MinIO Community EOL/Maintenance-Mode). Konzept: docs/_certvia-archiv/KONZEPT-garage-migration.md.
# Buckets/Keys werden NICHT über die S3-API angelegt, sondern vom Init-Job
# "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) —
# rein clusterintern, wie minio zuvor. Version gepinnt (kein latest).
@@ -401,7 +461,7 @@ services:
# Der zu importierende Garage-Key = App-Key (App-Env und Garage synchron).
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET:-isms-documents}
S3_BUCKET: ${S3_BUCKET:-craftvia-documents}
# Optionaler separater Backup-Bucket (nur falls Backup-Store auf S3 statt lokal).
BACKUP_S3_BUCKET: ${BACKUP_S3_BUCKET:-}
# Single-Node-Layout (nominale Kapazität/Zone; für Prod ggf. anheben).
+29 -18
View File
@@ -1,12 +1,18 @@
# Lokales Dev-Compose. Härtung ggü. Backlog-Findings:
# Craftvia — lokales Dev-Compose. Härtung:
# - F-11: alle Images auf konkrete Tags gepinnt (kein :latest / rollender Tag)
# - F-18: Host-Ports NUR auf 127.0.0.1 gebunden (nicht auf allen Interfaces des
# Entwicklerrechners); Redis mit Passwort (requirepass);
# - F-18: Host-Ports NUR auf 127.0.0.1 gebunden; Redis mit Passwort (requirepass);
# security_opt no-new-privileges auf allen Services.
# Für Prod/Testserver gilt docker-compose.coolify.yml (Netzsegmentierung, Limits etc.).
#
# Build-Targets (Dockerfile ist multi-stage; die LETZTE Stage ist das Garage-Image —
# ohne explizites target würde `build: .` dieses Image bauen, das weder node noch npx hat):
# runner → Next-Standalone-App
# migrate → Node + tsx + src (Worker, Provisioning-/Seed-Jobs)
services:
app:
build: .
build:
context: .
target: runner
ports:
- "127.0.0.1:3000:3000"
env_file: .env
@@ -23,11 +29,13 @@ services:
condition: service_completed_successfully
restart: unless-stopped
# Wird in Iteration 4 aktiviert (BullMQ-Scheduler, siehe docs/SPEC.md §4.5)
# Mail-Worker (BullMQ): Zustellung + täglicher Erinnerungslauf.
worker:
profiles: ["worker"]
build: .
command: ["npm", "run", "worker"]
build:
context: .
target: migrate
command: ["npm", "run", "worker:mail"]
env_file: .env
security_opt:
- "no-new-privileges:true"
@@ -41,9 +49,9 @@ services:
postgres:
image: pgvector/pgvector:0.8.0-pg16
environment:
POSTGRES_USER: ${POSTGRES_USER:-isms}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-isms}
POSTGRES_DB: ${POSTGRES_DB:-isms}
POSTGRES_USER: ${POSTGRES_USER:-craftvia}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-craftvia}
POSTGRES_DB: ${POSTGRES_DB:-craftvia}
volumes:
- pgdata:/var/lib/postgresql/data
ports:
@@ -51,7 +59,7 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-isms}"]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-craftvia}"]
interval: 5s
timeout: 5s
retries: 10
@@ -60,7 +68,7 @@ services:
image: redis:7.4.2-alpine
# F-18: Redis mit Passwort. REDIS_URL in .env muss das Passwort tragen, z. B.
# redis://:${REDIS_PASSWORD}@localhost:6379
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:-isms-redis}"]
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:-craftvia-redis}"]
volumes:
- redisdata:/data
ports:
@@ -68,9 +76,8 @@ services:
security_opt:
- "no-new-privileges:true"
# Objektspeicher: Garage (S3-kompatibel) — ersetzt MinIO (EOL). S3-API auf 3900,
# Admin-API auf 3903 (nur lokal gebunden). Dev-Secrets kommen aus .env (siehe
# .env.example: GARAGE_RPC_SECRET/GARAGE_ADMIN_TOKEN, S3_ACCESS_KEY/S3_SECRET_KEY).
# Objektspeicher: Garage (S3-kompatibel). S3-API auf 3900, Admin-API auf 3903 (nur lokal
# gebunden). Dev-Secrets kommen aus .env (siehe .env.example).
garage:
image: dxflrs/garage:v1.2.0
environment:
@@ -93,16 +100,20 @@ services:
start_period: 5s
# Idempotenter Provisioning-Job (Layout + Bucket + Key + Rechte via Admin-API).
# Nutzt das App-Image (tsx vorhanden); läuft einmalig vor der app.
# Läuft im migrate-Target (Node + tsx vorhanden); einmalig vor der app.
# Alternative ohne Image-Build (Host-Node):
# GARAGE_ADMIN_URL=http://localhost:3903 npx tsx scripts/garage-provision.ts
garage-provision:
build: .
build:
context: .
target: migrate
command: ["npx", "tsx", "scripts/garage-provision.ts"]
environment:
GARAGE_ADMIN_URL: http://garage:3903
GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN:-0000000000000000000000000000000000000000000000000000000000000003}
S3_ACCESS_KEY: ${S3_ACCESS_KEY:-GKdead0000beef0000cafe0000}
S3_SECRET_KEY: ${S3_SECRET_KEY:-0000000000000000000000000000000000000000000000000000000000000001}
S3_BUCKET: ${S3_BUCKET:-isms-documents}
S3_BUCKET: ${S3_BUCKET:-craftvia-documents}
GARAGE_CAPACITY_BYTES: ${GARAGE_CAPACITY_BYTES:-10000000000}
security_opt:
- "no-new-privileges:true"
-149
View File
@@ -1,149 +0,0 @@
# Branding-Umstellung auf Certvia — Inventar & Umsetzung
> Branch `dev-branding-certvia` (Basis `dev`) · Quelle: `Aufgabenpaket-Branding-Certvia.md` + `Certvia-Assets/`
> Leitplanke: **GEFIM bleibt Dachmarke** („Ein Produkt von GEFIM"), nur die **Produktmarke** wird Certvia.
> Reines Branding — **keine Funktionsänderung**, kein Umbau von Logik oder Datenmodell.
---
## 1. Design-Tokens (verbindliche Referenz)
**Markenfarben** (Logo, Print, Verläufe — `--brand-*`):
| Rolle | Token | Hex |
|---|---|---|
| Violett (Marke) | `--brand-violet` | `#5d52a3` |
| Magenta (Akzent „via", Haken) | `--brand-magenta` | `#812d80` |
| Hellblau | `--brand-blue` | `#8dc4e0` |
| Anthrazit (Text/Headlines) | `--brand-anthracite` | `#3b3b3a` |
| Violett auf Dunkel | `--brand-violet-on-dark` | `#7d6fd6` |
| Magenta auf Dunkel | `--brand-magenta-on-dark` | `#d17bcf` |
**UI Dark-Theme** (unverändert — **ist** die Certvia-Produktpalette, nicht „korrigieren"):
`--ui-primary #7d6fd6` · `--ui-primary-deep #5d52a3` · `--ui-blue #8dc4e0` · `--ui-magenta #b45bb0` ·
`--bg-0 #0e1220` · `--bg-1 #141a2e` · `--elevated #1a2138` · `--panel rgba(30,38,64,.72)` ·
`--panel-brd rgba(120,135,180,.18)` · `--foreground #e8ecf7` · `--muted-foreground #8b93ad` ·
`--ok #39c07f` · `--warn #f0ad4e` · `--risk #ff6b6b` · Primär-Verlauf `linear-gradient(135deg,#5d52a3,#7d6fd6)`
· Fokus-Ring `rgba(125,111,214,.35)`.
Single Source of Truth: **`src/app/globals.css`** (CSS) und **`src/lib/brand.ts`** (JS/TS — für alles,
was CSS-Variablen nicht auflösen kann: Canvas-/PNG-Export, Metadaten, künftige Dokument-Exporte).
**Typografie:** Headlines/Wortmarke **Poppins**, Fließtext **Open Sans** — self-hosted (`src/app/fonts/`).
---
## 2. Inventar (Story S0)
### 2.1 Produktmarke → Certvia
| Datei | Fundstelle | Maßnahme | Story |
|---|---|---|---|
| `src/app/globals.css` | `--gefim-violet/-deep/-blue/-magenta`, Kommentare „GEFIM-Theme" | Umbenannt auf `--brand-*` / `--ui-*`, Kommentare auf Certvia | S1/S9 |
| `src/app/globals.css` | Flächenfarben direkt als Hex (`#0e1220`, `#141a2e`, …) | Über `--bg-0`/`--bg-1`/`--elevated` geführt, semantische Tokens referenzieren sie | S1 |
| `src/components/risk-modals.tsx` | `bg-[#0e1220]` (Rest-Risiko-Marker, Legende) | → `bg-[var(--bg-0)]` | S1 |
| `src/app/(app)/risks/page.tsx` | `bg-[#0e1220]/75`, `hover:bg-[#0e1220]` (Heatmap-Chip) | → Token `--chip-overlay` / `var(--bg-0)` | S1 |
| `src/components/dependency-graph.tsx` | `#8dc4e0`, `#ff6b6b`, `#141a2e`, `#0e1220`, `#4a5372`, `#8b93ad` | CSS-Vars wo möglich; PNG-Export nutzt `BRAND.ui` aus `src/lib/brand.ts` (html-to-image löst keine CSS-Vars auf) | S1 |
| `public/gefim-logo.png` | Produktlogo in Sidebar + Login | Verschoben nach `public/assets/logo/gefim-logo.png`, dient nur noch der **Dachmarke** | S2 |
| `src/app/(app)/layout.tsx` | Sidebar-Logo `/gefim-logo.png`, Alt-Text „GEFIM …" | `<CertviaLogo variant="lockup" theme="dark">` über den Branding-Resolver | S2/S8 |
| `src/app/login/page.tsx` | Logo `/gefim-logo.png`, Alt-Text | Certvia-Lockup + Tagline + Dachmarken-Fußnote | S2/S7 |
| `src/app/platform/login/page.tsx` | kein Logo | Certvia-Lockup + „Plattform-Administration" | S2/S7 |
| `src/app/(platform)/layout.tsx` | `ShieldCheck` + „ISMS · Plattform-Administration" | Certvia-Mark + „Certvia · Plattform-Administration" | S2/S3 |
| `src/app/layout.tsx` | `title: "ISMS-Tool"`, keine Icons/OG/Manifest | Vollständige Certvia-Metadaten inkl. Icons, Manifest, `themeColor` | S3 |
| `src/app/favicon.ico` | Next-Default-Favicon | Entfernt; Icons laufen über `public/favicon/` + `metadata.icons` | S2 |
| `messages/de.json`, `messages/en.json` | `common.appName` = „ISMS-Tool"/„ISMS Tool" | → „Certvia" (+ `appTagline`, `appByline`) | S3 |
| `src/server/mfa.ts` | TOTP-Issuer `"ISMS-Tool · Plattform"` | → `"Certvia · Plattform"` | S3 |
| `src/components/ui/button.tsx` | Kommentar „GEFIM-Verlauf" | → „Certvia-Verlauf" | S9 |
| `src/components/mockup-ui.tsx` | Kommentar-Referenz auf Mockup-Datei | Formulierung entschärft (Dateiname bleibt, s. u.) | S9 |
| `src/lib/control-titles.ts` | Kommentar „aus dem GEFIM-Mockup übernommen" | → neutral „aus dem UI-Mockup" | S9 |
| `src/app/(app)/settings/page.tsx` | Hinweis „Logo-Upload folgt in Phase 2" | Ergänzt um Certvia-Default-Aussage | S8 |
| `src/proxy.ts` | Matcher schloss `.webmanifest` nicht aus | `/site.webmanifest` lieferte die Login-HTML statt JSON → Endung ergänzt (siehe §5) | S3 |
### 2.2 Dachmarke GEFIM → bleibt bewusst erhalten
| Ort | Grund |
|---|---|
| `public/assets/logo/gefim-logo.png` | Dachmarken-Logo für „Ein Produkt von GEFIM" |
| `src/components/brand/powered-by-gefim.tsx` | Neue Komponente, die genau diesen Hinweis rendert (Login, Plattform-Login, Print-/Export-Fußzeile, Mail-Footer) |
| `docs/ISMS-*-GEFIM.html`, Verweise in `README.md`/`AGENTS.md`/`docs/SPEC.md` | Historische Mockups/Spezifikation — Projektartefakte, keine Produkt-UI |
| `prisma/seed.ts` (`ORG_NAME` „GEFIM Demo GmbH") | **Demo-Mandantendaten** (fiktiver Kunde), nicht der Produktname |
| `prisma/import-managed.ts` (`portal.gefim.example`) | Demo-Registerzeile, nicht der Produktname |
Nach der Umstellung liefert `rg -i "gefim"` ausschließlich diese Kategorien.
---
## 3. Umgesetzte Stories
| Story | Inhalt | Status |
|---|---|---|
| **S0** | Inventar (dieses Dokument) | ✅ |
| **S1** | Token-Zentralisierung `--brand-*` / `--ui-*`, `src/lib/brand.ts`, hartkodierte Farben ersetzt | ✅ |
| **S2** | Assets eingebunden, `<CertviaLogo>`, Favicon/PWA-Icons, Sidebar + beide Logins | ✅ |
| **S3** | App-Name/Meta/OG/Manifest/i18n/MFA-Issuer auf Certvia | ✅ |
| **S4** | Poppins + Open Sans self-hosted bestätigt (waren es bereits), Fallback-Stack + `swap` geprüft | ✅ |
| **S7** | Login, Plattform-Login, Change-Password, Enroll-MFA, Konto-deaktiviert, 404, 500 gebranded | ✅ |
| **S9** | Produkt-Referenzen bereinigt, Dachmarke belassen | ✅ |
| **S5** | Branding-Layer für Dokument-/Export-CD (`src/lib/brand.ts` + Print-Stylesheet) | ✅ (Andockpunkt) |
| **S8** | Branding-Resolver mit Certvia-Default, Custom überschreibt nur wenn gesetzt | ✅ |
| **S6** | Brandfähige E-Mail-Template-Basis (SMTP selbst folgt in Paket 4) | ✅ (Basis) |
---
## 4. Wo liegt was (neu)
| Pfad | Inhalt |
|---|---|
| `src/lib/brand.ts` | Marken-/UI-Farben als JS-Konstanten, Produktname/Tagline/Byline, Asset-Pfade, `resolveTenantBranding()` |
| `src/lib/document-brand.ts` | Dokument-CD (helles Theme, Kopf-/Fußzeilen-Bausteine) — Andockpunkt für DOCX/PDF |
| `src/lib/email-brand.ts` | Brandfähige HTML-/Text-Mail-Basis für Paket 4 |
| `src/components/brand/certvia-logo.tsx` | `<CertviaLogo variant="lockup\|mark" theme="light\|dark" />` als Inline-SVG |
| `src/components/brand/tenant-brand.tsx` | Mandanten-Marke mit Certvia-Fallback |
| `src/components/brand/powered-by-gefim.tsx` | Dachmarken-Hinweis inkl. GEFIM-Logo |
| `public/assets/logo/` | Certvia-SVGs/PNGs + GEFIM-Dachmarkenlogo |
| `public/favicon/`, `public/favicon.ico`, `public/site.webmanifest` | App-Icons und PWA-Manifest |
| `docs/branding/` | Aufgabenpaket + Logo-README aus dem Design-Paket |
| `src/app/globals.css` (`@media print`) | Druck-/Dokument-CD |
---
## 5. Beim Einbau aufgefallen (Abweichungen vom Aufgabenpaket)
- **Lockup-SVG trägt Poppins jetzt eingebettet.** `public/assets/logo/certvia-lockup-positiv.svg`
enthält Poppins 300 + 700 als woff2-Data-URI (≈22 KB). Damit erscheint die Wortmarke auch
außerhalb der App korrekt (Druck, Export, Mail) — der offene Punkt „Text in Pfade wandeln" aus
`docs/branding/README-Logo.md` ist damit erledigt. Geometrie und Farben sind unverändert.
In der App rendert `<CertviaLogo>` das Zeichen ohnehin inline und nutzt die App-Schrift direkt.
- **`/site.webmanifest` war nicht erreichbar.** Der Route-Gate-Matcher in `src/proxy.ts` nahm nur
`svg|png|jpg|ico` aus — für `.webmanifest` lieferte der Server die Login-Seite statt JSON, das
PWA-Manifest wäre also wirkungslos geblieben. Endung ergänzt. Bewusst **keine** Verzeichnis-
Ausnahme für `assets/`: das ist zugleich die App-Route des Asset-Inventars und bleibt hinter dem
Gate (nachgeprüft: `/assets`, `/assets/xyz` → 307 auf `/login`).
- **`src/app/favicon.ico` entfernt.** Die Datei-Konvention von Next.js und eine `public/favicon.ico`
schließen sich gegenseitig aus; die Icons laufen jetzt vollständig über `metadata.icons`.
- **404-/500-Seiten neu angelegt** (`src/app/not-found.tsx`, `src/app/error.tsx`) — es gab bisher
keine. Sie erscheinen für angemeldete Nutzer; anonyme Aufrufe fängt weiterhin das Login-Gate ab.
### Kontrast (WCAG AA, geprüft)
| Kombination | Kontrast | Bewertung |
|---|---|---|
| Magenta `#812d80` auf Weiß (Wortmarke/Print) | 8,0 : 1 | AA + AAA |
| Magenta `#d17bcf` auf Karte `#141a2e` | 6,1 : 1 | AA |
| Magenta `#d17bcf` auf Seitengrund `#0e1220` | 6,6 : 1 | AA |
---
## 6. Bewusste Abgrenzungen
- **UI-Palette unverändert.** Die Dark-Theme-Farben wurden nur konsolidiert/umbenannt, nicht geändert.
`--brand-violet` (`#5d52a3`) gilt für Logo/Print/Verläufe, `--ui-primary` (`#7d6fd6`) für Interaktion.
- **Kein Light-Mode.** Die App ist bewusst dark-only; `<CertviaLogo theme="light">` existiert für
Dokument-/Druck-/Mail-Kontexte, wird in der UI aber nicht verwendet.
- **Poppins 400** ist nicht als woff2 im Repo (vorhanden: 300/500/600/700). Aktuell nutzt nichts
Poppins 400 — Headlines laufen auf 600/700, die Wortmarke auf 300/700. Sollte 400 gebraucht werden,
muss die woff2 ergänzt werden; ein CDN-Fetch bleibt ausgeschlossen.
- **Logo-Upload je Mandant** braucht Objektspeicher (Admin Phase 2). Der Resolver ist bereits darauf
vorbereitet (`logoUrl` optional), das Feld existiert im Schema noch nicht — bewusst keine Migration.
- **DOCX/PDF-Export** existiert noch nicht. S5 liefert den Branding-Layer (Konstanten + Kopf-/Fußzeile
+ Print-CSS), an den das Export-Feature andockt.
-290
View File
@@ -1,290 +0,0 @@
# Feindesign B1 — Assessment und Readiness für zwei Frameworks
**Stand:** 2026-08-27 · **Branch:** `feature/iso27001-framework-mapping` · **Status:** Entwurf zur Abnahme
> **Ausgangslage:** AP1–AP5 sind umgesetzt. Ein ISO-Mandant hat Dokumente, SoA-Modell, Kennzahlen,
> Managementbewertung und Korrekturmaßnahmen. Was fehlt, ist die **Bewertungs- und Readiness-Sicht**:
> `maturity.ts`, `scope-filter.ts`, `assessment-level.ts` und `readiness.ts` kennen kein Framework und
> rechnen durchgängig VDA ISA — AL2/AL3, Prüfziele, Reifegrad 0–3, MUSS/SOLL.
>
> **Belegter Bruch:** `soa-context.ts:151` und `export-context.ts:49` lesen `controlAssessment`.
> **Keine einzige Stelle liest `soaEntry`.** Das in AP3 gebaute SoA-Modul ist von der Auswertung
> abgekoppelt — die Daten liegen da, die Readiness greift nicht darauf zu.
Grundlage: `docs/KONZEPT-framework-iso27001.md` (Lane 3), `docs/FRAMEWORK-MAPPING-ISO27001.md`,
`docs/UEBERGABE-framework-iso27001.md`.
---
## 1. Leitentscheidung: die Belegbasis liegt **unterhalb** der Framework-Strategie
Es werden **nicht zwei Engines** gebaut. Der Belegstatus eines Controls — ist die zuständige Richtlinie
freigegeben, existiert das Verfahren, hängt ein Asset dran, gibt es einen Wirksamkeitsnachweis — ist
eine **Tatsache über die Organisation**, keine Frage der Norm. Ob R08 freigegeben ist, ändert sich
nicht dadurch, ob ISO oder VDA ISA danach fragt.
Würde man die Belegbasis mitparallelisieren, entstünden zwei Bewertungen derselben Realität, die
auseinanderlaufen können. Im Audit steht dann „Berechtigungsvergabe: TISAX Reifegrad 3" neben
„ISO: teilweise umgesetzt" — ein Widerspruch über denselben Sachverhalt.
**Regel: eine Belegbasis, zwei Bewertungen.**
```
Schicht 3 Sichten /soa · /audit-readiness · Exporte · Wizard-Schritte → Reiter je Framework
Schicht 2 Strategie Scope · Zielwert · Bewertung · Vokabular → je Framework
Schicht 1 Belegbasis loadEvidenceResolver → ControlEvidence → GETEILT
Schicht 0 Fachdaten PolicyDocument · Evidence · Asset · Risk · Measure → GETEILT
```
Das ist derselbe Schnitt wie bei den Richtlinien: ein Umsetzungstext, zwei Anforderungssichten.
---
## 2. Was generalisiert wird (Schicht 1)
`ControlEvidence` (`src/lib/maturity.ts:54`) ist bereits **framework-neutral** — Richtlinienstatus,
Verfahrensstatus, Asset-/Risiko-Verknüpfung, operativer Wirksamkeitsnachweis. Nur der *Eingabetyp* des
Resolvers ist TISAX-spezifisch.
**Änderung:** `C5ControlSpec` (`maturity.ts:16`) wird auf ein neutrales `ControlSpec` reduziert:
```ts
export interface ControlSpec {
control: string; // "4.1.2" | "A.8.5"
title: string;
policy: string[]; // ["R08"]
verfahren: string[]; // ["VA-03"]
needsAsset: boolean;
needsRisk: boolean;
}
// C5ControlSpec extends ControlSpec { target: number } — TISAX-Zusatzfeld bleibt dort
```
`loadEvidenceResolver(db, completeControls)` nimmt künftig `ControlSpec`. **Für TISAX ändert sich
nichts** — `C5ControlSpec` erfüllt das Interface.
### ISO-Specs kommen aus dem Mapping, nicht von Hand
`mapping-iso.json` trägt je Anforderung bereits `policy` (R-Code) und `verfahren` (VA-Codes). Daraus
lässt sich der ISO-`ControlSpec` erzeugen. `needsAsset`/`needsRisk` werden über den vorhandenen
Crosswalk `ISO_TO_ISA` (`src/lib/iso-isa-crosswalk.ts`, 87 der 120) vom ISA-Spec **geerbt**; für die
33 ISO-eigenen Abschnitte werden sie explizit gesetzt — sinnvoll nur bei A.5.9 (`needsAsset`) sowie
6.1.2/6.1.3/8.2/8.3 (`needsRisk`).
**Umsetzung:** `_generate_iso.py` erzeugt zusätzlich `src/lib/control-specs-iso.ts` — analog zu
`control-titles-iso.ts` und `iso-isa-crosswalk.ts`. Damit bleibt das Mapping die einzige Quelle und
niemand pflegt eine zweite Liste.
---
## 3. Die Strategie-Schnittstelle (Schicht 2)
```ts
export type FrameworkKey = "TISAX" | "ISO_27001";
/** Bewertung eines Controls — je Framework ein eigener Typ, nie gemischt. */
export type Verdict =
| { kind: "maturity"; suggested: 0 | 1 | 2 | 3; confirmed: number | null; target: 2 | 3 }
| { kind: "status"; applicable: boolean; suggested: ImplStatus; confirmed: ImplStatus | null };
export type ImplStatus = "umgesetzt" | "teilweise" | "geplant";
export interface AssessmentRow {
control: string;
title: string;
spec: ControlSpec;
evidence: ControlEvidence; // aus Schicht 1, identisch für beide Frameworks
verdict: Verdict; // je Framework interpretiert
gaps: OpenPoint[];
}
export interface FrameworkStrategy {
key: FrameworkKey;
/** Anzeigename für Reiter und Berichte. */
label: string; // "TISAX / VDA ISA 2027" | "ISO/IEC 27001:2022"
/** Datei im Vorlagenpaket. */
mappingFile: string; // "mapping.json" | "mapping-iso.json"
/** Controls im Geltungsbereich — TISAX: AL + Prüfziele · ISO: Anwendbarkeit aus der SoA. */
controlsInScope(db: TenantDb): Promise<ControlSpec[]>;
/** Bewertung eines Controls aus geteiltem Beleg + framework-eigener Regel. */
evaluate(spec: ControlSpec, ev: ControlEvidence, ctx: EvalContext): Verdict;
/** Offene Punkte, die aus der Lücke zum Zielwert folgen. */
gaps(spec: ControlSpec, ev: ControlEvidence, v: Verdict): OpenPoint[];
/** Kennzahl und Textband — framework-eigenes Vokabular, kein gemeinsamer Nenner. */
summarise(rows: AssessmentRow[]): ReadinessSummary;
}
```
`buildControlRows` (`soa-context.ts:146`) wird zu `buildAssessment(db, tenantId, framework)` und
delegiert an die Strategie. Die Belegauflösung bleibt, wo sie ist.
### TisaxStrategy — verhaltensgleich extrahieren
Reine Umverdrahtung, **kein** neues Verhalten: `loadScopeInput` + `controlsInScope` → `controlsInScope`,
`suggestMaturity` + `targetMaturity` → `evaluate`, `openPoints` → `gaps`, `computeReadiness` →
`summarise`. Regressionsschutz: Snapshot-Test gegen die heutige Ausgabe (siehe §7).
### IsoStrategy — neu
| Aspekt | Regel |
|---|---|
| **Scope** | `SoaEntry.applicable = true`. Kein AL, keine Prüfziele. Klauseln 4–10 sind **immer** im Scope (nicht Gegenstand der Anwendbarkeit). |
| **Zielwert** | `implementationStatus = "umgesetzt"` für jedes anwendbare Control. Keine Stufung. |
| **Vorschlag** | aus `ControlEvidence`: Richtlinie *und* Verfahren `validiert` + geforderte Verknüpfungen vorhanden → `umgesetzt`; teilweise belegt → `teilweise`; sonst `geplant`. |
| **Bestätigung** | `SoaEntry.implementationStatus` ist der bestätigte Wert; der Vorschlag überschreibt ihn nie (Muster wie `ControlAssessment.suggested` / `confirmedValue`). |
| **Lücken** | fehlende Begründung, fehlender Nachweis, Status ≠ „umgesetzt" bei anwendbarem Control. |
---
## 4. Vorbelegung bei Doppel-Framework — gegen Doppelerfassung
Führt ein Mandant beide Normen, darf der ISB **nicht zweimal dasselbe beantworten**. Über
`ISO_TO_ISA` ist bekannt, welches ISA-Control denselben Bibliotheksabschnitt trägt.
**Regel:** Ist das zugehörige ISA-Control bestätigt und erreicht seinen Zielreifegrad, schlägt die
ISO-Sicht `umgesetzt` vor — mit Verweis auf denselben Nachweis. Der ISB **bestätigt**, das System
setzt nichts automatisch.
Für die 33 ISO-Anforderungen ohne ISA-Gegenstück (Managementsystem-Klauseln, Clear Desk, DLP,
Kapazität, Zeitsynchronisation …) gibt es keine Vorbelegung — sie werden regulär bewertet. Das ist
zugleich die ehrliche Aussage an den Kunden: **das ist die Delta-Arbeit, die ISO gegenüber TISAX
zusätzlich verlangt.** Diese Liste ist ein verkaufbares Ergebnis für sich.
---
## 5. Sichten und Reiter (Schicht 3)
| Bereich | Reiter ISO / TISAX | Begründung |
|---|:--:|---|
| Controls- / SoA-Sicht (`/soa`) | **ja** | zwei Kataloge, zwei Zielsysteme |
| Readiness (`/audit-readiness`) | **ja** | je Norm eine eigene Aussage und ein eigenes Vokabular |
| Exporte | **ja** | VDA-ISA-Export bleibt TISAX; SoA und Annex-A-Gap sind ISO |
| Richtlinien (`/policies`) | **nein** | ein Dokumentensatz; Parallelanzeige im Dokument ist gebaut |
| Nachweise | **nein** | ein Beleg belegt eine Tatsache, unabhängig von der fragenden Norm |
| Assets, Risiken, Vorfälle, Lieferanten, Aufgaben | **nein** | framework-neutral |
**Reiter-Verhalten:** ein aktives Framework → kein Reiter, direkte Anzeige. Zwei aktive Frameworks →
Reiter, Vorauswahl `TenantFramework.isPrimary`. Die Auswahl gehört in die URL (`?fw=iso`), damit
Deep-Links und Berichte reproduzierbar sind.
**Harte Regel: keine gemischte Gesamtzahl.** Ein „Erfüllungsgrad 87 %" über beide Normen ist fachlich
sinnlos — ein Reifegrad 0–3 und ein Umsetzungsstatus lassen sich nicht mitteln. Je Norm eine Kennzahl,
immer getrennt ausgewiesen.
---
## 6. Vokabular
`REIFEGRAD_BANDS` (`readiness.ts:16`) ist TISAX-Sprache („Assessment-reif", „AL-Ziel"). Für ISO
gehören eigene Bänder auf Basis des Umsetzungsgrads — Vorschlag:
| Anteil „umgesetzt" | Band | Aussage |
|---|---|---|
| < 50 % | Aufbau | wesentliche Maßnahmen noch offen |
| 50–79 % | In Umsetzung | Struktur steht, Nachweise fehlen |
| 80–99 % | Zertifizierungsnah | wenige offene Punkte, Wirksamkeitsnachweise ergänzen |
| 100 % | Zertifizierungsreif | alle anwendbaren Controls umgesetzt und belegt |
**Ein ISO-Auditbericht darf nicht mit „Reifegrad 2,4" argumentieren.** Der Nachweis lautet
„umgesetzt, hier ist der Beleg". Der Reifegrad bleibt unter ISO ein optionales Beratungsinstrument —
sichtbar als Zusatzspalte, nie als Erfüllungsaussage (Entscheidung aus dem Fachgespräch, Variante 3).
---
## 7. Regressionsschutz
Die Extraktion der `TisaxStrategy` ist der riskanteste Teil. Absicherung **vor** dem Umbau:
1. **Snapshot erzeugen:** `buildControlRows` für den Demo-Mandanten (321 Anforderungen, 45 Controls)
als JSON festhalten — Reifegradvorschlag, Zielwert, Belegstatus und offene Punkte je Control.
2. **Nach dem Umbau vergleichen:** `buildAssessment(db, tenantId, "TISAX")` muss denselben Snapshot
liefern. Abweichung = Regression, kein „ist besser geworden".
3. Als `scripts/test-framework-assessment.ts` in der Hausform der übrigen `test-*.ts` ablegen.
Ergänzend unverändert gültig: `_verify.py`, `_verify_iso.py --lang de|en`, `_render_diff.py HEAD`,
`test-framework-dryrun.ts`.
---
## 7a. Priorisierung nach Kundenlage (Nachtrag 2026-08-27)
**Ist-Lage:** Die Mehrzahl der Mandanten führt TISAX, **ein** Mandant ISO, **keiner beide**.
Daraus folgt zweierlei:
1. **Das Regressionsrisiko dominiert.** Der riskanteste Teil ist nicht die ISO-Logik, sondern die
verhaltensgleiche Extraktion der `TisaxStrategy` — sie betrifft alle Bestandsmandanten. Schritt 1
(Snapshot) ist damit nicht Kür, sondern die wichtigste Einzelmaßnahme des ganzen Pakets.
2. **Zwei Themen lassen sich zurückstellen** — beide betreffen ausschließlich den Doppel-Mandanten,
den es heute nicht gibt:
| Zurückgestellt | Was entfällt vorerst | Wieder aufnehmen, wenn |
|---|---|---|
| **Schritt 7** — Vorbelegung über `ISO_TO_ISA` | Übernahmevorschlag aus dem jeweils anderen Framework | ein Mandant beide Normen führt |
| **Schritt 8b** — Reiter-UI | Umschalter in `/soa` und `/audit-readiness`; bei einem aktiven Framework wird direkt angezeigt | ein Mandant beide Normen führt |
**Was dabei nicht zurückgestellt werden darf:** die **Strategie-Schnittstelle** und der
**Framework-Parameter** in `buildAssessment`. Beide kosten jetzt fast nichts und sind später teuer
nachzurüsten, weil sonst 7 Aufrufstellen erneut angefasst werden müssen. Die Architektur bleibt
zweigleisig, nur die Oberfläche zeigt vorerst ein Gleis.
Erwartete Wirkung auf den Aufwand: **5–8 PT → 4–6 PT.**
---
## 8. Arbeitsschritte
| # | Schritt | Ergebnis |
|---|---|---|
| **1** | Snapshot-Test der heutigen TISAX-Ausgabe | Regressionsnetz steht, **vor** jedem Umbau |
| **2** | `ControlSpec` einführen, `loadEvidenceResolver` darauf umstellen | Belegbasis framework-neutral, TISAX unverändert |
| **3** | `_generate_iso.py` erzeugt `control-specs-iso.ts` | ISO-Specs aus dem Mapping, keine Zweitpflege |
| **4** | `FrameworkStrategy` + `TisaxStrategy` (reine Extraktion) | Snapshot grün |
| **5** | `IsoStrategy` (Scope aus SoA, Status statt Reifegrad) | ISO-Bewertung rechnet |
| **6** | `buildAssessment(db, tenantId, framework)` ersetzt `buildControlRows` | 7 Dateien rufen es direkt, 13 hängen an der Assessment-Logik insgesamt |
| ~~**7**~~ | ~~Vorbelegung über `ISO_TO_ISA`~~ | **zurückgestellt** — betrifft nur Doppel-Mandanten (§7a) |
| **8a** | ISO-Readiness-Bänder und framework-richtige Beschriftung | ISO-Mandant sieht ISO-Vokabular |
| ~~**8b**~~ | ~~Reiter in `/soa` und `/audit-readiness`~~ | **zurückgestellt** (§7a) |
| **9** | ISO-Exporte: SoA + Annex-A-Gap | Managementbewertung hat ihre Eingabe |
Schritte 1–4 sind Pflicht und hängen aneinander. 5–9 sind danach teilbar.
---
## 9. Definition of Done
| Prüfung | Erwartung |
|---|---|
| Snapshot-Test TISAX | 0 Abweichungen zur Ausgabe vor dem Umbau |
| Reiner ISO-Mandant | Readiness und SoA rechnen; keine AL-, Prüfziel- oder Reifegrad-Begriffe in der Oberfläche |
| Reiner TISAX-Mandant | Verhalten und Beschriftung unverändert |
| Doppel-Mandant | *zurückgestellt* — Architektur trägt es, Oberfläche zeigt es noch nicht (§7a) |
| ISO-Delta | die 33 Anforderungen ohne ISA-Gegenstück sind als eigene Liste ausweisbar |
| Gate | `tsc`, `lint`, `build`, `_verify*`, `_render_diff`, beide Testskripte grün |
---
## 10. Aufwand und Risiken
**Aufwand:** 4–6 PT im vorgezogenen Umfang (§7a), 5–8 PT vollständig. Schwerpunkt liegt auf Schritt 4 (verhaltensgleiche Extraktion) und Schritt 8
(zwei Sichten statt einer), nicht auf der ISO-Logik selbst — die ist schlank.
| Risiko | Gegenmaßnahme |
|---|---|
| TISAX-Regression bei der Extraktion | Snapshot-Test **vor** Schritt 2 anlegen, als Merge-Gate setzen |
| Belegbasis wandert versehentlich in die Strategie | Review-Kriterium: `loadEvidenceResolver` darf `FrameworkKey` nicht kennen |
| Gemischte Kennzahlen schleichen sich in die UI | `ReadinessSummary` je Framework typisieren, keine Aggregation über beide |
| ISO-Scope hängt an gepflegter SoA | leere SoA → Readiness meldet „Anwendbarkeit noch nicht erklärt" statt 0 % |
| Doppelte Datenpflege beim Doppel-Mandanten | Schritt 7 ist nicht optional |
---
## 11. Was ausdrücklich **nicht** gebaut wird
- Kein Reiter über Richtlinien, Nachweisen, Assets, Risiken oder Aufgaben.
- Keine gemeinsame Kennzahl über beide Frameworks.
- Kein Reifegrad als ISO-Erfüllungsaussage (optionale Zusatzspalte ja, Bewertungsgrundlage nein).
- Keine Änderung an der VDA-ISA-Logik über die reine Extraktion hinaus.
- Kein Umbau von `readiness.ts`/`gap-consolidation.ts` — die Rechen-Engines sind numerisch
standard-agnostisch und werden von beiden Strategien gefüttert.
-150
View File
@@ -1,150 +0,0 @@
# ISO/IEC 27001:2022 auf der gemeinsamen Dokumentenbibliothek (Variante A)
**Stand:** 2026-08-27 · **Paket:** `seed/isms-vorlagenpaket-v2` (+ `-en`) · **Status:** umgesetzt, ISB-Freigabe offen
> **Entscheidung:** Ein Dokumentensatz, zwei Framework-Mappings. Die bestehenden Richtlinien und
> Verfahren bleiben die einzige Quelle; ISO/IEC 27001 wird als zweites Mapping darübergelegt.
> Damit ersetzt diese Umsetzung die ursprüngliche Annahme aus `KONZEPT-framework-iso27001.md`
> (D4: zweites Seed-Paket `seed/isms-iso27001-v1/`).
---
## 1. Warum nicht zwei Pakete
`PolicyDocument` ist über `@@unique([tenantId, code])` eindeutig. Zwei Pakete mit denselben
Dokument-Codes (`L00`, `R01`…`R14`, `VA-01`…`VA-20`) können in einem Mandanten nicht nebeneinander
existieren — der zweite Import überschreibt beim Upsert den Inhalt des ersten und archiviert die
Dokumente, die im anderen Paket fehlen. Für einen Mandanten mit ISO **und** TISAX wäre das Ergebnis
das Gegenteil der Absicht.
Hinzu kommt der fachliche Punkt: Die Umsetzungsbeschreibung („Umsetzung bei {{ORG_NAME}}") ist
normunabhängig. Wie eine Organisation Berechtigungen vergibt, ändert sich nicht dadurch, ob ISO oder
VDA ISA danach fragt. Sie zweimal zu pflegen erzeugt Widersprüche.
## 2. Aufbau
| Bestandteil | Rolle |
|---|---|
| `richtlinien/`, `verfahren/` | **gemeinsame** Dokumente — ein Umsetzungstext je Thema |
| `mapping.json` | Framework-Mapping **VDA ISA 2027** (321 Anforderungen) |
| `mapping-iso.json` | Framework-Mapping **ISO/IEC 27001:2022** (120 Anforderungen) |
| `_iso_crosswalk.json` | fachliche Zuordnung ISO-Anforderung → Dokument + Abschnitt (redaktionell) |
| `_iso_sections.json` / `_iso_sections_en.json` | Texte der ISO-only-Abschnitte (redaktionell, je Sprache) |
| `_iso_texts_en.json` | englische Anforderungstexte (Struktur bleibt im Crosswalk) |
| `_generate_iso.py` | erzeugt aus beidem die Dokument-Patches, `mapping-iso.json` und die SoA |
| `_verify.py` / `_verify_iso.py` | prüfen je eine Framework-Sicht |
| `Statement-of-Applicability-ISO.md` | SoA-Gerüst mit den Pflichtangaben aus 6.1.3 d) |
### Sichtbarkeitssteuerung über Platzhalter
Zwei neue Flags in `variables.schema.json` steuern, welche Anforderungssicht ein Mandant sieht:
| Flag | Default | Wirkung |
|---|---|---|
| `FLAG_FW_TISAX` | `true` | VDA-ISA-Anforderungsblöcke sichtbar |
| `FLAG_FW_ISO27001` | `false` | ISO-Anforderungsblöcke und ISO-only-Abschnitte sichtbar |
Im Dokument sieht das so aus — der Umsetzungstext steht **einmal** und gilt für beide:
```markdown
### 3.2 Sichere Anmeldung
*Anforderungsbezug:* {{#if FLAG_FW_TISAX}}VDA ISA 4.1.2{{/if}}{{#if FLAG_FW_ISO27001}}{{#if FLAG_FW_TISAX}} · {{/if}}ISO/IEC 27001 A.8.5{{/if}}
**Anforderung**
{{#if FLAG_FW_TISAX}}
- **[MUSS]** Verfahren zur Benutzerauthentifizierung nach dem Stand der Technik werden angewandt.
{{/if}}
{{#if FLAG_FW_ISO27001}}
- **[ISO A.8.5]** Sichere Authentisierungstechnologien und -verfahren sind einzusetzen.
{{/if}}
**Umsetzung bei {{ORG_NAME}}**
<!-- IMPL 4.1.2 -->
Die Authentifizierungsverfahren sind risikobasiert ausgewählt … Passwortvorgaben nach BL-IAM-01 …
```
Beide Mappings zeigen mit `impl_anchor` auf denselben Block `IMPL 4.1.2`.
## 3. Abdeckung
- **120 ISO-Anforderungen:** 27 Klauseln (Kap. 4–10, inkl. 6.3 aus Amd 1:2024) + 93 Anhang-A-Controls
in der Verteilung 37/8/14/34.
- **43 Abschnitte** der Bibliothek werden von beiden Frameworks genutzt.
- **19 ISO-only-Abschnitte** wurden ergänzt, wo VDA ISA kein Gegenstück hat:
| Dokument | Neue Abschnitte | ISO-Bezug |
|---|---|---|
| L00 | Politik, Ziele, Kommunikation | 5.2, 6.2, 7.4, A.5.1 |
| R01 | Kontext/Scope · Änderungsplanung · Dokumentenlenkung · Behördenkontakte | 4.1–4.4, 6.3, 7.5.1–7.5.3, A.5.5, A.5.6 |
| R02 | Datenmaskierung | A.8.11 |
| R03 | SoA · Betriebliche Planung · Messung · Managementbewertung · CAPA | 6.1.3, 8.1, 9.1, 9.3, 10.1, 10.2 |
| R05 | Vorgehen bei Verstößen | A.6.4 |
| R07 | Umgebungsschutz/Versorgung/Verkabelung/Wartung · Clear Desk | A.7.5, A.7.7, A.7.8, A.7.11–A.7.13 |
| R10 | Bedrohungsinformationen · Betriebsabläufe · Kapazität · Datenabfluss · Zeitsynchronisation | A.5.7, A.5.37, A.8.6, A.8.12, A.8.17 |
Die neuen Abschnitte sind ausschließlich über Platzhalter individualisierbar — wie die
Bestandstexte. Dafür kamen elf Variablen und acht Baseline-Parameter hinzu
(`BL-OPS-10/11/12`, `BL-NET-03`, `BL-PHY-03/04`, `BL-GOV-02/03`).
**Ohne ISO-Bezug** bleibt nur der Abschnitt „Nutzung von KI-/GenAI-Diensten" (eigene Ergänzung) sowie
die Dokumente `P01` (Prototypenschutz) und `D01` (Datenschutz-Prüfziel) — beides TISAX-spezifisch.
## 4. Änderungen am Anwendungscode
Zwei Stellen, beide klein und rückwärtskompatibel:
1. **`prisma/import-policies.ts`** — der Umsetzungstext wird jetzt über `impl_anchor` aufgelöst
(`IMPL 4.1.2`), mit Rückfall auf die Anforderungs-ID und auf ein `implementation`-Feld im Mapping.
*Nebeneffekt:* Das behebt einen Bestandsfehler. Bisher wurde `implMap.get(a.id)` gesucht — die
Anforderungs-IDs (`4.1.2-M1`) haben aber keinen eigenen IMPL-Block, sodass **alle 321**
VDA-ISA-Anforderungen mit leerem Umsetzungstext importiert wurden. Sichtbar war das u. a. in der
Spalte „Umsetzung" unter `/policies` und in der Plattform-Vorlagenverwaltung. Nach der Korrektur
bleiben 12 leere Einträge (L00 — die Leitlinie hat bewusst keine IMPL-Blöcke).
2. **`src/lib/policy-render.ts`** — `buildContext` belegt fehlende Framework-Flags vor
(`FLAG_FW_TISAX = true`, `FLAG_FW_ISO27001 = false`). Ohne das würden Bestandsmandanten, deren
Vorlagenpaket noch nicht neu importiert wurde, die VDA-ISA-Anforderungsblöcke leer sehen.
## 5. Prüfung
```bash
cd seed/isms-vorlagenpaket-v2
python3 _generate_iso.py # deutsche Fassung, idempotent
python3 _generate_iso.py --lang en # englische Fassung
python3 _verify.py # TISAX-Sicht → OK
python3 _verify_iso.py # ISO-Sicht DE → OK
python3 _verify_iso.py --lang en # ISO-Sicht EN → OK
```
`_verify_iso.py` prüft Vollständigkeit (27 + 93), Auflösbarkeit aller Anker, nicht leere
Umsetzungsblöcke, das Rendering **beider** Sichten ohne offene Platzhalter sowie die Verknüpfung zu
den Verfahren.
## 6. Was noch fehlt (Anwendungsseite)
Das Paket ist fertig; die Anwendung kennt das zweite Mapping noch nicht. Offen bleibt aus
`KONZEPT-framework-iso27001.md`:
- **Lane 1** — `Framework`-Enum, `TenantFramework`, `framework` an `PolicyTemplateVersion` und
`PolicyPackageState`. Erst damit lässt sich `mapping-iso.json` überhaupt importieren; heute liest
`parsePackageFiles` fest `mapping.json`.
- **Lane 3/4** — ISO-Assessment (Applicability statt Reifegrad) und das **SoA-Modul**. Bis dahin wird
die SoA als gelenktes Dokument geführt (`Statement-of-Applicability-ISO.md`).
- **Setzen der Flags** — beim Provisionieren eines ISO-Mandanten müssen `FLAG_FW_ISO27001 = true`
und, falls kein TISAX, `FLAG_FW_TISAX = false` gesetzt werden.
Ebenfalls offen und unabhängig davon: `REVIEW_CYCLE` wird weiterhin für fünf verschiedene Zyklen
verwendet. Die neuen Abschnitte nutzen bereits die getrennten Variablen
(`POLICY_REVIEW_CYCLE`, `MGMT_REVIEW_CYCLE`, `RISK_REVIEW_CYCLE`); die Bestandstexte umzustellen ist
eine eigene, kleine Änderung.
## 7. Urheberrechtlicher Hinweis
Die ISO-Anforderungstexte in `mapping-iso.json` und in den Dokumenten sind **eigene Paraphrasen**;
die Referenzen sind exakt, damit in der erworbenen Norm nachgeschlagen werden kann. Wörtliche
Normzitate sind bewusst unterlassen. Das VDA-ISA-Mapping übernimmt die Anforderungen laut eigenem
Hinweis in `mapping.json` „1:1 aus ISA" — auch dieser Katalog ist geschützt. Bei der weiteren Pflege
der gemeinsamen Bibliothek sollte die vorsichtigere Linie des ISO-Mappings die gemeinsame werden
(vgl. `SPEC.md` §12).
-83
View File
@@ -1,83 +0,0 @@
# Handbuch Kundenbetreuung — certvia (ISMS-Tool)
**Stand:** 2026-09-03 · **Zielgruppe:** Kundenbetreuung / Customer Success · **Zweck:** Schneller, praxisnaher Einstieg für die Betreuung von certvia-Kunden — was das Produkt kann, wie ein Kunde aufgesetzt/betreut wird und wie typische Support-Fälle gelöst werden.
> Dieses Handbuch ist bewusst **produkt- und supportorientiert**. Technische Tiefe steht in `docs/SPEC.md`, `docs/HANDOVER-PM.md` und den `docs/KONZEPT-*.md`.
---
## 1. Was ist certvia (in einem Absatz)
certvia ist ein **Multi-Mandanten-ISMS-Tool** (Informationssicherheits-Managementsystem als SaaS). Jeder Kunde ist ein **Mandant** mit strikt getrennten Daten. Das Tool führt einen Kunden vom Onboarding über Strukturanalyse (Assets/Prozesse/Lieferanten), Risikomanagement, Richtlinien und Maßnahmen bis zur **Audit-Vorbereitung** — wahlweise nach **TISAX (VDA-ISA)** und/oder **ISO 27001**.
## 2. Zugang, Rollen, Anmeldung
- **Kunden-Login:** `https://<kunde-domain>/login` — E-Mail + Passwort, danach ggf. MFA (TOTP) und, bei mehreren Mandanten, Mandantenauswahl.
- **Plattform-/Superadmin:** `…/platform/login` — **getrennter Login** für die Betreiber-Administration (Mandanten anlegen, Module, Stammdaten). MFA-Einrichtung beim ersten Login.
- **Rollen (mandantenintern):** z. B. Mandanten-Admin, ISB (Informationssicherheitsbeauftragter), Auditor, Owner, User. Rechte hängen an Rollen; der **Mandanten-Admin** verwaltet Benutzer & Rollen unter **Einstellungen → Benutzer & Rollen**.
- **Passwort/MFA:** Initial-/Reset-Passwörter erzwingen einen Wechsel beim nächsten Login. MFA und Passkeys sind an die **Person (Identity)** gebunden, nicht an den Mandanten.
## 3. Einen neuen Kunden aufsetzen (Provisionierung)
Ein Kunde wird als **Mandant** angelegt (über die Plattform-/Superadmin-Konsole bzw. beim ersten Deployment per Bootstrap-Admin). Beim Anlegen wird automatisch:
- ein **Mandanten-Admin** erzeugt,
- die **Standard-Module** aktiviert,
- das **Richtlinien-Vorlagenpaket** importiert (inkl. Anforderungen, Variablen, Nachweisregister),
- das **Framework** gesetzt (Default **TISAX**; ISO 27001 ist zusätzlich/alternativ wählbar).
Die **Stammdaten** des Mandanten (Firmenname, Kürzel, Rollen wie ISB/IT-Leitung/DSB, Scope) pflegt der Kunde unter **Einstellungen** — diese speisen automatisch die ISMS-Variablen in den Richtlinien.
## 4. Framework-Wahl: ISO 27001 und/oder TISAX
- Ein Mandant kann **ein oder beide** Frameworks führen. Der Umsetzungstext der Richtlinien ist normunabhängig; je Framework gibt es eine eigene Katalog-/Anforderungssicht.
- **TISAX (VDA-ISA):** Reifegrad-Modell, Schutzbedarf/Assessment-Level (AL2/AL3), Prüfziele (Informationssicherheit/Prototypen/Datenschutz), VDA-ISA-Export.
- **ISO 27001:** Anwendbarkeitserklärung (SoA, Annex A 2022, 93 Controls) mit „anwendbar/ausgeschlossen + Begründung", Managementklauseln (Kennzahlen 9.1, Managementbewertung 9.3, Korrekturmaßnahmen 10.2) und Dokumentenlenkung.
- **Umschalten:** In der Admin-Konsole lassen sich die Normen je Mandant **nachträglich aktivieren/deaktivieren**.
## 5. Die Module (Kurzüberblick)
| Modul | Zweck |
|---|---|
| **Assets** | Informationswerte/Systeme/Anwendungen etc. inkl. Schutzbedarf (C/I/V) |
| **Prozesse** | Geschäftsprozesse + Verknüpfung zu Assets (Strukturanalyse) |
| **Risiken** | Risiko-Register (Eintritt × Auswirkung), Behandlung, Verknüpfung zu Assets/Prozessen |
| **Lieferanten** | Lieferanten-/Dienstleistersteuerung (Kritikalität, NIS2, TISAX-Label, Verträge/NDAs) |
| **Maßnahmen** | Maßnahmen zur Risikobehandlung, Wirksamkeit |
| **Richtlinien** | Richtlinien-/Verfahrensbibliothek aus Vorlagen, Freigabe, Coverage |
| **SoA** | ISO-Anwendbarkeitserklärung bzw. TISAX-Control-Assessment |
| **Audit-Readiness** | Reifegrad/Gap-Analyse, Nachweise, Abgabe/Export |
| **Vorfälle** | Incident-Management (inkl. NIS2-/DSGVO-Meldevorlagen) |
| **Aufgaben** | Kanban-Aufgaben, Fristen |
Module lassen sich je Mandant **ein-/ausschalten** (Admin-Konsole).
## 6. Onboarding-Wizard
Neue Mandanten durchlaufen einen geführten Wizard: **Kontext → Scope → Richtlinien → Rollen → Risikokriterien → Prozesse → Controls**. Er baut das ISMS Schritt für Schritt auf; der Fortschritt wird gespeichert.
## 7. Typische Support-Fälle
- **„Login klappt nicht / Anmeldung fehlgeschlagen":** falsche E-Mail/Passwort, oder Account nach mehreren Fehlversuchen kurz gesperrt (15 Min). Prüfen: richtige Seite (`/login` vs `/platform/login`), Passwort exakt. Bei Reset ein Initial-Passwort setzen (erzwingt Wechsel).
- **„MFA-Gerät verloren":** über Recovery-Codes anmelden; sonst MFA administrativ zurücksetzen (Mandanten-Admin/Betreiber).
- **„Ein Modul fehlt in der Navigation":** Modul ist für den Mandanten deaktiviert → in der Admin-Konsole aktivieren.
- **„Falsche Norm / ISO fehlt":** Framework des Mandanten prüfen und ggf. ISO 27001 aktivieren (Admin-Konsole).
- **„E-Mails kommen nicht an":** SMTP-Konfiguration prüfen (Betreiber); Postfach/From-Adresse.
## 8. Wo finde ich vertiefende Infos?
- **Produkt/Funktionsumfang:** `docs/SPEC.md`, `docs/HANDOVER-PM.md`
- **Kundennahe Mockups (im Browser):** `docs/ISMS-Prototyp-GEFIM.html`, `ISMS-Lieferantenmanagement-GEFIM.html`
- **Feature-Konzepte:** `docs/KONZEPT-framework-iso27001.md` (ISO/TISAX), `KONZEPT-incidents.md`, `KONZEPT-backup-restore.md`, `KONZEPT-identity-mandanten.md` (Login/Mandanten/MFA)
- **Aktueller Stand/Änderungen:** `docs/STAND-dev-branch.md`
- **Am besten:** die **Demo-/Testinstanz** durchklicken (Login `admin@demo.example` / `Demo1234!`).
## 9. Betrieb & Grenzen (gut zu wissen)
- **Test- vs. Produktivumgebung:** Änderungen werden erst auf einer Testinstanz erprobt, dann produktiv ausgerollt. Support arbeitet auf der Produktivumgebung nur mit Bedacht.
- **Backups:** Datenbank + Objektspeicher werden gesichert (Betrieb). Restore ist möglich; keine eigenmächtigen Löschaktionen an Produktivdaten.
- **Datenschutz/Mandantentrennung:** Jeder Mandant sieht nur seine eigenen Daten — niemals Kundendaten zwischen Mandanten kopieren.
- **Secrets/Zugänge:** niemals per E-Mail/Chat weitergeben; Passwörter setzt der Kunde selbst bzw. als Initial-Passwort mit Wechselzwang.
---
*Fehlt etwas oder ist ein Support-Fall unklar? Ergänze dieses Handbuch — es ist als lebendes Dokument gedacht.*
-197
View File
@@ -1,197 +0,0 @@
# Entwickler-Übergabe — ISMS-Tool
> 📌 **Aktueller Gesamtstand des `dev`-Branches (alle Entwicklungstätigkeiten, PM + Dev):** siehe **`docs/STAND-dev-branch.md`**.
> Stand: 2026-07-22 · Branch `dev` (Produktionshärtung + Benutzerverwaltung) · Basis `main`
> Zweck: Kontext, Setup und Konventionen, damit ein neuer Entwickler direkt weiterarbeiten kann.
> Fachlicher Status (fertig/offen) siehe **`docs/HANDOVER-PM.md`**. Neue Härtungs-/Verwaltungspakete: §10.
---
## 1. Repository & Zugriff
- **Lokaler Pfad:** `~/Projects/ISMS-Tool`
- **Git-Remote (Gitea, nur HTTP):**
`http://gitea-vkbhbn2qdkz5ppk9q4qgb0tn.192.168.1.207.sslip.io/msolarczek/ISMS-Tool.git`
(interner Server; kein SSH). **Zugangstoken** liegt im macOS-**Schlüsselbund** (nicht im Repo, nicht in Klartext weitergeben). Für `git push` wird der Token als HTTP-Passwort verwendet.
- **Default-Branch:** `main` (es wird direkt auf `main` committet; Commits sind fein granular je Thema).
- **Commit-Konvention:** deutschsprachige, aussagekräftige Messages; Referenz auf Spec-Abschnitte (z. B. „§7b"). Co-Authored-By-Trailer für KI-Beiträge.
---
## 2. Tech-Stack (verifizierte Versionen)
| Bereich | Technologie |
|--------|-------------|
| Runtime | **Node.js 26** |
| Framework | **Next.js 16** (App Router, React 19, Server Components + Server Actions, Turbopack im Dev) |
| Sprache | TypeScript (strict) |
| DB / ORM | **PostgreSQL** (mit **pgvector**) · **Prisma 7** (`@prisma/client` + `@prisma/adapter-pg`, `prisma.config.ts`) |
| Auth | **NextAuth v5** (Credentials, JWT-Session) · Passwörter mit `@node-rs/argon2` |
| i18n | **next-intl** (Default `de`, `en` vorbereitet; Catalog in `messages/de.json`/`en.json`) |
| UI | Tailwind **v4** + shadcn/ui (**Base-UI-Variante**), Dark-Theme über zentrale CSS-Tokens in `src/app/globals.css` |
| Spezial | Handlebars + `marked` (Richtlinien-Rendering), `@xyflow/react` + `@dagrejs/dagre` (Abhängigkeitsgraph), `zod` (Validierung) |
**Scripts** (`package.json`): `dev` (`next dev`), `build`, `start`, `lint` (`eslint`). Seed: `npx tsx prisma/seed.ts`.
---
## 3. Setup (von Null)
```bash
# 1. Repo klonen (Token als HTTP-Passwort)
git clone http://gitea-…/msolarczek/ISMS-Tool.git
cd ISMS-Tool
# 2. Abhängigkeiten
npm install
# 3. .env anlegen (Vorlage vorhanden)
cp .env.example .env
# Wichtige Variablen: DATABASE_URL (Postgres inkl. pgvector), AUTH_SECRET, AUTH_URL,
# optional REDIS_URL, AI_PROVIDER/AI_API_KEY, SMTP_* (noch ungenutzt).
# 4. Infrastruktur starten (docker-compose.yml im Repo-Root)
docker compose up -d postgres # Postgres mit pgvector (Image pgvector/pgvector:pg16)
# Weitere Services im Compose: app, worker, redis, minio (Objektspeicher, für späteren
# Logo-Upload), mailhog (SMTP-Dev). Für lokale Entwicklung reicht i. d. R. 'postgres'.
# 5. Prisma-Client + Migrationen
npx prisma generate
npx prisma migrate deploy # wendet alle Migrationen an (inkl. RLS-Policies)
# 6. Seed (Demo-Mandant, Kataloge, Richtlinienpaket, Admin-Konsole)
npx tsx prisma/seed.ts
# 7. Dev-Server
npm run dev # http://localhost:3000 (Port 3000, siehe .claude/launch.json)
```
**Demo-Logins** (Passwort `Demo1234!`): `admin@demo.example` (Mandanten-Admin + ISB), `bea.approver@demo.example` (ISB, zweiter Freigeber für den Vier-Augen-Workflow), `auditor@demo.example`, `owner@demo.example`, `user@demo.example` — alle über den **Mandanten-Login** `/login`.
**Plattform-Admin (Betrieb):** getrennter Store + eigener Login `/platform/login` (TOTP-MFA-Pflicht, Enrollment beim ersten Login). Demo-Konto: `admin@demo.example` / `Demo1234!` (gleiche Adresse, aber getrennte Session ohne Mandantenkontext). Das frühere `isPlatformAdmin`-Flag ist abgelöst; die Admin-Konsole `/admin` ist nur mit Plattform-Session erreichbar. Siehe **Paket 2** der Produktionshärtung.
---
## 4. Architektur & tragende Konventionen
### Mandantenfähigkeit (kritisch!)
- Jede Fachtabelle hat `tenant_id`. **Nie ohne Mandantenkontext queren.**
- Zentraler Guard **`dbForTenant(tenantId)`** in `src/server/db.ts`: filtert reads automatisch nach `tenantId`, injiziert ihn bei `create`, prüft Ownership bei `findUnique`/`update`/`delete`. Neue tenant-bezogene Modelle **müssen in `TENANT_MODELS`** (in `db.ts`) eingetragen werden.
- Zusätzlich **Postgres Row Level Security** je Tabelle (Policy `tenant_isolation`, gesetzt in den Migrationen). Superadmin/plattformweite Reads laufen über den **rohen `prisma`**-Client (nicht `dbForTenant`).
- Session (`src/server/auth.ts`) trägt `tenantId`, `roles`, `permissions`, `isPlatformAdmin`.
### RBAC
- Katalog + Rollen-Blueprints in **`src/server/rbac.ts`** (`PERMISSIONS`, `ROLE_DEFS`). Serverseitige Durchsetzung: `requirePermission(session, "x:y")` / `hasPermission(...)`. UI-Verstecken ist nur Komfort.
### Modul-Gating (§3.4)
- Modul-Katalog in **`src/lib/modules.ts`**. Je Mandant `TenantModule`-Zeilen (enabled). Navigation blendet deaktivierte Module aus (`layout.tsx`).
- **Serverseitige Durchsetzung:** `requireModule("key")` in `src/server/modules.ts`, angewandt als **Modul-`layout.tsx`** je Routenordner (`assets/`, `processes/`, `risks/`, `measures/`, `policies/`, `suppliers/`, `dependencies/`) → schützt auch Unterrouten. In Server-Actions läuft der Layout-Guard erst nach der Mutation, daher zusätzlich `requireModule` im Action-`guard()` (bisher exemplarisch nur `policies.ts` — **auf übrige Action-Dateien nachzuziehen**).
### UI-Muster
- **Detail/Bearbeiten/Anlegen** überwiegend als **URL-gesteuerte Popups** (`?detail=`/`?edit=`/`?new=1`, `src/components/modal.tsx`) — Ausnahme: **Richtlinien-Dokumente** wurden bewusst auf **eigene Seiten** (`/policies/[code]`, `.../edit`) umgestellt.
- Dark-Theme: **keine hartkodierten Hex-Werte** in Komponenten — zentrale Tokens verwenden (`--bg-0`, `--panel`, `--surface-soft`, `--band`, `--ok/--warn/--risk/--info`, …).
- Formulare mit „einem Speichern-Button" nutzen HTML-`form`-Attribut-Assoziation (`form="id"`).
- Alle sichtbaren Texte über den next-intl-Catalog (`messages/*.json`) — Admin-/Register-Detailtexte teils bewusst inline-Deutsch.
### Prisma-7-Migrationen (Eigenheit)
Nicht-interaktiv, in zwei Schritten:
```bash
npx prisma migrate diff --from-config-datasource prisma.config.ts \
--to-schema prisma/schema.prisma --script > prisma/migrations/<ts>_name/migration.sql
# RLS-DO-Block manuell an die migration.sql anhängen (siehe bestehende Migrationen)
npx prisma migrate deploy
```
(Die interaktiven `migrate dev`-Prompts sind in dieser Umgebung blockiert; Flag-Namen in Prisma 7 geändert: `--from-config-datasource`/`--to-schema`.)
---
## 5. Verzeichnisstruktur (Auszug)
```
src/
app/(app)/ # geschützter App-Bereich (Sidebar-Shell = layout.tsx)
dashboard/ assets/ processes/ risks/ measures/ dependencies/ suppliers/
policies/ # Richtlinien: page + [code]/(page,edit) + layout.tsx (Modul-Guard)
admin/ admin/[id]/ # Plattform-Admin-Konsole (Superadmin)
settings/ # Kunden-Einstellungen (tenant:manage)
app/login/ # Login
components/ # UI + Fach-Modals (asset-, supplier-, service-, policy-*.tsx …)
server/
auth.ts db.ts rbac.ts modules.ts provision.ts audit.ts
risk-calc.ts dependency-graph.ts
actions/ # Server Actions je Modul (assets, risks, measures, suppliers,
# services, policies, admin, tenant-settings)
lib/ # modules, policy-render, supplier(+include), risk, control-titles,
# isa-controls, levels, measure, utils
prisma/
schema.prisma seed.ts import-policies.ts import-managed.ts migrations/
messages/ de.json en.json
seed/isms-vorlagenpaket-v2/ # VDA-ISA-Vorlagenpaket (Richtlinien/Verfahren, mapping.json, Baseline …)
docs/ SPEC.md HANDOVER-PM.md HANDOVER-DEV.md *.html (Mockups)
```
---
## 6. Modul-Kurzreferenz (wo liegt was)
- **Richtlinien:** Rendering-Engine `src/lib/policy-render.ts` (Handlebars, Flags, BL-Entfernung, `applyProtection` für TISAX-Level), Import `prisma/import-policies.ts` + `prisma/import-managed.ts`, UI `src/app/(app)/policies/**` + `src/components/policy-*.tsx`, Actions `src/server/actions/policies.ts`. Control-Titel `src/lib/control-titles.ts`.
- **Lieferanten/IT-Service:** Engine `src/lib/supplier.ts`, Includes `src/lib/supplier-include.ts`, UI `src/components/supplier-modals.tsx`/`service-modals.tsx`/`supplier-cockpit.tsx`, Actions `suppliers.ts`/`services.ts`.
- **Admin/Mandanten:** Provisionierung `src/server/provision.ts` (von Seed **und** `actions/admin.ts` genutzt, idempotent), `actions/tenant-settings.ts` (Stammdaten → ISMS-Variablen via `syncPolicyVariablesFromSettings`).
- **Risiko/Graph:** `src/server/risk-calc.ts`, `src/server/dependency-graph.ts`, `src/lib/risk.ts`.
---
## 7. Datenmodell — zentrale Modelle
`Tenant`, `TenantSettings` (Quelle der ISMS-Variablen), `TenantModule`, `User` (`isPlatformAdmin`), `Role`/`Permission`/`RolePermission`/`UserRole`, `AuditLog` (`scope: tenant|platform`).
Fachlich: `Asset`/`AssetRelation`, `Process`/`ProcessAsset`/`BiaEntry`, `Risk`/`RiskAsset`/`Measure`/`RiskMeasure`, `Threat`/`Vulnerability`, `SupplierProfile`/`ITServiceProfile` (+ Assessments/Contracts/Ndas/Evidence/Raci/Maturity), Richtlinien: `PolicyDocument`/`PolicyRequirement`/`PolicyVariable`/`PolicyBaselineParam`/`PolicyEvidence` + verwaltete Register (`CryptoEntry`, `ClassificationClass`/`HandlingAspect`/`HandlingRule`, `RiskMatrixClass`/`RiskEwLevel`/`RiskDamageDimension`, `HandbookTopic`).
---
## 8. Verifikations-Workflow
Nach jeder Änderung: **`npx tsc --noEmit`** → **`npm run lint`** → **`npm run build`**. Danach – wo relevant – Browser-Verifikation (Dev-Server + Login `admin@demo.example`). Bei DB-Änderungen: Migration erzeugen/anwenden + `npx tsx prisma/seed.ts` neu laufen lassen. Für das Richtlinien-Rendering existiert ein Residue-Check-Muster (rückstandsfrei über Flag-Kombinationen, angelehnt an `seed/.../_verify.py`).
---
## 9. Bekannte Fallstricke
1. **Beschädigte Seed-Tokens:** Das VDA-ISA-Paket enthält in einigen VA-RACI-Tabellen (VA-05/08/09/10/12/13) abgeschnittene `{{VAR |`-Tokens. Bei jedem Paket-Update **erneut prüfen & reparieren** (sonst bricht Handlebars). Scan: unbalancierte `{{`/`}}` je Zeile.
2. **Richtlinien-Re-Import ist nicht-destruktiv** (Phase-1-Härtung Paket 3, `prisma/import-policies.ts`): Diff/Upsert über stabile Schlüssel (`code`/`reqId`/`key`/`blId`/`nr`). Vorhandenes wird inhaltlich aktualisiert, aber **Status/Override/Freigabe** (PolicyDocument) und **nutzergepflegte Variablenwerte** bleiben erhalten; entfernte Paket-Einträge werden **deaktiviert** (`archivedAt`) statt gelöscht (aktive Ansichten filtern `archivedAt: null`). Verwaltete Register aus `import-managed.ts` (CRYPTO/RISKMATRIX/CLASSIFICATION/HANDBUCH) liegen außerhalb des Paket-Namensraums und werden nicht angetastet. Jeder Lauf liefert einen Änderungsreport (Audit-Log, Entity `policy_package`); `{ dryRun: true }` erzeugt die Vorschau ohne Schreibzugriff. Akzeptanztest: `npx tsx scripts/test-reimport.ts`.
3. **RLS-Kontext:** RLS-Policies erwarten `current_setting('app.tenant_id')`. Die App nutzt primär den `dbForTenant`-Guard; wenn direkte DB-Zugriffe hinzukommen, `app.tenant_id` in der Transaktion setzen.
4. **Turbopack-HMR** kann veraltete Fehler/`MISSING_MESSAGE` zeigen, nachdem `messages/*.json` geändert wurde → Dev-Server neu starten. Produktions-Build ist maßgeblich.
5. **Prisma-7-Migrationsflow** wie in §4 (kein `migrate dev`).
6. **`.next`-Cache** nicht löschen, während der Dev-Server läuft (Turbopack-Korruption) → Server stoppen, `rm -rf .next`, neu starten.
7. **TISAX-Flags:** `FLAG_HIGH_PROTECTION` ist im Modell stets aktiv, `FLAG_ELEVATED_PROTECTION` wird **abgeleitet** (`applyProtection`) — nie manuell setzen. Effektiver Level = Dokument-Override sonst global.
---
## 10. Produktionshärtung & Benutzerverwaltung (umgesetzt, Branch `dev`)
Aufbauend auf dem Fundament wurden mehrere Härtungspakete umgesetzt (feingranulare Commits auf `dev`):
- **API-Modul-Durchsetzung (§3.4):** zentraler `moduleGuard("<key>")` in `src/server/action-guard.ts`; jede mutierende Action eines gegateten Moduls läuft über `guard(...)` (Session → `assertModuleEnabled` → RBAC). Vollständigkeitscheck `scripts/check-module-guards.ts` (Registry Action→Modul) als `prebuild` — Build failt bei nicht zugeordneter Action-Datei. Neue Action-Datei ⇒ dort eintragen (Modul-Key oder `EXEMPT`).
- **Plattform-Admins getrennt:** Store `PlatformAdmin` (kein `tenant_id`), eigene NextAuth-Instanz `src/server/platform-auth.ts` (eigener Cookie/basePath `/api/platform-auth`, Session ohne Tenant), Login `/platform/login`, Bereich unter `src/app/(platform)/…`. **MFA (TOTP, `src/server/mfa.ts`) ist optional** — Enrollment nur erzwungen, wenn `PlatformSetting.mfaRequired` (Singleton) an ist; Umschaltung + Self-Service unter `/platform/profile`. Rate-Limit/Lockout am Login bleiben. Audit `scope=platform` via `writePlatformAudit`.
- **Nicht-destruktiver Richtlinien-Re-Import:** siehe §9 Punkt 2.
- **Benutzer- & Rollenverwaltung (ohne E-Mail-Flow):**
- Plattform-Admin je Mandant: `src/server/actions/platform-users.ts` + `components/platform-tenant-users.tsx`, eingebettet in `/admin/[id]`. Anlegen mit **Initial-/Einmal-Passwort** (selbst setzen oder generiert, einmalig angezeigt), Rollen, Deaktivieren/Reaktivieren, Passwort-Reset.
- Mandanten-Admin intern: `src/server/actions/tenant-users.ts` + `components/tenant-users-manager.tsx`/`role-manager.tsx`, Seite `/settings/users` (nur `user:manage`/`role:manage`). Benutzer-CRUD **und** Rollen-CRUD (eigene Rollen + Permissions; Standardrollen schreibgeschützt/klonbar). Strikt `dbForTenant(session)` → kein Cross-Tenant. **Lockout-Schutz** für den letzten aktiven Mandanten-Admin.
- **Force-Change:** neue Nutzer starten mit `mustChangePassword=true`; das `(app)`-Layout leitet autoritativ (DB) auf `/change-password` und sperrt deaktivierte Konten. Passwort-Policy: `src/lib/password-policy.ts` (Validierung, client-safe) + `src/server/password.ts` (Argon2id-Hash + Generator); Quelle `TenantSettings.securityPolicy.password`.
- **Nutzer-MFA optional:** Tenant-Login (`auth.ts`) verlangt TOTP nur bei eingerichteter MFA; Self-Service unter `/account`. `role:manage` ist neu im RBAC-Katalog (Mandanten-Admin).
- **Bearbeiten:** je Nutzer sind **Name & E-Mail** (E-Mail eindeutig je Mandant), Rollen, Aktiv/Deaktiviert und Passwort-Reset editierbar — in beiden Konsolen.
- **Zuständigkeit Einstellungen (Superadmin vs. Tenant-Admin):** **Kern-Einstellungen** (aktive Module, TISAX-/Schutzbedarf-**Tiefe**, Richtlinienpaket) steuert ausschließlich der **Superadmin** in der Admin-Konsole (`/admin/[id]`, u. a. `setTenantTisaxLevel`). Der **Tenant-Admin** pflegt in `/settings` nur seinen Bereich (Stammdaten/Branding → ISMS-Variablen) + Benutzer/Rollen; die TISAX-Tiefe ist dort nur noch als Read-only-Anzeige.
- **Zukunftssicher:** `createTenantUser`/`createUser` kapseln die Aktivierung über Initial-/Einmal-Passwort — der spätere **E-Mail-Einladungs-Flow (Paket 4)** lässt sich als alternative Aktivierung (Token statt Passwort) einhängen, ohne die UI umzubauen.
- **Benutzer-UI als Popup:** Anlegen/Bearbeiten über URL-gesteuerte Modals (`?new`/`?edit`, generische `components/user-forms.tsx` + `user-table.tsx`) in `/settings/users` und `/admin/[id]`; die Tabelle zeigt nur.
- **Aufgaben-/Freigabe-Modul (`tasks`):** generisches `Task`/`TaskComment` (RLS, in `TENANT_MODELS`). Erster Typ `policy_approval`: beim Einreichen wählt der Autor einen **konkreten Freigeber** (aktiver Nutzer mit `policy:approve`, ≠ Einreicher) → Aufgabe. Freigeben/Ablehnen (mit Grund)/Kommentieren im Bereich **`/tasks`** (nur der zugewiesene Freigeber; Vier-Augen), Verlauf historisiert; **Dashboard-Kachel** zählt offene Freigaben. Actions: `server/actions/tasks.ts` (+ `submitForApproval` in `policies.ts`). Der Editor zeigt nur noch Status/Freigeber + Link zur Aufgabe.
- **Richtlinien-Governance zentralisiert:** zentrale Variablen (Organisation/Rollen/Schutzbedarf-Flags, `lib/policy-variables.ts`) sind im Editor gesperrt (nur Einstellungen); **Schutzbedarf/TISAX** ausschließlich Superadmin (kein Per-Doc-Override, kein globaler Schalter im Modul); **Coverage-Matrix** filtert nach aktivem Assessment-Level (AL2 ohne „sehr hoch").
## 11. Nächste sinnvolle Aufgaben (Einstiegspunkte)
- **SMTP + Einladungs-/Aktivierungs-/Reset-Flow (Paket 4, M):** transaktionale Mails (Nodemailer), signierte Einladungs-/Reset-Tokens; ersetzt/ergänzt den Initial-Passwort-Weg.
- **Admin Phase 2 — Impersonation (M):** neues Modell `ImpersonationSession`, Cookie-basierter effektiver Tenant + Banner, Ablauf, Audit.
- **Tenant-weite MFA-Pflicht scharfschalten (S):** `securityPolicy.mfaRequired` wird von `disableOwnMfa` bereits respektiert; Enrollment-Erzwingung analog zum Force-Change-Gate (Seite außerhalb der `(app)`-Shell) nachziehen.
- **NIS2-Modul (L):** eigenes Modul inkl. Incident-Reporting mit Fristen-Timern.
- **Richtlinien-Versionierung/Diff (M–L)** und **DOCX/PDF-Export (M)**.
Weitere Details, Priorisierung und Aufwände: **`docs/HANDOVER-PM.md`**. Projekt-Spec: **`docs/SPEC.md`** + Modul-Prompts/Mockups.
-98
View File
@@ -1,98 +0,0 @@
# Projektübergabe — ISMS-Tool (Stand für Projektmanagement)
> 📌 **Konsolidierter Gesamtstand aller Entwicklungstätigkeiten auf `dev` (PM + Dev):** siehe **`docs/STAND-dev-branch.md`**.
> Stand: 2026-07-22 · Branch `dev` (Produktionshärtung + Benutzerverwaltung; noch nicht nach `main` gemerged)
> Zweck: Statusüberblick für die Weiterplanung — was ist umgesetzt, was ist offen (mit Priorität & grobem Aufwand).
## 🆕 Neu auf `dev` (Produktionshärtung Phase 1 + Benutzerverwaltung)
| Thema | Status | Kurz |
|-------|:------:|------|
| **API-seitige Modul-Durchsetzung** | ✅ | Deaktiviertes Modul sperrt jetzt auch Writes serverseitig; automatischer Vollständigkeitscheck (Build-Gate). |
| **Separater Superadmin-Store + eigener Login** | ✅ | Eigener Store/Login (`/platform/login`), Session ohne Mandantenbezug; `isPlatformAdmin`-Umweg abgelöst. |
| **MFA für Superadmins** | ✅ (optional) | Zunächst als Pflicht gebaut, dann auf **optional** umgestellt; Policy-Flag „MFA-Pflicht" (aus) stellt die Erzwingung wieder her. |
| **Nicht-destruktiver Richtlinien-Re-Import** | ✅ | Diff/Upsert; Status/Override/Freigabe/Variablenwerte bleiben, entfernte Einträge werden deaktiviert; Änderungsreport + Vorschau. |
| **Benutzer- & Rollenverwaltung** | ✅ | Plattform-Admin legt je Kunde Nutzer an (Initial-/Einmal-Passwort); Mandanten-Admin verwaltet Nutzer **und** Rollen intern (eigene Rollen + Rechte, Standardrollen klonbar), mandantengetrennt + Lockout-Schutz. Force-Change beim ersten Login, Passwort-Policy. |
| **Nutzer-MFA (Mandant)** | ✅ (optional) | Je Nutzer aktivierbar (`/account`); Login verlangt Code nur bei aktiver MFA. |
| **SMTP + Einladungs-/Aktivierungs-/Reset-Flow (Paket 4)** | ⏸️ zurückgestellt | E-Mail-Versand bewusst später; die Nutzer-Aktivierung läuft vorerst über Initial-/Einmal-Passwort und ist so gekapselt, dass der Einladungs-Flow ohne Umbau ergänzt werden kann. |
## Was ist das Produkt
Multi-Tenant-**SaaS für Informationssicherheits-Management (ISMS)**, ausgerichtet auf **ISO/IEC 27001:2022** und **TISAX / VDA-ISA 2027**. Web-App (Deutsch, EN vorbereitet), Dark-Theme, mandantenfähig (mehrere Kunden, Nutzer, Rollen). Mehrere fachliche Module + Plattform-/Kundenverwaltung.
**Aufwands-Legende:** **S** = klein (≤ 1 Tag) · **M** = mittel (2–4 Tage) · **L** = groß (> 1 Woche).
---
## ✅ Umgesetzt (produktiv nutzbar)
| # | Modul | Umfang (Kurz) |
|---|-------|---------------|
| 1 | **Fundament** | Login/Auth (lokale Accounts, NextAuth v5, JWT), **RBAC** (granulare Rechte, Rollen je Mandant), **Mandanten-Isolation** (tenant_id + Postgres-RLS + zentraler App-Guard), i18n (de/en), Dark-Theme (zentrale Tokens), Audit-Log. |
| 2 | **Assets & BIA** | Asset-Inventar + Geschäftsprozesse/BIA als ein Modul; C/I/A-Schutzbedarf, Vererbung, Abhängigkeiten; Detail/Bearbeiten/Anlegen als Popups; zugeordnete Risiken. |
| 3 | **Risikoanalyse** | 5×5-Heatmap, Risikoregister, Risiko-Detail mit Maßnahmen (dezimale Minderung, Brutto→Rest visualisiert), Bedrohungs-/Schwachstellen-Kataloge, Control-Verknüpfung. |
| 4 | **Maßnahmen** | Kanban-Board (Drag & Drop), berechnetes Restrisiko aus Maßnahmen. |
| 5 | **Abhängigkeiten & kritische Pfade** | Interaktiver Graph (React Flow + dagre), Single Points of Failure, kritische Pfade. |
| 6 | **Lieferanten- & IT-Service-Management** | Asset-basiert (Lieferant/IT-Service **sind** Assets); Anforderungs-Engine (Schutzbedarf→Stufen), Gate-Logik (erzeugt echte Risiken), ISB-Reifegrad-Freigabe; **RACI-Matrix** über ISA-Controls (VDA-ISA 6.1.3); Cockpit direkt aus dem Asset-Inventar öffenbar. |
| 7 | **Richtlinien & Verfahren (VDA-ISA 2027)** | Siehe Detailblock unten — größtes Modul. |
| 8 | **Admin-Konsole & Mandantenverwaltung (Phase 1)** | Plattform-Admin (`/admin`): Kunden anlegen + **automatisch provisionieren**, Module-Toggles, Lebenszyklus (aktiv/gesperrt/archiviert). Kunden-Einstellungen (`/settings`): Stammdaten → speisen ISMS-Variablen, Branding, TISAX-Level. **Serverseitige Modul-Durchsetzung** (deaktivierte Module gesperrt, nicht nur ausgeblendet). |
### Modul 7 „Richtlinien & Verfahren" im Detail (umgesetzt)
- **Import** des VDA-ISA-2027-Vorlagenpakets: **28 Dokumente** (Leitlinie L00, R01–R14, VA-01–VA-13), **316 Anforderungen** (122 MUSS · 132 SOLL · 43 HOHER · 19 SEHR HOHER Schutzbedarf) über **45 Controls**; Baseline-Parameter, Variablen, Nachweisregister.
- **Rendering-Engine**: Handlebars (verschachtelte `{{#if}}`-Flags), Variablen aus einer Pflegestelle, Deep-Links, BL-Referenzen im Lesemodus entfernt; rückstandsfrei über alle Flag-Kombinationen.
- **Bibliothek** + **Referenz-/Coverage-Matrix** (zwei Richtungen: nach Control / nach Dokument).
- **Lesemodus** als eigene Seite (kein Popup); **Bearbeitungsmodus** variablenbasiert + **Vier-Augen-Freigabe** (Entwurf → In Freigabe → Freigegeben); **Experten-Modus** (Rohtext-Editor + Formatier-/Einfügehilfen + Auto-Anlage neuer Variablen).
- **Verwaltete Register** (editierbar): Verschlüsselungsregister (mit Ablaufüberwachung), Risiko-Bewertungsmatrix (FB-80-04-Defaults), Klassifizierungs-Handhabungsmatrix (AA-80-20). **Anwender-Handbuch** (kuratiert, Baseline-synchron, Deep-Links).
- **Schutzbedarf-/TISAX-Level-Schalter (AL2/AL3)** global + **Override je Richtlinie**.
---
## 🟥 Offen — Hohe Priorität
| Thema | Aufwand | Anmerkung |
|-------|:------:|-----------|
| **NIS2-Modul** (nie begonnen) | **L** | Framework Art. 20/21 + Control-Mapping, Einrichtungs-Einstufung + BSI-Registrierung, **Incident-Reporting-Workflow mit Fristen-Timern (24 h / 72 h / 1 Monat)**, NIS2-Dashboard. War „Teil C" der ursprünglichen Planung. |
| **Admin-Konsole Phase 2 — Impersonation** | **M** | Zeitlich begrenzter, protokollierter Support-Zugriff in einen Mandanten; „Support-Sitzung aktiv"-Banner; automatischer Ablauf. |
| **Admin-Konsole Phase 2 — Plan/Limits** | **M** | Lizenzstufen, Limits (Nutzer/Speicher/Assets), Warnung/Sperre bei Überschreitung. |
| **Separater Superadmin-Store + eigener Login + MFA-Pflicht** | **M–L** | Aktuell ist der Superadmin ein `isPlatformAdmin`-Nutzer im Mandanten (Demo). Spec fordert getrennte Accounts + eigenen Login-Pfad. |
| **Richtlinien: echte Versionierung + Diff** | **M–L** | Aktuell ändert die Freigabe nur den Status (ohne Versionshistorie/Diff). Entwurf-neben-Freigegeben, block-/klauselgenauer Diff. |
| **Richtlinien: DOCX/PDF-Export** | **M** | Voll gerendert im GEFIM-Corporate-Design; Referenz-/Nachweisdoku separat. |
---
## 🟧 Offen — Mittlere Priorität
| Thema | Aufwand | Anmerkung |
|-------|:------:|-----------|
| **Richtlinien: Status „Revoked" + Auto-Version** | **S** | Vom Kunden ausdrücklich für später gemerkt: Status-Auswahl inkl. Revoked (manuell), Version zählt bei jedem Speichern automatisch hoch. |
| **Richtlinien: Word-Upload (.docx-Import)** | **M** | Eigene Dokumente hochladen → Block-Modell + Original als Anhang, versioniert, im Freigabeprozess. |
| **Richtlinien: KI-Wizard** | **L** | Geführte Erstellung/Anpassung (control-getrieben, Feature-Flags/Reifegrad), KI formuliert Blocktext. |
| **Richtlinien: KI-Formulierungshilfe im Experten-Modus** | **M** | Braucht Anthropic-API-Anbindung. |
| **Zentrale Baseline-/Variablen-Einstellseite** | **S–M** | Eine Pflegestelle mit Freigabe-Durchlauf. |
| **Admin Phase 2 — Logo-Upload / Objektspeicher je Mandant** | **M** | Für UI-Kopf + Export (Branding/Whitelabel). |
| **Admin Phase 2 — SMTP/Benachrichtigungen + Einladungs-/Aktivierungs-Flow** | **M** | E-Mail-Einladung, Passwort-Setzen, Benachrichtigungsregeln (Fristen/Freigaben/Vorfälle). |
| **Lieferanten Phase 2** | **M–L** | Fragebogen-Builder, Self-Service-Portal (externer Lieferantenzugang), Scheduler (Review-/Ablauf-Erinnerungen). |
| **Risikomatrix zusammenführen** | **M** | Bewertungsmatrix im Richtlinienmodul ist eigene 4×4-Pflegestelle (FB-80-04); Risikoanalyse nutzt 5×5. Auf eine gemeinsame, zentrale Skala vereinheitlichen. |
| **Platzhalter-Module ausbauen** | je **M–L** | Sidebar-Punkte vorhanden, aber nicht gebaut: **SoA & Controls**, **Vorfälle**, **Nachweise**, **Management-Review**, **KI-Chat**. |
---
## 🟩 Offen — Niedrige Priorität / Technische Schulden
| Thema | Aufwand | Anmerkung |
|-------|:------:|-----------|
| **Modul-Durchsetzung auf API-Ebene vervollständigen** | **S** | Route-Zugriff (GET) ist für alle Module gesperrt; der `requireModule`-Guard in Server-Actions ist bisher **exemplarisch nur für Richtlinien** gesetzt. Einzeiler je Action-Guard nachziehen. |
| **Richtlinien-Re-Import ist destruktiv** | **M** | Re-Import löscht + legt Dokumente/Anforderungen neu an (setzt per-Dokument-Status/Override/Freigabe zurück). Spec wünscht „deaktivieren statt löschen" + Historie erhalten. |
| **Coverage zeigt alle statt nur aktive Anforderungen** | **S** | Referenzmatrix listet vollständig (gut für Audit); optional Filter „nur aktive" nach effektiven Flags. |
| **Admin Phase 2 — Datenexport/Löschung/Retention (DSGVO)** | **L** | Mandantenvollständiger Export, Löschkonzept, Aufbewahrungsfristen. |
| **Optionale Zusatzfeatures** | je **M** | SSO (OIDC/SAML), API-Keys/Webhooks, Onboarding-Wizard, E-Mail-Domain-Allowlist, Wartungs-/Status-Banner. |
---
## Hinweise für die Planung
- **Spezifikationen** liegen im Repo: `docs/SPEC.md` (Gesamt-Spec) sowie die Übergabe-Prompts und Mockups je Modul (Richtlinien, Admin-Konsole). Neue Anforderungen kamen bisher als „Delta-Prompts" (z. B. TISAX-Level-Update).
- **Nächster logischer Block:** entweder **NIS2** (letztes großes Framework, hängt am Incident-/Vorfälle-Modul) oder **Admin-Konsole Phase 2** (Impersonation + Plan/Limits + Superadmin-Login), je nach Vertriebs-/Compliance-Priorität.
- **Demo-Umgebung:** Mandant „demo", Login `admin@demo.example` (ist Superadmin + Mandanten-Admin), Passwort im Seed. 4 Demo-Rollen vorhanden.
- **Qualität:** Jede Iteration wurde mit `tsc` + `lint` + `build` und – wo möglich – Browser-Verifikation abgeschlossen; Commits sind fein granular und je Thema.
-238
View File
@@ -1,238 +0,0 @@
# Implementierungsfaden — TISAX-Onboarding-Wizard Neustruktur
> Status: Entwurf · Grundlage: Konzept v2 (4 Ebenen, nach TISAX-Fachprüfung) + IST-Analyse des Task-Moduls (Branch `dev-tasks-kanban`, `ce1e847`).
> Ziel: Aus dem linearen 9-Schritt-Monolithen `/onboarding` werden vier Ebenen — **Fundament → Strukturanalyse → Cockpit → Audit** — informationszentriert, prozessgeführt erhoben, bereichsbasiert umgesetzt.
---
## 0. Ausgangslage & Auswirkung der Kanban-Anpassung
Das Task-Modul wurde zwischenzeitlich auf ein **Kanban-Board** umgestellt (`src/components/kanban-board.tsx`, `src/app/(app)/tasks/page.tsx`, Action `updateTaskStatus`, Status `IN_PROGRESS`). Das ist die Basis für Ebene 3 — das Bereichs-Board wird **additiv** darauf gebaut, nicht neu.
**Bereits vorhanden (nutzbar):** generisches Task-Modell, Kanban mit DnD-Statuswechsel, `assigneeId` + `createdById` + Vier-Augen-Freigabe, PROPOSED→OPEN/DISCARDED-Vorschlagsmechanik, Wizard-Trigger (`task-triggers.ts`, `proposeTasksFromTriggers`), polymorphe `links`-Json + harte `entityType/entityId`-Kopplung, Teil-Sync Task→Objekt bei Policy-Freigabe (`approveTask`).
**Fehlt (dieser Plan liefert es):** `domain`/Bereich, RACI/Mitwirkende, `orderIdx`, Recurrence/Wiedervorlage, Auto-Completion-Deckel, Bereichs-/Personen-Filter, harte Control-/Evidence-Verknüpfung, Information als Kernobjekt.
**Altlasten (Vorarbeit M0):**
- `TASK_STATUSES` in `src/lib/tasks.ts` enthält `IN_PROGRESS` nicht, obwohl Board/Actions ihn nutzen → Inkonsistenz beheben.
- `src/app/(app)/tasks/page.tsx:166` referenziert `open` — im File nicht definiert (nur `myOpen`/`active`/`proposals`/`terminal`). Verifizieren & fixen.
---
## 1. Datenmodell (Prisma-Migrationen)
Konventionen wie im Bestand: `cuid()`-IDs, `tenantId`, `@@map(snake_case)`, `@@unique([tenantId, …])`. Enums additiv; freie `String`-Statusfelder brauchen keine Migration.
### 1.1 Werte-Modell: Information IST ein (primärer) Asset — kein Extra-Objekt
**Korrektur ggü. erstem Entwurf.** Ein Informationswert ist kein neues Objekt neben dem Asset, sondern ein **primärer Asset** (ISO 27005 / TISAX: primäre Werte = Informationen + Prozesse; sekundäre = Träger). Euer Schema bildet das bereits ab:
- `AssetType.INFORMATION` / `DATA` = primäre Werte; `SYSTEM/APPLICATION/LOCATION/SUPPLIER/IT_SERVICE/SOFTWARE/PERSON` = sekundär/Träger.
- `ProcessAsset.role = PRIMARY | SECONDARY` trennt primär/sekundär je Prozess-Beziehung.
- `AssetRelation` bildet Träger-Abhängigkeiten ab; `confidentiality/integrity/availability` liegen schon am `Asset`.
→ **Kein** `InformationValue`/`ProcessInformation`/`InformationAsset`. Stattdessen kleine Ergänzungen am Bestand:
```prisma
enum InfoLabel { NONE INFO_HIGH INFO_VERY_HIGH PROTOTYPE PERSONAL_DATA }
model Asset {
// … Bestand (type, C/I/A, ownerId, status, tags) …
normalizedName String? // Dedup-Schlüssel (s. 2.1)
label InfoLabel @default(NONE) // Info hoch/sehr hoch, Prototyp, personenbezogen → steuert Scope/Bereiche
@@unique([tenantId, normalizedName]) // verhindert Exakt-Duplikate (v.a. type INFORMATION/DATA)
}
```
**Primär/sekundär** bleibt über `ProcessAsset.role`. Optional als intrinsische Klassifikation `Asset.tier (PRIMARY|SUPPORTING)`, ableitbar aus `type` — nur einführen, falls die Rolle je Prozess nicht ausreicht (s. offene Entscheidung 4.1).
**Schutzbedarf:** bleibt am `Asset`. Primäre Informations-Assets tragen den echten C/I/A-Wert; sekundäre erben per **Maximum** der von ihnen getragenen primären Assets.
**„Erhebung über den Prozess":** Im Prozess-Schritt werden primäre Informations-Assets erfasst (Dedup/Autocomplete gegen bestehende Assets, 2.1), dann Träger-Assets via `ProcessAsset(SECONDARY)` + `AssetRelation` verknüpft. Anker = das primäre Informations-Asset. **Keine Datenmigration nötig** — nur additive Spalten.
### 1.2 Standard-Prozess-Katalog (Ebene 2)
```prisma
model ProcessCatalogEntry { // global, kein tenantId
id String @id @default(cuid())
code String @unique
name String
category ProcessCategory // CORE | MANAGEMENT | SUPPORT (Neben→SUPPORT)
suggestedAssetTypes AssetType[]
suggestedRiskCodes String[] // → RiskCatalogEntry.code
suggestedInfoLabels InfoLabel[]
@@map("process_catalog")
}
```
### 1.3 Team / Funktionszuordnung (Ebene 1)
Heute sind ISMS-Rollen read-only Variablen. Neu: echte Zuordnung Funktion→User(n) inkl. „unbesetzt".
```prisma
model ProjectFunctionAssignment {
id String @id @default(cuid())
tenantId String
functionKey String // ISB, PM, HR_LEAD, IT_LEAD, BCM, AUDITOR_INT, DPO …
userId String? // null = unbesetzt → erzeugt Task „Funktion besetzen"
domain Domain? // Default-Bereich dieser Funktion (Sichtbarkeit)
invitedEmail String? // Einladung, falls Account noch nicht existiert
createdAt DateTime @default(now())
@@index([tenantId, functionKey])
@@map("project_function_assignments")
}
```
### 1.4 Bereich (Domain) & Control→RACI-Mapping (Ebene 3)
```prisma
enum Domain { GOVERNANCE HR PHYSICAL BCM IT PROCUREMENT COMPLIANCE DATA_PROTECTION PROTOTYPE }
enum RaciKind { RESPONSIBLE ACCOUNTABLE CONSULTED INFORMED }
model ControlDomainMap { // global default; tenant-Override via tenantId
id String @id @default(cuid())
tenantId String? // null = globaler Default
control String // "3.1.4" oder Kapitel-Präfix "3"
domain Domain
raci RaciKind @default(RESPONSIBLE)
functionKey String? // Default-Verantwortliche Funktion
@@map("control_domain_map")
}
```
Seed-Defaults (Kapitel → Bereich), fachlich korrigiert:
| Kapitel | Domain | Anmerkung |
|---|---|---|
| 1 | GOVERNANCE | ISB + Management |
| 2 | HR | |
| 3 (phys.) | PHYSICAL | |
| 3 (BCM) | BCM | **aus K3 gelöst** — eigener Bereich |
| 4 IAM | IT + **HR (CONSULTED)** | Joiner-Mover-Leaver |
| 5 | IT | |
| 6 | PROCUREMENT + **ISB/Recht (CONSULTED)** | |
| 7 | COMPLIANCE + **DPO/IT (CONSULTED)** | |
| Prototyp | PROTOTYPE | nur bei Label |
| Datenschutz | DATA_PROTECTION | nur bei Label |
### 1.5 Task-Erweiterungen (Ebene 3)
```prisma
model Task {
// … Bestand …
domain Domain? // Bereich (Default aus ControlDomainMap)
orderIdx Int @default(0) // manuelle Sortierung je Bereich
recurrence String? // ISO-8601-Dauer/RRULE, z.B. "P1Y"
remindAt DateTime?
effectiveUntil DateTime? // Wirksamkeitsintervall (Wiedervorlage)
participants TaskParticipant[]
@@index([tenantId, domain, status])
}
model TaskParticipant { // RACI zusätzlich zu assigneeId (=primär Responsible)
id String @id @default(cuid())
tenantId String
taskId String
userId String
raci RaciKind
@@unique([tenantId, taskId, userId, raci])
@@map("task_participants")
}
```
### 1.6 Nachweis-/Evidence-Register (Ebene 3+4)
```prisma
model Evidence {
id String @id @default(cuid())
tenantId String
title String
kind String // record | protocol | screenshot | export
fileRef String?
taskId String?
control String?
validFrom DateTime?
validUntil DateTime? // koppelt an Task.effectiveUntil
createdAt DateTime @default(now())
@@index([tenantId, control])
@@map("evidence")
}
```
---
## 2. Querschnitts-Bausteine
### 2.1 Dedup-Erkennung & Autovervollständigung für Informationswerte
Kernanforderung: Menschen erfassen über den Prozess, sollen aber denselben Wert nicht doppelt anlegen (auch nicht bei abweichender Groß-/Kleinschreibung).
- **Normalisierung → `normalizedName`:** `trim` → Mehrfach-Whitespace kollabieren → Unicode-NFC → `toLowerCase` (locale-aware `de`) → Umlaut-Faltung (ä→ae, ö→oe, ü→ue, ß→ss) → Satzzeichen entfernen. Ergebnis ist der `@@unique`-Schlüssel.
- Beispiel: „Kundendaten", „kundendaten", „ Kunden­daten " → alle `kundendaten`.
- **Exakt-Duplikate:** durch `@@unique([tenantId, normalizedName])` unmöglich; bei Kollision wird das bestehende Asset *verknüpft* (`ProcessAsset`) statt neu angelegt.
- **Autovervollständigung:** As-you-type-Suche im Server (`searchAssets(query)`, gefiltert auf `type INFORMATION/DATA`) gegen (a) das Tenant-Asset-Register und (b) globale Katalog-Vorschläge. Treffer zeigen „bereits erfasst in Prozess X" → ein Klick verknüpft.
- **Fuzzy-Nah-Duplikate (weiche Warnung):** Trigramm-/Levenshtein-Ähnlichkeit auf `normalizedName`; ab Schwelle „Meintest du *Kundendaten*?" **vor** dem Anlegen. Bei Postgres optional `pg_trgm` (`similarity()`), sonst in-app Levenshtein auf der Kandidatenliste.
- **UI:** Combobox (bestehendes `components.json`/shadcn-Setup) mit Vorschlagsliste, Badge „neu" vs. „verknüpfen".
### 2.2 Auto-Completion-Engine — gedeckelt
Zentrale Funktion `syncTaskFromObject(entityType, entityId)`, aufgerufen bei Statuswechsel von Policy/Risk/Control/Asset. Fachprüfungs-Regel: **Existenz ≠ Wirksamkeit.**
| Auslöser | Task geht auf … | Deckel |
|---|---|---|
| `PolicyDocument.status = FREIGEGEBEN` | `DONE` (dokumentiert) | Reifegrad 3 / audit-ready nur mit verknüpftem `Evidence` + Vier-Augen |
| Asset C/I/A + Owner gesetzt | `DONE` | — |
| `Risk.status = ACCEPTED/CLOSED` | `DONE` | Restrisiko gesetzt |
| `ControlImplementation.status = erledigt` | `DONE` (umgesetzt) | Reifegrad-Anhebung getrennt bestätigen |
- Kein Auto-`DONE` ohne mindestens dokumentierten Zustand; „audit-ready" ist ein **separater, manueller** Schritt mit Nachweis.
- **Wiedervorlage:** Tasks mit `recurrence` erzeugen bei `DONE` automatisch eine Folge-Task mit neuem `dueDate`/`effectiveUntil` (Serienlogik).
### 2.3 Bereichs-Sichtbarkeit & RACI
- Default-Sicht: **eigene Bereiche** (aus `ProjectFunctionAssignment.domain` des Users) + eigene/zugewiesene Tasks + Pool.
- **PM + ISB:** `task:read_all` (existiert bereits) → alle Bereiche.
- Gezielte Mitwirkung: Eintrag in `TaskParticipant` (CONSULTED/INFORMED) macht Task für die Person sichtbar, ohne ihr den ganzen Bereich zu öffnen.
- Kanban erhält einen **Bereichs-Selektor** (Lane-Filter) zusätzlich zu den Status-Spalten; Sortierung je Bereich nach `orderIdx`, dann Priorität.
---
## 3. Meilenstein-Faden (jeder Schritt einzeln lauffähig)
### M0 · Vorarbeit & Bereinigung
- `TASK_STATUSES` um `IN_PROGRESS` ergänzen (`src/lib/tasks.ts`); `open`-Bug in `tasks/page.tsx:166` verifizieren/fixen.
- Enums `Domain`, `RaciKind`, `InfoLabel`, `InfoProcessRole` + Task-Felder `domain`/`orderIdx` (nullable) migrieren — noch ohne UI-Wirkung.
- *Risiko: minimal. Kein Verhaltensbruch.*
### M1 · Ebene 1 „Fundament" (Wizard-Umbau Teil 1)
- Onboarding-Steps in `src/lib/onboarding/register-steps.ts` neu ordnen/ergänzen: `context` → `scope` (einfrieren) → `policy` (Leitlinie, verlinkt Richtlinien-Modul) → `roles`(Team) → `criteria` (Risikoakzeptanz + C/I/A-Skala).
- **Team-Entität** `ProjectFunctionAssignment` + Zuweisen/Einladen-Flow (Schritt `roles`). Neue Funktionen: BCM, unabhängiger interner Auditor, DPO, Asset-/Risk-Owner-Kennzeichnung.
- Leitlinie als Fundament-Artefakt (nicht als Cockpit-Task).
- *Betroffen: `src/app/(app)/onboarding/steps/*`, `src/server/roles.ts`, `src/server/actions/onboarding*.ts`.*
### M2 · Ebene 2 „Strukturanalyse" (Information = primärer Asset)
- Additive `Asset`-Spalten (`normalizedName`, `label`) + `ProcessCatalogEntry` migrieren. **Keine** Datenmigration — Bestand bleibt gültig.
- Wizard-Fluss **prozessgeführt**: Prozesse wählen (Katalog) → je Prozess primäre Informations-Assets erfassen (Combobox mit Dedup/Autocomplete, 2.1) → Träger-Assets (`ProcessAsset SECONDARY`: System/Anwendung/Raum/Person/Dienstleister/Standort) → Schutzbedarf am Asset (Maximum/Kumulation/Verteilung) → Risiken aus Katalog.
- *Betroffen: neue Steps `processes`/`assets`/`protection`/`risks`, `RiskCatalogEntry`-Matching (existiert).*
### M3 · Ebene 3 „Cockpit" auf bestehendem Kanban
- `ControlDomainMap` seeden (Tabelle 1.4); Task-Erzeugung (`soa.ts`, `task-triggers.ts`, `gap.ts`) setzt `domain` + Default-RACI.
- Kanban erweitern: Bereichs-Lane/-Filter + Personen-Filter; Default „meine Bereiche"; `orderIdx`-Sortierung (DnD persistiert Reihenfolge).
- `TaskParticipant` (RACI) + Sichtbarkeitslogik (2.3).
- **Auto-Completion-Engine** (2.2) + Recurrence/Wiedervorlage; `Evidence`-Register.
- Fragebogen (`context`) auf Bereiche verteilen → Fragen erzeugen Tasks im jeweiligen Bereich.
- *Betroffen: `kanban-board.tsx`, `tasks/page.tsx`, `src/server/actions/tasks.ts`, `soa.ts`, `policies.ts` (Sync-Hook).*
### M4 · Ebene 4 „Audit-Wizard" abtrennen
- Neuer, aktivierbarer Wizard aus heutigen Steps `gap` + `readiness`; **internes Audit** (unabhängig, AL3-Pflicht) als eigener Schritt ≠ Readiness-Snapshot.
- Reifegrad + Nachweise laufen bereits kontinuierlich (M3) → hier nur Konsolidierung + Management-Review.
- *Betroffen: neue Route `/audit-readiness` o. ä., Auslagern aus `onboarding/register-steps.ts`.*
---
## 4. Offene Entscheidungen
1. **Primär/sekundär — intrinsisch oder relational?** Reicht `ProcessAsset.role` (Rolle je Prozess), oder soll ein intrinsisches `Asset.tier (PRIMARY|SUPPORTING)` als Wahrheit ergänzt werden (ableitbar aus `type`)? → Vorschlag: mit `role` starten, `tier` nur bei Bedarf. *(C/I/A-Ablageort ist entschieden: bleibt am `Asset`, sekundär erbt per Maximum.)*
2. **Fuzzy-Matching-Technik:** `pg_trgm` (DB-nah, schnell) vs. in-app Levenshtein (DB-agnostisch). → Abhängig davon, ob Postgres-Extension im Coolify-Deployment verfügbar ist.
3. **RACI-Tiefe:** volle RACI oder zunächst nur Responsible + Consulted (Sichtbarkeit)? → Vorschlag: klein starten (R + C), A = ISB/PM implizit.
4. **Audit-Wizard-Aktivierung:** manuell vs. ab Coverage-Schwelle.
---
## 5. Reihenfolge-Logik (warum dieser Faden)
M0 legt risikolos das Datenfundament. M1–M2 bauen die *sequenziellen* Ebenen (Setup/Strukturanalyse), auf denen alles Weitere aufsetzt — inkl. der informationszentrierten Struktur, dem größten Modell-Hebel. M3 nutzt maximal das bereits vorhandene Kanban (additiv statt Neubau). M4 ist die saubere Abtrennung am Ende. Jeder Meilenstein ist für sich lauffähig und liefert sichtbaren Nutzen.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-162
View File
@@ -1,162 +0,0 @@
# KONZEPT: Mehr-Framework-Fähigkeit — ISO 27001 neben TISAX (pro Mandant wählbar)
**Stand:** 2026-08-21 · **Zielgruppe:** mehrköpfiges Entwicklerteam + PM · **Status:** Entwurf zur Abnahme
> **Nachtrag 2026-08-21 — D4 und Lane 2 sind entschieden und umgesetzt:** Nicht zwei Seed-Pakete,
> sondern **ein Dokumentensatz mit zwei Framework-Mappings** (Variante A). Umsetzung und Begründung:
> `docs/FRAMEWORK-MAPPING-ISO27001.md`. Die übrigen Lanes bleiben unverändert gültig.
> **Ziel:** Kunden sollen pro Mandant **ISO 27001 und/oder TISAX** wählen können. Je Framework gibt es u. a. ein eigenes Vorlagenpaket, einen eigenen Control-Katalog und eine eigene Audit-/Readiness-Logik. Heute ist das Tool durchgängig **implizit auf VDA-ISA 2027 / TISAX** verdrahtet.
---
## 1. Ausgangslage (Ist-Analyse, belegt)
Das Fachmodell kennt **kein** „Framework/Standard" pro Mandant. Alles ist implizit VDA-ISA/TISAX:
- **Kein Framework-Feld:** `Tenant` (`prisma/schema.prisma:31`) hat nur `sector` + generisches `config Json`; `TenantSettings` (`:52`) hat als einzigen Standard-Anker `tisaxLevel` (`:70`, „AL2|AL3"). ISO 27001 erscheint nur als Marketing-Text (`src/lib/brand.ts:57`) und in einem Kommentar (`src/server/actions/tenant-users.ts:197`).
- **Control-Katalog** liegt als **String-ID** vor (kein Enum): `ControlAssessment.control` (`:967`, z. B. „1.3.1"/„5.3.4-KI", Werte 0–3), `ControlImplementation.reqId` (`:988`), `ControlDescription` (`:1217`, „VDA-ISA-Spalte 4"). Titel/IDs kommen aus VDA-ISA (`src/lib/control-titles.ts:1`). `Domain`-Enum (`:1029`) enthält TISAX-only `PROTOTYPE`.
- **Ein** Vorlagenpaket verdrahtet: Parser `prisma/import-policies.ts` (Codes L00/R../VA-, `:115`), globale Ablage `PolicyTemplateVersion` (`:1637`, **ohne** Framework-Dimension), Auflösung `prisma/template-store.ts` (`resolvePackageForTenant` wählt nur nach Locale die *eine* neueste PUBLISHED-Version, `:147`), Sync-Skript `scripts/sync-policy-templates.ts:20` (fest `seed/isms-vorlagenpaket-v2` de/en, `meta.standard:"VDA ISA 2027"`, `version:"2.1"`).
- **Readiness/Reifegrad** ist TISAX: Reifegrad **0–3** (`src/lib/maturity.ts:11`), Zielgrad aus AL2/AL3 + Schutzbedarf (`src/server/assessment-level.ts`), Anforderungsschema MUSS/SOLL/HOCH/SEHR-HOCH + Prüfziele IS/Prototyp(8.)/Datenschutz(9.) (`src/lib/scope-filter.ts`, `src/lib/export/vda-isa.ts:9`). Die reinen Rechen-Engines `src/lib/readiness.ts` und `src/lib/gap-consolidation.ts` sind numerisch **standard-agnostisch**.
- **SoA** existiert als Modul-Key `soa` (`src/lib/modules.ts:19`), ist faktisch aber ein **VDA-ISA-Reifegrad-Assessment** (`src/server/actions/soa.ts`, Werte 0–3), **nicht** die ISO-typische Applicability-Erklärung (anwendbar/ausgeschlossen + Begründung je Annex-A-Control).
- **Provisionierung**: `provisionTenant` (`src/server/provision.ts:53`) ist die zentrale Stelle — schreibt `tisaxLevel`, aktiviert alle Module, importiert das *eine* Paket, setzt TISAX-Schutzbedarf-Flags. `ProvisionOpts` kennt kein `framework`.
**Bereits standard-agnostisch (wiederverwendbar):** Multi-Tenancy/RLS, Modul-Toggle (`TenantModule`), Vorlagen-**Mechanik** (Parser-Struktur, nicht-destruktiver `reconcilePackage`, DB-Versionsablage, Locale-Fallback), Control-Speicherung als String-ID, Onboarding-Registry/Progress, die Rechen-Engines readiness/gap.
**Hart an TISAX gekoppelt (abstrahieren/duplizieren):** AL2/AL3-Schutzbedarf + Flags, Reifegrad 0–3 + C5-Belegtabelle, MUSS/SOLL-Schema + Prüfziele 8./9., der eine Vorlagenpaket-Pfad + single-published-Auflösung, VDA-ISA-Exporte/Labels, Control-Titel, `Domain.PROTOTYPE`, die SoA-Semantik, zahlreiche i18n-Labels „TISAX"/„VDA-ISA".
---
## 2. Zielbild
Ein **Framework als First-Class-Dimension**: Mandant wählt ein oder mehrere Frameworks (`ISO_27001`, `TISAX`). Je Framework werden Vorlagenpaket, Control-Katalog, Scope-/Assessment-Modell, Readiness/SoA-Sicht und Export **framework-spezifisch** aufgelöst — über eine **Strategie-Schicht**, damit die generische Mechanik (Tenancy, Vorlagen-Import, Wizard-Shell, Rechen-Engines) unverändert bleibt.
Leitprinzipien:
- **Additiv / Expand-Contract**: bestehende (Test-)Mandanten laufen unverändert als TISAX weiter; neue Spalten/Tabellen additiv, keine Datenmigration von Inhalten (nur Testdaten).
- **TISAX-Verhalten bleibt bit-genau erhalten** (Regressionsschutz) — ISO wird *daneben* gebaut, nicht *statt*.
- **Ein Mandant kann beide Frameworks führen** (n:m) — geteilte Belege/Policies, aber getrennte Katalog-/SoA-Sichten.
- **Feature-Flag**: ISO bleibt hinter einem Schalter, bis Katalog + Readiness + SoA abgenommen sind.
---
## 3. Entscheidungen (D) — vom Team/PO zu bestätigen
| # | Entscheidung | Empfehlung | Begründung |
|---|--------------|-----------|------------|
| **D1** | Framework-Kardinalität | **n:m** (Mandant kann ISO **und** TISAX) via eigener Tabelle `TenantFramework` | Nutzeranforderung „oder/und"; erlaubt per-Framework-Attribute (z. B. TISAX-Level, ISO-Zertifizierungsziel). |
| **D2** | `tisaxLevel` | bleibt vorerst auf `TenantSettings`, wird als **TISAX-scoped** dokumentiert (nur relevant, wenn TISAX aktiv); optional später in `TenantFramework.config` verschieben | Minimiert Migration + die ~356 dbForTenant-Aufrufstellen; kein Umbau bestehender Reads. |
| **D3** | Control-Speicherung | **String-ID beibehalten**, Framework als zusätzliche Dimension (kein Enum-Umbau) | `ControlAssessment.control` etc. nehmen ISO-Annex-A-IDs (A.5.1 …) ohne Schema-Bruch auf. |
| **D4** | Vorlagenpaket | ~~zweites Seed-Paket~~ → **entschieden 2026-08-21: ein Dokumentensatz, zwei Mappings** (`mapping.json` + `mapping-iso.json` in `seed/isms-vorlagenpaket-v2`). `framework`-Dimension auf `PolicyTemplateVersion` bleibt nötig (+ Unique `(framework, version)`). | Gleiche Dokument-Codes können in einem Mandanten nicht zweimal existieren (`@@unique([tenantId, code])`); der Umsetzungstext ist ohnehin normunabhängig. Siehe `FRAMEWORK-MAPPING-ISO27001.md`. |
| **D5** | Assessment-Modell | **Strategie-Interface** je Framework (Scope/Reifegrad/Ziel/SoA), TISAX = heutige 0–3/AL-Logik, ISO = SoA-Applicability + Umsetzungsstatus | ISO kennt kein AL2/AL3 und keine VDA-ISA-Reifegrade; saubere Trennung ohne TISAX-Regression. |
| **D6** | ISO-SoA | echte **Statement of Applicability** (Annex-A-Liste, anwendbar/ausgeschlossen + Begründung, Verknüpfung Policy/Evidence) — neu für ISO; TISAX behält sein Reifegrad-Assessment | ISO-27001-Kernartefakt fehlt heute fachlich. |
| **D7** | Rollout | **Feature-Flag** „ISO" + erst Test-Instanz; TISAX unverändert | Risikoarme Einführung; TISAX-Kunden unberührt. |
| **D8** | Katalog-Grundlage ISO | **Annex A (ISO/IEC 27001:2022, 93 Controls, 4 Themen)** + Klauseln 4–10 als Managementsystem-Anforderungen | Aktueller Normstand; 2022er Struktur. |
---
## 4. Zielarchitektur
### 4.1 Datenmodell (additiv)
- **`enum Framework { ISO_27001, TISAX }`**.
- **`model TenantFramework`** (n:m): `tenantId`, `framework`, `isPrimary Boolean`, `config Json` (per-Framework-Attribute, z. B. `{ tisaxLevel: "AL3" }` bzw. `{ certScope, certBodyTarget }`), Unique `(tenantId, framework)`. → in `TENANT_MODELS` (RLS) aufnehmen.
- **`PolicyTemplateVersion`**: neue Spalte `framework Framework`; Unique `(framework, version)` statt nur `version`; Default-Backfill `TISAX`.
- **`PolicyPackageState`** (Mandanten-Merker, `schema:1612`): um `framework` erweitern (je Framework eine importierte Version).
- **ISO-SoA** (neu): `model SoaEntry { tenantId, framework=ISO_27001, control (A.x.y), applicable Boolean, justification String, implementationStatus enum, linkedPolicyCode?, linkedEvidenceId? }` — RLS-scoped.
- **`Domain`-Enum**: ISO-Themen ergänzen bzw. `PROTOTYPE` als TISAX-only markieren; ISO-Controls mappen auf die 4 Annex-A-Themen (Organizational/People/Physical/Technological) → entweder neue Enum-Werte oder eine framework-abhängige Domain-Auflösung.
### 4.2 Strategie-Schicht (Kernstück)
Ein `FrameworkStrategy`-Interface kapselt alle TISAX-spezifischen Annahmen; je Framework eine Implementierung:
```
interface FrameworkStrategy {
key: Framework
resolvePackage(locale): PublishedPackage // template-store, framework-parametrisiert
loadCatalog(): { controls, titles, scope } // c1/c5/mapping bzw. Annex-A/Klauseln
scopeFilter(settings): ControlRow[] // TISAX: AL/Prüfziel · ISO: Applicability
targetFor(control, settings): AssessmentTarget // TISAX: Reifegrad 0–3 · ISO: Umsetzungsstatus/SoA
readinessView(rows): ReadinessSummary // nutzt generische readiness/gap-Engines
export(): ExportArtifact // TISAX: VDA-ISA · ISO: SoA + Annex-A-Gap
wizardSteps(): StepKey[] // framework-abhängige Sichtbarkeit/Inhalte
}
```
Bestehende Dateien werden hinter diese Schnittstelle gezogen: `assessment-level.ts`, `maturity.ts`, `scope-filter.ts`, `control-titles.ts`, `export/vda-isa*.ts` → `TisaxStrategy`. Die generischen Engines `readiness.ts`/`gap-consolidation.ts` bleiben und werden von beiden Strategien gefüttert.
### 4.3 Auflösung zur Laufzeit
- `template-store.ts`: `resolvePackageForTenant(tenant, framework, locale)` — wählt PUBLISHED-Version je `(framework, locale)`.
- `provisionTenant`: `ProvisionOpts.frameworks: Framework[]` → schreibt `TenantFramework`-Zeilen, importiert **je Framework** das passende Paket + Katalog, setzt nur bei TISAX die AL-Flags.
- UI/Server lösen die aktive Framework-Sicht über die Mandanten-`TenantFramework` + eine aktive Auswahl (bei Mehr-Framework: Umschalter, analog Mandantenwahl).
---
## 5. Workstreams / Lanes für das Team
Fünf Lanes + PM. Abhängigkeiten in Klammern.
### Lane 1 — Framework-Kern (Datenmodell, Provision, Auflösung) *(Fundament, zuerst)*
- `Framework`-Enum, `TenantFramework`-Tabelle (+ RLS/`TENANT_MODELS`), `PolicyTemplateVersion.framework` (+ Unique), `PolicyPackageState.framework` — additive Expand-Migrationen + Backfill bestehender Daten auf `TISAX`.
- `ProvisionOpts.frameworks` + framework-abhängige Paket-/Katalog-Auflösung in `provision.ts`.
- `template-store.ts` framework-parametrisieren.
- **DoD:** bestehende Mandanten laufen unverändert (framework=TISAX), neuer Mandant kann mit `frameworks:[ISO_27001]` **oder** `[TISAX]` **oder** beiden provisioniert werden; Gate grün.
### Lane 2 — ISO-Mapping & -Katalog *(inhaltlicher Teil erledigt; Rest braucht L1)*
- ✅ **erledigt (2026-08-21):** `mapping-iso.json` (27 Klauseln + 93 Annex-A-Controls) auf der bestehenden
Bibliothek; 19 ISO-only-Abschnitte ergänzt; Sichtbarkeit über `FLAG_FW_ISO27001`/`FLAG_FW_TISAX`;
SoA-Gerüst mit den Pflichtangaben aus 6.1.3 d); `_verify_iso.py` grün.
- ⬜ ISO-`control-titles`, ISO-Scope-/Control-Kataloge (Pendants zu `c1-scope.json`/`c5-controls.json`).
- ⬜ `parsePackageFiles`/`sync-policy-templates.ts` über **Frameworks × Sprachen** iterieren — heute ist
`mapping.json` fest verdrahtet, `mapping-iso.json` wird noch nicht gelesen.
- ✅ **erledigt:** Englische Fassung `isms-vorlagenpaket-v2-en` nachgezogen (eigene Texte, `--lang en`).
- **DoD:** ISO-Mapping importiert als eigene `PolicyTemplateVersion(framework=ISO_27001)`; ein ISO-Mandant
erhält Annex-A-Controls und sieht die ISO-Anforderungssicht.
### Lane 3 — Assessment-/Readiness-Abstraktion *(braucht L1; parallel zu L2)*
- `FrameworkStrategy`-Interface; heutige TISAX-Logik als `TisaxStrategy` extrahieren (verhaltensgleich!).
- `IsoStrategy`: Scope = Applicability; Ziel = Umsetzungsstatus (statt Reifegrad 0–3); Readiness/Gap über die bestehenden generischen Engines.
- `readiness.ts`/`gap-consolidation.ts` framework-parametrisiert füttern; keine TISAX-Regression.
- **DoD:** TISAX-Readiness identisch zu heute (Snapshot-Test); ISO liefert eine erste Readiness-/Gap-Sicht auf Annex-A-Basis.
### Lane 4 — ISO-SoA + Exporte *(braucht L2+L3)*
- Echte **SoA-Sicht** (`SoaEntry`): Annex-A-Liste, anwendbar/ausgeschlossen + Begründung, Verknüpfung Policy/Evidence, Umsetzungsstatus; Modul-Key `soa` beherbergt beide Sichten (TISAX-Reifegrad bleibt).
- ISO-Export: SoA-Dokument + Annex-A-Gap-Report; VDA-ISA-Export bleibt TISAX-only.
- **DoD:** ISO-Mandant kann eine vollständige SoA pflegen und exportieren.
### Lane 5 — Wizard, Settings/Admin & i18n *(braucht L1; UI-Feinschliff am Ende)*
- Framework-Auswahl in Admin (Mandant anlegen) + `/settings`; `setTenantTisaxLevel` → TISAX-scoped, ISO-Zertifizierungsziel analog.
- Onboarding-Registry framework-aware (`getVisibleSteps` guard je Framework): TISAX behält Scope/AL/Prüfziel + Controls-Reifegrad; ISO ersetzt AL-Schritt durch Applicability/SoA-Schritt.
- i18n: framework-neutrale Labels + per-Framework-Overrides; „TISAX/VDA-ISA"-Strings entkoppeln (`messages/de.json`/`en.json`, `settings/page.tsx`, `audit-readiness/*`, `admin/page.tsx`).
- **DoD:** Kunde wählt im Admin ISO und/oder TISAX; der Wizard zeigt die passenden Schritte; keine „TISAX"-Labels bei reinen ISO-Mandanten.
**PM:** Reihenfolge L1 → (L2 ∥ L3) → L4 → L5; Abnahme je Lane; Regressions-Gate für TISAX (Snapshot der heutigen Readiness/Exporte) als Pflicht vor jedem Merge.
---
## 6. Migration & Rollout
- **Expand:** additive Migrationen (Enum, `TenantFramework`, Spalten); Backfill: für jeden bestehenden Mandanten `TenantFramework(framework=TISAX, isPrimary=true)`; `PolicyTemplateVersion.framework=TISAX`.
- **Feature-Flag** „ISO 27001" (Plattform-Setting) gated Admin-Auswahl + Provisionierung, bis L2–L4 abgenommen.
- **Keine Inhaltsmigration** (nur Testdaten); neue ISO-Mandanten frisch provisioniert.
- **Contract (später):** ungenutzte TISAX-only-Felder erst nach stabilem Mehr-Framework-Betrieb aufräumen (z. B. `tisaxLevel` → `TenantFramework.config`).
## 7. Risiken & Gegenmaßnahmen
| Risiko | Gegenmaßnahme |
|--------|---------------|
| TISAX-Regression durch Refactoring | `TisaxStrategy` verhaltensgleich extrahieren; Snapshot-Tests der heutigen Readiness/Exporte als Merge-Gate. |
| ISO-Katalog-Qualität (Annex-A ↔ Policies) | Fachliches Mapping-Review (ISMS-Experte) vor L4; `mapping.json` als Single Source. |
| SoA-Semantik ist fachlich neu | Eigene Lane (L4) mit klarer Definition applicable/exclusion + Begründungspflicht. |
| Mehr-Framework-Komplexität in UI | Aktive-Framework-Umschalter analog Mandantenwahl; getrennte Katalog-Sichten. |
| `Domain.PROTOTYPE`/AL nur TISAX | Framework-abhängige Domain-/Scope-Auflösung; ISO ignoriert AL/Prototyp. |
| i18n-Wildwuchs „TISAX" | Zentrale framework-neutrale Keys + Overrides; Lint auf verbleibende Hardcodes. |
## 8. Grobe Aufwandsschätzung
| Lane | Aufwand (PT, grob) |
|------|--------------------|
| L1 Framework-Kern | 4–6 |
| L2 ISO-Paket & Katalog (inkl. fachliches Mapping) | 8–12 |
| L3 Assessment-Abstraktion | 5–8 |
| L4 ISO-SoA + Exporte | 5–8 |
| L5 Wizard/Settings/i18n | 4–6 |
| PM/Fachreview | durchgehend |
| **Summe** | **~26–40 PT**, L2/L3 parallelisierbar |
## 9. Offene Punkte (vom PO/Fachexperten zu klären)
- Umfang ISO-Vorlagenpaket: nur Annex-A-Controls oder auch Managementsystem-Klauseln 4–10 als geführte Artefakte? (Empfehlung: beides.)
- ~~Wie stark sollen ISO- und TISAX-Sicht bei Doppel-Framework **Belege/Policies teilen**?~~ → **entschieden:** ein gemeinsames Policy-Set, zwei Mappings (Variante A, 2026-08-21).
- ISO-Reifegrad optional zusätzlich zur Applicability (manche Kunden wollen Reifegrade auch unter ISO)?
- Zielformat ISO-Exporte (SoA-Dokument als Word/PDF/XLSX; Annex-A-Gap als XLSX).
-121
View File
@@ -1,121 +0,0 @@
# Incident-Management — Fachkonzept (Certvia)
Modul „Vorfälle" (Placeholder im Bestand). Umfang: **Standard** — erfassen → kategorisieren/bewerten → bearbeiten (Verantwortliche, Aufgaben, Maßnahmen) → abschließen + Lessons Learned. Rahmen: **ISO 27001** (A.5.24–5.28), **NIS2** (Meldepflicht-Bewusstsein + Fristen), **TISAX/VDA-ISA** (1.6.x). Kanäle: **intern manuell** + **E-Mail-to-Ticket**. **Keine Behörden-API** — NIS2-/DSGVO-Meldung wird **vorbereitet** (Fristen-Timer + Meldevorlage/Export), Übermittlung erfolgt manuell.
---
## 1. Rollen (RACI-Kurz)
| Rolle | Aufgabe |
|---|---|
| **Melder** (intern / E-Mail) | Meldet den Verdacht/Vorfall (Titel, Beschreibung, was/wann). |
| **ISB / Incident-Manager** | Triage, Kategorisierung, Bewertung, Steuerung, **Meldepflicht-Entscheidung**, Abschluss. |
| **IT / Bearbeiter** | Sofort-/Behebungsmaßnahmen umsetzen (als Aufgaben). |
| **Geschäftsführung** | Eskalation, Freigabe externer Meldungen. |
| **DSB** (optional) | Bei Personenbezug (DSGVO Art. 33/34). |
## 2. Kanäle (Intake)
- **Intern manuell:** berechtigte Rollen legen Vorfall über die UI an (Popup wie bei Assets/Aufgaben).
- **E-Mail-to-Ticket:** Zustellung an ein **Certvia-seitiges Eingangspostfach** (nicht an ein Kundenpostfach). Certvia betreibt eine Inbound-Domain und vergibt **je Mandant eine eindeutige, nicht erratbare Adresse** (z. B. `vorfall-<token>@in.certvia.de`); der Kunde nutzt sie direkt **oder** leitet von seiner eigenen Adresse (`vorfall@kunde.de`) dorthin **weiter**. Über die Zieladresse erfolgt die **Mandantenzuordnung**. Eingehende Mail → Vorfall im Status **Neu/Triage** (Betreff→Titel, Text→Beschreibung, Absender→Melder). Dedupe über Message-Header; **Absender-Allowlist** (nur interne Kundendomänen erzeugen Tickets) + SPF/DKIM/DMARC-Prüfung + Spam-Filter; externe/unbekannte Absender werden „extern" markiert (Triage). Anhänge später (Storage-Paket).
- **Inbound ist ein eigener Kanal:** SEC1 deckte nur den **Ausgang** (SMTP) ab; für den Empfang braucht es einen Empfangsweg. **Warum kein Kundenpostfach:** kein Speichern/Pollen von Kunden-IMAP/OAuth-Credentials → weniger Aufwand, robuster, DSGVO-/sicherheitsseitig sauberer.
- **Konkrete Umsetzung (All-inkl · einfachster Weg, gewählt):** Subdomain **`in.certvia.de`** bei All-inkl mit **Catch-all-Postfach** — alle `vorfall-<token>@in.certvia.de` landen in **einem** Postfach. *(Catch-all in All-inkl KAS: E-Mail → E-Mail-Postfach → „Neues Postfach anlegen", das **Adressfeld leer lassen**. Catch-all ist spam-anfällig → eigene, nirgends veröffentlichte Intake-Subdomain hält die Spam-Fläche klein; zusätzlich Allowlist/DKIM/Review-Pfad.)* Certvia holt die Mails per **IMAP** (kurzes Polling/IDLE, BullMQ-Job), parst sie (mailparser), ermittelt den **Token aus dem Empfänger-Header** (`Delivered-To`/`X-Envelope-To` — **nicht** `To`, da dort bei Weiterleitung die Kundenadresse steht), ordnet dem Mandanten zu, legt den Vorfall an und verschiebt die Mail nach „Verarbeitet"/„Fehler". Idempotenz über `Message-ID`; unbekannter/kein Token → **Betreiber-Review** statt Drop. **Kein Postfach je Kunde** (Catch-all + **Auto-Token**), keine KAS-API nötig; einmaliger globaler Setup (Subdomain + Catch-all + IMAP-Zugang als Plattform-Secret).
- **Weiterleitungs-Fallstricke:** Weiterleitung bricht i. d. R. **SPF** (DKIM bleibt meist gültig) → **nicht** hart auf SPF-Fail ablehnen; Vertrauen über **Absender-Allowlist + DKIM**, SPF nur als Signal. Auto-Reply/Bounce-Schleifen über `Auto-Submitted`/Precedence-Header erkennen und ignorieren. Größenlimit/Spam-Filter beachten.
- **Mail-Einstellungen:** die **mandantenspezifischen** Einstellungen (Intake-Adresse/Routing, Benachrichtigungspräferenzen, Absender-Anzeige) werden im **Einstellungs-Modul** gepflegt. Die **SMTP-Zugangsdaten/der Transport** bleiben aus Sicherheitsgründen auf **Plattform-/Secret-Store-Ebene** (SEC1) — kein Mandant legt Server-Credentials selbst an.
## 3. Lebenszyklus / Statusmodell
**Primärfluss:**
`Neu/Eingegangen → Triage → In Bearbeitung → Eingedämmt (contained) → Behoben → Abgeschlossen` · (+ `Wiedereröffnet`)
- Jeder Übergang: Pflichtfelder-Check, Zeitstempel, Akteur, Audit-Eintrag.
- **Parallel-Track „Meldung"** (nur wenn meldepflichtig): `Meldepflicht geprüft → Erstmeldung (24 h) → Folgemeldung (72 h) → Abschlussbericht (1 Monat)` — als **Status + Timer**, Übermittlung manuell.
## 4. Datenmodell (Incident-Objekt)
- **Kennung:** `refNo` (z. B. `INC-2026-0042`), `tenantId` (RLS).
- **Basis:** Titel, Beschreibung, **Kanal/Quelle** (manuell/E-Mail), Melder (+Kontakt).
- **Zeiten:** `occurredAt` (Eintritt), `detectedAt` (Entdeckung), `reportedAt` (interne Meldung), Timeline.
- **Kategorie** (Taxonomie): Schadsoftware · Phishing/Social Engineering · Unbefugter Zugriff · Datenabfluss/-verlust · Systemausfall/Verfügbarkeit · Physisch (Zutritt/Diebstahl) · Fehlbedienung/Konfiguration · Lieferant/Drittpartei · **Prototyp/Kundendaten** (TISAX) · Sonstiges.
- **Betroffenheit:** verknüpfte **Assets/Prozesse (BIA)**, Schutzziel-Impact **C/I/A**, Datenkategorien, **Personenbezug** (→ DSGVO-Flag), **Prototyp/Kundendaten** (→ TISAX-Flag).
- **Bewertung:** **Schweregrad/Priorität** (siehe §5).
- **Steuerung:** `owner` (Incident-Manager), Bearbeiter, Status.
- **Meldepflicht:** `nis2Relevant`, `dsgvoRelevant` (bool) + Meldestatus + Fristen (§6).
- **Behebung:** Sofortmaßnahmen, **Ursache (Root Cause)**, Lösung/Resolution.
- **Abschluss:** Abschlussnotiz, **Lessons Learned**, verknüpfte **CAPA-Aufgaben**.
- **Verknüpfungen:** **Maßnahmen** (im zentralen Maßnahmen-Modul, §9), **Risiken** (bestätigt/neu), **Controls** (welche versagten/betroffen), **Nachweise**.
- **Kommentare/Notizen:** **Kommentar-Thread** am Vorfall (Autor, Zeitstempel, editierbar nach Regel) für die Zusammenarbeit — **getrennt** von der automatischen, manipulationssicheren **Audit-Timeline** (§8/§9). Interne vs. sichtbare Kommentare optional.
- **Anhänge:** Belege (Screenshots/Logs) — Modell vorbereiten, Datei-Persistenz mit Storage-Paket.
## 5. Schweregrad / Priorisierung
- **Schweregrad** aus **Auswirkung** (C/I/A-Verletzung × Umfang: einzelnes System … unternehmensweit … Kunde/Lieferkette) und **Dringlichkeit** → Klassen **niedrig · mittel · hoch · kritisch**.
- Treibt **interne SLA** (Reaktion/Behebung), **Eskalation** und **Benachrichtigungen**. Matrix mandantenkonfigurierbar (Default vorgegeben).
## 6. Fristen & Timer (vorbereitet, ohne Behörden-API)
| Auslöser | Frist | Umsetzung im Tool |
|---|---|---|
| **NIS2** – Früh-/Erstmeldung | **24 h** ab Kenntnis | Timer/Countdown + Erinnerung (SEC1) + Meldevorlage |
| **NIS2** – Meldung | **72 h** | Timer + Vorlage (Aktualisierung) |
| **NIS2** – Abschlussbericht | **1 Monat** | Timer + Abschluss-Vorlage/Export |
| **DSGVO** Art. 33 (bei Personenbezug) | **72 h** | Timer + Datenschutz-Meldevorlage |
| **Interne SLA** (je Severity) | konfigurierbar | Reaktions-/Behebungs-Timer |
- Timer nur, wenn **Meldepflicht = ja** (NIS2-Betroffenheit des Mandanten aus den Einstellungen). Countdown im Vorfall + Dashboard-Kachel; **Eskalation** bei drohender/verpasster Frist. **Übermittlung an die Behörde erfolgt manuell** (Vorlage/Export bereitgestellt).
## 7. Benachrichtigungen (SEC1)
Neuer Vorfall → ISB/Incident-Manager; Zuweisung → Bearbeiter; Statuswechsel → Beteiligte; **Fristen-Erinnerung/Eskalation**; Abschluss → Melder/GF. Respektiert Benachrichtigungspräferenzen + Mandantenisolation.
## 8. Abschluss & Lessons Learned
- Pflicht beim Abschluss: **Ursache**, **Lösung**, Wirksamkeit der Maßnahmen, **Lessons Learned**.
- **CAPA**: korrigierende/präventive Maßnahmen als **Aufgaben** anlegen; **Risikoregister** aktualisieren (Risiko bestätigt/neu); Bezug zu betroffenen **Controls**.
- Optionaler **Post-Incident-Review** (Kurzbericht) → speist **Management-Review**.
## 9. Verknüpfung zu bestehenden Modulen
- **Maßnahmen-Modul** (vorhanden): Sofort- und CAPA-Maßnahmen werden **im zentralen Maßnahmen-Modul angelegt/gepflegt** (nicht doppelt im Vorfall) und mit dem Vorfall **verknüpft**; im Vorfall erscheinen sie als verknüpfte Liste mit „Maßnahme direkt aus dem Vorfall anlegen". Verantwortliche/Fristen/Status kommen aus dem Maßnahmen-Modul; Bezug zu Risiken/Controls möglich. *(Hinweis: falls „Maßnahmen"/„Aufgaben" heute getrennt sind, an **eine** zentrale Maßnahmen-/Aufgaben-Backbone andocken.)*
- **Kommentare** dagegen liegen **am Vorfall** (Thread, §4) — sie sind Zusammenarbeit, keine trackbare Maßnahme.
- **Assets/BIA · Risiken · Controls**: Betroffenheit und Wirkung verknüpfen; Vorfall kann Risiko bestätigen/erzeugen.
- **Audit-Log**: Vorfall-Timeline (wer/wann/was) manipulationssicher (getrennt von den Kommentaren).
- **Nachweise/Export**: Vorfallregister + Einzelbericht (DOCX/PDF über Export-Layer), NIS2-/DSGVO-Meldevorlagen (vorbefüllt) für die manuelle Übermittlung.
## 10. Framework-Mapping
| Rahmen | Bezug |
|---|---|
| **ISO 27001:2022** | A.5.24 Planung/Vorbereitung · A.5.25 Bewertung/Entscheidung · A.5.26 Reaktion · A.5.27 Lernen · A.5.28 Beweissicherung |
| **TISAX / VDA-ISA** | 1.6.x Incident-/Ereignismanagement; Prototyp-/Kundendaten-Bezug |
| **NIS2** (Art. 23) | Meldekette 24 h/72 h/1 Monat — im Tool als Timer/Vorlage vorbereitet |
| **DSGVO** | Art. 33/34 Datenpanne (72 h) — Timer/Vorlage |
## 11. Rechte / Sicherheit
- Strikt **mandantengebunden (RLS)**; rollenbasierte Rechte (melden vs. bearbeiten vs. abschließen/melden).
- **Vertraulichkeit:** sensible Vorfälle optional auf einen eingeschränkten Personenkreis begrenzbar.
- Meldevorlagen/Exports datensparsam; jede Aktion auditiert.
## 12. Technische Einordnung
- **Eigenes Modul „Vorfälle"** (`incidents`), modul-gated (Superadmin-Toggle); **Maßnahmen** über das zentrale **Maßnahmen-Modul** (verknüpft); **Kommentare** als eigener Thread am Vorfall; **Mail** über SEC1; **Timeline** über Audit; Verknüpfungen zu Assets/Risiken/Controls.
- **Mail-Einstellungen:** mandantenseitig im **Einstellungs-Modul** (Intake-Adresse, Benachrichtigungen); SMTP-Transport plattformseitig (SEC1).
- Popups/Bedienung im etablierten Muster (wie Assets/Maßnahmen).
## 12a. Provisionierung des Intake-Postfachs (Betreiberportal & Onboarding)
Das Anlegen der Inbound-Route (`vorfall-<token>@in.certvia.de`) ist zunächst ein **manueller Betreiber-Schritt** — dafür braucht es die richtigen Infos beim Onboarding und einen sichtbaren Status.
**Beim Kunden-Onboarding erfassen** (Admin-Konsole „Kunde anlegen", nur wenn Modul „Vorfälle" + E-Mail-to-Ticket aktiv):
- **Absender-/Weiterleitungs-Domäne(n)** des Kunden (z. B. `kunde.de`) → **Allowlist**.
- optional konkrete **Quelladresse** (`vorfall@kunde.de`), von der weitergeleitet wird.
- **Intake-Adresse** wird von Certvia **automatisch generiert** (`vorfall-<token>@in.certvia.de`) und angezeigt (der Kunde richtet die Weiterleitung darauf ein).
- optional Benachrichtigungsempfänger/Sprache.
**Provisionierungs-Status je Kunde** (Betreiberportal): `Postfach anzulegen → angelegt → verifiziert`.
- Solange „anzulegen": **Hinweis-Badge** am Kundendatensatz („⚠ Intake-Postfach anlegen") **und** eine **Sammelliste offener Provisionierungen** auf dem Betreiber-Dashboard.
- **Betreiber-Schritt:** Inbound-Route/Alias im Inbound-Dienst anlegen (manuell **oder** per Provider-API) → Status „angelegt". **Verifizierung** per Test-Mail (analog SEC1-Testversand) → „verifiziert".
- **Ausbau:** bei Inbound-Providern mit API kann die Route beim Modul-Aktivieren **automatisch** angelegt werden → Status springt direkt auf „angelegt"; bis dahin bleibt es der manuelle Hinweis-Schritt.
- Alle Provisionierungs-Aktionen im **Plattform-Audit** (`scope=platform`).
> **Mit der gewählten Variante (All-inkl Catch-all + Auto-Token):** Es muss **kein Postfach je Kunde** angelegt werden; der **Token wird automatisch generiert**. Der einmalige Setup (Subdomain `in.certvia.de` + Catch-all-Postfach + IMAP-Zugang) ist ein **globaler** Betreiber-Schritt, nicht je Kunde. Der **per-Kunde-Schritt reduziert sich** darauf, die **Weiterleitung des Kunden per Test-Mail zu verifizieren** → vereinfachter Status je Kunde: `Weiterleitung ausstehend → verifiziert`. Der Onboarding-Datenbedarf bleibt (Allowlist-Domänen, Quelladresse); die Intake-Adresse wird automatisch erzeugt/angezeigt.
## 13. Abgrenzung / spätere Ausbaustufen
- **Direkte Behörden-Übermittlung** (BSI-Meldeportal/-API) — jetzt bewusst **nicht** (nur Vorlage/Export).
- Automatische Erkennung/**SIEM-Integration**, **externe/anonyme** Meldung, SLA-Automationen, KI-gestützte Klassif/Zusammenfassung — spätere Stufen.
## 14. Offene Entscheidungen
1. **NIS2-Betroffenheit je Mandant** — woher (Mandanten-Einstellungen: „wichtige/wesentliche Einrichtung"?), steuert die Timer.
2. **Severity-Matrix** — fester Default oder mandantenkonfigurierbar?
3. **Inbound (entschieden):** All-inkl **Catch-all-Postfach** auf `in.certvia.de` + **IMAP-Abholung**, **Auto-Token**, kein Postfach je Kunde. Rest-Details: Polling-Intervall vs. IMAP-IDLE, welcher Header trägt den Token (`Delivered-To`/`X-Envelope-To` — beim ersten Test verifizieren), Ordner-/Fehler-Handling.
4. **Anhänge** — abhängig vom Storage-Paket (bis dahin nur Referenz/Text).
5. **Vertraulichkeits-Stufen** für sensible Vorfälle — jetzt oder später?
---
*Nächster Schritt auf Wunsch: Entwickler-Task-Paket (Branches, Stories, DoD) — Maßnahmen auf das Task-Modul aufsetzend, Mail über SEC1, Timer/Meldevorlagen NIS2/DSGVO.*
-355
View File
@@ -1,355 +0,0 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>certvia – BIA-Prozessübersicht (Mockup)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
<style>
:root{
--violet:#5d52a3; --violet2:#7d6fd6; --magenta:#812d80; --blue:#8dc4e0; --ink:#3b3b3a;
--violet-050:#f2f0f9; --violet-100:#e6e2f3; --blue-050:#eef7fb;
--grad-soft:linear-gradient(135deg,#5d52a3 0%,#8dc4e0 100%);
--bg:#f5f6fa; --card:#ffffff; --line:#e7e8ef; --line2:#eff0f5; --muted:#7a7b86;
--soft:#fafbfd;
--ok:#2e9e6b; --warn:#e0982e; --risk:#d64c4c; --info:#3a86c8;
--ok-bg:#e9f6ef; --warn-bg:#fdf3e2; --risk-bg:#fbe9e9; --info-bg:#e9f2fb; --gray-bg:#eef0f4;
--shadow:0 1px 3px rgba(30,25,60,.06),0 6px 20px rgba(30,25,60,.05);
}
*{box-sizing:border-box}
html,body{margin:0}
body{font-family:"Open Sans",system-ui,sans-serif;color:var(--ink);background:var(--bg);font-size:14px}
h1,h2,h3,h4{font-family:"Poppins",sans-serif;font-weight:600;margin:0}
.wrap{max-width:1180px;margin:0 auto;padding:26px 26px 60px}
/* Page head */
.crumb{font-size:12px;color:var(--muted);margin-bottom:3px}
.pagehead{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}
.pagehead h1{font-size:23px}
.pagehead .sub{color:var(--muted);margin-top:5px;font-size:13px;max-width:640px;line-height:1.5}
.head-actions{display:flex;align-items:center;gap:10px}
.toggle{display:flex;background:#fff;border:1px solid var(--line);border-radius:10px;padding:3px;gap:2px}
.toggle button{border:0;background:transparent;font-family:Poppins;font-weight:600;font-size:12.5px;color:var(--muted);padding:6px 12px;border-radius:8px;cursor:pointer;display:flex;align-items:center;gap:6px}
.toggle button.active{background:var(--violet-100);color:var(--violet)}
.btn{border:0;border-radius:10px;padding:9px 15px;font-weight:600;font-family:Poppins;font-size:13px;cursor:pointer;display:inline-flex;align-items:center;gap:7px}
.btn.primary{background:var(--grad-soft);color:#fff}
/* KPI */
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:20px 0 6px}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);padding:15px 16px}
.kpi .lbl{color:var(--muted);font-size:12px;font-weight:600}
.kpi .val{font-family:Poppins;font-weight:700;font-size:27px;margin:5px 0 0;line-height:1}
.kpi .val small{font-size:14px;color:var(--muted);font-weight:600}
.kpi .bar{height:6px;border-radius:6px;background:#eef0f6;overflow:hidden;margin-top:11px}
.kpi .bar>i{display:block;height:100%;border-radius:6px}
/* Legend */
.legend{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin:16px 2px 4px;font-size:12px;color:var(--muted)}
.legend .grp{display:flex;align-items:center;gap:8px}
.legend .sw{width:12px;height:12px;border-radius:3px;display:inline-block}
.legend .dot{width:10px;height:10px;border-radius:50%;display:inline-block}
.legend b{color:var(--ink);font-weight:600}
/* Lanes */
.lane{margin-top:22px}
.lane-head{display:flex;align-items:center;gap:10px;margin:0 2px 12px}
.lane-head .tag{font-family:Poppins;font-weight:600;font-size:14px}
.lane-head .cnt{font-size:11.5px;color:var(--muted);background:#fff;border:1px solid var(--line);border-radius:999px;padding:2px 9px;font-weight:600}
.lane-head .rule{flex:1;height:1px;background:var(--line)}
.lane-mgmt .tag{color:var(--magenta)}
.lane-core .tag{color:var(--violet)}
.lane-supp .tag{color:var(--info)}
/* Main process card */
.mp{background:var(--card);border:1px solid var(--line);border-left-width:4px;border-radius:14px;box-shadow:var(--shadow);margin-bottom:13px;overflow:hidden}
.mp.st-komplett{border-left-color:var(--ok)}
.mp.st-teilweise{border-left-color:var(--warn)}
.mp.st-offen{border-left-color:#c4c8d2}
.mp-head{display:flex;align-items:center;gap:14px;padding:14px 16px;cursor:pointer;user-select:none}
.mp-head:hover{background:var(--soft)}
.chev{width:16px;height:16px;flex:0 0 16px;color:var(--muted);transition:transform .18s}
details[open] .chev{transform:rotate(90deg)}
.mp-title{min-width:0;flex:1}
.mp-title .nm{font-family:Poppins;font-weight:600;font-size:14.5px;display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.mp-title .meta{margin-top:4px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;color:var(--muted);font-size:12px}
.owner{display:inline-flex;align-items:center;gap:6px}
.owner .av{width:19px;height:19px;border-radius:50%;background:var(--grad-soft);color:#fff;display:grid;place-items:center;font-size:9.5px;font-family:Poppins;font-weight:700}
/* metrics strip */
.metrics{display:flex;gap:8px;flex:0 0 auto}
.m{background:var(--soft);border:1px solid var(--line2);border-radius:9px;padding:5px 10px;text-align:center;min-width:58px}
.m .k{font-size:9.5px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);font-weight:700}
.m .v{font-family:Poppins;font-weight:600;font-size:13.5px;margin-top:1px}
/* pills */
.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:999px;font-size:11px;font-weight:700;white-space:nowrap}
.pill .dot{width:7px;height:7px;border-radius:50%}
.crit-1{background:var(--gray-bg);color:#5b6070}
.crit-2{background:var(--info-bg);color:var(--info)}
.crit-3{background:var(--warn-bg);color:#b9761b}
.crit-4{background:var(--risk-bg);color:var(--risk)}
.st-pill.komplett{background:var(--ok-bg);color:var(--ok)}
.st-pill.teilweise{background:var(--warn-bg);color:#b9761b}
.st-pill.offen{background:var(--gray-bg);color:#5b6070}
.tag-cat{background:var(--violet-050);color:var(--violet);border:1px solid var(--violet-100);padding:2px 8px;border-radius:6px;font-size:10.5px;font-weight:700}
/* dependencies row */
.deps{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:0 16px 12px 46px}
.deps .lab{font-size:11px;color:var(--muted);font-weight:600;display:inline-flex;align-items:center;gap:5px}
.dep{display:inline-flex;align-items:center;gap:6px;background:#fff;border:1px solid var(--line);border-radius:999px;padding:3px 10px;font-size:11.5px;font-weight:600;color:#4b4c57}
.dep.crossref{border-style:dashed;border-color:var(--violet2);color:var(--violet)}
.dep .ic{width:12px;height:12px;opacity:.7}
/* sub processes tree */
.subs{padding:2px 16px 14px 30px;background:var(--soft);border-top:1px dashed var(--line)}
.subs-h{font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);font-weight:700;margin:10px 0 8px 16px}
.sub{position:relative;display:flex;align-items:center;gap:12px;padding:9px 12px 9px 16px;margin-left:16px;border-radius:10px}
.sub:hover{background:#fff}
/* tree connector */
.sub::before{content:"";position:absolute;left:0;top:-6px;bottom:50%;width:1px;background:#d6dae3}
.sub::after{content:"";position:absolute;left:0;top:50%;width:12px;height:1px;background:#d6dae3}
.sub:last-child::before{bottom:50%}
.sub .sdot{width:9px;height:9px;border-radius:50%;flex:0 0 9px;z-index:1;box-shadow:0 0 0 3px var(--soft)}
.sdot.komplett{background:var(--ok)} .sdot.teilweise{background:var(--warn)} .sdot.offen{background:#c4c8d2}
.sub .snm{flex:1;min-width:0}
.sub .snm .t{font-weight:600;font-size:13px}
.sub .snm .s{color:var(--muted);font-size:11.5px;margin-top:1px;display:flex;gap:10px;flex-wrap:wrap}
.sub .metrics .m{background:#fff}
.sub .right{display:flex;align-items:center;gap:8px}
.mini{font-size:11px;color:var(--muted);display:inline-flex;align-items:center;gap:5px}
.note{margin-top:26px;background:var(--violet-050);border:1px solid var(--violet-100);border-radius:12px;padding:14px 16px;font-size:12.5px;color:#4b4c57;line-height:1.6}
.note b{color:var(--violet)}
.foot{margin-top:22px;color:var(--muted);font-size:11.5px;text-align:center}
@media(max-width:820px){
.kpis{grid-template-columns:repeat(2,1fr)}
.metrics{display:none}
.mp-head{flex-wrap:wrap}
}
</style>
</head>
<body>
<div class="wrap">
<div class="pagehead">
<div>
<div class="crumb">Strukturanalyse · GEFIM (TISAX)</div>
<h1>Business Impact Analyse — Prozessübersicht</h1>
<div class="sub">Prozesse gegliedert nach Haupt- und Teilprozessen. Farbe = BIA-Status, Kritikalität nach Maximumprinzip aus den Teilprozessen abgeleitet. Abhängigkeiten zeigen, welche Prozesse voneinander bzw. von gemeinsamen Diensten abhängen.</div>
</div>
<div class="head-actions">
<div class="toggle">
<button class="active" title="Gruppierte Prozesshaus-Ansicht">▤ Prozesshaus</button>
<button title="Klassische Tabelle">≣ Tabelle</button>
</div>
<button class="btn primary">+ Prozess</button>
</div>
</div>
<!-- KPIs -->
<div class="kpis">
<div class="kpi"><div class="lbl">Prozesse gesamt</div><div class="val">24 <small>/ 6 Haupt</small></div><div class="bar"><i style="width:100%;background:var(--grad-soft)"></i></div></div>
<div class="kpi"><div class="lbl">Im Scope</div><div class="val">21 <small>/ 24</small></div><div class="bar"><i style="width:87%;background:var(--violet2)"></i></div></div>
<div class="kpi"><div class="lbl">BIA vollständig</div><div class="val">13 <small>/ 21</small></div><div class="bar"><i style="width:62%;background:var(--ok)"></i></div></div>
<div class="kpi"><div class="lbl">Kritische Prozesse</div><div class="val" style="color:var(--risk)">4</div><div class="bar"><i style="width:19%;background:var(--risk)"></i></div></div>
</div>
<!-- Legend -->
<div class="legend">
<div class="grp"><b>BIA-Status:</b></div>
<div class="grp"><span class="dot" style="background:var(--ok)"></span> komplett</div>
<div class="grp"><span class="dot" style="background:var(--warn)"></span> teilweise</div>
<div class="grp"><span class="dot" style="background:#c4c8d2"></span> offen</div>
<div class="grp" style="margin-left:8px"><b>Kritikalität:</b></div>
<div class="grp"><span class="sw crit-1" style="background:#d3d7e0"></span> 1 niedrig</div>
<div class="grp"><span class="sw" style="background:var(--info)"></span> 2 mittel</div>
<div class="grp"><span class="sw" style="background:var(--warn)"></span> 3 hoch</div>
<div class="grp"><span class="sw" style="background:var(--risk)"></span> 4 kritisch</div>
</div>
<!-- ============ MANAGEMENT ============ -->
<div class="lane lane-mgmt">
<div class="lane-head"><span class="tag">Managementprozesse</span><span class="cnt">1 Hauptprozess · 3 Teilprozesse</span><span class="rule"></span></div>
<details class="mp st-teilweise" open>
<summary class="mp-head">
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M9 6l6 6-6 6"/></svg>
<div class="mp-title">
<div class="nm">Unternehmenssteuerung &amp; ISMS <span class="tag-cat">Management</span></div>
<div class="meta">
<span class="owner"><span class="av">GF</span> M. Geschäftsführung</span>
<span>·</span><span>3 Teilprozesse</span>
</div>
</div>
<div class="metrics">
<div class="m"><div class="k">RTO</div><div class="v">24 h</div></div>
<div class="m"><div class="k">RPO</div><div class="v">24 h</div></div>
<div class="m"><div class="k">MTD</div><div class="v">72 h</div></div>
</div>
<span class="pill crit-2"><span class="dot" style="background:var(--info)"></span> Krit. 2</span>
<span class="pill st-pill teilweise">teilweise</span>
</summary>
<div class="deps">
<span class="lab">↳ benötigt:</span>
<span class="dep crossref"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg> IT-Betrieb</span>
<span class="dep"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg> Personalmanagement</span>
</div>
<div class="subs">
<div class="subs-h">Teilprozesse</div>
<div class="sub">
<span class="sdot komplett"></span>
<div class="snm"><div class="t">Risikomanagement</div><div class="s"><span>Owner: ISB</span><span>Schnittstelle: Risiko-Modul</span></div></div>
<div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">48 h</div></div><div class="m"><div class="k">RPO</div><div class="v">24 h</div></div><div class="m"><div class="k">MTD</div><div class="v">1 W</div></div></div>
<div class="right"><span class="pill crit-2">Krit. 2</span></div>
</div>
<div class="sub">
<span class="sdot teilweise"></span>
<div class="snm"><div class="t">Interne Audits</div><div class="s"><span>Owner: ISB</span></div></div>
<div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">1 W</div></div><div class="m"><div class="k">RPO</div><div class="v">1 W</div></div><div class="m"><div class="k">MTD</div><div class="v">2 W</div></div></div>
<div class="right"><span class="pill crit-1">Krit. 1</span></div>
</div>
<div class="sub">
<span class="sdot offen"></span>
<div class="snm"><div class="t">Managementbewertung</div><div class="s"><span>Owner: GF</span><span style="color:var(--warn)">BIA offen</span></div></div>
<div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">—</div></div><div class="m"><div class="k">RPO</div><div class="v">—</div></div><div class="m"><div class="k">MTD</div><div class="v">—</div></div></div>
<div class="right"><span class="mini">BIA erfassen →</span></div>
</div>
</div>
</details>
</div>
<!-- ============ KERNPROZESSE ============ -->
<div class="lane lane-core">
<div class="lane-head"><span class="tag">Kernprozesse</span><span class="cnt">2 Hauptprozesse · 8 Teilprozesse</span><span class="rule"></span></div>
<details class="mp st-komplett" open>
<summary class="mp-head">
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M9 6l6 6-6 6"/></svg>
<div class="mp-title">
<div class="nm">Produktentwicklung / Engineering <span class="tag-cat">Kern</span></div>
<div class="meta"><span class="owner"><span class="av">HE</span> H. Entwicklung</span><span>·</span><span>4 Teilprozesse</span></div>
</div>
<div class="metrics">
<div class="m"><div class="k">RTO</div><div class="v">8 h</div></div>
<div class="m"><div class="k">RPO</div><div class="v">4 h</div></div>
<div class="m"><div class="k">MTD</div><div class="v">24 h</div></div>
</div>
<span class="pill crit-4"><span class="dot" style="background:var(--risk)"></span> Krit. 4</span>
<span class="pill st-pill komplett">komplett</span>
</summary>
<div class="deps">
<span class="lab">↳ benötigt:</span>
<span class="dep crossref"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg> IT-Betrieb · CAD-Systeme</span>
<span class="dep"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg> Einkauf · Musterteile</span>
<span class="dep" style="border-style:dashed;border-color:var(--magenta);color:var(--magenta)">◆ Prototypenschutz</span>
</div>
<div class="subs">
<div class="subs-h">Teilprozesse</div>
<div class="sub"><span class="sdot komplett"></span><div class="snm"><div class="t">Konstruktion (CAD)</div><div class="s"><span>Owner: H. Entwicklung</span><span>Assets: CAD-Server, PLM</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">8 h</div></div><div class="m"><div class="k">RPO</div><div class="v">4 h</div></div><div class="m"><div class="k">MTD</div><div class="v">24 h</div></div></div><div class="right"><span class="pill crit-4">Krit. 4</span></div></div>
<div class="sub"><span class="sdot komplett"></span><div class="snm"><div class="t">Prototypenbau</div><div class="s"><span>Owner: Werkstattleitung</span><span style="color:var(--magenta)">◆ Prototypenschutz</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">1 T</div></div><div class="m"><div class="k">RPO</div><div class="v">8 h</div></div><div class="m"><div class="k">MTD</div><div class="v">3 T</div></div></div><div class="right"><span class="pill crit-3">Krit. 3</span></div></div>
<div class="sub"><span class="sdot komplett"></span><div class="snm"><div class="t">Anforderungsmanagement</div><div class="s"><span>Owner: Projektleitung</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">2 T</div></div><div class="m"><div class="k">RPO</div><div class="v">1 T</div></div><div class="m"><div class="k">MTD</div><div class="v">1 W</div></div></div><div class="right"><span class="pill crit-2">Krit. 2</span></div></div>
<div class="sub"><span class="sdot teilweise"></span><div class="snm"><div class="t">Erprobung &amp; Test</div><div class="s"><span>Owner: QS</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">2 T</div></div><div class="m"><div class="k">RPO</div><div class="v">1 T</div></div><div class="m"><div class="k">MTD</div><div class="v">1 W</div></div></div><div class="right"><span class="pill crit-2">Krit. 2</span></div></div>
</div>
</details>
<details class="mp st-teilweise">
<summary class="mp-head">
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M9 6l6 6-6 6"/></svg>
<div class="mp-title">
<div class="nm">Auftragsabwicklung <span class="tag-cat">Kern</span></div>
<div class="meta"><span class="owner"><span class="av">VL</span> Vertriebsleitung</span><span>·</span><span>4 Teilprozesse</span></div>
</div>
<div class="metrics">
<div class="m"><div class="k">RTO</div><div class="v">4 h</div></div>
<div class="m"><div class="k">RPO</div><div class="v">1 h</div></div>
<div class="m"><div class="k">MTD</div><div class="v">24 h</div></div>
</div>
<span class="pill crit-4"><span class="dot" style="background:var(--risk)"></span> Krit. 4</span>
<span class="pill st-pill teilweise">teilweise</span>
</summary>
<div class="deps">
<span class="lab">↳ benötigt:</span>
<span class="dep crossref"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg> IT-Betrieb · ERP</span>
<span class="dep"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg> Einkauf &amp; Lieferanten</span>
</div>
<div class="subs">
<div class="subs-h">Teilprozesse</div>
<div class="sub"><span class="sdot komplett"></span><div class="snm"><div class="t">Fertigung</div><div class="s"><span>Owner: Produktionsleitung</span><span>Assets: MES, Maschinen</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">4 h</div></div><div class="m"><div class="k">RPO</div><div class="v">1 h</div></div><div class="m"><div class="k">MTD</div><div class="v">24 h</div></div></div><div class="right"><span class="pill crit-4">Krit. 4</span></div></div>
<div class="sub"><span class="sdot teilweise"></span><div class="snm"><div class="t">Produktionsplanung</div><div class="s"><span>Owner: Arbeitsvorbereitung</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">8 h</div></div><div class="m"><div class="k">RPO</div><div class="v">4 h</div></div><div class="m"><div class="k">MTD</div><div class="v">2 T</div></div></div><div class="right"><span class="pill crit-3">Krit. 3</span></div></div>
<div class="sub"><span class="sdot komplett"></span><div class="snm"><div class="t">Versand &amp; Logistik</div><div class="s"><span>Owner: Logistik</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">1 T</div></div><div class="m"><div class="k">RPO</div><div class="v">8 h</div></div><div class="m"><div class="k">MTD</div><div class="v">3 T</div></div></div><div class="right"><span class="pill crit-2">Krit. 2</span></div></div>
<div class="sub"><span class="sdot offen"></span><div class="snm"><div class="t">Angebot &amp; Kalkulation</div><div class="s"><span>Owner: Vertrieb</span><span style="color:var(--warn)">BIA offen</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">—</div></div><div class="m"><div class="k">RPO</div><div class="v">—</div></div><div class="m"><div class="k">MTD</div><div class="v">—</div></div></div><div class="right"><span class="mini">BIA erfassen →</span></div></div>
</div>
</details>
</div>
<!-- ============ UNTERSTÜTZEND ============ -->
<div class="lane lane-supp">
<div class="lane-head"><span class="tag">Unterstützende Prozesse</span><span class="cnt">3 Hauptprozesse · 8 Teilprozesse</span><span class="rule"></span></div>
<details class="mp st-komplett" open>
<summary class="mp-head">
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M9 6l6 6-6 6"/></svg>
<div class="mp-title">
<div class="nm">IT-Betrieb <span class="tag-cat">Support</span> <span class="pill" style="background:var(--violet-050);color:var(--violet);border:1px dashed var(--violet2)">▲ 3 Prozesse hängen hiervon ab</span></div>
<div class="meta"><span class="owner"><span class="av">IT</span> IT-Leitung</span><span>·</span><span>4 Teilprozesse</span></div>
</div>
<div class="metrics">
<div class="m"><div class="k">RTO</div><div class="v">4 h</div></div>
<div class="m"><div class="k">RPO</div><div class="v">1 h</div></div>
<div class="m"><div class="k">MTD</div><div class="v">24 h</div></div>
</div>
<span class="pill crit-4"><span class="dot" style="background:var(--risk)"></span> Krit. 4</span>
<span class="pill st-pill komplett">komplett</span>
</summary>
<div class="deps">
<span class="lab">▲ wird benötigt von:</span>
<span class="dep crossref">Produktentwicklung</span>
<span class="dep crossref">Auftragsabwicklung</span>
<span class="dep crossref">Unternehmenssteuerung</span>
</div>
<div class="subs">
<div class="subs-h">Teilprozesse</div>
<div class="sub"><span class="sdot komplett"></span><div class="snm"><div class="t">Netzwerk &amp; Infrastruktur</div><div class="s"><span>Owner: Netzwerkadmin</span><span>Assets: Core-Switch, FW</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">4 h</div></div><div class="m"><div class="k">RPO</div><div class="v">1 h</div></div><div class="m"><div class="k">MTD</div><div class="v">24 h</div></div></div><div class="right"><span class="pill crit-4">Krit. 4</span></div></div>
<div class="sub"><span class="sdot komplett"></span><div class="snm"><div class="t">Backup &amp; Recovery</div><div class="s"><span>Owner: IT-Betrieb</span><span>VA-08</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">8 h</div></div><div class="m"><div class="k">RPO</div><div class="v">1 h</div></div><div class="m"><div class="k">MTD</div><div class="v">24 h</div></div></div><div class="right"><span class="pill crit-4">Krit. 4</span></div></div>
<div class="sub"><span class="sdot komplett"></span><div class="snm"><div class="t">Client-/Server-Betrieb</div><div class="s"><span>Owner: IT-Betrieb</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">8 h</div></div><div class="m"><div class="k">RPO</div><div class="v">4 h</div></div><div class="m"><div class="k">MTD</div><div class="v">2 T</div></div></div><div class="right"><span class="pill crit-3">Krit. 3</span></div></div>
<div class="sub"><span class="sdot teilweise"></span><div class="snm"><div class="t">Berechtigungsverwaltung</div><div class="s"><span>Owner: IT + ISB</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">1 T</div></div><div class="m"><div class="k">RPO</div><div class="v">8 h</div></div><div class="m"><div class="k">MTD</div><div class="v">3 T</div></div></div><div class="right"><span class="pill crit-3">Krit. 3</span></div></div>
</div>
</details>
<details class="mp st-offen">
<summary class="mp-head">
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M9 6l6 6-6 6"/></svg>
<div class="mp-title">
<div class="nm">Einkauf &amp; Lieferantenmanagement <span class="tag-cat">Support</span></div>
<div class="meta"><span class="owner"><span class="av">EK</span> Einkaufsleitung</span><span>·</span><span>2 Teilprozesse</span><span>·</span><span style="color:var(--warn)">BIA offen</span></div>
</div>
<div class="metrics">
<div class="m"><div class="k">RTO</div><div class="v">—</div></div>
<div class="m"><div class="k">RPO</div><div class="v">—</div></div>
<div class="m"><div class="k">MTD</div><div class="v">—</div></div>
</div>
<span class="pill crit-1"><span class="dot" style="background:#c4c8d2"></span> offen</span>
<span class="pill st-pill offen">offen</span>
</summary>
<div class="subs">
<div class="subs-h">Teilprozesse</div>
<div class="sub"><span class="sdot offen"></span><div class="snm"><div class="t">Lieferantenauswahl</div><div class="s"><span>Owner: Einkauf</span><span style="color:var(--warn)">BIA offen</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">—</div></div><div class="m"><div class="k">RPO</div><div class="v">—</div></div><div class="m"><div class="k">MTD</div><div class="v">—</div></div></div><div class="right"><span class="mini">BIA erfassen →</span></div></div>
<div class="sub"><span class="sdot offen"></span><div class="snm"><div class="t">Wareneingang / QS</div><div class="s"><span>Owner: QS</span><span style="color:var(--warn)">BIA offen</span></div></div><div class="metrics"><div class="m"><div class="k">RTO</div><div class="v">—</div></div><div class="m"><div class="k">RPO</div><div class="v">—</div></div><div class="m"><div class="k">MTD</div><div class="v">—</div></div></div><div class="right"><span class="mini">BIA erfassen →</span></div></div>
</div>
</details>
</div>
<div class="note">
<b>Was ist neu ggü. der heutigen Ansicht?</b> Statt einer flachen Tabelle aller Prozesse werden sie nach <b>Prozesskategorie</b> (Management / Kern / Support) gruppiert und in <b>Haupt- → Teilprozesse</b> aufgeklappt. Jeder Hauptprozess rollt Kritikalität (Maximumprinzip) und die schärfsten RTO/RPO/MTD-Werte seiner Teilprozesse zusammen. <b>Abhängigkeiten</b> („↳ benötigt" / „▲ wird benötigt von") machen sichtbar, welche Prozesse auf gemeinsame Dienste wie den IT-Betrieb angewiesen sind — ein Ausfall dort trifft alle abhängigen Prozesse. Datenbasis ist bereits vorhanden (<code>Process.parentId</code>, <code>category</code>, <code>biaStatus</code>, <code>BiaEntry</code>); es ist reine Darstellung, kein Datenmodell-Umbau.
</div>
<div class="foot">Mockup · certvia BIA-Prozessübersicht · Beispieldaten (GEFIM) · nicht verbindlich</div>
</div>
</body>
</html>
-67
View File
@@ -1,67 +0,0 @@
# Onboarding-Prompt für einen neuen Claude-Code-Agenten
> Diesen Prompt dem neuen Agenten als erste Nachricht geben. Er enthält bewusst **keinen**
> inhaltlichen Projektstand — der steht in `docs/STAND-dev-branch.md` und wird nur dort
> gepflegt (kein doppelter, veraltender Stand im Prompt).
---
```text
Du übernimmst die Weiterentwicklung des ISMS-Tools (Multi-Tenant-SaaS für
ISO 27001:2022 / TISAX / VDA-ISA 2027). Mach dich zuerst mit dem Stand vertraut.
Beginne noch KEINE Aufgabe — lies ein, bestätige dein Verständnis und warte dann
auf meine Anweisung.
## Repo & Umgebung
- Arbeitsverzeichnis: das lokale Repo `ISMS-Tool` (kein Gitea-/Remote-Zugang!).
- Du kannst NICHT pushen. Arbeite auf Branch `dev`, committe lokal. Pushen mache ich selbst.
Prüfe mit `git branch --show-current`, dass du auf `dev` bist (NICHT `main`).
- Stack: Next.js 16 (App Router, Turbopack, Server Actions), React 19, TypeScript strict,
Prisma 7 (+ @prisma/adapter-pg), PostgreSQL + pgvector, NextAuth v5, Tailwind.
## Zuerst lesen (in dieser Reihenfolge) — das ist die maßgebliche Statusquelle
1. `docs/STAND-dev-branch.md` — konsolidierter Gesamtstand (PM + Technik): was fertig/offen
ist, wo was liegt, Datenmodelle, Migrationen, Fallstricke, Demo-Logins.
2. `docs/HANDOVER-DEV.md` — Setup, Stack, Konventionen, Migrations-Flow (§1–§10).
3. `docs/SPEC.md` — fachliche Spezifikation.
Verlasse dich auf diese Dokumente statt zu raten. Den inhaltlichen Projektstand NICHT aus
diesem Prompt ableiten — er steht ausschließlich in der Doku (Single Source of Truth).
## Arbeitskonventionen (verbindlich)
- Commits auf Deutsch, granular pro Thema, mit Trailer:
`Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`
- Neue mutierende Server-Action → über `moduleGuard("<key>")` und in
`scripts/check-module-guards.ts` eintragen (sonst failt der Build).
- Neue mandantengebundene Prisma-Modelle → in `TENANT_MODELS` (`src/server/db.ts`)
UND RLS-Policy in der Migration (`tenant_isolation` via `current_setting('app.tenant_id')`).
- Migrations-Flow Prisma 7:
`npx prisma migrate diff --from-config-datasource prisma.config.ts --to-schema prisma/schema.prisma --script`
→ RLS-DO-Block manuell anhängen → `npx prisma migrate deploy` → `npx prisma generate`.
(tsx-Skripte brauchen `import "dotenv/config"`.)
- Zentrale Richtlinien-Variablen (Organisation/Rollen/Schutzbedarf) sind nur in `/settings`
pflegbar und serverseitig geschützt — nicht im Richtlinien-Editor.
- Editier-UIs immer als Popup/Modal (Muster: Assets/Lieferanten/Software/Projekte).
- Doku mitpflegen: Nach jeder abgeschlossenen Arbeit `docs/STAND-dev-branch.md`
aktualisieren (Executive Summary, PM-Statustabelle, ggf. neue Modelle/Migrationen,
„Wo liegt was", Commit-Übersicht, offene Punkte). Das ist die Single Source of Truth —
sie muss den `dev`-Stand jederzeit korrekt widerspiegeln. Der Doku-Update gehört in einen
eigenen Commit (oder denselben Themen-Commit), nicht separat vergessen.
## Jede Iteration abschließen mit
`npx tsc --noEmit` → `npm run lint` → `npm run build` (führt `prebuild`-Guard-Check aus).
Bei Änderungen am Vorlagenpaket zusätzlich `python3 seed/isms-vorlagenpaket-v2/_verify.py`
(muss `OK` liefern). Wo im Browser sichtbar: Dev-Server (`npm run dev`, Port 3000) und selbst
verifizieren — nicht den Nutzer manuell prüfen lassen. Hinweis: geänderte Server-Actions
greifen im Turbopack-Dev teils erst nach Neustart des Dev-Servers. Voraussetzung: lokale
Postgres-DB läuft und `.env` mit `DATABASE_URL` ist vorhanden (siehe HANDOVER-DEV.md).
## Lokale Demo-Daten
Seed: `npx tsx prisma/seed.ts` (idempotent). Demo-Logins Passwort `Demo1234!`:
`admin@demo.example` (Mandanten-Admin+ISB), `bea.approver@demo.example` (2. Freigeber für
Vier-Augen), `auditor@`, `owner@`, `user@`. Plattform-Login unter `/platform/login`.
## Ablauf jetzt
Lies die drei Dokumente, fasse mir in wenigen Sätzen zusammen, was der aktuelle Stand ist
und was als Nächstes offen wäre, und WARTE dann auf meine konkrete Aufgabe. Fang nichts
Eigenes an. Bei Unklarheiten: gezielt nachfragen.
```
-59
View File
@@ -1,59 +0,0 @@
# Kickoff-Prompt — Lane Konfigurierbarer Backup-Zielspeicher (certvia)
> Diesem Prompt einem Entwickler/Claude-Code-Agenten geben. Bootstrapt in certvia + diese Lane.
```text
Du übernimmst die Lane „Konfigurierbarer Backup-Zielspeicher" am Produkt „certvia" (ISMS-Tool,
Multi-Tenant Next.js-16 / Prisma-7 / Postgres+RLS / Auth.js-v5). Repo: ~/Projects/ISMS-Tool, Branch `dev`.
WICHTIG: certvia ist strikt getrennt von jedem anderen Produkt (insb. „visitvia"). Nichts vermischen.
BRANCH/CHECKOUT:
Arbeits-Branch: `lane-backup-target` (KEIN `feature/`-Prefix — origin-Namespace-Konflikt).
git fetch origin && git checkout dev && git checkout -b lane-backup-target.
Remotes: origin = https://git.certvia.de/msolarczek/certvia.git · local-gitea (intern).
1) PFLICHTLEKTÜRE:
- docs/HANDOVER-DEV.md, docs/SPEC.md, docs/STAND-dev-branch.md
- docs/KONZEPT-backup-target.md ← dein Fahrplan
- docs/KONZEPT-backup-restore.md §9 ← Verschlüsselung/Secrets-Kohärenz
- Bestand ansehen: src/server/storage/backup-store.ts (BackupStore, S3-/Local-Store, createBackupStore)
2) AUFTRAG:
Den Backup-Zielspeicher im Betreiber-Portal konfigurierbar machen + lokale (persistente) Option.
a) DATENMODELL: PlatformSetting erweitern (backupTarget local|s3, backupLocalDir, backupS3Endpoint/
Bucket/Region/AccessKey, backupS3SecretKeyEnc). Additive Migration (nullable, Default local).
b) FACTORY: statisches `backupStore` → `getBackupStore(): Promise<BackupStore>`, liest PlatformSetting,
entschlüsselt den S3-Key (secret-crypto). Präzedenz DB → Env (S3_*/BACKUP_LOCAL_DIR) → lokaler
Default `.backups`. Cachen + bei Settings-Änderung invalidieren. Die 3 Nutzer umstellen:
src/server/backup/export.ts, restore.ts, ops.ts.
c) UI: Seite im Plattform-Portal (z. B. /admin/backup): Ziel wählen (Lokal/S3), Config, „Verbindung
testen" (Probe put/get/remove). Gated requirePlatformFullAdmin + MFA-Step-up. Speichern via Action
analog setPlatformMfaRequired (platformSetting.upsert); S3-Secret VOR dem Schreiben verschlüsseln.
d) PERSISTENZ: docker-compose.coolify.yml — persistentes Volume `backups:` (wie pgdata/miniodata) in
app+worker mounten (Pfad = backupLocalDir, z. B. /app/.backups).
e) TESTS: scripts/test-backup-* erweitern (Store-Auflösung DB local/s3, Präzedenz, Test-Verbindung,
fail-secure bei unvollständiger S3-Config, Export→Restore gegen beide Backends).
3) VERBINDLICHE REGELN:
- S3-Secret NUR verschlüsselt in der DB (src/server/secret-crypto.ts: encryptSecret/decryptSecret) — nie Klartext.
- Fail-secure: backupTarget=s3 mit unvollständiger Config → klarer Fehler, NICHT still auf lokal fallen.
- Env-Fallback erhalten (Rückwärtskompatibilität bestehender Deployments).
- BACKUP_ENC_KEY (Artefakt-Verschlüsselung) bleibt GETRENNT vom Zielspeicher — nicht vermischen.
- RLS/Mandanten-Isolation der Artefakt-Keys (`<tenantId>/backups/…`) unverändert lassen.
- Config-Bearbeitung nur Full-Admin + Step-up, auditiert.
4) ARBEITSWEISE:
- Gate vor JEDEM Merge: npx tsc --noEmit · npm run lint · npm run build · ALLE scripts/test-*.ts.
- migrate reset gegen lokale DB braucht Nutzer-Zustimmung (Prisma-Guard).
- UI im Browser verifizieren: Ziel umschalten Lokal↔S3, „Verbindung testen", Export→Restore lokal.
- DevOps: lane-backup-target → git merge --no-ff nach dev → Gate grün → Push auf BEIDE Remotes →
docs/STAND-dev-branch.md pflegen.
5) NICHT TUN: kein Umbau am Auth-/RLS-Kern; keine Mehrfachziele/Offsite-Profile (Phase 2); Artefakt-
Verschlüsselung (crypto.ts) nicht anfassen.
Erste Schritte: (a) Pflichtlektüre + backup-store.ts lesen, 10-Zeilen-Zusammenfassung + Fragen,
(b) PR-Plan (Schema+Migration, getBackupStore-Refactor, UI, Compose-Volume, Tests), (c) nach Freigabe
umsetzen. Warte nach (a)/(b) auf Bestätigung.
```
-57
View File
@@ -1,57 +0,0 @@
# Kickoff-Prompt — Lane Backup/Restore/DSGVO (certvia)
> Diesem Prompt einem Entwickler/Claude-Code-Agenten geben. Bootstrapt in certvia + diese Lane.
```text
Du übernimmst die Lane „Datensicherung, Wiederherstellung & DSGVO" am Produkt „certvia"
(ISMS-Tool, Multi-Tenant Next.js-16 / Prisma-7 / Postgres+RLS / Auth.js-v5). Repo:
~/Projects/ISMS-Tool, Integrationsbranch `dev`.
WICHTIG: certvia ist strikt getrennt von jedem anderen Produkt (insb. „visitvia"). Nichts vermischen.
BRANCH/CHECKOUT:
Arbeits-Branch: `lane-backup` (KEIN `feature/`-Prefix — auf origin blockiert der Branch
`feature` diesen Namespace). Aus `dev` erstellen: git fetch origin && git checkout dev &&
git checkout -b lane-backup.
Remotes: origin = https://git.certvia.de/msolarczek/certvia.git · local-gitea (intern).
1) PFLICHTLEKTÜRE (erst lesen, nicht sofort coden):
- docs/HANDOVER-DEV.md, docs/SPEC.md, docs/STAND-dev-branch.md
- docs/KONZEPT-backup-restore.md ← dein Fahrplan (Schicht A/B, Portal, DSGVO, §9 Verschlüsselung)
- docs/KONZEPT-haertung.md §3 ← operativer Bezug „verschlüsselte Backups"
2) AUFTRAG (Phasen aus dem Konzept §11):
(1) Schicht A: pgBackRest/wal-g + PITR, client-seitig AES-256 (§9) — Ops-nah, sofort möglich.
(2) Tenant-scoped Export/Restore-Engine über TENANT_MODELS (FK-Reihenfolge), REPEATABLE READ.
(3) Betreiber-Portal-Restore (Worker-Job, Kontrollen §4/§10).
(4) DSGVO-Export (per-Mandant + per-Person).
(5) DSGVO-Löschung (Anonymisieren vs. Hard-Delete + Löschnachweis + Tombstone-on-Restore).
3) VERBINDLICHE REGELN:
- Engine läuft über den Owner-`prisma`-Client (BYPASSRLS), NIE den RLS-Client.
- Jede Operation `tenant_id`-gescopt (Export SELECT, Restore DELETE+INSERT, Löschung) →
beweisbar keine Fremdmandanten berührt. cuid-PKs: Reinsert kollisionsfrei.
- `Identity` ist GLOBAL: Tenant-Restore holt Mitgliedschaften (`User`), NICHT den globalen
Credential-Store (der liegt in Schicht A). Fehlende Identity beim Reinsert sauber behandeln.
- Verschlüsselung = §9-Primitiv: client-seitig AES-256, Keys pro Umgebung. Backup-Artefakte
enthalten KEINE Umgebungs-Secrets (Pepper/MFA_ENC_KEY) → Restore-Kohärenz dokumentieren.
- MinIO-Dateien (Prefix je Mandant) gehören zu Export/Restore/Löschung dazu.
- Destruktive Portal-Aktionen: requirePlatformFullAdmin + MFA-Step-up + getippte Bestätigung
+ Mandant-Sperre + Pre-Restore-Snapshot + Audit.
4) ARBEITSWEISE:
- Gate vor JEDEM Merge: npx tsc --noEmit · npm run lint · npm run build · ALLE scripts/test-*.ts.
Jede Story bringt ihren Test mit (Isolation: Restore/Export/Löschung berührt nur den Zielmandanten).
- DevOps: lane-backup → git merge --no-ff nach dev → Gate grün → Push auf BEIDE Remotes →
docs/STAND-dev-branch.md pflegen.
5) KOORDINATION:
- §9-Verschlüsselung ist gemeinsam mit Lane Härtung: Mechanismus/Keys sind entschieden
(client-seitig AES-256, Keys pro Umgebung) — nur EINMAL abstimmen, dann unabhängig.
- prisma migrate reset gegen Test/Coolify braucht Nutzer-Zustimmung (Prisma-Guard).
6) NICHT TUN: keine Änderung am Auth-/RLS-Kern; TENANT_MODELS bleibt Quelle der tenant-Tabellen.
Erste Schritte: (a) Pflichtlektüre + 10-Zeilen-Zusammenfassung/Fragen, (b) FK-Reihenfolge aus
TENANT_MODELS ableiten + Engine-PR-Plan skizzieren, (c) nach Freigabe umsetzen. Warte nach (a)/(b).
```
-120
View File
@@ -1,120 +0,0 @@
# Kickoff-Prompt — B1: Assessment und Readiness für zwei Frameworks
Du baust die **Bewertungs- und Readiness-Schicht** für ISO 27001 neben TISAX. Alles andere steht bereits: Der Dokumentensatz trägt beide Normen, `mapping-iso.json` liefert 120 ISO-Anforderungen, die Framework-Dimension im Datenmodell ist gebaut (AP1–AP5), ein ISO-Mandant hat SoA, Kennzahlen, Managementbewertung und Korrekturmaßnahmen.
**Was fehlt:** `maturity.ts`, `scope-filter.ts`, `assessment-level.ts` und `readiness.ts` kennen kein Framework und rechnen durchgängig VDA ISA — AL2/AL3, Prüfziele, Reifegrad 0–3, MUSS/SOLL. Der belegte Bruch: `src/server/soa-context.ts:151` und `src/server/export-context.ts:49` lesen `controlAssessment`; **keine einzige Stelle liest `soaEntry`**. Das SoA-Modul ist von der Auswertung abgekoppelt.
**Pflichtlektüre vor der ersten Zeile Code:** `docs/FEINDESIGN-framework-assessment.md` — vollständig, insbesondere §1 (Leitentscheidung) und §7a (Priorisierung). Hintergrund: `docs/FRAMEWORK-MAPPING-ISO27001.md`, `docs/UEBERGABE-framework-iso27001.md`.
## Repo & Branch
- **origin:** `git.certvia.de/msolarczek/certvia`; zweiter Remote `local-gitea`.
- **Basis ist `feature/iso27001-framework-mapping`**, nicht `dev`.
- Arbeite auf `feature/framework-assessment`, PR gegen den Basis-Branch.
## Kundenlage — sie bestimmt, was jetzt gebaut wird
Die Mehrzahl der Mandanten führt **TISAX**, **ein** Mandant **ISO**, **keiner beide**.
Daraus folgt: **Das Regressionsrisiko dominiert dieses Paket.** Der gefährlichste Teil ist nicht die ISO-Logik — die ist schlank — sondern die verhaltensgleiche Extraktion der bestehenden TISAX-Logik. Sie betrifft jeden Bestandsmandanten. Schritt 1 ist deshalb keine Kür.
**Zurückgestellt** (betrifft ausschließlich den Doppel-Mandanten, den es heute nicht gibt):
- Vorbelegung über `ISO_TO_ISA` (Übernahmevorschlag zwischen den Frameworks)
- Reiter-UI in `/soa` und `/audit-readiness` — bei einem aktiven Framework wird direkt angezeigt
**Nicht zurückstellen:** die **Strategie-Schnittstelle** und den **Framework-Parameter** in `buildAssessment`. Beides kostet jetzt fast nichts und ist später teuer, weil sonst sieben Aufrufstellen erneut angefasst werden. Die Architektur bleibt zweigleisig, die Oberfläche zeigt vorerst ein Gleis.
## Die eine Leitentscheidung
**Die Belegbasis liegt unterhalb der Framework-Strategie.** Ob R08 freigegeben ist, ist eine Tatsache über die Organisation, keine Frage der Norm. `ControlEvidence` (`src/lib/maturity.ts:54`) wird geteilt; nur Scope, Zielwert, Bewertung und Vokabular sind framework-eigen.
```
Schicht 3 Sichten /soa · /audit-readiness · Exporte → je Framework (Reiter später)
Schicht 2 Strategie Scope · Zielwert · Bewertung → je Framework
Schicht 1 Belegbasis loadEvidenceResolver → ControlEvidence → GETEILT
Schicht 0 Fachdaten PolicyDocument · Evidence · Asset … → GETEILT
```
**Review-Kriterium, hart:** `loadEvidenceResolver` darf `FrameworkKey` nicht kennen. Sobald die Belegauflösung anfängt, das Framework zu fragen, ist der Schnitt gerissen und wir bekommen zwei Bewertungen derselben Realität, die auseinanderlaufen.
## Nicht anfassen
- **`seed/isms-vorlagenpaket-v2/**` und `-en/**`** — generiert. Änderungen nur über `_iso_crosswalk.json` / `_iso_sections.json` / `_iso_texts_en.json` + `python3 _generate_iso.py [--lang en]`.
- **Die VDA-ISA-Fachlogik inhaltlich** — `suggestMaturity`, `targetMaturity`, `openPoints`, `activeRequirements` werden **verschoben, nicht verändert**.
- **`readiness.ts` und `gap-consolidation.ts`** — numerisch standard-agnostisch, werden von beiden Strategien gefüttert.
## Arbeitsschritte
### 1 — Snapshot der heutigen TISAX-Ausgabe *(zuerst, vor jeder Änderung)*
`buildControlRows` für den Demo-Mandanten festhalten: Reifegradvorschlag, Zielwert, Belegstatus und offene Punkte je Control, als JSON im Repo. Ablage als `scripts/test-framework-assessment.ts` in der Hausform der übrigen `test-*.ts`.
**DoD:** Test läuft grün gegen den unveränderten Stand und ist als Merge-Gate gesetzt.
### 2 — `ControlSpec` einführen
`C5ControlSpec` (`src/lib/maturity.ts:16`) auf ein neutrales `ControlSpec` reduzieren (`control`, `title`, `policy[]`, `verfahren[]`, `needsAsset`, `needsRisk`); `C5ControlSpec` erweitert es um `target`. `loadEvidenceResolver` (`src/server/soa-context.ts:50`) nimmt künftig `ControlSpec`.
**DoD:** Snapshot unverändert grün, `tsc` sauber.
### 3 — ISO-Specs generieren
`_generate_iso.py` erzeugt zusätzlich `src/lib/control-specs-iso.ts` — analog zu `control-titles-iso.ts` und `iso-isa-crosswalk.ts`. Quelle ist `mapping-iso.json` (`policy` und `verfahren` stehen dort je Anforderung); `needsAsset`/`needsRisk` über `ISO_TO_ISA` vom ISA-Spec erben, für die 33 ISO-eigenen Abschnitte explizit setzen — sinnvoll nur bei A.5.9 (`needsAsset`) sowie 6.1.2/6.1.3/8.2/8.3 (`needsRisk`).
**DoD:** Generator idempotent, `_verify_iso.py` DE und EN grün, keine handgepflegte Zweitliste.
### 4 — `FrameworkStrategy` + `TisaxStrategy` *(reine Extraktion)*
Interface nach §3 des Feindesigns. `TisaxStrategy` verdrahtet die vorhandenen Funktionen um: `loadScopeInput` + `controlsInScope` → `controlsInScope`, `suggestMaturity` + `targetMaturity` → `evaluate`, `openPoints` → `gaps`, `computeReadiness` → `summarise`.
**DoD:** Snapshot bitgenau grün. Jede Abweichung ist eine Regression, kein „ist besser geworden".
### 5 — `IsoStrategy`
| Aspekt | Regel |
|---|---|
| Scope | `SoaEntry.applicable = true`; Klauseln 4–10 immer im Scope |
| Zielwert | `implementationStatus = "umgesetzt"`, keine Stufung |
| Vorschlag | Richtlinie *und* Verfahren `validiert` + geforderte Verknüpfungen → `umgesetzt`; teilweise → `teilweise`; sonst `geplant` |
| Bestätigung | `SoaEntry.implementationStatus`; der Vorschlag überschreibt ihn nie |
| Lücken | fehlende Begründung, fehlender Nachweis, Status ≠ „umgesetzt" bei anwendbarem Control |
**DoD:** ISO-Mandant bekommt eine Bewertung über alle anwendbaren Controls; leere SoA meldet „Anwendbarkeit noch nicht erklärt" statt 0 %.
### 6 — `buildAssessment(db, tenantId, framework)`
Ersetzt `buildControlRows` (`src/server/soa-context.ts:146`) und delegiert an die Strategie. Sieben Dateien rufen es direkt; insgesamt hängen 13 an der Assessment-Logik.
**DoD:** Alle Aufrufstellen umgestellt, Snapshot grün, `tsc`/`lint`/`build` grün.
### 7 — ISO-Readiness-Bänder und Beschriftung
`REIFEGRAD_BANDS` (`src/lib/readiness.ts:16`) ist TISAX-Sprache („Assessment-reif", „AL-Ziel"). ISO bekommt eigene Bänder auf Basis des Umsetzungsgrads: < 50 % Aufbau · 50–79 % In Umsetzung · 80–99 % Zertifizierungsnah · 100 % Zertifizierungsreif.
**DoD:** In der ISO-Sicht erscheint kein AL-, Prüfziel- oder Reifegradbegriff. Ein ISO-Bericht argumentiert mit „umgesetzt, hier ist der Beleg", nicht mit „Reifegrad 2,4".
### 8 — ISO-Exporte
SoA-Export und Annex-A-Gap-Report. Der VDA-ISA-Export bleibt TISAX-only und unverändert.
**DoD:** Der ISO-Mandant kann die Eingaben für seine Managementbewertung aus dem Tool ziehen.
**Zusatznutzen, bitte mitnehmen:** Die 33 ISO-Anforderungen ohne ISA-Gegenstück als eigene Liste ausweisbar machen. Das ist genau die Delta-Arbeit, die ISO gegenüber TISAX zusätzlich verlangt — die erste Frage jedes TISAX-Kunden, der über ISO nachdenkt.
## Validierungs-Gate vor jedem PR
```bash
npx tsc --noEmit && npm run lint && npm run build
npx tsx scripts/test-framework-assessment.ts # Snapshot TISAX → 0 Abweichungen
npx tsx scripts/test-framework-dryrun.ts # Paket + Import → alle Prüfungen bestanden
cd seed/isms-vorlagenpaket-v2
python3 _generate_iso.py && python3 _generate_iso.py --lang en # beide idempotent
python3 _verify.py && python3 _verify_iso.py && python3 _verify_iso.py --lang en
python3 _render_diff.py HEAD # TISAX-Renderdiff → 0 Abweichungen
```
Weitere Konventionen: Migrationsflow Prisma 7 mit manuell angehängtem RLS-DO-Block (`docs/HANDOVER-DEV.md:100`); neue mandantengebundene Modelle in `TENANT_MODELS` (`src/server/db.ts:81`) **und** RLS-Policy in der Migration; jede neue Datei unter `src/server/actions/` in `scripts/check-module-guards.ts` eintragen, sonst schlägt der Build fehl.
## Definition of Done (gesamt)
| Prüfung | Erwartung |
|---|---|
| Snapshot TISAX | 0 Abweichungen zur Ausgabe vor dem Umbau |
| Bestandsmandant (TISAX) | Verhalten und Beschriftung unverändert |
| ISO-Mandant | Readiness und SoA rechnen; kein TISAX-Vokabular in der Oberfläche |
| Architektur | Strategie-Schnittstelle und Framework-Parameter vorhanden, auch wenn die UI nur ein Gleis zeigt |
| Belegbasis | `loadEvidenceResolver` kennt `FrameworkKey` nicht |
| Kennzahlen | je Framework eine eigene, **keine** gemischte Gesamtzahl |
| Gate | alle Prüfungen oben grün |
## Aufwand
4–6 PT im vorgezogenen Umfang. Schritte 1–4 hängen aneinander und sind Pflicht; 5–8 sind danach teilbar. Schwerpunkt liegt auf Schritt 4, nicht auf der ISO-Logik.
## Nicht dein Scope
Vorbelegung zwischen den Frameworks und die Reiter-UI (siehe Kundenlage). Ebenso Paket- und Freigabearbeit: `REVIEW_CYCLE`-Split an 32 Bestandsstellen, ISB-Freigabe der 19 ISO-Abschnittstexte, Review des Crosswalks.
-151
View File
@@ -1,151 +0,0 @@
# Kickoff-Prompt — Framework-Dimension: ISO 27001 neben TISAX
Du übernimmst den **Anwendungsumbau** für die Mehr-Framework-Fähigkeit. Die **Inhaltsseite ist fertig**: `seed/isms-vorlagenpaket-v2` trägt seit Branch `feature/iso27001-framework-mapping` **zwei Framework-Mappings auf einem Dokumentensatz** — `mapping.json` (VDA ISA, 321 Anforderungen) und `mapping-iso.json` (ISO/IEC 27001:2022, 120 Anforderungen). Die Anwendung kennt das zweite Mapping nicht: `parsePackageFiles` liest `mapping.json` fest verdrahtet.
**Pflichtlektüre vor der ersten Zeile Code:** `docs/UEBERGABE-framework-iso27001.md` — insbesondere **§1 „Die vier Fallen"**. Hintergrund und Begründung der Entscheidung: `docs/FRAMEWORK-MAPPING-ISO27001.md`. Gesamtarchitektur: `docs/KONZEPT-framework-iso27001.md` (D4 und Lane 2 sind dort per Nachtrag korrigiert).
## Repo & Branch
- **origin:** `git.certvia.de/msolarczek/certvia`; zweiter Remote `local-gitea`.
- **Basis ist `feature/iso27001-framework-mapping`**, nicht `dev` — dort liegt die Vorarbeit (Commits `492d315`, `bbde804`, `7769908`, `b28d0f0`).
- AP1 auf `feature/framework-core`, PR gegen den Basis-Branch. AP2–AP5 danach je eigener Branch, parallelisierbar.
## Nicht anfassen
- **`seed/isms-vorlagenpaket-v2/**`** — generiert. Inhaltliche Änderungen laufen ausschließlich über `_iso_crosswalk.json` / `_iso_sections.json` + `python3 _generate_iso.py`, nie direkt in den Markdown-Dateien (der Generator überschreibt sentinel-begrenzte Blöcke).
- **Die TISAX-Logik verhaltensgleich lassen:** `src/lib/maturity.ts`, `src/lib/scope-filter.ts`, `src/server/assessment-level.ts`, `src/lib/control-titles.ts`, `src/lib/export/vda-isa*.ts`. Wenn du sie hinter eine `FrameworkStrategy` ziehst, muss das Verhalten identisch bleiben.
## Vier Invarianten — hier geht es sonst schief
1. **`reconcilePackage` archiviert fremde Anforderungen.** `prisma/import-policies.ts:368-371` setzt `archivedAt` auf jede `PolicyRequirement`, deren `reqId` nicht im importierten Paket steht. Ein ISO-Import in einen TISAX-Mandanten legt damit **alle 321 VDA-ISA-Anforderungen still** — und umgekehrt. Lösung: `PolicyRequirement.framework` ergänzen (Backfill `TISAX`) und den Archivierungslauf auf `where: { tenantId, framework }` einschränken. Für Dokumente, Variablen, Baseline und Nachweisregister gilt das **nicht** — die sind geteilt und identisch, sie dürfen genau einmal je Mandant abgeglichen werden.
2. **Zwei Unique-Constraints brechen.** `PolicyTemplateVersion.version @unique` (`prisma/schema.prisma:1639`) → `@@unique([framework, version])`, sonst kollidieren ISO 2.1 und TISAX 2.1. `PolicyPackageState.tenantId @unique` (`:1614`) → `@@unique([tenantId, framework])`, sonst merkt sich ein Mandant nur eine Paketversion.
3. **TISAX darf sich nicht verändern.** Messlatte ist `python3 seed/isms-vorlagenpaket-v2/_render_diff.py HEAD` → 0 Abweichungen.
4. **Den Fail-Safe in `src/lib/policy-render.ts` nicht entfernen.** `buildContext` belegt fehlende Framework-Flags vor (`FLAG_FW_TISAX = true`, `FLAG_FW_ISO27001 = false`), damit Bestandsmandanten vor dem Paket-Re-Import keine leeren Anforderungsblöcke sehen.
## AP1 — Framework-Dimension *(Fundament, blockiert alles)*
**Schema, additiv:**
```prisma
enum Framework { ISO_27001 TISAX }
model TenantFramework {
id String @id @default(cuid())
tenantId String @map("tenant_id")
framework Framework
isPrimary Boolean @default(false) @map("is_primary")
config Json? // z. B. { tisaxLevel: "AL3" } bzw. { certScope }
createdAt DateTime @default(now()) @map("created_at")
@@unique([tenantId, framework])
@@index([tenantId])
@@map("tenant_frameworks")
}
```
Dazu `PolicyRequirement.framework`, `PolicyTemplateVersion.framework`, `PolicyPackageState.framework`. Backfill aller Bestandsdaten auf `TISAX`. `TenantFramework` gehört in **`TENANT_MODELS`** (`src/server/db.ts:81`) und braucht eine **RLS-Policy in der Migration**.
**Code:**
| Datei | Änderung |
|---|---|
| `prisma/import-policies.ts` | `parsePackageFiles(seedDir, mappingFile = "mapping.json")`; Requirements framework-scoped reconcilen |
| `prisma/template-store.ts:147` | `resolvePackageForTenant(prisma, tenantId, seedDir, framework)`; `loadPublishedPackage(prisma, locale, framework)`; `getAvailableVersion` ebenso |
| `scripts/sync-policy-templates.ts:23` | über **Frameworks × Sprachen** iterieren |
Die vier Aufrufer bekommen den Parameter durchgereicht: `src/server/provision.ts:139`, `src/server/actions/policy-package.ts:28`, `src/server/actions/admin.ts`, `src/app/(app)/policies/updates/page.tsx:44`. **Das Seed-Verzeichnis bleibt für beide Frameworks dasselbe** — die fünf `SEED_DIR`-Konstanten ändern sich nicht, nur der Mapping-Dateiname.
**DoD:** Bestandsmandanten laufen unverändert als TISAX; ein Mandant lässt sich mit `["ISO_27001"]`, `["TISAX"]` oder beiden provisionieren; bei Doppel-Framework koexistieren 321 + 120 Anforderungen und **keine** ist fälschlich archiviert.
## AP2 — Provisionierung und Flags *(klein, direkt nach AP1)*
`ProvisionOpts` (`src/server/provision.ts:37`) um `frameworks: Framework[]`. `provisionTenant` schreibt die `TenantFramework`-Zeilen, importiert je Framework das passende Mapping und setzt die Sichtbarkeits-Flags als `PolicyVariable`: nur TISAX → `true/false`, nur ISO → `false/true`, beides → `true/true`.
**Reihenfolge beachten:** `reconcilePackage` erhält nutzergepflegte Variablenwerte und überschreibt sie nicht — die Flags also **nach** dem Import setzen, sonst bleibt der Schema-Default stehen und ein ISO-Mandant sieht die VDA-ISA-Sicht. `tisaxLevel` bleibt auf `TenantSettings`, ist aber TISAX-scoped; bei einem reinen ISO-Mandanten keine AL-Flags setzen.
**DoD:** Frisch provisionierter ISO-Mandant öffnet `/policies` und sieht je Abschnitt `*Anforderungsbezug:* ISO/IEC 27001 …` plus die 19 ISO-only-Abschnitte; keine VDA-ISA-Anforderungen.
## AP3 — SoA-Modul *(das fehlende ISO-Kernartefakt)*
Der Modul-Key `soa` (`src/lib/modules.ts:19`) zeigt auf `/soa`, **die Route existiert nicht**; die heutige Logik liegt als Wizard-Schritt in `src/server/actions/soa.ts` und ist ein VDA-ISA-Reifegrad-Assessment (0–3), nicht die ISO-Anwendbarkeitserklärung.
```prisma
model SoaEntry {
id String @id @default(cuid())
tenantId String @map("tenant_id")
framework Framework
control String // "A.5.15"
applicable Boolean @default(true)
justification String // Begründung Einbeziehung ODER Ausschluss
source String? // Risiko-ID / gesetzliche / vertragliche Anforderung
implementationStatus String @default("geplant") // umgesetzt | teilweise | geplant
ownerId String? @map("owner_id")
policyCode String? @map("policy_code")
evidenceId String? @map("evidence_id")
@@unique([tenantId, framework, control])
@@index([tenantId])
@@map("soa_entries")
}
```
`applicable`, `justification`, `implementationStatus` und die Ausschlussbegründung sind **normative Pflichtangaben** (ISO/IEC 27001:2022, 6.1.3 d) — ohne sie ist die SoA im Zertifizierungsaudit angreifbar. Vorbefüllung aus `mapping-iso.json` (93 Controls); das Feld `condition` steuert die Default-Anwendbarkeit. Fachliche Vorlage für Aufbau und Spalten: `seed/isms-vorlagenpaket-v2/Statement-of-Applicability-ISO.md`.
**DoD:** SoA vollständig pflegbar und als PDF/XLSX exportierbar; ein Control ohne Begründung wird als unvollständig markiert.
## AP4 — Kennzahlen, Managementbewertung, Korrekturmaßnahmen
| Klausel | Modell | Inhalt |
|---|---|---|
| 9.1 | `Kpi` / `KpiValue` | Kennzahl, Datenquelle, Zielwert, Turnus, Verantwortlicher, Messwerte je Periode |
| 9.3 | `ManagementReview` | Datum, Eingaben nach 9.3.2, Ergebnisse nach 9.3.3, Beschlüsse mit Verantwortlichem und Termin |
| 10.2 | `Nonconformity` + `CorrectiveAction` | Herkunft, Sofortkorrektur, Ursachenanalyse, Maßnahme, Wirksamkeitsbewertung |
Aufsetzen auf Vorhandenes: `Task.recurrence` (RRULE), `Task.remindAt`, `Task.effectiveUntil` (Wirksamkeitsintervall, gekoppelt an `Evidence.validUntil`), `TaskParticipant` (RACI), `AuditLog`. Datenquellen für Kennzahlen liegen bereits im Tool: Aufgabenfristen und Überfälligkeit, Incident-SLA und Meldefristen (`src/lib/incident-deadlines.ts`), Reifegrade je Control, Maßnahmenstatus. Die Feldinhalte stehen fachlich in R03 der Bibliothek (`ISO-MS-MESSUNG`, `ISO-MS-MGMTREVIEW`, `ISO-MS-CAPA`).
**DoD:** Kennzahlenblatt mit Zielwerten über zwei Perioden auswertbar; Management-Review entlang der 9.3.2-Agenda protokollierbar; ein Maßnahmenfall inklusive dokumentierter Wirksamkeitsprüfung abschließbar.
## AP5 — Dokumentenlenkung *(klein, hohe Auditwirkung)*
- `PolicyDocument.reviewCycle` + `nextReviewAt` — heute führt nur `ManagedRegister` einen `reviewCycle`; A.5.1 verlangt die Überprüfung „in geplanten Abständen".
- `PolicyAcknowledgement { tenantId, policyDocumentId, version, userId, acknowledgedAt }` — Lesebestätigung, in `SPEC.md` §4.6 vorgesehen und bis heute nicht gebaut. Zugleich der einfachste Nachweis für Klausel 7.3 und Control A.6.3.
- Änderungshistorie je Dokumentversion — aus `AuditLog` (Vorher/Nachher) ableitbar oder eigene Tabelle.
**DoD:** Übersicht „Prüfung fällig"; Auswertung der Lesebestätigungen je Richtlinienversion; Dokumenthistorie über mindestens zwei Versionen sichtbar.
## Validierungs-Gate vor jedem PR
```bash
npx tsc --noEmit && npm run lint && npm run build
cd seed/isms-vorlagenpaket-v2
python3 _verify.py # TISAX-Sicht → OK
python3 _verify_iso.py # ISO-Sicht → OK (0 Befunde)
python3 _render_diff.py HEAD # TISAX-Regression → 0 Abweichungen
```
Zusätzlich beachten:
- **Migrationsflow Prisma 7** (`docs/HANDOVER-DEV.md:100`): `migrate diff --from-config-datasource … --to-schema … --script`, danach den **RLS-DO-Block manuell** an die `migration.sql` anhängen, dann `migrate deploy`.
- **`scripts/check-module-guards.ts`** läuft als `prebuild`-Gate: jede neue Datei unter `src/server/actions/` dort eintragen (Modul-Key oder `EXEMPT`), sonst schlägt der Build fehl.
- Bei paralleler Lane-Entwicklung teilen sich die Worktrees dieselbe lokale Postgres-DB: beim Erzeugen einer Migration nur die **eigenen** DDL-Blöcke übernehmen, Fremd-Drops von Hand entfernen.
## Definition of Done (gesamt)
| Prüfung | Erwartung |
|---|---|
| Bestandsmandant (TISAX) nach Deploy | Readiness und Exporte identisch zum Snapshot vor dem Umbau |
| Import ISO in Mandant mit TISAX | 120 neue Anforderungen, **0 archivierte** ISA-Anforderungen |
| Import ISO, Umsetzungstexte | 120 von 120 gefüllt |
| Dokumente bei Doppel-Framework | 39 Dokumente, **nicht** doppelt |
| Parallelbetrieb im Dokument | beide Anforderungssichten unter einem gemeinsamen Umsetzungstext |
| Gate | tsc, lint, build, beide `_verify*`, `_render_diff` grün |
`parsePackageFiles` ist reine Dateiarbeit und ohne Datenbank testbar; für den Mandanten-Import gibt es `reconcilePackage(..., { dryRun: true })` — Änderungsreport ohne Schreibzugriff, geeignet als Freigabebedingung.
## Reihenfolge und Aufwand
```
AP1 Framework-Dimension 4–6 PT ← blockiert alles
├─ AP2 Provisionierung 1–2 PT
├─ AP3 SoA-Modul 5–8 PT
├─ AP4 Managementkl. 5–8 PT
└─ AP5 Dok.-Lenkung 2–3 PT
```
AP3–AP5 sind nach AP1 parallelisierbar. **Feature-Flag:** ISO bleibt laut Entscheidung D7 hinter einem Plattform-Schalter, bis AP3 abgenommen ist.
## Nicht dein Scope
Diese Punkte gehören dem ISB bzw. der Redaktion: Nachweisregister um Zeilen für Kennzahlenblatt, Management-Review-Protokoll und Maßnahmenregister ergänzen; VA-15 trennen und ein Verfahren für Korrekturmaßnahmen ergänzen (**Nummer ab VA-21**, VA-20 ist belegt); `REVIEW_CYCLE` an den 33 Bestandsstellen auf die getrennten Zyklus-Variablen umstellen; ISB-Freigabe der 19 neuen Abschnittstexte.
-42
View File
@@ -1,42 +0,0 @@
# Kickoff-Prompt — Lane D: Test, Cutover & Abnahme
Du übernimmst **Lane D** (Integration/Abnahme) der Umstellung **MinIO → Garage**. Lies `docs/KONZEPT-garage-migration.md` (v. a. §7 Runbook, §8 Rollback, §9 Abnahmekriterien). **Randbedingung:** nur Test-Instanzen, keine Datenmigration — Neu-Deploy.
## Repo & Branch
- **origin:** `git.certvia.de/msolarczek/certvia` — Basis-Branch **`dev`**.
- Zweiter Remote (interner Coolify-Test): `local-gitea` = `gitea.192.168.1.155.sslip.io/msolarczek/ISMS-Tool`. Push auf `dev` löst den Test-Deploy aus.
- Deploy-Ziel: interne Coolify-Instanz, `certvia.192.168.1.155.sslip.io`.
## Ziel
Die integrierten Ergebnisse aus Lane A–C auf der Test-Instanz **per Neu-Deploy** in Betrieb nehmen und **formal abnehmen** — plus abgenommenes Runbook (auch für spätere Prod).
## Aufgaben
1. **Cutover (Runbook §7)** an der Test-Instanz durchspielen:
- Im Compose `minio` → `garage` + `garage-provision`; Volumes `garage_meta`/`garage_data`.
- Coolify-Env: `S3_ENDPOINT=http://garage:3900`, `S3_REGION=us-east-1`, `GARAGE_RPC_SECRET`, `GARAGE_ADMIN_TOKEN` (literal!), `S3_ACCESS_KEY/S3_SECRET_KEY` = provisionierter Garage-Key.
- Deploy; Garage „ready", `garage-provision` legt Bucket/Key/Rechte an.
- Optional DB-Reset (wie in bisherigen Deploys), falls alte Objekt-Referenzen stören.
2. **End-to-End-Abnahme (§9)** — alle grün:
- Upload (Richtlinie/Nachweis) → Objekt in Garage, `<tenantId>/uploads/…`-Präfix korrekt.
- Download (`/files/[...key]`) inkl. korrektem Dateinamen/Content-Type.
- Backup-Export `.cvb` (CVB1-Header), Persistenz + Download.
- DSGVO-ZIP erzeugt + lesbar.
- Restore → Datenintegrität + **Mandanten-Isolation**.
- Backup-Historie: List + Aufräumen (Delete-Prefix).
- Negativfall: fehlender Bucket → sprechender Konfigfehler (kein stiller CreateBucket).
- `scripts/test-backup-*.ts` + `scripts/test-garage-storage.ts` grün gegen die Instanz.
3. **Runbook & Rollback** in `docs/DEPLOY-COOLIFY.md` finalisieren (Schritt-für-Schritt, inkl. „`garage_meta` sichern").
4. **`minio`-Service + Volumes** erst nach erfolgreicher Abnahme entfernen (bis dahin Rollback-Sicherheitsnetz).
## Vorgaben
- **Keine echten Secrets in Chat/Repo/Logs** (`GARAGE_*`, S3-Key). In Coolify **literal** setzen (Interpolationsfalle).
- Bei „Container weg ohne Logs" die bekannte **Log-Capture-Technik** nutzen (Logs während des Deploys in Dateien mitschreiben; siehe Deployment-Erfahrungen).
- Gefundene Bugs an die jeweilige Lane (A/B/C) zurückspielen, nicht selbst quer patchen.
## Definition of Done
- Test-Instanz läuft auf Garage, alle Abnahmekriterien (§9) abgehakt.
- Runbook + Rollback dokumentiert und einmal real durchgespielt.
- `minio` entfernt (oder bewusst als Netz belassen, dokumentiert). Freigabe für spätere Prod.
## Abhängigkeiten
Integriert **Lane A + B + C**. PM koordiniert Reihenfolge (A→B, C parallel) und Go für die Abnahme.
-36
View File
@@ -1,36 +0,0 @@
# Kickoff-Prompt — Lane C: App-Code Garage-tauglich
Du übernimmst **Lane C** der Umstellung **MinIO → Garage**. Lies `docs/KONZEPT-garage-migration.md` (v. a. §3 und §4 D3). **Kern:** Der S3-Code bleibt inhaltlich gleich (AWS SDK v3, `forcePathStyle`), aber die Selbstheilung `ensureBucket()` (HeadBucket→**CreateBucket**) muss weg — Garage unterstützt S3-`CreateBucket` nicht; Buckets werden von Lane B vorab provisioniert.
## Repo & Branch
- **origin:** `git.certvia.de/msolarczek/certvia` — Basis-Branch **`dev`**.
- Zweiter Remote: `local-gitea` = `gitea.192.168.1.155.sslip.io/msolarczek/ISMS-Tool`.
- Arbeite auf `feature/garage-code` (aus `dev`), PR nach `dev`. **Unabhängig von Lane A/B** entwickelbar (gegen einen lokalen Garage-Container testen).
## Scope (genau diese Dateien)
- `src/server/storage/adapter.ts` — `ensureBucket()` anpassen.
- `src/server/storage/backup-store.ts` — `ensureBucket()` anpassen.
- `.env.coolify.example` + `.env.example` — Kommentare MinIO → Garage, `S3_ENDPOINT`-Beispiel `http://garage:3900`, `forcePathStyle`-Begründung bleibt gültig.
- Tests unter `scripts/` (neuer Smoke-Test).
- **Nicht anfassen:** Compose/Infra (Lane A), Provisioning (Lane B), Fachlogik/UI.
## Aufgaben
1. **`ensureBucket()` in beiden Stores** von „HeadBucket→CreateBucket" auf **nur prüfend** umstellen:
- `HeadBucketCommand` → wenn ok, weiter.
- Wenn Bucket fehlt/kein Zugriff → **klarer Konfigurationsfehler** werfen: „Bucket `<name>` nicht provisioniert/kein Zugriff — Garage-Provisioning (Lane B) ausführen." **Kein** `CreateBucketCommand` mehr.
- `CreateBucketCommand`-Import entfernen, wenn ungenutzt (tsc/lint sauber halten).
- Beachte: `backup-store.ts` schluckt heute den Fehler still — das durch die neue, sprechende Variante ersetzen.
2. **Region/Endpoint-Doku** aktualisieren (Default `S3_REGION=us-east-1` bleibt, passend zu Garage-`s3_region`).
3. **Smoke-Test** (`scripts/test-garage-storage.ts` o. ä.): gegen einen lokalen Garage-Container Put→Get→(List/Delete beim Backup-Store) grün; plus Negativfall „Bucket fehlt → sprechender Fehler".
## Vorgaben
- **API der Storage-Abstraktion unverändert** (`StorageAdapter`/`BackupStore`-Interfaces, Key-Schema, Local-/Stub-Fallback bleiben).
- Keine neuen Pflicht-Env; `S3_*`-Kontrakt bleibt.
- **Validierungs-Gate vor PR:** `npx tsc --noEmit`, Lint, `npm run build`, alle `scripts/test-*.ts` grün (inkl. neuem Test). Denk an die bekannte Falle: **nichts, was Secrets liest, auf Modulebene aufrufen** (Build-Kompatibilität).
## Definition of Done
- Beide `ensureBucket()` prüfen nur noch, mit sprechendem Fehler bei fehlendem Bucket.
- Doku aktualisiert; Smoke-Test grün gegen lokale Garage; Gate grün.
## Abhängigkeiten
Zur **Laufzeit** auf **Lane B** angewiesen (Bucket muss existieren), aber **Code + Tests unabhängig** entwickelbar (lokaler Garage-Container).
-35
View File
@@ -1,35 +0,0 @@
# Kickoff-Prompt — Lane A: Garage Infra & Deployment
Du übernimmst **Lane A** der Objektspeicher-Umstellung **MinIO → Garage**. Lies zuerst das Gesamtkonzept: `docs/KONZEPT-garage-migration.md` (v. a. §3 Reibungspunkt, §5 Zielarchitektur, §6 Lane A, §7 Runbook). **Randbedingung:** nur Test-Instanzen, keine Datenmigration — kompletter Neu-Deploy.
## Repo & Branch
- **origin:** `git.certvia.de/msolarczek/certvia` — Basis-Branch **`dev`**.
- Zweiter Remote (interner Coolify-Test): `local-gitea` = `gitea.192.168.1.155.sslip.io/msolarczek/ISMS-Tool`. Ein Push auf `dev` löst den Test-Deploy aus.
- Arbeite auf `feature/garage-infra` (aus `dev`), PR nach `dev`. Nach Merge `dev` auf **beide** Remotes pushen.
## Ziel
Garage als Compose-Service, der `minio` in `docker-compose.coolify.yml` ersetzt — lauffähig auf der Test-Instanz, Admin-API erreichbar, Single-Node-Layout „ready".
## Scope (nur diese Dateien/Bereiche)
- `docker-compose.coolify.yml`: `minio`-Service durch `garage` ersetzen (nicht sofort löschen — auskommentiert/parallel lassen, bis Lane D abnimmt).
- `garage.toml` (neu, als Config-Mount oder über Env) + ggf. `docs/DEPLOY-COOLIFY.md` ergänzen.
- Keine App-Code-Änderung (das ist Lane C).
## Vorgaben
- **Image pinnen** auf eine aktuelle stabile Garage-Version (`dxflrs/garage:vX.Y.Z`), nicht `latest`.
- **Ports:** 3900 = S3-API (von der App genutzt), 3903 = Admin-API (clusterintern, für Healthcheck/Provisioning), 3902 = Web optional. **Nichts nach außen (Traefik) freigeben** — genau wie MinIO bisher.
- **`garage.toml`:** `replication_factor = 1` (Single-Node), `metadata_dir`, `data_dir`, `rpc_secret` (aus `GARAGE_RPC_SECRET`), Block `[s3_api] s3_region = "us-east-1"`, `api_bind_addr = "[::]:3900"`, Block `[admin] api_bind_addr = "[::]:3903"`, `admin_token` (aus `GARAGE_ADMIN_TOKEN`).
- **Volumes:** `garage_meta → /var/lib/garage/meta` (KRITISCH), `garage_data → /var/lib/garage/data`.
- **Security analog Bestand:** `security_opt: no-new-privileges`, `cap_drop: ALL`, Resource-Limits, `restart: unless-stopped`.
- **Healthcheck** gegen Admin-API `/health` (3903).
- **Layout-Bootstrap** dokumentieren: nach erstem Start Node einer Zone mit Kapazität zuweisen (`garage layout assign …` → `garage layout apply`). Falls möglich, an Lane B (Provisioning-Job) delegieren — dann hier nur vorbereiten.
- **Secrets:** `GARAGE_RPC_SECRET` (32-byte hex) und `GARAGE_ADMIN_TOKEN` als Coolify-Env, **literal** setzen (nicht über `${…}` referenzieren — bekannte Coolify-Interpolationsfalle). **Keine echten Secrets ins Repo oder in Logs.**
## Definition of Done
- Garage-Container startet in der Test-Instanz, Admin-`/health` grün, Layout „ready".
- `S3_ENDPOINT=http://garage:3900` intern erreichbar (kurzer `aws s3 ls`/curl-Nachweis).
- `garage_meta` als zu sicherndes Volume in `docs/DEPLOY-COOLIFY.md` vermerkt.
- Übergabepunkt an Lane B (Provisioning) klar dokumentiert (wie CLI/Admin-API erreichbar ist).
## Abhängigkeiten
Lane B (Provisioning) baut direkt auf dir auf. Stimme das CLI-/Admin-API-Zugriffsmodell mit Lane B ab.
-37
View File
@@ -1,37 +0,0 @@
# Kickoff-Prompt — Lane B: Garage Provisioning (Bucket/Key/Rechte)
Du übernimmst **Lane B** der Umstellung **MinIO → Garage**. Lies `docs/KONZEPT-garage-migration.md` (v. a. §3 „einziger Reibungspunkt", §4 D3, §6 Lane B). **Kern:** Garage legt Buckets/Keys **nicht** über S3-`CreateBucket` an, sondern über **Admin-API/CLI** — das automatisieren wir hier, idempotent.
## Repo & Branch
- **origin:** `git.certvia.de/msolarczek/certvia` — Basis-Branch **`dev`**.
- Zweiter Remote: `local-gitea` = `gitea.192.168.1.155.sslip.io/msolarczek/ISMS-Tool` (Push auf `dev` = Test-Deploy).
- Arbeite auf `feature/garage-provision` (aus `dev`), PR nach `dev`.
## Ziel
Ein **idempotenter Init-Job `garage-provision`** (Compose-Service, `restart: no`, analog `migrate`), der aus einer frisch gestarteten Garage reproduzierbar den betriebsbereiten Zustand herstellt.
## Scope
- Neuer Compose-Service `garage-provision` in `docker-compose.coolify.yml` (mit Lane A abstimmen; `depends_on: garage`).
- Provisioning-Skript (`scripts/garage-provision.*` oder Shell im Job) — CLI **oder** Admin-API (HTTP). Entscheidung im Skript-Kommentar begründen.
- Doku in `docs/DEPLOY-COOLIFY.md`.
## Aufgaben (alle idempotent — mehrfach ausführbar ohne Fehler)
1. **Layout** sicherstellen (falls Lane A das nicht schon macht): Node zuweisen + `layout apply`.
2. **Bucket(s)** anlegen: `isms-documents` (Uploads); Backup-Bucket nur, falls Backup-Store auf S3 statt `BACKUP_LOCAL_DIR` läuft.
3. **Access-Key** bereitstellen — **deterministisch**: bevorzugt vorhandenen Key **importieren** (`garage key import` mit den Werten aus `S3_ACCESS_KEY`/`S3_SECRET_KEY` der Coolify-Env), damit App-Env und Garage garantiert synchron sind. (Alternative: Key erzeugen und Ausgabe in Coolify-Secrets übernehmen — nur wenn Import nicht praktikabel; Trade-off dokumentieren.)
4. **Rechte** setzen: Key → Bucket read/write (owner nach Bedarf).
5. **Verifikation:** am Ende prüfen, dass Bucket existiert und der Key Schreib-/Leserecht hat; bei Fehlkonfiguration mit klarer Meldung + Exit ≠ 0 abbrechen.
## Vorgaben
- **Idempotenz zwingend:** vor jedem `create` Existenz prüfen; „already exists" als Erfolg werten (der Job läuft bei JEDEM Deploy).
- **Kein Secret in Logs** (Key/Secret nicht ausgeben). `GARAGE_ADMIN_TOKEN` aus Env.
- Nicht crash-loopen bei fehlender Config: klare Fehlermeldung, definierter Exit (der Job ist `restart:no`, kein Dauerdienst).
- **Keine App-Code-Änderung** (Lane C).
## Definition of Done
- Aus „leerer Garage" stellt ein einziger Job-Lauf Bucket + Key + Rechte her; zweiter Lauf ist ein sauberer No-Op.
- Danach greift `HeadBucket` der App erfolgreich (Übergabe an Lane C/D).
- Runbook-Schritt in `docs/DEPLOY-COOLIFY.md` dokumentiert.
## Abhängigkeiten
Braucht **Lane A** (Garage-Service + Admin-Zugriff). Liefert die Vorbedingung für **Lane C** (`ensureBucket` prüft nur) und **Lane D** (Abnahme).
-55
View File
@@ -1,55 +0,0 @@
# Kickoff-Prompt — Lane Sicherheitshärtung (certvia)
> Diesem Prompt einem Entwickler/Claude-Code-Agenten geben. Bootstrapt in certvia + diese Lane.
```text
Du übernimmst die Lane „Sicherheitshärtung" am Produkt „certvia" (ISMS-Tool, Multi-Tenant
Next.js-16 / Prisma-7 / Postgres+RLS / Auth.js-v5). Repo: ~/Projects/ISMS-Tool, Branch `dev`.
WICHTIG: certvia ist strikt getrennt von jedem anderen Produkt (insb. „visitvia"). Nichts vermischen.
BRANCH/CHECKOUT:
Arbeits-Branch: `lane-haertung` (KEIN `feature/`-Prefix — origin-Namespace-Konflikt).
git fetch origin && git checkout dev && git checkout -b lane-haertung.
Remotes: origin = https://git.certvia.de/msolarczek/certvia.git · local-gitea (intern).
1) PFLICHTLEKTÜRE:
- docs/HANDOVER-DEV.md, docs/SPEC.md, docs/STAND-dev-branch.md
- docs/KONZEPT-haertung.md ← dein Fahrplan
- docs/KONZEPT-backup-restore.md §9 ← das gemeinsame Verschlüsselungs-Primitiv
2) AUFTRAG (Phasen aus dem Konzept §7):
(1) PEPPER (App) — JETZT, solange test/dev-DBs frisch/leer sind.
(2) Host-Encryption (LUKS/Volume) + verschlüsselte Backups (Ops, mit Backup-Lane).
(3) Secrets-Register formalisieren.
(4) DB-TLS (sslmode) + Vault/KMS = Phase 2.
3) VERBINDLICHE REGELN:
- PEPPER: über Argon2-`secret` bei hash() UND jeder verify()-Stelle. Env `PASSWORD_PEPPER`
(32-Byte hex). Nach Option C ist der verify-Pfad zentral (Login gegen Identity) → wenn nötig
einen verifyPassword(hash,pw)-Wrapper einführen, damit Pepper EINE Stelle ist.
- ⚠ Pepper ist NICHT rotierbar ohne Passwort-Reset für alle (wie MFA_ENC_KEY) → bewusst
JETZT setzen, leere DBs nutzen. Restore-Kohärenz: Pepper ist Umgebungs-Secret, nicht im Artefakt.
- Argon2-PARAMETER sind ERLEDIGT (src/server/password.ts, ARGON2_OPTIONS) — nicht anfassen,
nur den `secret` ergänzen.
- Host-Encryption/Backup-Verschlüsselung sind OPS-Runbook (kein App-Code) → in
docs/DEPLOY-PROD-CONTABO.md als Go-Live-Punkte ergänzen; §9-Mechanismus (AES-256 client-seitig,
Keys pro Umgebung) verwenden.
- Secrets-Register: AUTH_SECRET · MFA_ENC_KEY · PASSWORD_PEPPER · pgBackRest-Key · age-Keypair;
je Umgebung getrennt, nie im Artefakt-Bucket, Offline-Kopie versiegelt.
4) ARBEITSWEISE:
- Gate vor JEDEM Merge: tsc · lint · build · ALLE scripts/test-*.ts. Der Pepper braucht einen Test
(hash+verify mit gesetztem Pepper; verify schlägt fehl bei falschem/fehlendem Pepper).
- DevOps: lane-haertung → git merge --no-ff nach dev → Gate grün → Push auf BEIDE Remotes →
STAND pflegen.
5) KOORDINATION: §9-Verschlüsselung gemeinsam mit Lane Backup (Mechanismus/Keys entschieden — einmal
abstimmen). Pepper-Rollout NUR auf Umgebungen mit leerer/frischer DB (sonst Passwort-Reset nötig).
6) NICHT TUN: keine Auth-Logik-Änderung außer dem Pepper; keine Rotation eines gesetzten Peppers/
MFA_ENC_KEY ohne expliziten Reset-Plan; Argon2-Parameter unverändert lassen.
Erste Schritte: (a) Pflichtlektüre + Fragen, (b) Pepper-PR-Plan (Env, verify-Wrapper, Test) +
Bestätigung „Pepper jetzt setzen, DBs leer" einholen, (c) umsetzen. Warte nach (a)/(b).
```
-58
View File
@@ -1,58 +0,0 @@
# Kickoff-Prompt — Lane Betreiber-Konsole-UX + i18n (certvia)
> Diesem Prompt einem Entwickler/Claude-Code-Agenten geben. Bootstrapt in certvia + diese Lane.
```text
Du übernimmst die Lane „Betreiber-Konsole-UX + vollständige i18n" am Produkt „certvia" (ISMS-Tool,
Multi-Tenant Next.js-16 / Prisma-7 / Postgres+RLS / Auth.js-v5). Repo: ~/Projects/ISMS-Tool, Branch `dev`.
WICHTIG: certvia ist strikt getrennt von jedem anderen Produkt (insb. „visitvia"). Nichts vermischen.
BRANCH/CHECKOUT:
Arbeits-Branch: `lane-ui-i18n` (KEIN `feature/`-Prefix — origin-Namespace-Konflikt).
git fetch origin && git checkout dev && git checkout -b lane-ui-i18n.
Remotes: origin = https://git.certvia.de/msolarczek/certvia.git · local-gitea (intern).
1) PFLICHTLEKTÜRE:
- docs/HANDOVER-DEV.md, docs/SPEC.md, docs/STAND-dev-branch.md
- docs/KONZEPT-ui-i18n.md ← dein Fahrplan (Bugfix 1+2, Ergänzungen A/B)
2) AUFTRAG:
BUGFIX 1 — Betreiber-Konsole (src/app/(platform)/admin/[id]/page.tsx):
- Module → Popup (searchParam `?modules=1`, <Modal>); Benutzer → Popup (`?users=1`).
Muster existiert bereits auf der Seite (?new/?edit/?audit) — analog kapseln.
- Hauptfenster stattdessen: Stammdaten (aus TenantSettings) + Hauptkontakt sichtbar.
Offene Kleinentscheidung: Hauptkontakt = neue Felder in TenantSettings vs. Ableitung aus
tenant-admin — mit PM klären.
BUGFIX 2 — Vollständige UI-Sprache + per-Mitarbeiter-Umschaltung:
- BEFUND: messages/en.json existiert (~95%), aber src/i18n/request.ts ist HART auf "de"
(Zeile 8). TenantSettings.locale steuert nur die Richtlinien-Import-Sprache, NICHT die UI.
- Neu: Identity.uiLocale (de|en, Default de) — persönliche Präferenz, folgt der Person.
- request.ts liest uiLocale der aktiven Session-Identity (Fallback de).
- Umschalter im Nutzer-Menü (Header/Profil) → Server-Action setUiLocale → speichern + reload.
- messages/en.json auf 100% prüfen/auffüllen; hartkodierte deutsche Strings in Komponenten
aufspüren und in den Katalog überführen.
ERGÄNZUNG A — Login: Organisationsfeld (`tenant`) aus src/app/login/page.tsx ENTFERNEN
(nach Option C überflüssig; Mandant kommt über /select-tenant). login-ticket-Signaturen entschlacken.
3) VERBINDLICHE REGELN:
- UI-Sprache (Identity.uiLocale, Person) und Vorlagen-Import-Sprache (TenantSettings.locale,
Inhalt) sind ZWEI getrennte Achsen — nicht vermischen.
- Popups über das bestehende searchParam/<Modal>-Muster, KEINE neue Modal-Infrastruktur.
- Stammdaten haben EINE Quelle (TenantSettings) — keine Doppelpflege einführen.
- RLS/Datenpfade unverändert; das ist eine reine UI-/Präferenz-Lane.
4) ARBEITSWEISE:
- Gate vor JEDEM Merge: tsc · lint · build · ALLE scripts/test-*.ts.
- UI-Verifikation im Browser: Betreiber-Popups (Module/Benutzer), Sprachumschaltung DE↔EN
(Menü + Beschreibungen wechseln), Login ohne Organisationsfeld, /select-tenant unverändert.
- DevOps: lane-ui-i18n → git merge --no-ff nach dev → Gate grün → Push auf BEIDE Remotes →
STAND pflegen.
5) NICHT TUN: keine Änderung am Auth-Kern/an /select-tenant-Logik; Identity.uiLocale nur als
Präferenzfeld ergänzen (keine Auth-Semantik).
Erste Schritte: (a) Pflichtlektüre + Fragen (v.a. Hauptkontakt-Feld), (b) PR-Plan je Bugfix
(Migration Identity.uiLocale, request.ts, Menü-Switcher; Popup-Umbau admin/[id]), (c) umsetzen.
Warte nach (a)/(b).
```
@@ -1,71 +0,0 @@
# Übergabe-Prompt — Auth-Umbau „Zentrale Identität + Mandanten-Mitgliedschaften"
> Diesen Prompt einem neuen Entwickler bzw. dessen Claude-Code-Agenten geben. Er bootstrapt in certvia und diese Aufgabe. Zugehörige Dokumente liegen im selben Branch.
```text
Du übernimmst Entwicklungsarbeit am Produkt „certvia" (ISMS-Tool, Multi-Tenant-
Next.js-16 / Prisma-7 / Postgres+RLS / Auth.js-v5 SaaS). Repo: ~/Projects/ISMS-Tool,
Integrationsbranch `dev`. Deine Aufgabe: den Auth-Umbau „Zentrale Identität mit
Mandanten-Mitgliedschaften" (Option C) umsetzen.
BRANCH / CHECKOUT:
Arbeits-/Doku-Branch: `identity-mandanten`
- origin = https://git.certvia.de/msolarczek/certvia.git (Branch: identity-mandanten)
- local-gitea = interner Spiegel (Branch: identity-mandanten)
Auschecken: git fetch origin && git checkout identity-mandanten
Die unten genannten docs/ liegen auf genau diesem Branch.
WICHTIG: certvia ist strikt getrennt von jedem anderen Produkt (insb. „visitvia").
Nichts vermischen.
1) PFLICHTLEKTÜRE — erst lesen, nicht sofort coden, in dieser Reihenfolge:
- docs/HANDOVER-DEV.md, docs/SPEC.md (Projektgrundlagen)
- docs/STAND-dev-branch.md (aktueller Entwicklungsstand)
- docs/UEBERGABE-identity-mandanten.md (Kurzübergabe zu genau dieser Aufgabe)
- docs/FEINDESIGN-identity-mandanten.md (der umsetzbare Bauplan — dein Fahrplan)
- docs/KONZEPT-identity-mandanten.md (Warum/was, getroffene Entscheidungen A–E)
2) AUFTRAG:
Setze Option C um. Reihenfolge = die Workstreams/Meilensteine aus dem FEINDESIGN.
Beginne mit WS0 (Fundament): neues globales `Identity`-Modell, Schema-Recut von
`User` zur Mitgliedschaft (+identityId), Migration, `TENANT_MODELS` anpassen,
Reseed. WS0 ist Blocker — bau es als Pairing und lass es reviewen.
3) VERBINDLICHE REGELN (aus dem FEINDESIGN, nicht verhandelbar):
- `User.id` = Mitgliedschaft, STABIL lassen; Auth-Felder wandern auf `Identity`.
- `Identity` ist GLOBAL: NICHT in `TENANT_MODELS`, kein tenant_id, keine RLS-Policy.
- Genau EIN aktiver Mandant pro Session; `session.user.tenantId` = aktiver Mandant;
server-autoritativ; bei Mandantenwechsel Membership+MFA re-validieren.
- Nutzeranlage NUR per Einladung (kein „Passwort direkt setzen" mehr).
- MFA/Passwort gehören der Identity — KEIN Mandanten-Admin-Reset.
- MFA beim Login als 2. Schritt (erst E-Mail+Passwort, dann MFA); dazwischen KEINE
volle Session (kurzlebiger MFA-pending-State).
- Plattform-Admins (`platform_admins`) bleiben getrennt — nicht anfassen.
4) ARBEITSWEISE:
- Validierungs-Gate vor JEDEM PR/Merge: `npx tsc --noEmit`, `npm run lint`,
`npm run build`, und ALLE `scripts/test-*.ts` müssen grün sein. Jede Story bringt
ihren eigenen Test mit (Identity-Login, Tenant-Switch-Isolation, MFA-Enforcement
multi-tenant, Einladung, Two-Step-Bypass).
- DevOps: Feature-Branch → `git merge --no-ff` nach `dev` → Gate grün →
Push auf BEIDE Remotes (origin=git.certvia.de, local-gitea) →
docs/STAND-dev-branch.md pflegen.
- Definition of Done: siehe FEINDESIGN §9.
5) KOORDINATION:
Parallel läuft „Richtlinien-Upload im Adminportal". Funktional unabhängig, ABER
beide editieren src/app/(platform)/admin/[id]/page.tsx (Policy-Upload = Module-Karte,
Identity-Umbau = Benutzerverwaltung). Reihenfolge auf dieser Datei abstimmen.
6) NICHT TUN:
- Getroffene Entscheidungen A–E nicht umwerfen (bei echtem Problem eskalieren, nicht
eigenmächtig ändern).
- Phase-2-Punkte (per-Mandant „immer Step-up", E-Mail-Änderung als Identity-Op,
PlatformAdmin-Konsolidierung) sind bewusst ausgeklammert — nicht mitbauen.
- Keine Datenmigration bauen: es gibt nur Testdaten → Schema-Recut + Reseed.
Erste Schritte: (a) Pflichtlektüre lesen und mir eine 10-Zeilen-Zusammenfassung +
offene Fragen zurückgeben, (b) WS0 als PR-Plan skizzieren (Schema-Diff, Migrationen,
TENANT_MODELS-Änderung, Reseed), (c) nach Freigabe umsetzen. Warte nach (a)/(b) auf
Bestätigung, bevor du WS0 mergst.
```
-43
View File
@@ -1,43 +0,0 @@
# Übergabe-Prompt: Kundenbetreuung certvia
**Zweck:** Onboarding eines neuen Kundenbetreuers (Customer Success/Support). Kann direkt gelesen oder einem KI-Assistenten (z. B. Claude) als Kontext-Prompt gegeben werden. Hauptquelle im Detail: `docs/HANDBUCH-KUNDENBETREUUNG.md`.
---
Rolle: Du übernimmst die Kundenbetreuung (Customer Success/Support) für „certvia" — ein Multi-Mandanten-ISMS-Tool (Informationssicherheits-Managementsystem als SaaS). Ziel: certvia-Kunden onboarden, betreuen und im Alltag unterstützen.
Was ist certvia (in einem Satz):
Jeder Kunde ist ein eigener Mandant mit strikt getrennten Daten. Das Tool führt Kunden von der Strukturanalyse (Assets/Prozesse/Lieferanten) über Risikomanagement, Richtlinien und Maßnahmen bis zur Audit-Vorbereitung — wahlweise nach TISAX (VDA-ISA) und/oder ISO 27001.
Deine ersten Schritte (Woche 1):
1. Lies das Handbuch: docs/HANDBUCH-KUNDENBETREUUNG.md (im Projekt-Repo). Es ist deine Hauptquelle.
2. Klick die Demo-/Testinstanz komplett durch — am besten lernt man das Produkt hands-on:
- Mandanten-Login /login: admin@demo.example / Demo1234!
- Superadmin /platform/login (MFA-Einrichtung beim ersten Login).
- Schau alle Module an: Assets, Prozesse, Risiken, Lieferanten, Maßnahmen, Richtlinien, SoA, Audit-Readiness, Vorfälle, Aufgaben.
3. Verstehe die Framework-Wahl: ein Mandant kann ISO 27001 und/oder TISAX führen (in der Admin-Konsole je Mandant aktivierbar). Unterschiede stehen im Handbuch (TISAX = Reifegrad/AL2/AL3; ISO = Anwendbarkeitserklärung/SoA + Managementklauseln).
Deine Kernaufgaben:
- Neue Kunden beim Onboarding begleiten (Stammdaten, Framework-Wahl, Onboarding-Wizard, Erfassung der Bestandsdaten).
- Support im Alltag: Login/MFA/Passwort, Rollen & Rechte, Module, Vorlagen/Richtlinien.
- Fachliche Beratung „welche Norm passt" (ISO vs. TISAX) auf Basis des Handbuchs.
Typische Support-Fälle (Details im Handbuch §7):
- „Anmeldung fehlgeschlagen": richtige Seite (/login vs /platform/login), Passwort exakt, ggf. kurze Sperre nach 5 Fehlversuchen (15 Min).
- „MFA-Gerät verloren": Recovery-Codes; sonst MFA administrativ zurücksetzen.
- „Modul fehlt": in der Admin-Konsole für den Mandanten aktivieren.
- „ISO/TISAX fehlt": Framework des Mandanten prüfen/aktivieren.
Grenzen & Eskalation (wichtig):
- Niemals Kundendaten zwischen Mandanten kopieren (Mandantentrennung/Datenschutz).
- Keine eigenmächtigen Löschungen an Produktivdaten; keine Secrets per E-Mail/Chat weitergeben.
- Bei technischen Themen (Deployment, Fehlermeldungen im Betrieb, Backups, TLS/Domain, Datenbank) an das DevOps-/Entwicklerteam eskalieren — nicht selbst am Produktivsystem eingreifen.
Wo du alles findest:
- Handbuch: docs/HANDBUCH-KUNDENBETREUUNG.md
- Produkt/Funktionsumfang: docs/SPEC.md, docs/HANDOVER-PM.md
- Feature-Konzepte: docs/KONZEPT-framework-iso27001.md (ISO/TISAX)
- Kundennahe Mockups (im Browser): docs/ISMS-Prototyp-GEFIM.html, docs/ISMS-Lieferantenmanagement-GEFIM.html
- Aktueller Stand: docs/STAND-dev-branch.md
Arbeitsweise: Wenn du unsicher bist, prüfe zuerst im Handbuch und in der Demo-Instanz. Dokumentiere wiederkehrende Support-Fälle, damit das Handbuch wächst.
-310
View File
@@ -1,310 +0,0 @@
# ISMS-Plattform – Umsetzungsspezifikation
> **Version 1.2** · Ergänzt gegenüber 1.1: **Assets & BIA als ein gemeinsames Modul**, zugeordnete Risiken in Asset-/Prozess-Detailansichten, ausgearbeitete **Risiko-Detailansicht** (Maßnahmen, betroffene Assets, Control-Verknüpfung, Verlauf).
>
> Diese Datei ist die Projekt-Spec für die Umsetzung durch Claude Code.
> Sie beschreibt **Architektur, Datenmodell, Module, Rollen, API und Akzeptanzkriterien**.
> Sprache der Anwendung: **Deutsch (i18n-fähig, EN vorbereitet)**.
## 0. Kontext & Leitentscheidungen
Es wird eine Web-Applikation zum Betrieb eines Informationssicherheits-Managementsystems (ISMS) entwickelt, die von mehreren Kunden (Mandanten) mit mehreren Nutzern und Rollen genutzt wird. Ausrichtung auf **ISO/IEC 27001:2022** und **TISAX / VDA-ISA 6.0**.
Getroffene Entscheidungen (aus Anforderungsklärung):
| Thema | Entscheidung |
|---|---|
| Betriebsmodell | Zentrale SaaS, **Multi-Tenant** (gemeinsame DB, Mandanten-ID + strikte Row-Level-Trennung) |
| Container | Betrieb in Docker (Compose), horizontal skalierbar |
| KI / Assistent + Chat | **Cloud-LLM** (Anthropic/OpenAI) über austauschbaren Provider-Adapter |
| Tech-Stack | Modernes Web-Frontend, Technologie offen → **empfohlener Stack unten** |
| Authentifizierung | **Lokale Accounts (MVP)** mit RBAC; SSO (OIDC/SAML) als spätere Erweiterung vorbereiten |
| Umfang | Alle Module gleichwertig (kein hartes Phasing), aber sinnvolle Iterationsreihenfolge |
| Norm-Inhalte | **ISO 27001:2022 Annex A (93 Controls)** vorbefüllt + SoA; **VDA-ISA** Struktur/Mapping vorbereitet, Katalog-Inhalte per Import (lizenzrechtlich, siehe §12) |
| Sprache | Deutsch, i18n-ready |
| UX | Modern, einfach, geringe Komplexität, Drag-and-Drop, grafische Oberflächen |
## 1. Empfohlener Tech-Stack
Ziel: **eine** Codebasis, geringe Betriebs- und Wartungskomplexität, moderne UX.
- **Frontend & Backend:** Next.js 15 (App Router, React 19, TypeScript) — SSR + API in einem Deployment.
- **API-Layer:** tRPC (typsicher) oder REST (OpenAPI). Empfehlung: tRPC intern, zusätzlich schlanke REST-Endpunkte für Integrationen/Webhooks.
- **ORM/DB:** Prisma + **PostgreSQL 16**. Vektorsuche für den Chat/RAG über **pgvector** (kein zusätzlicher Vektor-DB-Dienst nötig).
- **Auth:** Auth.js (NextAuth) mit Credentials-Provider (lokale Accounts), Argon2id-Hashing, TOTP-2FA. OIDC/SAML-Provider als Feature-Flag vorbereitet.
- **Hintergrundjobs / wiederkehrende Aufgaben:** BullMQ + Redis (Scheduler für Reviews, Audits, Fristen, Erinnerungen).
- **Dateispeicher:** S3-kompatibel (MinIO im Compose-Stack), verschlüsselt.
- **UI-Kit:** Tailwind CSS + shadcn/ui, Icons via lucide-react, Diagramme via Recharts.
- **Drag-and-Drop:** dnd-kit (Kanban-Boards, Risiko-Matrix-Einordnung, Aufgaben, Datei-Uploads).
- **KI-Anbindung:** Provider-Adapter (`AiProvider`-Interface) für Anthropic/OpenAI; RAG-Pipeline auf Dokumenten des Mandanten.
- **E-Mail:** SMTP (Benachrichtigungen, Fristen, Eskalationen).
- **Tests:** Vitest (Unit), Playwright (E2E). Linting: ESLint + Prettier.
> Alternative bei starkem KI-/Analytics-Fokus: zusätzlicher **Python-FastAPI-Microservice** nur für RAG/Embedding. Für geringe Komplexität zunächst **nicht** empfohlen — alles in Next.js.
## 2. Architektur (High-Level)
```
┌─────────────────────────────────────────────┐
Browser ───▶ │ Next.js (App Router) │
(DE UI) │ - React UI (Tailwind + shadcn/ui, dnd-kit) │
│ - tRPC/REST API │
│ - Auth.js (RBAC, Mandanten-Guard) │
└───────┬───────────────┬──────────────┬───────┘
│ │ │
Prisma BullMQ AiProvider
│ (Worker) (Adapter)
┌───────▼───────┐ ┌───▼────┐ ┌────▼─────────┐
│ PostgreSQL 16 │ │ Redis │ │ Anthropic / │
│ + pgvector │ │ │ │ OpenAI API │
└───────────────┘ └────────┘ └──────────────┘
┌───────────────┐
│ MinIO (S3) │ Dokumente, Nachweise, Uploads
└───────────────┘
```
**Mandantenfähigkeit (Multi-Tenant):** Jede fachliche Tabelle trägt `tenant_id`. Zugriff ausschließlich über einen zentralen Prisma-Middleware-/Query-Guard, der `tenant_id` aus der Session erzwingt (Row-Level-Isolation). Zusätzlich Postgres **Row Level Security (RLS)** als zweite Verteidigungslinie. Kein Query ohne Mandantenkontext.
## 3. Rollen- und Rechtemodell (RBAC)
Rollen sind pro Mandant vergeben. Ein Nutzer kann mehrere Rollen haben.
| Rolle | Beschreibung | Kernrechte |
|---|---|---|
| **Plattform-Admin** (mandantenübergreifend) | Betreiber der SaaS | Mandanten anlegen/sperren, globale Kataloge pflegen, keine Einsicht in Kundendaten außer für Support (protokolliert) |
| **Mandanten-Admin** | Kundenadministrator | Nutzer/Rollen im Mandanten verwalten, Stammdaten, Konfiguration |
| **ISB / CISO** | Informationssicherheitsbeauftragter | Vollzugriff fachlich: Risiken, Assets, BIA, Maßnahmen, Vorfälle, Freigaben, Chat-Eskalationsziel |
| **Auditor** (intern/extern) | Prüfer | Lesezugriff + Audit-Durchführung, Findings anlegen, keine Bearbeitung der Fachdaten |
| **Asset-/Risk-Owner** | Fachverantwortliche | Bearbeitung zugewiesener Assets/Risiken/Maßnahmen |
| **Mitarbeiter / User** | Standardnutzer | Chat nutzen, Richtlinien lesen, Vorfälle melden, eigene Aufgaben |
Rechte werden als granulare **Permissions** (`asset:read`, `risk:write`, `incident:manage`, …) implementiert und über Rollen gebündelt. UI blendet nicht-erlaubte Aktionen aus; API prüft serverseitig.
## 4. Module (funktionaler Kern)
### 4.1 Assets & BIA (ein gemeinsames Modul)
Asset-Inventar und Business Impact Analyse bilden **ein zusammenhängendes Modul** (gemeinsamer Navigationsbereich, durchgängige Detailansichten). Assets, Prozesse und deren Kritikalität werden im selben Kontext gepflegt.
#### 4.1.1 Asset-Inventar
Zentrales Verzeichnis aller Werte (Assets): Informationen, Systeme, Anwendungen, Standorte, Lieferanten, Personen/Rollen, Datenkategorien.
- Attribute: Name, Typ, Owner, Standort/Prozesszuordnung, Klassifizierung (C/I/A — Vertraulichkeit/Integrität/Verfügbarkeit als Schutzbedarf 1–4), Lieferanten/Verantwortliche, Status, Tags.
- Beziehungen zwischen Assets (Abhängigkeiten) modellierbar → speist BIA und Risiko.
- Import/Export (Excel/CSV), Bulk-Bearbeitung, Versionshistorie.
- **Grafische Ansicht:** filterbare Tabelle + optionale Abhängigkeits-/Netzwerkgrafik.
- **Asset-Detailansicht** zeigt neben Stammdaten und Beziehungen auch:
- **Zugeordnete Risiken** (mit Risikowert, Status, Behandlung) inkl. Absprung in die Risiko-Detailansicht,
- zugeordnete Prozesse (mit Rolle primär/sekundär) und daraus vererbter Schutzbedarf,
- verknüpfte Maßnahmen, Vorfälle und Nachweise.
#### 4.1.2 Business Impact Analyse (BIA)
Ermittelt Kritikalität von Prozessen/Assets und Wiederanlaufparameter.
- Erfassung von Geschäftsprozessen, Zuordnung zu Assets.
- **Asset-Rollen je Prozess (Detailansicht):** In der Prozess-Detailansicht werden die zugeordneten Assets nach Rolle unterschieden:
- **Primäres Asset** = das im Prozess erzeugte/verantwortete Ergebnis-Asset (z. B. der erzeugte Datensatz/das Informationsobjekt). Genau ein oder wenige je Prozess.
- **Sekundäre Assets** = alle unterstützenden Assets, die zur Prozessdurchführung benötigt werden (Systeme, Anwendungen, Infrastruktur, Personen/Rollen, Lieferanten).
- Die Rollenzuordnung speist die Abhängigkeitsanalyse (siehe 4.11) und die Schutzbedarfsvererbung: Der Schutzbedarf des primären Assets/Prozesses vererbt sich auf die sekundären Assets (max. Prinzip).
- Schadensszenarien und Schadenshöhe je Schutzziel und Zeitverlauf.
- Kennzahlen: **RTO, RPO, MTD/MTPD**, maximal tolerierbarer Ausfall.
- Ergebnis: Kritikalitätsstufe je Prozess/Asset → priorisiert Risikoanalyse und Maßnahmen.
- **Prozess-Detailansicht** zeigt zusätzlich die **zugeordneten Risiken** des Prozesses und seiner Assets (aggregiert, mit Risikowert und Status).
- Geführter Wizard (Schritt-für-Schritt), Ergebnis als Report exportierbar.
### 4.2 Risikoanalyse & -behandlung
Aufbauend auf Assets + BIA.
- Risiken = (Asset/Prozess) × Bedrohung × Schwachstelle.
- Bewertung: Eintrittswahrscheinlichkeit × Auswirkung → Risikowert; konfigurierbare Skalen (z. B. 5×5).
- **Interaktive Risiko-Matrix (Heatmap)** mit Drag-and-Drop-Einordnung/Filter.
- Risikobehandlung: Vermeiden / Vermindern / Übertragen / Akzeptieren; Maßnahmen (Controls) verknüpfen.
- Rest-Risiko nach Maßnahme, Risikoakzeptanz mit Freigabe-Workflow (ISB/Leitung).
- Vererbung: Schutzbedarf aus Asset/BIA wird vorbelegt.
- Verknüpfung zu ISO-27001-Annex-A-Controls und VDA-ISA-Zielen (SoA-Bezug).
- **Risiko-Detailansicht (vollständig):**
- Stammdaten: Beschreibung, Bedrohung/Schwachstelle, Owner, Status, Termine/Reviews.
- Bewertung: Brutto-Risiko (Wahrscheinlichkeit × Auswirkung), **Rest-Risiko** nach Maßnahmen, Bewertungshistorie (Verlauf des Risikowerts über Zeit).
- **Notwendige/verknüpfte Maßnahmen:** Liste der behandelnden Maßnahmen mit Status, Fälligkeit und Owner; neue Maßnahme direkt aus dem Risiko heraus anlegbar.
- **Betroffene Assets/Prozesse:** alle verknüpften Assets und Prozesse mit Schutzbedarf/Kritikalität, Absprung in deren Detailansichten.
- Verknüpfte Annex-A-Controls / VDA-ISA-Ziele (SoA-Bezug) und ggf. auslösende Vorfälle.
- Freigabe-/Akzeptanz-Workflow mit Kommentar und Audit-Trail.
### 4.3 Control-Kataloge & Statement of Applicability (SoA)
- **ISO 27001:2022 Annex A** mit 93 Controls in 4 Themen (Organisatorisch 37, Personenbezogen 8, Physisch 14, Technologisch 34) **vorbefüllt**.
- **SoA:** je Control Anwendbarkeit (ja/nein + Begründung), Umsetzungsstatus, Verweise auf Maßnahmen/Nachweise, Verantwortliche.
- **VDA-ISA 6.0**: 9 Kapitel/Prüfziele als Struktur + Mapping-Tabelle ISO↔VDA-ISA (siehe §12 zur Lizenz).
- Reifegrad-Bewertung (VDA-ISA-Reifegradmodell 0–5) je Prüfziel.
### 4.4 Maßnahmenverwaltung (Controls/Tasks)
- Maßnahmen mit Owner, Fälligkeit, Status, Priorität, Nachweisen (Dateien).
- **Kanban-Board (Drag-and-Drop)** und Listen-/Kalenderansicht.
- Verknüpfung zu Risiken, Controls, Vorfällen, Audits.
- **Maßnahmen-Detailansicht** zeigt die **zugeordneten Risiken** (welche Risiken behandelt diese Maßnahme, mit Risikowert vor/nach) sowie verknüpfte Controls, Vorfälle und Nachweise.
### 4.5 Wiederkehrende Aufgaben & Fristen (Scheduler)
Automatische Erzeugung/Erinnerung für u. a.:
- **Benutzer-/Zugriffs-Review** (z. B. quartalsweise),
- **Interne Audits** und **externe Audits/Re-Zertifizierung** (Zyklen),
- **Management-Review**, Richtlinien-Review, Risiko-Review, Lieferanten-Review.
- Konfigurierbare Wiederholung (cron-artig), Zuweisung, Eskalation bei Überfälligkeit, E-Mail-Benachrichtigung, Dashboard-Fälligkeitsanzeige.
### 4.6 Richtlinien-Management & Implementierungs-Assistent
- Richtlinien-Bibliothek mit Versionierung, Freigabe- und Lese-Bestätigungs-Workflow.
- **KI-Assistent** führt Nutzer durch die Anpassung von Muster-Richtlinien an das eigene Unternehmen.
- **Zwei Individualisierungs-Ebenen:**
1. **Basisdaten/Metadaten:** Firmenname, verantwortliche Rollen, Geltungsbereich, Platzhalter.
2. **Inhaltliche Anpassung der Klauseln:** Die KI formuliert und passt die eigentlichen Richtlinientexte (Klauseln/Abschnitte) auf Basis der Unternehmensangaben an (z. B. erlaubte Geräte, Fristen, Verantwortlichkeiten, branchenspezifische Anforderungen). Vorschläge sind akzeptier-/verwerfbar.
- **Inline-Editor (WYSIWYG):** Nutzer bearbeiten die generierten Klauseltexte direkt im Tool; KI-Assistenz („umformulieren", „kürzen", „an Control X ausrichten") auf Absatzebene.
- **In-Tool-Darstellung:** Die Richtlinie wird innerhalb der Anwendung gerendert lesbar dargestellt (formatierte Ansicht, Inhaltsverzeichnis, verknüpfte Controls), nicht nur als Download.
- **Governance:** Versionierung mit Änderungsverlauf/Diff, Freigabe-Workflow, Lesebestätigung durch Mitarbeiter, Verknüpfung zu ISO-/VDA-Controls.
- Export als PDF/DOCX; Ablage in der Richtlinien-Bibliothek; Anbindung an den Chat (RAG) als Wissensquelle.
### 4.7 Interaktiver Chat (RAG + Eskalation)
- Nutzer stellen Fragen; der Chat sucht per **RAG** in der Dokumentation/Richtlinien des Mandanten (pgvector) und antwortet mit Quellenangabe.
- Wenn keine belastbare Antwort/Berechtigung → **Eskalation/Ticket an den ISB** (Weiterleitung, Benachrichtigung, Nachverfolgung).
- Nur mandanten-eigene Dokumente im Kontext; keine mandantenübergreifende Vermischung.
### 4.8 Sicherheitsvorfall-Management (Incident)
- Meldung von Vorfällen (auch niedrigschwellig durch Mitarbeiter, Formular + Chat).
- Workflow: Erfassung → Triage/Kategorisierung → Bearbeitung → Eskalation → Abschluss → Lessons Learned.
- Schweregrad, betroffene Assets, SLA/Fristen, Aufgaben, Zeitleiste, Nachweise.
- Verknüpfung zu Risiken (neue/erhöhte Risiken) und ggf. Meldepflichten-Hinweis.
### 4.9 Dashboards & Reporting
- Rollenbezogene Dashboards (ISB, Auditor, Owner): offene Risiken, überfällige Aufgaben, SoA-Erfüllungsgrad, Reifegrade, Vorfälle.
- Exporte: SoA, Risikoregister, Maßnahmenplan, Audit-Report, Management-Review — als PDF/Excel.
### 4.10 Audit-Management
- Audit-Plan (intern/extern), Scope, Prüfpunkte (aus Katalogen), Findings, Maßnahmen aus Findings, Nachverfolgung, Audit-Report.
### 4.11 Abhängigkeits- & Kritische-Pfade-Analyse
Visualisiert die Verkettung von Prozessen und Assets, um Single Points of Failure und kritische Pfade sichtbar zu machen.
- **Netzwerkgraph:** Knoten = Prozesse und Assets (primär/sekundär), Kanten = Abhängigkeiten (aus Asset-Relationen und BIA-Rollenzuordnung).
- **Kritische Pfade** werden anhand von Kritikalität/Schutzbedarf (aus BIA) berechnet und farblich hervorgehoben; Engstellen (Assets, von denen viele kritische Prozesse abhängen) werden markiert.
- Interaktiv: Filtern nach Prozess/Kritikalität, Knoten anklicken → Detail/Sprung zum Asset, Hervorheben aller abhängigen Elemente.
- Speist Risikoanalyse (Konzentrationsrisiken) und BCM/Notfallplanung.
- Umsetzungshinweis: Rendering mit einer Graph-Bibliothek (z. B. Cytoscape.js oder D3-Force); Datenbasis sind `AssetRelation` und die BIA-Primär-/Sekundär-Zuordnung.
### 4.12 Nachweis- & Dokumentenmanagement
- Zentrale, auditfeste Ablage für Nachweise/Evidenzen (Dateien, Screenshots, Protokolle).
- Verknüpfung eines Nachweises mit Controls (SoA), Maßnahmen, Audits, Risiken und Vorfällen.
- Metadaten: Gültigkeit/Ablaufdatum, Verantwortliche, Version, Vertraulichkeit; Erinnerung bei ablaufenden Nachweisen.
- Volltext-/Metadatensuche; Nachweise sind Quelle für den RAG-Chat.
### 4.13 Lieferanten- & Dienstleister-Management (Third-Party)
Für TISAX besonders relevant.
- Verzeichnis externer Dienstleister/Lieferanten mit Kontakt, Leistungen, Kritikalität.
- Sicherheitsbewertung/Fragebögen, Zertifikatsnachweise (z. B. ISO 27001/TISAX-Label), Ablaufüberwachung.
- Vertrags-/AV-Verwaltung (DSGVO Art. 28), Wiedervorlage/Review-Zyklen (siehe wiederkehrende Aufgaben).
- Verknüpfung zu Assets (welcher Dienstleister betrifft welche Assets) und Risiken (Third-Party-Risiko).
### 4.14 Management-Review & Kennzahlen (KPIs)
ISO-27001-Pflichtthemen zur Wirksamkeitsmessung.
- Definierbare Kennzahlen/Metriken (z. B. SoA-Erfüllungsgrad, offene Risiken, Reaktionszeiten Vorfälle, überfällige Aufgaben, Reifegradentwicklung) mit Zielwerten und Trend.
- Strukturierte **Management-Review**-Vorlage (Eingaben, Ergebnisse, Beschlüsse, Verantwortliche, Termine) mit Historie.
- Wirksamkeitsbewertung von Maßnahmen; Export als Management-Report (PDF).
## 5. Datenmodell (Kern-Entitäten, vereinfacht)
Alle fachlichen Tabellen enthalten `tenant_id`, `created_at`, `updated_at`, `created_by`.
- **Tenant**(id, name, status, config)
- **User**(id, tenant_id, email, password_hash, name, status, mfa_secret)
- **Role**(id, tenant_id, key, name) / **Permission** / **UserRole** / **RolePermission**
- **Asset**(id, tenant_id, name, type, owner_id, classification_c/i/a, status, tags)
- **AssetRelation**(asset_id, related_asset_id, type)
- **Process**(id, tenant_id, name, owner_id) — BIA-Bezug
- **ProcessAsset**(id, tenant_id, process_id, asset_id, **role** = `primary` | `secondary`) — Asset-Rolle je Prozess (BIA)
- **BiaEntry**(id, tenant_id, process_id, rto, rpo, mtd, impact_scores, criticality)
- **Threat** / **Vulnerability** (Kataloge, teils global)
- **Risk**(id, tenant_id, asset_id/process_id, threat_id, likelihood, impact, score, treatment, residual_score, owner_id, status)
- **RiskAsset**(risk_id, asset_id) — n:m betroffene Assets je Risiko (zusätzlich zum Hauptbezug)
- **RiskMeasure**(risk_id, measure_id) — n:m Risiko ↔ behandelnde Maßnahmen
- **Control**(id, framework, ref, title, theme) — global (ISO/VDA)
- **Soa**(id, tenant_id, control_id, applicable, justification, status, maturity, owner_id)
- **Measure/Task**(id, tenant_id, title, owner_id, due_date, status, priority, links[])
- **RecurringTask**(id, tenant_id, type, schedule_cron, next_run, assignee)
- **Policy**(id, tenant_id, title, version, status, body, ack_required)
- **Document/Evidence**(id, tenant_id, name, storage_key, embedding[] via pgvector)
- **ChatSession/ChatMessage**(…, sources[], escalated_to_isb)
- **Incident**(id, tenant_id, title, severity, status, category, affected_assets[], timeline[])
- **Audit**(id, tenant_id, type, scope, planned_date) / **Finding**(…)
- **Supplier**(id, tenant_id, name, criticality, contract_ref, av_status, cert_status, cert_expiry, review_cron) / **SupplierAsset**(supplier_id, asset_id)
- **Kpi**(id, tenant_id, name, target, unit) / **KpiValue**(kpi_id, period, value)
- **ManagementReview**(id, tenant_id, date, inputs, results, decisions, owner_id)
- **AuditLog**(id, tenant_id, actor, action, entity, before/after, timestamp)
## 6. API-Design (Auszug)
REST-/tRPC-Ressourcen je Modul mit CRUD + Aktionen:
`/assets`, `/bia`, `/risks`, `/controls`, `/soa`, `/measures`, `/recurring-tasks`, `/policies`, `/chat`, `/incidents`, `/audits`, `/reports`, `/admin/tenants`, `/admin/users`.
Querschnitt: `GET /export/{modul}` (Excel/PDF), `POST /import/{modul}` (Excel/CSV), Webhooks für Fristen/Eskalationen. Jede Anfrage: Auth-Guard → Mandanten-Guard → Permission-Check → Handler → AuditLog.
## 7. Nicht-funktionale Anforderungen
- **Sicherheit:** Verschlüsselung at-rest (DB/Objektspeicher) und in-transit (TLS), Argon2id-Passwörter, TOTP-2FA, Session-Härtung, CSRF-/XSS-/SQLi-Schutz, striktes RBAC + Mandanten-Isolation (RLS), vollständiges Audit-Log.
- **Datenschutz (DSGVO):** Auftragsverarbeitung, Löschkonzept, Datenminimierung; für Cloud-LLM: DPA mit KI-Anbieter, konfigurierbares Opt-out des Trainings, optionale Anonymisierung sensibler Felder vor KI-Aufruf.
- **Performance:** Listen mit Server-Pagination/Filter; Zielantwortzeit < 300 ms für Standardabfragen.
- **Verfügbarkeit:** Stateless App-Container (horizontal skalierbar), Backups DB + Objektspeicher, Health-/Readiness-Endpunkte.
- **Barrierefreiheit & UX:** WCAG-AA-orientiert, responsive, konsistente Komponenten, geringe Klicktiefe.
- **i18n:** Alle UI-Texte über Message-Katalog (de default, en vorbereitet).
- **Nachvollziehbarkeit:** Versionierung von Richtlinien, Risiken, SoA; lückenloses Änderungsprotokoll (auditfest).
## 8. Deployment (Docker)
`docker-compose.yml` mit Services: `app` (Next.js), `worker` (BullMQ), `postgres` (pgvector-Image), `redis`, `minio`, optional `mailhog` (Dev). Konfiguration über `.env` (DB, Redis, S3, SMTP, `AI_PROVIDER`, `AI_API_KEY`). Migrations via Prisma. Seed-Skript befüllt globale Kataloge (ISO Annex A, VDA-ISA-Struktur, Threat-/Vuln-Beispiele) und einen Demo-Mandanten.
## 9. UX-Leitlinien
Modern, aufgeräumt, geringe Komplexität. Konsequenter Einsatz von: geführten Wizards (BIA, Richtlinien-Assistent), **Drag-and-Drop** (Kanban für Maßnahmen/Aufgaben, Risiko-Heatmap, Datei-Uploads), grafischen Auswertungen (Heatmaps, Reifegrad-Radar, Fortschrittsbalken), Inline-Bearbeitung und klaren, rollenbezogenen Startseiten. Fachbegriffe mit Tooltips erklärt.
Das HTML-Mockup (`docs/ISMS-Prototyp-GEFIM.html`) dient als **Orientierung** für Navigation, Layout und Views — die Module werden fachlich darüber hinaus vervollständigt (z. B. Risiko-Verknüpfungen in Detailansichten, siehe §4).
## 10. Akzeptanzkriterien (Definition of Done je Modul)
- Ein Nutzer eines Mandanten sieht **niemals** Daten eines anderen Mandanten (durch Test nachgewiesen: RLS + Guard).
- RBAC serverseitig erzwungen; UI blendet unerlaubte Aktionen aus.
- Asset → BIA → Risiko → SoA/Maßnahme bilden eine durchgängige, verknüpfte Kette — in beide Richtungen navigierbar (Asset zeigt Risiken, Risiko zeigt Maßnahmen/Assets, Maßnahme zeigt Risiken).
- Wiederkehrende Aufgaben erzeugen automatisch Instanzen, benachrichtigen und eskalieren bei Überfälligkeit.
- Chat beantwortet Fragen mit Quellen aus Mandanten-Dokumenten und eskaliert korrekt an den ISB.
- Incident-Workflow von Meldung bis Abschluss inkl. Zeitleiste und Nachweisen.
- Exporte (SoA, Risikoregister, Audit-Report) erzeugen valide PDF/Excel.
- E2E-Tests (Playwright) für die Kernpfade grün; Audit-Log erfasst alle schreibenden Aktionen.
## 11. Empfohlene Umsetzungsreihenfolge (Iterationen)
Auch wenn Module gleichwertig sind, minimiert diese Reihenfolge das Risiko:
1. **Fundament:** Projektsetup, Docker-Compose, Auth (lokale Accounts), Mandanten-Isolation (RLS + Guard), RBAC, Audit-Log, i18n-Gerüst.
2. **Kern-Fachdaten:** Assets & BIA (ein Modul) → Risikoanalyse (inkl. Heatmap + Detailansicht).
3. **Compliance:** ISO-Annex-A-Katalog + SoA, VDA-ISA-Struktur + Mapping, Reifegrade.
4. **Betrieb:** Maßnahmen-Kanban, wiederkehrende Aufgaben/Scheduler, Benachrichtigungen.
5. **Vorfälle:** Incident-Management inkl. Meldeformular.
6. **KI:** RAG-Chat mit Eskalation, Richtlinien-Implementierungs-Assistent.
7. **Auswertung:** Dashboards, Reporting/Exporte, Audit-Management.
8. **Härtung:** Sicherheits-/Datenschutz-Review, Tests, Doku.
## 12. Lizenz-/Rechtshinweis zu Katalog-Inhalten (empfohlenes Vorgehen)
- **ISO 27001:2022** Control-**Titel/Referenzen** (Annex A, A.5–A.8) können als Katalog abgebildet werden; der **volle Normtext** ist urheberrechtlich geschützt und darf nicht mitgeliefert werden. → Nur Kurzbezeichnungen + eigene Umsetzungshinweise, Volltext verweist der Kunde auf seine erworbene Norm.
- **VDA-ISA 6.0 / TISAX:** Der ISA-Katalog wird vom VDA/ENX bereitgestellt (ISA6 als Excel). Empfehlung: **Struktur (9 Kapitel/Prüfziele) und Mapping** abbilden, die konkreten Katalog-Inhalte per **Import** aus der offiziellen ENX-Datei einspielen (Import-Funktion für `ISA6-EN.xlsx`). So bleibt die Anwendung lizenzkonform und aktualisierbar.
- **Mapping ISO 27001 ↔ VDA-ISA** als pflegbare Tabelle vorsehen (Controls beeinflussen mehrere Prüfziele und umgekehrt).
## 13. Offene Punkte / spätere Erweiterungen
- SSO (OIDC/SAML, z. B. Entra ID) — Feature-Flag bereits vorgesehen.
- Self-hosted-LLM-Option je Mandant (falls Datenschutzanforderungen es verlangen).
- Meldepflichten-Assistent (NIS2/DSGVO-Fristen) im Incident-Modul.
- Mobile App / PWA für Vorfallmeldung.
---
### Quellen
- VDA-ISA / TISAX Katalog v6.0: https://portal.enx.com/en-us/TISAX/downloads/
- VDA ISA 6.0 verpflichtend: https://vda-isa-berater.com/en/mandatory-vda-isa-catalog-version-6-0-for-all-those-who-order-the-tisax-assessment-now/
- VDA Information Security: https://www.vda.de/en/topics/digitization/data/information-security
-404
View File
@@ -1,404 +0,0 @@
# Entwicklungsstand `dev` — Konsolidierte Übergabe (PM + neue Entwickler)
> Stand: 2026-07-30 · Branch **`dev`** · **Sync-Hinweis:** lokal **deutlich vor `origin/dev`** (`origin/dev` = `9b479ab`); enthält u. a. Prod-Deployment-Vorbereitung, Wizard-Kickoff + Story A1-1 und diese Doku. `dev` ist auf **`origin` (git.certvia.de)** und **`local-gitea` (intern)** gepusht. Vor dem nächsten Push zuerst `git fetch` und prüfen, ob niemand weitere Commits hat. **Alle Feature-Branches** (Dev A: A1–A8; Dev B: F1–F4, B1–B7 inkl. Prüfziel-Follow-up) sowie **Certvia-Branding**, der **Wizard-Parallelisierungs-Fix** und zuletzt das **Security-Härtungspaket P1/P2/P3 (F-01…F-20, u. a. RLS scharf F-04, moduleGuard-DB-Authz F-06, MFA-Replay, Container-/Supply-Chain-Härtung F-11/F-18)** sowie **SEC1 (Mailversand/Queue)**, **SEC2 (Auth-Self-Service: Passwort-Reset/-Wechsel/E-Mail-Änderung, Session-Invalidierung, Rate-Limit)** und zuletzt die **TISAX-Umstrukturierung (M0–M4: Fundament, Strukturanalyse, Cockpit mit RACI/Evidence, Audit-Wizard, TISAX-Tasks, Wizard-Neustruktur; v3: Prozesshaus + geführtes BIA je Prozess)** sowie **SEC3/SEC4 (MFA pro Mandant erzwingen, WebAuthn/Passkeys, TOTP-Verschlüsselung at rest, Plattform-Admin-Verwaltung)** sind per DevOps-Integration additiv nach `dev` konsolidiert (Gate grün); noch offene Feature-Branches rebasen auf `dev`. **Deployment-Pflichtschritte** aus dem Security-Paket siehe §1a (F-04 RLS scharfschalten, F-06×F-10 `scripts/sync-role-permissions.ts` nach `migrate`). **SEC1/SEC2 fürs Deployment:** eigener **Mail-Worker-Service** (`npm run worker:mail` = `scripts/mail-worker.ts`) nötig, echte **SMTP-Variablen** setzen, und die Queue nutzt **Redis** (`REDIS_URL` mit Passwort, BullMQ/ioredis) — der Worker fehlt noch in `docker-compose.coolify.yml`. · Basis-Doku: `docs/HANDOVER-DEV.md`, `docs/HANDOVER-PM.md`, `docs/SPEC.md`
> Zweck: Ein Dokument, das den kompletten Stand der Weiterentwicklung auf `dev` zusammenfasst — fachlich (für PM) und technisch (für einen neuen Entwickler). `dev` ist noch **nicht** nach `main` gemergt.
> **Neu (2026-08-07):** `feature/settings-roles-platform-ui` integriert (Einstellungen umstrukturiert: Risikokriterien/Rollen als Menü-Karten + Link zur Plattform-Administration; neue Route `/settings/risk-criteria`). Zusätzlich **Audit-Trail einsehbar je Mandant**: wiederverwendbares server-gerendertes Popup (`src/components/audit-trail.tsx`, Muster wie die übrigen Popups über `?audit=1`) an **zwei** Stellen — Mandanten-Einstellungen `/settings` (eigener Mandant, TENANT_MODELS/RLS-gefiltert) und Plattform-Konsole `/admin/[id]` (Superadmin, cross-tenant über Owner-Client auf den Mandanten gefiltert). **Keine neue Migration.** Gate grün (tsc/lint/build + 17/17 Tests), Popup im Browser verifiziert.
>
> **Fix (2026-08-07):** `dev-fix-platform-admin` integriert — **Proxy-Gate für Plattform-Routen** (`src/proxy.ts`): `/platform/login` + `/api/platform-auth` sind jetzt public, und `/admin`, `/admins`, `/profile`, `/platform` gaten aufs Plattform-Session-Cookie und leiten anonyme Besucher auf **`/platform/login`** statt fälschlich auf die Mandanten-Login-Maske. Plus klarerer Modul-Toggle in `/admin/[id]` (Status-Pill + eigener „Aktivieren/Deaktivieren"-Button). Verifiziert: `/admin|/admins|/profile` → 307 `/platform/login`, `/dashboard` weiterhin → `/login`. Gate grün (17/17).
>
> **Fix (2026-08-07, 2):** Plattform-Auth bekommt **eigene Cookie-Namen für ALLE** Auth.js-Cookies, nicht nur `sessionToken` (`src/server/platform-auth.ts`): auch `csrfToken` (`__Host-platform-authjs.csrf-token`) und `callbackUrl` (`platform-authjs.callback-url`). Grund: zwei Auth.js-Instanzen auf derselben Domain teilten sich die Default-Cookies `authjs.csrf-token`/`authjs.callback-url`; da ein Nutzer zugleich Mandanten- und Plattform-Konto sein kann, überschrieb die zuletzt aktive Instanz das gemeinsame CSRF-Cookie → CSRF-Validierung der Plattform-Instanz schlug fehl (Sign-out warf, CSRF-POSTs/Session-Erneuerung landeten auf der Login-Maske). Getrennte Cookie-Namen isolieren beide Auth-Domänen vollständig. Gate grün (17/17).
>
> **Fix (2026-08-07, 3):** Plattform-Cookie-`secure`/Prefix folgt jetzt dem **AUTH_URL-Protokoll** (`https:`) statt `NODE_ENV` (`src/server/platform-auth.ts`, `PLATFORM_SECURE_COOKIES`), exakt wie Auth.js es für die Mandanten-Instanz tut (`useSecureCookies ?? url.protocol === "https:"`). Grund: im Container ist `NODE_ENV=production`, der interne Testserver wird aber ggf. über **http** bedient (AUTH_URL=http bzw. fehlendes `X-Forwarded-Proto`); ein hart auf `__Secure-`/`__Host-`/`secure` gesetztes Cookie wird vom Browser über http verworfen → Plattform-Session bleibt leer (jede Aktion → Login-Maske, Logout wirft), während die Mandanten-Session weiterläuft. **Betriebshinweis:** In Prod `AUTH_URL=https://…` setzen (dann greifen wieder secure-Cookies + `__Host-`/`__Secure-`-Prefixe). Gate grün (17/17).
>
> **Identity/Mandanten (Option C), WS0 (Fundament) — 2026-08-11:** globales `Identity`-Modell (kein `tenant_id`, **nicht** in `TENANT_MODELS`, keine RLS) + `User.identityId`; Migration `identity_foundation`; Reseed auf Identity+Membership inkl. 2. Mandant `demo2` + Multi-Membership-Fixture `multi@demo.example`. **Expand/Contract** (User-Auth-Felder bleiben vorerst Legacy; Contract-Migration droppt sie am Ende von WS1–WS4). Neuer Test `scripts/test-identity-schema.ts`. Grundlage/Fahrplan: `docs/FEINDESIGN-identity-mandanten.md`, `docs/KONZEPT-identity-mandanten.md`, `docs/UEBERGABE-identity-mandanten.md`, `docs/PROMPT-uebergabe-identity-mandanten.md`. Nächste Schritte: WS1 (Login gegen Identity, Two-Step + MFA-pending) → WS2 (`/select-tenant`), parallel WS3/WS4/WS6; Abschluss = Contract-Migration.
>
> **Identity/Mandanten (Option C), WS1/WS3/WS4/WS6 — 2026-08-11** (`feature/identity-auth-core` → `dev`, `--no-ff`, Gate grün: tsc/lint/build + **21/21** Tests; **noch nicht auf die Remotes gepusht**):
> - **WS1 Auth-Kern:** `auth.ts` authentifiziert gegen die globale `Identity` (Passwort/MFA/Lockout an der Identity); Mitgliedschaften werden geladen, der aktive Mandant per Organisations-Slug oder Single-Membership gewählt (mehrere **ohne** Slug ⇒ noch kein Login → `/select-tenant` = WS2). Login-Kern als `authorizeTenantCredentials()` exportiert. Session/JWT (`next-auth.d.ts`) neu: `identityId`/`activeMembershipId`/`memberships[]` (optional, da Plattform-Auth dieselben Typen nutzt); `tenantId` = aktiver Mandant → **356 dbForTenant-Call-Sites unverändert**. Test `scripts/test-identity-login.ts`.
> - **WS4 Passwort/MFA an Identity:** Passwortwechsel, MFA-Enroll/-Disable, Recovery-Codes und der **Session-Kill-Switch** (`sessionsValidAfter`) gehören der Identity (`account.ts`, `account/page.tsx`, `sessions.ts`). Guards (`action-guard.ts`, `(app)/layout.tsx`, `change-password`, `enroll-mfa`) lesen `mustChangePassword`/`sessionsValidAfter`/`mfaEnrolledAt` + globalen `identity.status` aus der Identity; Membership-Status/Rechte weiter aus `User`. Reset-Kette (`auth-recovery`/`auth-selfservice`/`auth-token`) auf `PrincipalType "identity"`. **Mandanten-Admin-Passwort-Reset entfernt** (goldene Regel 5). **E-Mail-Änderung für Mandanten-Konten deaktiviert = Phase 2.** Test `scripts/test-identity-account.ts`. **Abgetrennt (offen): WS4b WebAuthn→Identity** (Schema-Migration + `TENANT_MODELS` + `webauthn.ts`; Passkey-Login läuft bis dahin mandantengebunden).
> - **WS3 Einladungs-Lifecycle:** Nutzeranlage **nur per Einladung** (goldene Regel 4) — `TokenType "invitation"` (7 Tage) + neue öffentliche Seite `/invite` + `redeemInvitation`. `createTenantUser`/`createUser`/`inviteFunctionHolder` ohne pwMode „set"; **bekannte Identity → nur Mitgliedschaft ergänzen** (kein Passwort-Reset), unbekannt → Identity + Einladung; **neutrale Rückmeldung** (kein Cross-Tenant-Leak). `user-forms.tsx` = reines Einladungsformular. Test `scripts/test-invitation.ts`.
> - **WS6 Seed/Provision/Bootstrap:** bereits durch WS0 abgedeckt (`provisionTenant`/`seed.ts` Identity-fähig, `sync-role-permissions.ts` orthogonal) — kein Netto-neuer Code.
> - **Keine neue Migration** (WS1/WS3/WS4/WS6 sind code-only; das WS0-Schema trägt).
>
> **Identity/Mandanten (Option C) — WS2/WS4b/WS5 + Contract, UMBAU KOMPLETT — 2026-08-11** (`feature/identity-tenant-context` → `dev`, `--no-ff`, Gate grün: tsc/lint/build + **23/23**; **noch nicht gepusht**):
> - **WS2 Mandantenkontext:** Multi-Membership-Login ohne Organisations-Slug ergibt eine Session OHNE aktiven Mandanten → `(app)/layout` leitet auf **`/select-tenant`**. Wechsel server-autoritativ über **`setActiveTenant`** (`actions/tenant-switch.ts`, `unstable_update` + async jwt-`update`-Trigger → `resolveActiveMembership`: Rechte je Wechsel NEU aufgelöst). **Sidebar-`TenantSwitcher`** (nur bei >1 Mitgliedschaft). **Browser-verifiziert:** Wechsel demo↔demo2 inkl. Mandantenisolation (demo: 9 Assets, demo2: 0). Test `scripts/test-tenant-switch.ts`.
> - **WS4b WebAuthn→Identity:** Passkeys sind identitätsgebunden (Migration `20260811140000_webauthn_identity`: `webauthn_credentials` verliert `tenant_id`/RLS, verweist auf `identities`); `WebAuthnCredential` **raus aus `TENANT_MODELS`**. Passkey-Login/-Verwaltung über `identityId`.
> - **Contract-Migration** (`20260811150000_contract_user_auth_columns`): die 10 ungenutzten `User`-Auth-Spalten entfernt (`password_hash, must_change_password, failed_logins, locked_until, mfa_secret, mfa_enrolled_at, recovery_codes, last_totp_step, sessions_valid_after, is_platform_admin`) → **`User` = reine Mitgliedschaft** (`tenantId, identityId, email, name, status`). Expand/Contract abgeschlossen. DROP COLUMN → kein Reset.
> - **WS5 Two-Step-Login (Entscheidung A):** `/login` Schritt 1 (E-Mail+Passwort) → bei aktiver MFA kurzlebiger, signierter, einzweckiger `mfa_pending`-Cookie (KEINE Session) → **`/login/mfa`** Schritt 2 → `login-ticket`-Provider prägt die Session. Bausteine `verifyIdentityPassword`/`verifyIdentityMfa`/`finalizeIdentityLogin` (kein Passwort-Orakel, Lockout wie beim vollen Login). `src/server/login-ticket.ts` (HMAC). Runtime-verifiziert. Test `scripts/test-two-step-login.ts`.
> - **Damit ist der Umbau „Zentrale Identität mit Mandanten-Mitgliedschaften" vollständig** (alle 5 goldenen Regeln + Entscheidung A). Zwei neue Migrationen. **Offen: nur der Push auf beide Remotes** (origin + local-gitea) — bewusst zurückgehalten. Phase-2 (per-Mandant Step-up, E-Mail-Änderung als Identity-Op, PlatformAdmin-Konsolidierung) bleibt ausgeklammert.
>
> **Richtlinien-Vorlagen Plattform-Editor + EN-Paket — 2026-08-11:** `dev-fix-platform-admin` integriert — globale Vorlagen-Modelle + Migration `policy_templates`, Plattform-Editor (Entwurf/Veröffentlichen, Editoren für Anforderungen/Variablen), **Sprachwahl je Mandant** (`setTenantLocale`) und das **komplette EN-Übersetzungspaket** der Richtlinien-Vorlagen (Leitlinie, R01–R14, VA-01–VA-20, D01/P01, Baseline/Nachweisregister). Mandanten-Import liest die veröffentlichte DB-Version.
>
> **Härtung Argon2id — 2026-08-11:** Passwort-Hash-Parameter fixiert/dokumentiert (`src/server/password.ts`, `ARGON2_OPTIONS`: m=19456 KiB, t=2, p=1, outputLen=32; Argon2id = Lib-Default). Salt automatisch pro Hash (PHC-String), **kein** Pepper (dokumentiert). Nur neue Hashes betroffen.
>
> **Härtung Phase 2–3 (Ops-Doku) — 2026-08-11** (Lane `lane-haertung-ops`, **nur Markdown, kein App-Code/Migration, noch nicht nach `dev` gemergt**): **Phase 1 (Passwort-Pepper) ist erledigt** (Argon2-`secret`, zentral, in `dev`) — unverändert. Phasen 2–3 liegen jetzt als **Ops-Runbook** vor: `docs/DEPLOY-PROD-CONTABO.md` um **Host-Encryption at-rest** (LUKS/dm-crypt fürs Daten-Volume `pgdata`+MinIO, Boot-Unlock-Verfahren A/B, Passphrase im Passwortmanager) und **verschlüsselte Backups** (pgBackRest `aes-256-cbc` + `age` je Umgebung + restic für MinIO, Coolify-Cron/Retention/Restore-Test, Bezug zur App-Backup-Engine `BACKUP_ENC_KEY`/§9) ergänzt; neues **`docs/SECRETS-REGISTER.md`** (Ownership + Rotationsregel je Umgebung für `AUTH_SECRET`/`MFA_ENC_KEY`/`PASSWORD_PEPPER`/`BACKUP_ENC_KEY`/pgBackRest-Key/`age`-Keypair; `PASSWORD_PEPPER`+`MFA_ENC_KEY` als **nicht rotierbar** = Reset markiert). **Restore-Kohärenz-Regel** prominent in beiden Docs: Umgebungs-Secrets stehen nicht im Artefakt → Restore in fremde Umgebung bricht Passwort-/MFA-Prüfung bzw. Entschlüsselung. **DB-TLS (`sslmode`) + Vault/KMS bleiben Phase 2** (offen). Gate: nur `.md`, tsc/lint unberührt.
>
> **Merge-Zyklus 2026-08-11:** WS0 + Richtlinien/EN-Lane + Argon2 zusammen nach `dev` konsolidiert. Konflikt nur in `schema.prisma` (User-Relationsblock → Union mit `identity`) + `provision.ts` (auto-merge: Identity-Upsert **und** Policy-Variablen). DB neu gebaut (`migrate reset`, **52 Migrationen**, beide neuen koexistieren) + Seed. Gate grün: tsc/lint/build + **18/18 Tests**.
>
> **Konfigurierbarer Backup-Zielspeicher — 2026-08-17** (`lane-backup-target` → `dev`, `--no-ff`): Backup-/DSGVO-Zielspeicher jetzt **im Betreiber-Portal wählbar** (`/admin/backup`, Full-Admin + MFA-Step-up) — **Lokal** (persistentes Volume) oder **S3/MinIO**. `PlatformSetting` erweitert (`backupTarget`, `backupLocalDir`, `backupS3*`, `backupS3SecretKeyEnc` **verschlüsselt at-rest**), Migration `backup_target_config` (additiv). Statisches `backupStore` → **`getBackupStore()`** mit Präzedenz **DB → Env (`S3_*`/`BACKUP_LOCAL_DIR`) → lokaler Default `.backups`**, fail-secure bei unvollständiger S3-Config; alle Call-Sites umgestellt. **Persistentes `backups`-Volume** (app + backup-worker, `/app/.backups`) — Lokal überlebt Redeploys. Neuer Test `scripts/test-backup-target.ts`. Gate grün (tsc/lint/build + **28/28**), `/admin/backup` im Browser klickgeprüft (Lokal↔S3, Secret-Feld „nie Klartext"). Konzept/Prompt: `docs/KONZEPT-backup-target.md`, `docs/PROMPT-lane-backup-target.md`.
>
> **Modul „Vorfälle" (Incident-Management) IM-A — 2026-08-17** (`lane-incidents-a` → `dev`, vom Team): Fundament + Kern-Lifecycle/UI (Migration `incidents`, `src/app/(app)/incidents/*`, `src/server/actions/incidents.ts`, `docs/KONZEPT-incidents.md`, Test `scripts/test-incidents.ts`). IM-B (Meldefristen/Notifications) noch **in Arbeit** (eigener Branch `lane-incidents-b`).
>
> **Direkt-Download der Sicherung + S3-Bucket-Selbstheilung — 2026-08-17** (`lane-backup-download` @ `fe13a84` → `dev`, `--no-ff`): Neue Route `/api/platform/backup/download` (Full-Admin) — `exportTenant(persist:false)` streamt die verschlüsselte `.cvb` **inline in den Browser**, **ohne Worker/Redis/S3** („Sicherung herunterladen"-Button im Export-Popup). `S3BackupStore.ensureBucket()` legt einen fehlenden MinIO-Bucket beim ersten `put` **automatisch** an (behebt „The specified bucket does not exist"). Neuer Test `scripts/test-backup-download.ts`. Gate grün (tsc/lint/build + **30/30**). Merge über isoliertes Worktree (paralleler Team-Arbeitsbaum unberührt).
>
> **Modul „Vorfälle" IM-B/C/D (komplett) + Backup-Docker-Fix — 2026-08-18** (vom Team nach `dev` gemergt; von mir validiert + gepusht): **IM-B** (Meldefristen/Timer + Meldepflicht + Benachrichtigungen), **IM-C** (Verknüpfungen, Abschluss/Lessons-Learned, Export/Meldevorlagen), **IM-D** (E-Mail-to-Ticket Inbound + Provisionierung; Migration `incident_inbound_d`, Test `scripts/test-incident-inbound.ts`). Damit ist das Modul „Vorfälle" funktional komplett (IM-A…IM-D). **Docker-Fix:** `prisma/schema.prisma` wird jetzt auch in die `runner`-Stage kopiert — der Inline-Backup-Download läuft in der App (standalone), und die Backup-Topologie liest die Schema-Datei zur Laufzeit (Prisma 7 entfernt FK-Relationen aus dem Laufzeit-DMMF); sonst `ENOENT` → „Export fehlgeschlagen". Gate grün (tsc/lint/build + **33/33**).
---
## 1. Executive Summary
Auf `main` lag das Fundament (Auth/RBAC/Mandanten-Isolation, Assets/BIA, Risiko, Maßnahmen, Abhängigkeiten, Lieferanten, Richtlinien Phase 1/2a, Admin-Konsole Phase 1). Der Branch **`dev`** ergänzt fünf große Blöcke — alle mit `tsc`+`lint`+`build`, Modul-Guard-Check und (wo relevant) Browser-Verifikation abgeschlossen:
1. **Produktionshärtung Phase 1** — API-seitige Modul-Durchsetzung, getrennter Superadmin-Store mit eigenem Login + MFA, nicht-destruktiver Richtlinien-Re-Import.
2. **Benutzer- & Rollenverwaltung** (ohne E-Mail-Flow) — Plattform-Admin verwaltet Nutzer je Mandant; Mandanten-Admin verwaltet Nutzer **und** Rollen intern; Force-Change-Passwort, Passwort-Policy, optionale MFA je Nutzer; Popup-UI wie bei Assets.
3. **Freigabe-Workflow + Aufgaben-Modul** — Richtlinien-Freigabe an eine konkrete Person, Bearbeitung im neuen Modul „Aufgaben", Dashboard-Kachel; plus zentralisierte Richtlinien-Governance (zentrale Variablen, Schutzbedarf, Coverage-Filter nach Assessment-Level).
4. **GAP-Report-Umsetzung (Richtlinien-Vorlagenpaket)** — WP1–WP4 + E2: sechs neue Verfahrensanweisungen (ISB-Volltexte), Inline-VA-Verlinkung, Rendering-Fix, generisches editierbares Register-Datenmodell.
5. **Register-Konsolidierung in Fachmodule** — vier generische Register in die Fachmodule überführt: **Software** und **Projekte** als eigene Asset-Typen (Popups wie bei Assets/Lieferanten), **kritische IT-Dienste** als schreibgeschützte Auto-Sicht aus BIA, REG-NET auf Referenzfeld reduziert; Richtlinien-Links zeigen jetzt auf die Module statt auf Register.
6. **Produktions-Deployment-Vorbereitung** (Commits `cc8d486`, `f368d8f`) — Prod-Bootstrap `scripts/bootstrap-admin.ts` (legt Erst-Superadmin im `platformAdmin`-Store + Mandant/Mandanten-Admin idempotent an, ersetzt in Prod den Demo-Seed; per `BOOTSTRAP_*`-Env im `migrate`-Job der `docker-compose.coolify.yml`); `.env.prod.example`; **Fonts selbst gehostet** (`next/font/local`, committete woff2 in `src/app/fonts` → reproduzierbarer Offline-Build, DSGVO); Runbook **`docs/DEPLOY-PROD-CONTABO.md`** (VPS/Coolify/Gitea, TLS `app.certvia.de`, Bootstrap, Backups, Go-Live-Checkliste).
7. **Branding-Umstellung auf Certvia** — **in `dev` integriert (Gate grün)**: getrennte Token-Ebenen `--brand-*` (Logo/Print/Export) vs. `--ui-*` (Produktpalette, inhaltlich unverändert) mit `src/lib/brand.ts` als JS-Pendant; Certvia-Logo als Inline-SVG-Komponente (`<CertviaLogo>`), Favicon-/PWA-Icons + `site.webmanifest`, Metadaten/OG/i18n/TOTP-Issuer, gebrandete Auth-Seiten sowie **neue 404-/500-Seiten**, Druck-/Dokument-CD (`@media print` + `src/lib/document-brand.ts`) als Andockpunkt für den offenen DOCX/PDF-Export, brandfähige E-Mail-Basis (`src/lib/email-brand.ts`), Mandanten-Branding-Default (`resolveTenantBranding`). **GEFIM bleibt Dachmarke** („Ein Produkt von GEFIM"). Reines Branding, keine Funktionsänderung — Ausnahme: der Route-Gate-Matcher in `src/proxy.ts` musste `.webmanifest` freigeben, sonst lieferte `/site.webmanifest` die Login-HTML. Details: **`docs/BRANDING-CERTVIA.md`**.
**In Arbeit (parallele Entwicklung, 2-Lane Dev A × Dev B):** **Onboarding-Wizard** (`docs/wizard-uebergabe/`). **Beide Lanes bis hierher in `dev` konsolidiert** (inkl. Dev-B **B4**, per Fast-Forward integriert)**:**
- **Dev A:** A1-1 — Wizard-Shell: Modul `onboarding`, Step-Registry (`registerStep`, Keys 1–9, `guard` kann Schritte ausblenden), resumierbarer Fortschritt (`OnboardingProgress`). **F2** — generalisiertes `ObjectReviewStatus` (5 Zustände inkl. `zurueckgewiesen`) als wiederverwendbares Review-Enum; RBAC-Recht `validate_objects` + klonbare Rolle `external_validator`; Wizard-State-Machine mit Rechte-Trennung: Bearbeiter (`onboarding:use`) advance/rework/reset, Validator (`validate_objects`) validieren/zurückweisen (mit Begründung). „Weiter"-Gate: nur `validiert` zählt. Migration `object_review_status`. **A1-2** — Dashboard-Kachel „Onboarding-Fortschritt" (Anteil validierter Schritte + nächster offener Schritt, verlinkt in den Wizard). **A2-1** — Assessment-Level (AL2/AL3) als **einzige** Quelle des Schutzbedarfs: `protectionFlags(level)` (`src/server/assessment-level.ts`) leitet `FLAG_HIGH_PROTECTION`/`FLAG_VERY_HIGH_PROTECTION` zentral ab (Wizard/Provisioning seeden daraus, **nicht** mehr aus dem Fragebogen); Schutzbedarf-Frage `Q-FEAT-01` + Regeln `feat01-*` entfernt. **A2-2** — Scope-Objekt `WizardScope` + Filter-Engine (`src/lib/scope-filter.ts`: `activeRequirements`/`scopeSummary`, client-safe): filtert die Anforderungen nach AL-Baseline, `FLAG_INCLUDE_SHOULD` und Prüfzielen; Grunddaten `seed/scoping/c1-scope.json` (412 Anforderungen, generiert via `scripts/build-c1-scope.ts`); Action `saveScope` (`moduleGuard('onboarding')`) am Wizard-Schritt „scoping"; Migration `wizard_scope`; Tests `scripts/test-scope-filter.ts` (grün). **A3-1** — Validierungs-Workflow generalisiert: wiederverwendbares generisches Objekt-Review (`src/server/object-review.ts`, `src/lib/object-review.ts`, `src/components/object-review.tsx`) über Objekttypen hinweg (u. a. Risiken), an Risk-/Task-Actions angebunden. **A4** — ISMS-Rollen als Wizard-Schritt 3 „roles" inkl. Funktionstrennung FT-01…06 (`src/lib/ft-rules.ts`, Tests `scripts/test-ft-rules.ts`), ISB-Bestellung (`src/lib/isb-bestellung.ts`) und Rollen-Logik (`src/server/roles.ts`). **A5-1** — Asset-Schritt (Wizard-Schritt 5) auf dem Bestandsmodul (`steps/assets/step.tsx`). **A7** — **Control-Assessment** (Wizard-Schritt 7): Reifegrad-Engine R0–R3 + Zielreifegrad (`src/lib/maturity.ts`, Tests `scripts/test-maturity.ts`), Modell `ControlAssessment` (Migration `control_assessments`, RLS, in `TENANT_MODELS`) mit Belegstatus/Reifegrad-Bestätigung/Gap-Aufgaben; SoA-Kontext (`src/server/soa-context.ts`, `actions/soa.ts`), Control-Grunddaten `seed/scoping/c5-controls.json`. **A6** — **Standard-Risikokatalog (C4)**: globaler Katalog `RiskCatalogEntry` (Migration `risk_catalog`, kein RLS) + Import/Übernahme (`prisma/import-risks.ts`, `/risks/catalog`); A6-2 Standardmaßnahme→Aufgabe und **Restrisiko-Akzeptanz** (Migration `risk_acceptance`: Felder `accepted_at/by/rationale` an `risks`, VA-09). **A8** — **Gap-Konsolidierung** (Wizard-Schritt 8): Aggregations-/Priorisierungs-Engine (`src/lib/gap-consolidation.ts`: Dedup, Priorisierung, Quick-Wins), Task-Abgleich + Kontext (`src/server/gap-context.ts`, `src/server/actions/gap.ts`), Tests `scripts/test-gap-consolidation.ts` (keine neue Migration/Modell).
- **Dev B:** F1 (Task-Objekt um Wizard-Felder `type/owner/dueDate/priority/status/resources/origin/links` erweitert, Migration `tasks_wizard_fields`), B1 (Auto-Generierung von Aufgaben-Vorschlägen aus Triggern, C2 §8; `src/lib/task-triggers.ts`, `src/lib/tasks.ts`), F4 (Wizard-Flags in `variables.schema.json`: `FLAG_PROTOTYPE_PROTECTION`, `FLAG_ISB_EXTERNAL`, `FLAG_ISB_INTERNAL`; `_verify.py` = OK). **F3+B2** — Regel-/Mapping-Engine: deklarative DSL (`src/lib/rules/dsl.ts`), Auswerter (`engine.ts`, `evaluateCondition`) und C2-§5-Regelwerk (`feature-rules.ts`: Feature-Flags, Schutzbedarf-Ableitung, Control-Scope, Risiko-/Aufgaben-Trigger); Tests `scripts/test-rules.ts` (18 Fälle, grün). **B3** — Fragebogen als Wizard-Schritt 2 „context": Frage-Katalog A–F (`src/lib/onboarding/questions.ts`), Fakten-Ableitung (`facts.ts`, `deriveContext` über die Regel-Engine), Schritt-Komponente + `saveFacts`-Action (`onboarding-facts.ts`), Modell `WizardFact` (Migration `wizard_facts`, RLS, in `TENANT_MODELS`); echte Schritte via Side-Effect-Import `register-steps.ts` in die Registry eingeklinkt (überschreibt Platzhalter). Der Fragebogen schreibt **nur** Fakten/Flags, nie die gesperrten Zentralvariablen. **B4** (Richtlinien-Import/Upload) — **in `dev` integriert (Fast-Forward), Gate grün**: **B4-1** nicht-destruktiver Vorlagen-Import bei Aktivierung des Richtlinien-Moduls + Self-Service-Button (`/policies`, Report-Banner) und Admin-Button (`src/server/actions/policy-package.ts`, Trigger in `toggleTenantModule`); **B4-2** „Eigene Richtlinie hochladen" (`/policies/upload`, `policy-upload.ts`) mit Pflicht-Control-Zuordnung + gekapseltem Storage-Adapter-Stub (`src/server/storage/adapter.ts`, echtes Backend = Epic S1) → EIGENES-Dokument (`EIG-*`) + PolicyRequirement-Zeilen (Coverage/Nachweislage); **B4-3** neue Vorlagen `P01_Prototypenschutz` + `VA-20` (Kap. 8.x) und `D01_Datenschutz` (9.x) + 5 `mapping.json`-Anforderungen (condition-gated per Prüfziel-Flag), `_verify.py` = OK. **Keine neue Migration** (nur String-Status/Seed-Content). **B5** (Umsetzungshinweise C6) — **in `dev` integriert, Gate grün**: **B5-1** Datenmodell `ImplementationHint` (Migration `implementation_hints`; **globaler C6-Katalog**, identisch je Mandant → kein `tenantId`/RLS) + C6-Import (~397 Hinweis-Blöcke via `prisma/import-hints.ts` / `scripts/import-c6-hints.ts`); **B5-2** kontextsensitives Umsetzungshinweis-Panel (`/policies/hints`, `src/server/actions/hints.ts`), aus `/policies` verlinkt. **Prüfziele Single Source** (Follow-up zu A2): der context-Step seedet die Prüfziele aus `WizardScope` statt aus einer zweiten Quelle (Doppelquelle behoben; `feature-rules.ts`/`facts.ts`/`questions.ts`, `test-rules.ts` angepasst). **B6** — Vorlagenpaket-**Versionierung** + kontrollierte **Diff-Übernahme**: Modell `PolicyPackageState` (Migration `policy_package_state`, RLS, in `TENANT_MODELS`) hält je Mandant Paket-Version/-Stand; `stampPackageState` (in `prisma/import-policies.ts`) beim Import; Updates-Ansicht `/policies/updates` zeigt/übernimmt Änderungen kontrolliert. **B7-1** — Assessment-**Readiness-Dashboard** als Wizard-Schritt 9 „readiness" (`src/lib/readiness.ts`, Tests `scripts/test-readiness.ts`) mit Interpretationstexten (C9 §1/§2). **B7-2** — **VDA-ISA-Katalog-Export** (CSV) + **Management-Zusammenfassung** (C9 §3/§4): Export-Route `onboarding/export/route.ts`, Export-Engine `src/lib/export/vda-isa.ts` + `src/server/export-context.ts`, Zusammenfassungs-Seite `onboarding/summary`, Tests `scripts/test-vda-isa.ts`. Damit ist **B7 vollständig** (keine neue Migration). **Wizard-Schritte 4 + 6 (echte Komponenten)** — Richtlinien-Schritt „policies" (Schritt 4, Guard: nur bei aktivem Richtlinien-Modul) und Risiken-Schritt „risks" (Schritt 6, Guard: nur bei aktivem Risiko-Modul) zeigen jetzt den echten Modul-Status statt Platzhalter (`steps/policies/step.tsx`, `steps/risks/step.tsx`, `src/server/actions/onboarding-steps.ts`) — keine Doppel-Datenhaltung. Damit sind **alle Wizard-Schritte 1–9 echte Komponenten**.
- **Offen:** **Keine offenen Feature-Branches mehr** — alle bestehenden Lanes in `dev` konsolidiert (Dev A: A1–A8; Dev B: F1/B1/F4/F3+B2/B3/B4/B5/B6/B7 + Prüfziel-Follow-up). Nächste geplante Stories siehe §6.
**Migrationen (31 neu, laufen beim Deploy automatisch über den Init-Container):**
`platform_admins` · `policy_lifecycle_archived` · `user_mgmt_and_platform_settings` · `tasks` · `strip_tool_variable_articles` · `managed_registers` · `software_and_project_assets` · `onboarding_progress` · `tasks_wizard_fields` · `object_review_status` · `wizard_facts` · `wizard_scope` · `implementation_hints` · `policy_package_state` · `control_assessments` · `risk_catalog` · `risk_acceptance` · `task_description` · `control_implementations` · `login_lockout` · `mail_fundament` (SEC1) · `auth_tokens_sessions` (SEC2) · `tisax_m0_foundation` · `tisax_m1_m4_consolidated` · `tisax_rekey_onboarding_steps` · `tisax_v3_process_fields` · `tisax_v4_catalog_parent` · `tisax_v5_audit` · `tisax_v7_policy_domain` · `webauthn_credentials` (SEC3) · `platform_admin_role` (SEC4)
---
## 1a. Sicherheitspaket P1/P2 (Branch `dev-security-p1`, Stand 2026-07-30)
Nach einem externen Secure-Code-Review (AEGIS-SAST, 21 Findings) wurde ein Härtungspaket umgesetzt: **alle P1 (2) und P2 (5) sowie mehrere P3** sind behoben. Das Paket liegt auf **`dev-security-p1`** (von `dev` abgezweigt, Gate grün, noch **nicht** nach `dev` gemergt). Parallelisiert in drei konfliktfreien Lanes plus Vorlauf, danach additiv integriert.
| Finding | Priorität | Behebung | Kern-Dateien |
|---|---|---|---|
| **F-01** Auth.js „fail open" + Next.js-CVEs | P1 | `next-auth`→`5.0.0-beta.32` (`@auth/core` 0.41.3), `next`→`16.2.12`; Guards **positiv** statt existenzbasiert; **Fail-Secure-Startprüfung** `assertSecureEnv()` (lazy/memoisiert, build-safe) | `package.json`, `src/server/env.ts`, `auth.ts`, `platform-auth.ts` |
| **F-02** Cross-Tenant-Leck bei `findUnique`+`select` | P1 | Tenant-Guard **fail-closed**: skalare `where`→`findFirst` mit `tenantId`-Vorfilter (verhindert statt erkennt), Compound-Unique→`tenantId`-Injektion + harter Abbruch; drei Aufrufstellen zusätzlich explizit gefiltert; Regressionstest | `src/server/db.ts`, `actions/risks.ts`, `actions/risk-catalog.ts`, `scripts/test-tenant-isolation.ts` |
| **F-03** Stored XSS im Richtlinien-Rendering | P2 | `renderPolicyHtml` sanitisiert `marked`-Output gegen strikte Allowlist (`sanitize-html`); Variablenwerte + Upload-Titel HTML-kodiert vor `noEscape`-Handlebars | `src/lib/policy-render.ts`, `actions/policy-upload.ts` |
| **F-05** Kein Brute-Force-Schutz Mandanten-Login | P2 | Kontosperre (5/15 min) + `denied`-Audit analog Plattform-Login; Enumeration via Dummy-Hash konstanter Laufzeit; DoS-Ausnahme letzter aktiver Admin | `auth.ts`, Migration `login_lockout` (`User.failedLogins/lockedUntil`) |
| **F-07** Keine Security-Header/CSP | P2 | CSP + HSTS + `nosniff` + `X-Frame-Options: DENY` + Referrer-/Permissions-Policy; `unsafe-eval`/`ws:` nur im Dev | `next.config.ts` |
| **F-08** Kritische Kontoänderung ohne Re-Auth | P2/P3 | Passwortwechsel verlangt aktuelles Passwort (+ TOTP bei aktiver MFA); MFA-Deaktivierung erfordert TOTP-Code; Ausnahme nur beim erzwungenen Erstwechsel | `actions/account.ts`, `actions/platform.ts` |
| **F-09** 30-Tage-Sessions | P3 | `maxAge` 8 h (Mandant) / 2 h (Plattform) + `updateAge` | `auth.ts`, `platform-auth.ts` |
| **F-12** Weitere verwundbare Abhängigkeiten | P3 | `overrides` für `postcss`/`sharp`/`valibot`; **Produktionsbaum (`--omit=dev`): 0 kritisch / 0 hoch** (vorher 2/5) | `package.json` |
| **F-15** Upload ohne Validierung | P3 | Größenlimit vor RAM-Read, Endungs-/MIME-Allowlist, Magic-Byte-Prüfung, kanonischer MIME statt `f.type` | `actions/policy-upload.ts` |
| **F-16** (Teil) Isolationsverletzung unsichtbar | P3 | Verletzungen als `[SECURITY]`-Log; zentrale Audit-Anbindung (`action:"denied"`) als Folge-TODO offen (Importzyklus `audit.ts`↔`db.ts`) | `src/server/db.ts` |
| **F-19** Unvalidiertes `callbackUrl` | P4 | Allowlist (nur eigene absolute Pfade), doppelt validiert | `src/app/login/page.tsx` |
**Korrekturen am Bericht** (dessen Codevorschläge waren an drei Stellen falsch): `npm install next-auth@latest` hätte auf **v4** downgegradet (Major-Bruch) — korrekt ist `5.0.0-beta.32`. Der pauschale `findUnique`→`findFirst`-Umbau bricht an den Compound-Unique-Keys (`tenantId_key` etc.) → stattdessen Hybrid-Guard. Das RLS-Snippet aus F-04 funktioniert nicht (Kontext in falscher Transaktion) → F-04 bewusst herausgelöst.
| **F-06** JWT-Sessions ohne Widerruf | P2 | ✅ **Branch `dev-security-f06-session-auth`** (auf `dev-security-f04-rls`): `moduleGuard` prüft Kontostatus, `mustChangePassword` und effektive Rechte je Mutation **autoritativ aus der DB** statt aus dem JWT → Deaktivierung/Rechteentzug wirkt sofort (vorher bis Token-Ablauf); `requirePlatformSession` prüft Plattform-Admin-Status. Test `test-action-guard-authz.ts` (4 Nachweise), Browser-Regression ok | `src/server/action-guard.ts`, `platform-auth.ts` |
| **F-04** RLS definiert, aber wirkungslos | P2 | ✅ **Separates Paket, Branch `dev-security-f04-rls`** (auf `dev-security-p1` aufgesetzt): env-gesteuert (`RLS_ENFORCED`). Zweite Verbindung als `isms_app` (`RLS_DATABASE_URL`, NOBYPASSRLS); `dbForTenant` setzt `app.tenant_id` transaktionslokal auf derselben Connection; Policies mit `USING`+`WITH CHECK` + `FORCE` auf allen **49** Tenant-Tabellen. Owner-Rolle (Superuser/BYPASSRLS) bleibt für Migrationen/Seed/Login und lokal unberührt → Default-Betrieb unverändert. Test `test-rls-enforcement.ts` (5 Nachweise) | `src/server/db.ts`, Migration `rls_enforce`, `docker-compose.coolify.yml`, `.env*.example`, `DEPLOY-PROD-CONTABO.md` |
**Gate (kombinierter Stand A+B+C):** `tsc`=0 · `lint` sauber · `build` grün · `_verify.py`=OK · `test-tenant-isolation`=OK (15 Fälle) · Browser-Smoke (Login, Dashboard, Richtlinien-Register, CSP-Header, Guard-Redirect) verifiziert. **F-04 separat:** `tsc`/`lint`/`build` grün · `test-rls-enforcement`=OK (5/5, inkl. Nachweis „Owner-Betrieb heil unter FORCE") · Owner-Pfad-Smoke verifiziert.
### P3/P4-Paket (Branch `dev-security-p3`, auf `dev-security-f06-session-auth`, Stand 2026-07-30)
Vier dateidisjunkte Lanes, additiv gemergt, kombiniertes Gate grün. **Nur die MFA-Lane migriert.**
| Finding | Prio | Behebung | Kern-Dateien |
|---|---|---|---|
| **F-10** Aufgaben-Modul ohne Rechteprüfung | P3 | Neues Recht `task:write`; `guard("task:write")` in `createTask`/`proposeTasksFromTriggers`/`claimTask`/`updateTask`; `CreateTaskInput` per Zod (Längen-/Größenlimits); `owner`/`assignee` gegen aktiven Mandanten-Nutzer geprüft | `rbac.ts`, `actions/tasks.ts` |
| **F-13** Privilege Escalation über `role:manage` | P3 | **Pragmatische Variante** (Entscheidung): Delegation erlaubt (Admin darf fachliche/geklonte Rollen für andere anlegen), aber **Selbstzuweisung** höher privilegierter Rollen gesperrt → Selbst-Eskalation ausgeschlossen; volle Auditierung | `actions/tenant-users.ts` |
| **F-20** Schwache Passwort-Policy bei Provisionierung | P3 | `validatePassword(…, DEFAULT_PASSWORD_POLICY)` statt `min(8)` | `actions/admin.ts` |
| **F-16-zentral** Sicherheitsereignisse nur im Log | P3 | Importzyklus per Lazy-Import gelöst → Isolationsverletzung wird echter `denied`-Audit; Error-Boundary `withActionErrors` (generische Außenmeldung, volles internes Log) bereitgestellt | `db.ts`, neu `action-error.ts` |
| **F-17** Recovery-Codes SHA-256, TOTP-Replay | P3 | Recovery-Codes → Argon2id, Entropie 40→80 Bit (Alt-Codes per Kompatibilitätspfad bis Neuausstellung); TOTP-Replay-Schutz via `lastTotpStep` (User+PlatformAdmin) | `mfa.ts`, `auth.ts`, `platform-auth.ts`, `account.ts`, `platform.ts`, Migration `mfa_totp_replay` |
| **F-11** Supply Chain | P3 | `npm ci` (Base-Image → `node:22.14.0-slim`/glibc, npm 11 gepinnt), alle Images auf feste Tags/Digests, schlanke `migrate`-Stage, SBOM dokumentiert; lokaler `docker build` grün (386 MB) | `Dockerfile`, `docker-compose*.yml` |
| **F-18** Container-Härtung | P3/P4 | Redis-Passwort, internes Netz (`internal: true`), `no-new-privileges`/`cap_drop: ALL`/Ressourcenlimits, Dev-Compose auf `127.0.0.1` gebunden | `docker-compose*.yml`, `.env*.example` |
| **CI/Renovate** (Begleitmaßnahmen) | — | `.gitea/`+`.github/workflows/ci.yml` (tsc/lint/build + `npm audit --omit=dev --audit-level=high`-Gate), `renovate.json` (Auth-Updates manuell) | `.gitea/`, `.github/`, `renovate.json` |
**Gate (kombiniert):** `tsc`=0 · `lint` · `build` · `_verify.py`=OK · 7 Sicherheitstests grün (`tenant-isolation`, `rls-enforcement`, `action-guard-authz`, `tenant-users-authz`, `mfa-hardening`, `action-error`) · Browser-Smoke (Task-Anlage F-10) verifiziert.
> ✅ **Deployment-Schritt (F-06 × F-10) — automatisiert:** Da F-06 Rechte **DB-autoritativ** prüft, wirkt eine neue Permission (z. B. `task:write`) erst, wenn `scripts/sync-role-permissions.ts` gegen die Ziel-DB läuft (legt Permission + Rolle→Recht-Verknüpfung an; additiv, idempotent). Ein reiner Re-Login genügt NICHT. Das läuft jetzt **bei jedem Deploy automatisch im `migrate`-Init-Job** (`docker-compose.coolify.yml`, direkt nach `prisma migrate deploy`, über die Owner-`DATABASE_URL`) — deckt jede künftig neu eingeführte Permission ab. Betroffene Nutzer danach neu einloggen. Manuell nachziehen nur, falls ohne Redeploy nötig: `npx tsx scripts/sync-role-permissions.ts`.
**Noch offen:** **F-14** (Demo-Seed-Härtung — auf Wunsch bewusst zurückgestellt), **F-21** (Monitoring/Log-Aggregation/Tamper-Schutz Audit-Trail — eigenes Betriebspaket). Damit sind **alle P1, alle P2 und die adressierten P3** behoben. Details: AEGIS-Bericht.
**Aktivierung F-04 in Prod:** Rolle `isms_app` LOGIN+starkes Passwort geben, `RLS_DATABASE_URL` setzen, `RLS_ENFORCED=true` am `app`-Service; Migrations-/Owner-Rolle muss BYPASSRLS/Superuser sein. Anleitung in `docs/DEPLOY-PROD-CONTABO.md`. Lokal bewusst **aus**.
---
## 2. Für das Projektmanagement — fachlicher Status
### ✅ Neu fertig auf `dev`
| Thema | Nutzen | Status |
|-------|--------|--------|
| **API-seitige Modul-Durchsetzung** | Deaktiviertes Modul sperrt jetzt auch Schreibzugriffe serverseitig (nicht nur Navigation); automatischer Vollständigkeitscheck als Build-Gate | ✅ |
| **Getrennter Superadmin-Login** | Betreiber-Zugang unter `/platform/login` mit eigenem Store, ohne Mandantenkontext; Kundenfachdaten für Superadmin gesperrt | ✅ |
| **MFA (optional)** | TOTP für Plattform-Admins **und** Mandanten-Nutzer freiwillig; Policy-Flag „MFA-Pflicht" kann Erzwingung wiederherstellen; Recovery-Codes | ✅ |
| **Benutzerverwaltung (Superadmin)** | Superadmin legt je Kunde Nutzer an (Initial-/Einmal-Passwort), weist Rollen zu, deaktiviert/reaktiviert, setzt Passwörter zurück | ✅ |
| **Benutzer- & Rollenverwaltung (Kunde)** | Mandanten-Admin verwaltet **intern** Nutzer und Rollen (eigene Rollen + granulare Rechte, Standardrollen klonbar); strikt mandantengetrennt; Lockout-Schutz | ✅ |
| **Nutzer-Onboarding ohne E-Mail** | Start mit Initialpasswort + erzwungenem Wechsel beim ersten Login; E-Mail-Einladung (Paket 4) später nahtlos ergänzbar | ✅ |
| **Popup-Bedienung** | Anlegen/Bearbeiten von Nutzern über Popups wie bei Assets; Tabelle nur Anzeige | ✅ |
| **Zuständigkeiten geklärt** | Superadmin steuert Kern-Einstellungen (Module, TISAX-Tiefe); Mandanten-Admin nur seinen Bereich (Stammdaten, Nutzer/Rollen) | ✅ |
| **Richtlinien-Governance zentral** | Zentrale Variablen (Unternehmensname, Rollen, Schutzbedarf) nur in Einstellungen pflegbar; Coverage-Matrix zeigt nur Controls des aktiven Assessment-Levels (AL2 ohne „sehr hoch") | ✅ |
| **Scoping & zentraler Schutzbedarf (A2)** | Anforderungs-Scope leitet sich zentral aus Assessment-Level (AL2/AL3), `FLAG_INCLUDE_SHOULD` und den Prüfzielen ab — nicht mehr aus dem Fragebogen; Schutzbedarf hat damit eine einzige, konsistente Quelle (AL → Flags). Scope-Filter (`scope-filter.ts`) + `c1-scope.json` (412 Anforderungen) | ✅ |
| **Umsetzungshinweise (B5)** | Kontextsensitive C6-Umsetzungshinweise (~397 Blöcke) zu den Controls, als eigenes Panel unter `/policies/hints` und aus den Richtlinien verlinkt — praktische Hilfestellung bei der Umsetzung | ✅ |
| **Validierungs-Workflow generalisiert (A3)** | Das Vier-Augen-/Review-Muster ist jetzt objekttyp-übergreifend nutzbar (nicht nur Aufgaben) — z. B. für Risiken | ✅ |
| **ISMS-Rollen & Funktionstrennung (A4)** | Wizard-Schritt „Rollen": ISMS-Rollen zuweisen, Funktionstrennungs-Regeln FT-01…06 prüfen (z. B. ISB ≠ IT-Leitung), ISB-Bestellung | ✅ |
| **Vorlagen-Versionierung & Diff (B6)** | Das Vorlagenpaket ist versioniert; Updates werden je Mandant kontrolliert per Diff-Ansicht (`/policies/updates`) übernommen statt blind überschrieben | ✅ |
| **Assessment-Readiness & Export (B7)** | Readiness-Dashboard (Reifegrad-Interpretation, C9) **plus** VDA-ISA-Katalog-Export (CSV) und Management-Zusammenfassung — Grundlage für Reporting/Abschluss | ✅ |
| **Wizard-Schritte Richtlinien & Risiken (B)** | Onboarding-Schritte 4 (Richtlinien) und 6 (Risiken) zeigen den echten Modul-Status statt Platzhaltern — der Wizard ist damit über alle 9 Schritte durchgängig | ✅ |
| **Umsetzungshinweise im Control-Schritt (#10)** | Je Control-Anforderung lassen sich die Umsetzungshinweise **dokumentieren, abhaken und in eine Aufgabe überführen**; der Umsetzungsstatus je Spiegelstrich fließt in den Reifegrad ein (`ControlImplementation`) | ✅ |
| **Aufgaben/Maßnahmen-Vereinheitlichung (7-9)** | Aufgaben und Maßnahmen im selben Bedien-Design (gemeinsamer Anlage-Dialog); Standardmaßnahmen aus dem Risikokatalog werden zu echten, verknüpften Maßnahmen; neue Rolle **`pm`** mit Voll-Sicht auf Aufgaben (`task:read_all`) | ✅ |
| **Wizard-UX-Feinschliff (#1/#3/#6)** | Interne Identifier aus den Wizard-Texten entfernt, Assets-Schritt gibt Rückmeldung beim „Aufgaben anlegen", Aufgaben-Sichtbarkeit als Pool-Popup | ✅ |
| **Asset-Schritt (A5)** | Wizard-Schritt 5 bindet das Bestands-/Asset-Modul ein — Assets werden im Onboarding erfasst statt separat | ✅ |
| **Control-Assessment & Reifegrad (A7)** | Wizard-Schritt 7: Controls bewerten (Belegstatus, Reifegrad R0–R3, Zielreifegrad); offene Punkte erzeugen automatisch Gap-Aufgaben | ✅ |
| **Standard-Risikokatalog (A6)** | Vorgefertigter Risikokatalog (C4) zum Übernehmen; Standardmaßnahmen erzeugen Aufgaben; dokumentierte **Restrisiko-Akzeptanz** (VA-09) | ✅ |
| **Gap-Konsolidierung (A8)** | Wizard-Schritt 8 bündelt alle offenen Punkte/Gaps aus den Schritten — dedupliziert, priorisiert (Quick-Wins) und gleicht sie mit Aufgaben ab | ✅ |
| **Freigabe-Workflow + Aufgaben** | Einreicher wählt Freigeber → Aufgabe; Freigeben/Ablehnen (mit Kommentar) im Modul „Aufgaben" (Vier-Augen); Dashboard-Kachel für offene Freigaben | ✅ |
| **Nicht-destruktiver Re-Import** | Richtlinien-Paket-Updates erhalten Status/Freigabe/Overrides/Variablenwerte; entfernte Einträge werden deaktiviert statt gelöscht; Änderungsreport | ✅ |
| **GAP-Report Richtlinien** | 6 neue Verfahrensanweisungen (VA-14..19), alle zuständigen VAs inline verlinkt, Rendering-Fehler behoben, verwaltete Register editierbar | ✅ |
| **Software-Verwaltung** | Software ist ein Asset (Typ SOFTWARE) und wird im Lieferantenbereich unter „Software" gepflegt (Popups wie IT-Services): Anbieter/Lieferant, Version/Patch-Stand, Freigabestatus, Freigeber, Review. Ersetzt die frühere „Software-Whitelist" | ✅ |
| **Projekte** | Projekte sind Assets (Typ PROJECT) im Assetinventar: bewertbar wie Assets (Kritikalität C/I/A), Risiken zuordenbar, IS-Klassifizierung, ISB-Einbindung, Projektstatus — Bedienung als Popup | ✅ |
| **Kritische IT-Dienste** | Schreibgeschützte Auto-Sicht (`/assets?view=critical`): leitet kritische Dienste automatisch aus Verfügbarkeit und BIA ab, mit RTO/RPO aus den verknüpften Prozessen und Abhängigkeiten — keine gepflegte Doppelliste mehr | ✅ |
| **Register-Konsolidierung** | Doppelte Register (externe IT-Dienste, Software-Whitelist, kritische Dienste, Projekte) entfernt und in die Fachmodule überführt; Richtlinien-Verweise zeigen jetzt auf die Module. REG-NET (Netzplan) auf ein Referenz-/Speicherort-Feld reduziert | ✅ |
| **Produktmarke Certvia** | Die Anwendung heißt und erscheint durchgängig als **Certvia** — Logo und Wortmarke, Tab-/App-Icon, Seitentitel, Anmelde- und Fehlerseiten, Druckansicht. GEFIM bleibt als Dachmarke sichtbar („Ein Produkt von GEFIM"). Ohne eigenes Mandanten-Logo zeigt jeder Kunde Certvia. Reine Design-Umstellung ohne Funktionsänderung | ✅ |
| **Wizard-Parallelisierung (Fix)** | Bearbeitung und Validierung im Onboarding-Wizard entkoppelt — Schritte können parallel bearbeitet werden, statt streng nacheinander; dazu `scripts/sync-role-permissions.ts` zum Nachziehen neuer Rechte für bestehende Mandanten | ✅ |
### 🟥 Bewusst zurückgestellt / offen
| Thema | Anmerkung |
|-------|-----------|
| **Paket 4 — SMTP + E-Mail-Einladungs-/Reset-Flow** | Auf Kundenwunsch später; Aktivierung läuft vorerst über Initial-/Einmal-Passwort, gekapselt für spätere Token-Aktivierung |
| **Netzplan-Datei-Upload (REG-NET)** | Datei-Upload-/Storage-Infrastruktur existiert noch nicht; REG-NET verweist vorerst per Feld auf den extern gepflegten Netzplan. Upload als eigenes Paket (Storage-Backend + Coolify-Volume) |
| **Tenant-weite MFA-Pflicht scharfschalten** | Flag `securityPolicy.mfaRequired` vorhanden + von „MFA deaktivieren" respektiert; Enrollment-Erzwingung beim Login noch nicht verdrahtet (analog Force-Change-Gate) |
| **Admin Phase 2** | Impersonation, Plan/Limits, Logo-Upload, DSGVO-Export/Retention |
| **NIS2-Modul** | nie begonnen (großes Framework, Incident-Reporting mit Fristen-Timern) |
| **Richtlinien-Versionierung/Diff, DOCX/PDF-Export** | offen |
| **ISB-Freigabe der neuen GAP-Texte** | fachlicher Prozessschritt (kein Code): neue/geänderte VA-/Richtlinien-Texte durch den ISB freigeben |
| **Platzhalter-Module** | SoA & Controls, Vorfälle, Nachweise, Management-Review, KI-Chat |
---
## 3. Für neue Entwickler — technische Landkarte
> Setup, Stack, Konventionen und Fallstricke unverändert in **`docs/HANDOVER-DEV.md`** (§1–§9). Hier nur, was `dev` ergänzt.
### 3.1 Neue Datenmodelle (Prisma)
| Modell | Zweck | Mandantengebunden? |
|--------|-------|--------------------|
| `PlatformAdmin` | Getrennter Superadmin-Store (kein `tenant_id`), Argon2id, TOTP-Secret, Recovery-Codes, Lockout | nein (plattformweit) |
| `PlatformSetting` (Singleton) | Plattform-Policy, u. a. `mfaRequired` | nein |
| `Task` / `TaskComment` | Generisches Aufgaben-/Freigabe-Modell (erster Typ `policy_approval`), Kommentar-Historie | ja |
| `ManagedRegister` / `RegisterRow` | Generisches editierbares Register (code, columns JSON, Cross-Links supplier/asset) | ja |
| `SoftwareProfile` | 1:1 zu `Asset` (Typ SOFTWARE): Anbieter-Verknüpfung (`providerAssetId`→SUPPLIER), Version, Freigabestatus (`SoftwareApprovalStatus`), Freigeber, Kritikalität, Review | ja |
| `ProjectProfile` | 1:1 zu `Asset` (Typ PROJECT): IS-Klassifizierung, ISB-Einbindung, `ProjectStatus`. Kritikalität via C/I/A am Asset, Risiken via `RiskAsset` | ja |
| `AssetType` neu | Enum um `SOFTWARE` und `PROJECT` erweitert | — |
| `User.*` neu | `mustChangePassword`, `mfaEnrolledAt`, `recoveryCodes` | ja |
| `PolicyDocument.archivedAt`, `PolicyRequirement.archivedAt` | Lifecycle für nicht-destruktiven Re-Import (deaktivieren statt löschen) | ja |
| `AuditLog.tenantId` nullable | Plattform-Ereignisse ohne Mandantenbezug (`scope=platform`) | — |
| `WizardScope` (A2) | Scope des Onboarding-Wizards: Prüfziele, Geltungsbereich, Standorte, Ausschlüsse — Basis der Scope-Filter-Engine (`scope-filter.ts`) | ja (RLS) |
| `ImplementationHint` (B5) | Globaler C6-Umsetzungshinweis-Katalog je Anforderung (organisatorisch/technisch/Nachweise/Ressourcen, AL-Filter) | nein (globaler Katalog, kein RLS) |
| `PolicyPackageState` (B6) | Je Mandant: Version/Stand des importierten Vorlagenpakets — Basis für kontrollierte Diff-Übernahme von Updates | ja (RLS) |
| `ControlAssessment` (A7) | Je Mandant/Control: Belegstatus, Reifegrad (R0–R3), Zielreifegrad — Basis für Gap-Aufgaben und SoA | ja (RLS) |
| `RiskCatalogEntry` (A6) | Globaler Standard-Risikokatalog (C4) zum Übernehmen — Content identisch je Mandant | nein (globaler Katalog, kein RLS) |
| `ControlImplementation` (#10) | Je Mandant/Control-Spiegelstrich: Umsetzungsstatus (dokumentiert/abgehakt), koppelt an Reifegrad und Aufgaben | ja (RLS) |
| `Task.description` (7-9) | Beschreibungsfeld am bestehenden `Task` (Migration `task_description`) für die Aufgaben/Maßnahmen-Parität | ja |
| `Risk.acceptedAt/By/Rationale` (A6) | Restrisiko-Akzeptanz-Felder am bestehenden `Risk` (Migration `risk_acceptance`) | ja |
Alle neuen tenant-gebundenen Modelle sind in **`TENANT_MODELS`** (`src/server/db.ts`) und haben **RLS-Policies** (in den jeweiligen Migrationen).
### 3.2 Auth-Architektur (wichtig!)
- **Zwei getrennte NextAuth-Instanzen:** Mandanten-Login (`src/server/auth.ts`, `/login`) und **Plattform-Login** (`src/server/platform-auth.ts`, eigener Cookie + basePath `/api/platform-auth`, `/platform/login`). Plattform-Session trägt **keinen** `tenantId`.
- **Superadmin-Bereich** liegt unter `src/app/(platform)/…` (Layout erzwingt Plattform-Session + ggf. MFA); Mandanten-App unter `src/app/(app)/…`.
- **MFA-Helfer** `src/server/mfa.ts` (otplib TOTP + Recovery-Codes) — von Plattform- und Nutzer-MFA gemeinsam genutzt.
- **Force-Change:** `(app)`-Layout prüft Kontostatus/`mustChangePassword` autoritativ aus der DB → `/change-password` bzw. Abmelde-Screen bei deaktivierten Konten.
- **Passwort-Policy:** `src/lib/password-policy.ts` (reine Validierung, client-safe) + `src/server/password.ts` (Argon2id + Generator); Quelle `TenantSettings.securityPolicy.password`.
### 3.3 Modul-Durchsetzung (§3.4) — Konvention für neue Actions
- Jede mutierende Server-Action eines **gegateten Moduls** läuft über `moduleGuard("<key>")` (`src/server/action-guard.ts`): Session → `assertModuleEnabled` → RBAC.
- **Vollständigkeitscheck** `scripts/check-module-guards.ts` (als `prebuild` verdrahtet): jede Datei in `src/server/actions/` muss dort eingetragen sein (Modul-Key oder `EXEMPT`), sonst **failt der Build**. → Neue Action-Datei? Dort eintragen.
- Neues Modul **`tasks`** in `src/lib/modules.ts` (für Bestands-Tenants per Default aktiv, da fehlende `TenantModule`-Zeile = aktiv).
### 3.4 Wo liegt was (neu)
- **Superadmin/Plattform:** `src/app/(platform)/admin/**`, `/platform/{login,enroll-mfa,profile}`, `src/server/actions/{admin,platform,platform-users}.ts`.
- **Benutzer-/Rollenverwaltung (Kunde):** `src/app/(app)/settings/users/`, `src/server/actions/{tenant-users,account}.ts`, Komponenten `user-table.tsx`/`user-forms.tsx`/`role-manager.tsx`.
- **Aufgaben/Freigabe:** `src/app/(app)/tasks/`, `src/server/actions/tasks.ts` (+ `submitForApproval` in `policies.ts`); Dashboard-Kachel in `dashboard/page.tsx`.
- **Register:** `src/components/generic-register.tsx`, `src/server/actions/register.ts`, Definitionen in `prisma/import-managed.ts` (`GENERIC_REGISTERS` + `RETIRED_REGISTERS`-Cleanup).
- **Software (Lieferantenbereich):** Tab in `src/app/(app)/suppliers/page.tsx` (`?tab=software`), `src/components/software-modals.tsx`, `src/server/actions/software.ts`, `SOFTWARE_INCLUDE` in `src/lib/supplier-include.ts`.
- **Projekte (Assetinventar):** Typ-Filter/Popup in `src/app/(app)/assets/page.tsx`, `src/components/project-modals.tsx`, `src/server/actions/projects.ts`, `PROJECT_INCLUDE` in `src/lib/supplier-include.ts`.
- **Kritische IT-Dienste:** Auto-Sicht-Zweig in `src/app/(app)/assets/page.tsx` (`?view=critical`) — abgeleitet aus Verfügbarkeit + BIA (`ProcessAsset`→`Process`→`BiaEntry`); Link-Ziel von `resolveLink("REG-CRIT-SERVICES")`.
- **Richtlinien-Import (nicht-destruktiv):** `prisma/import-policies.ts` (Diff/Upsert, `{ dryRun }`, Änderungsreport, `archivedAt`).
- **Richtlinien-Import/Upload (B4):** Actions `src/server/actions/policy-package.ts` (Import-Kapselung: Self-Service `importPolicyPackage` + Plattform `importPolicyPackageForTenant`) und `policy-upload.ts` (`uploadOwnPolicy`); Aktivierungs-Trigger in `toggleTenantModule` (`admin.ts`); Storage-Adapter `src/server/storage/adapter.ts` (Stub, Epic S1); UI `src/app/(app)/policies/page.tsx` (Buttons + Report-Banner) + `src/app/(app)/policies/upload/page.tsx`. Eigene Uploads liegen im Namensraum `EIG-*` (außerhalb des Paket-Namensraums → vom Re-Import unberührt).
- **Branding (in `dev`):** Tokens in `src/app/globals.css` (`--brand-*` / `--ui-*`) mit JS-Pendant `src/lib/brand.ts`; Komponenten `src/components/brand/{certvia-logo,tenant-brand,powered-by-gefim}.tsx`; Assets `public/assets/logo/`, `public/favicon/`, `public/site.webmanifest`; Dokument-/Mail-CD `src/lib/{document-brand,email-brand}.ts`; Doku `docs/BRANDING-CERTVIA.md` (+ Design-Paket unter `docs/branding/`).
- **Vorlagenpaket (Source of Truth):** `seed/isms-vorlagenpaket-v2/` — 28→**37 Dokumente** (VA-14..19; **P01/D01/VA-20** neu für Prototypen-/Datenschutz, B4-3), `mapping.json` (321 Anforderungen), `variables.schema.json`, Baseline, Nachweisregister, **`_verify.py`** (Rendering-/Anker-Check; nach Änderungen `python3 _verify.py` → **OK**).
### 3.5 Konventionen / Fallstricke (Ergänzungen)
- **Dev-Server & Server-Actions:** Änderungen an Server-Actions greifen im Turbopack-Dev manchmal erst nach **Neustart** des Dev-Servers.
- **Migrations-Flow Prisma 7** wie gehabt (`migrate diff --from-config-datasource … --to-schema … --script`, dann RLS-DO-Block manuell anhängen, `migrate deploy`). Data-Migrationen (z. B. `strip_tool_variable_articles`) sind reine SQL-`UPDATE`-Migrationen.
- **Geteilte DB bei paralleler 2-Lane-Entwicklung (Dev A × Dev B):** Beide Worktrees zeigen auf **dieselbe** lokale Postgres-DB. Dadurch enthält `migrate diff --from-config-datasource` **fremde**, vom anderen Branch angewandte Schemaänderungen (z. B. wollte A1-1 fälschlich Dev Bs `tasks`-Spalten `links/origin/priority/resources` droppen). **Regel:** beim Erzeugen einer Migration nur die **eigenen** DDL-Blöcke übernehmen und Fremd-Drops von Hand entfernen; anschließend prüfen, dass die Spalten/Objekte des anderen erhalten sind. Migrations-Reihenfolge & „nie gleichzeitig" strikt nach Contracts §6. **Merge-Nachsorge:** Wird eine Migration im anderen Branch neu erzeugt (anderer Timestamp) und die geteilte DB hat noch die alte angewandt, meldet `migrate status` „applied ≠ lokal". Fix ohne Datenverlust: stalen `_prisma_migrations`-Eintrag löschen + `prisma migrate resolve --applied <neuer_name>` (kein SQL läuft neu). So geschehen bei `tasks_wizard_fields` (`…103650` in DB → `…104412` committet).
- **Zentrale Variablen** (Organisation/Rollen/Schutzbedarf-Flags, `src/lib/policy-variables.ts`) sind im Richtlinien-Editor gesperrt und werden serverseitig geblockt — nur in `/settings` pflegbar.
- **Rendering (Variante B):** Tool-Variablen-Defaults ohne führenden Artikel (`TOOL_TICKET`=„Ticketsystem" etc.); der Fließtext setzt Artikel/Deklination. Bestandswerte werden per Migration migriert.
- **Register vs. Managed-Register-Docs:** die verwalteten Register (CRYPTO/RISKMATRIX/CLASSIFICATION/HANDBUCH) sind spezifische Modelle; die verbliebenen `REG-*` (SENS-ROLES, AUDIT-PLAN, NET) nutzen das **generische** `ManagedRegister`-Modell. `importPolicies` archiviert nur den Paket-Namensraum (L00/R*/VA-*/BASELINE/NACHWEIS) — Register bleiben unberührt.
- **Register-Konsolidierung (Block 5):** REG-EXT-SERVICES / REG-SW-WHITELIST / REG-CRIT-SERVICES / REG-PROJECTS sind **stillgelegt** (Liste `RETIRED_REGISTERS` in `import-managed.ts` löscht Dokument + `ManagedRegister` + Zeilen idempotent beim Import). Ihre `{{LINK:REG-…}}` in den Seed-Texten bleiben unverändert — nur `resolveLink` (`src/lib/policy-render.ts`) biegt sie auf die Modul-Routen um (`/suppliers?tab=services|software`, `/assets?view=critical`, `/assets?type=PROJECT`). Wer ein Register neu stilllegen will: Code aus `GENERIC_REGISTERS` entfernen, in `RETIRED_REGISTERS` eintragen, `resolveLink`-Fall ergänzen.
- **Software/Projekte als Asset-Typen:** folgen exakt dem bestehenden Muster von SUPPLIER/IT_SERVICE (Asset + 1:1-Profil, `refNo` je Mandant, Cockpit-Popups auf `/suppliers` **und** `/assets`, backHref-gesteuert). Neue Action-Dateien (`software.ts`→`suppliers`, `projects.ts`→`assets`) sind in `scripts/check-module-guards.ts` registriert.
### 3.6 Demo-Logins (Passwort `Demo1234!`)
- Mandanten-Login `/login`: `admin@demo.example` (Mandanten-Admin + ISB), **`bea.approver@demo.example`** (ISB, zweiter Freigeber für den Vier-Augen-Workflow), `auditor@…`, `owner@…`, `user@…`.
- Plattform-Login `/platform/login`: `admin@demo.example` (getrennte Session; MFA optional, Enrollment beim ersten Login nur wenn Policy es verlangt).
---
## 4. Commit-Übersicht (`main..dev`, neueste zuerst)
```
55260a7 Merge SEC3+SEC4 (MFA pro Mandant erzwingen, WebAuthn/Passkeys, TOTP-Verschlüsselung at rest, Plattform-Admin-Verwaltung) in dev
8a154f0 Merge TISAX v7-Fortsetzung (Fachbereich-Ableitung beim Import, Richtlinien nach Fachbereich im Onboarding, parametrisierbares Redirect-Ziel) in dev
a0c0d14 Merge TISAX v6/v7 (ABGABE-xlsx-Export, echter MinIO/S3-Adapter, PolicyDocument.domain/Fachbereich, Audit-Permissions) in dev
850b691 Merge TISAX v5 (Audit-Vorbereitung Phase 1: Audit-Übersicht/Wizard-Shell, Readiness/GAP-Tab, Nachweise-Tab, Control-Beschreibungen mit KI-Entwurf + ABGABE-CSV-Export) in dev
6370cee Merge TISAX v4 (Prozesshaus-Ausbau: Löschen, Katalog-Teilprozesse parentCode, Details-Overlay; BIA-Popup vereinfacht + Träger-/Risiko-Neuanlage) in dev
23df560 Merge TISAX v3 (Prozesshaus + geführtes BIA je Prozess, Popup-Bearbeitung Rollen/Kriterien, zusätzliche Prozess-Felder) in dev
f1c11aa Feature: Plattform-Konsole legt Tenant-Nutzer per Einladungslink an (wie Mandanten-Admin)
2bfc071 Merge TISAX-Integration (M0–M4 + Wizard-Neustruktur) in dev — Fundament, Strukturanalyse, Cockpit (RACI/Evidence), Audit-Wizard, TISAX-Tasks, Content-Seed; 3 Migrationen + RLS für neue Tabellen
2fd522c Feature: Benutzer-Anlegen verschickt Einladungslink per Mail (invitation-Template, /reset-Token 7 Tage) statt Passwort-Anzeige
4d28c86 Fix: Benutzer-/Mail-Actions graceful statt Fehlerseite + Formular-Werterhalt
6b4c6fb Coolify: Mail-Worker-Service (SEC1) ergänzt
c47ace0 Merge SEC1+SEC2: Mailversand/Queue (SEC1, BullMQ/ioredis/nodemailer, Worker) + Auth-Self-Service (SEC2, AuthToken, Reset/Passwortwechsel/E-Mail-Änderung, Session-Invalidierung, Rate-Limit) in dev
3998f9f Fix: /tasks SSR-Crash — entfernte Variable `open` im Untertitel wiederhergestellt
3932ff2 Merge 7-9/4: Aufgaben als Kanban-Board (Maßnahmen-Design) + Vorschläge/Bearbeiten korrigiert in dev
00e7e79 Dockerfile: Fachcontent (docs/wizard-uebergabe) ins migrate-Image für Demo-Seed (C4/C6)
fb1935e Dockerfile: Richtlinien-Vorlagenpaket (seed/) in migrate- und runner-Image
d0dfe0b Merge Security: P1/P2/P3-Härtungspaket (F-01…F-20) in dev — Tenant-Isolation, Auth/Session, RLS scharf (F-04), moduleGuard-DB-Authz (F-06), Web-Härtung, MFA-Replay, Supply-Chain/Container (F-11/F-18)
63b583d Merge Dev-A: A6-1 + A6-2 (Standard-Risikokatalog C4, Standardmaßnahme→Aufgabe, Restrisiko-Akzeptanz) in dev
e6c5e51 Merge Dev: UX-Fixes (#1/#3/#6) + Aufgaben/Maßnahmen-Vereinheitlichung (7-9) + Umsetzungshinweise Control-Schritt (#10, ControlImplementation) in dev
6b4b190 Merge Dev-B: Wizard-Schritte 4 (Richtlinien) + 6 (Risiken) — echte Komponenten statt Platzhalter in dev
0629613 Merge Branding: Umstellung auf Certvia (Design-Tokens, Logo/Icons, Metadaten, i18n, Auth-/Systemseiten, Dokument-CD) in dev
7f4a85c Merge Fix: Wizard-Bearbeitung von Validierung entkoppeln (Parallelisierung) + Rollen-Rechte-Sync-Skript in dev
6b20e13 Merge Dev-B: B7-2 (VDA-ISA-Katalog-Export CSV + Management-Zusammenfassung, C9) in dev
2cc422c Merge Dev-A: A8-1/A8-2 (Gap-Konsolidierungs-Engine) + A8 (Gap-Schritt 8: Aggregation/Priorisierung/Task-Abgleich) in dev
76cfebc Merge Dev-A: A5-1 (Asset-Schritt) + A7-1/A7-2 (Control-Assessment + Reifegrad-Engine) in dev
7dc5ae1 Merge Dev-B: B6 (Vorlagenpaket-Versionierung + kontrollierte Diff-Übernahme) in dev
1238564 Merge Dev-B: B7-1 (Assessment-Readiness-Dashboard + Interpretationstexte C9) in dev
de76856 Merge Dev-A: A4 (ISMS-Rollen Schritt 3 + Funktionstrennung FT-01…06) in dev
012c0fa Merge Dev-B: Prüfziele als Single Source aus WizardScope (Doppelquelle behoben) in dev
227d7a3 Merge Dev-A: A3-1 (generisches Objekt-Review / Validierungs-Workflow) in dev
8c9f648 Merge Dev-B: B5-1 + B5-2 (Umsetzungshinweise C6, kontextsensitives Panel) in dev [+ 98d5e26 B5-1, 93e2f44 B5-2]
d5c11df Merge Dev-A: A2-1 + A2-2 (Assessment-Level zentral, Scope-Filter) in dev [+ 69cc25a A2-1, 177df8b A2-2]
78be114 Doku: STAND B4 · c473cad B4-3 (P01/D01/VA-20) · eb7c899 B4-2 (Upload) · 269506b B4-1 (Import) — Dev B, per FF in dev
4e1b116 Merge Dev-B: B3 Fragebogen (Schritt 2 context) in dev [+ f818093 B3-Feature-Commit]
872c35e Merge Dev-B: F3+B2 Regel-/Mapping-Engine in dev [+ 20afd63 B2-Feature-Commit]
(DevOps-Integration: dev-b2-regel-engine + dev-b3-fragebogen additiv gemergt; Gate grün)
b9d8027 Story A1-2: Dashboard-Kachel „Onboarding-Fortschritt" (Dev A)
949d7c5 Story F2: ObjectReviewStatus + external_validator + Wizard-Validierung (Dev A)
12c94fe Merge Dev-B-Lane in dev: F1 + B1 + F4 (Konsolidierung beider Lanes)
0d52090 F4: Wizard-Flags in variables.schema.json (Dev B)
ddd2dc4 B1: Auto-Generierung von Aufgaben-Vorschlägen (C2 §8) (Dev B)
30c3638 F1: Task-Objekt um Wizard-Felder erweitern (Contracts §1) (Dev B)
97b3b4e Story A1-1: Onboarding-Wizard-Shell (Step-Registry, State-Machine, Gate) (Dev A)
(darüber Wizard-Kickoff-Commits: Sign-off, Dev-B-Bestätigung, Contracts, Übergabepaket)
(sowie lokale Doku-Commits — Onboarding-Prompt + STAND-Updates)
f368d8f Fix: Fonts selbst hosten (next/font/local) + Bootstrap an Superadmin-Store anpassen
cc8d486 Feat: Prod-Bootstrap (Erst-Superadmin) + Contabo-Runbook
── ab hier auf origin/dev (gepusht) ──
9b479ab Software und Projekte als Assets + Auto-Sicht kritische Dienste
150a1d9 Register-Konsolidierung: 4 Register in Fachmodule überführt
0806030 Datenmodell: Asset-Typen SOFTWARE und PROJECT mit Fachprofilen
2ec0230 Doku: Konsolidierte Übergabe des dev-Stands (PM + neue Entwickler)
95d4479 GAP WP3.1: IMPL-Texte mit verwalteten Registern verdrahtet
bd56a47 GAP WP3.0: Generisches, editierbares Register-Datenmodell + Editor
b9784e5 GAP E2: ISA 3.1.3-Stub entfernt (3.1.2 deprecated)
b7d5df4 GAP WP2.2 (Rendering Variante B) + WP4 (Feinschliff)
36c2903 GAP WP1: Neue VAs 14–19 + Register + Baseline + Eltern-Wiring
522508a GAP WP2.1: Inline-VA-Verweise + Verify-Fix (F17)
742277f Doku: Benutzer-Popups, Aufgaben-Modul, Richtlinien-Governance, Demo-Approver
1d8f563 Freigabe-Workflow + Aufgaben-Modul + Dashboard-Kachel
73c9813 Benutzerverwaltung als Popup (Einstellungen + Admin)
959d816 Richtlinien-Fixes: zentrale Variablen, Schutzbedarf zentral, Coverage nach Level
d38b265 Benutzer bearbeiten (Name/E-Mail) + Kern-Einstellungen nur Superadmin
303dcd0 Doku: Produktionshärtung + Benutzer-/Rollenverwaltung
d0821e2 Paket B — Mandanten-Admin: Benutzer- & Rollenverwaltung
3db820e Paket C — MFA optional (Superadmin + Tenant)
908b098 Paket A — Plattform-Admin: Benutzerverwaltung je Mandant
db36c79 Fundament Benutzer-/Rollenverwaltung: Schema, Passwort-Policy, Force-Change
e07dcd9 Nicht-destruktiver Richtlinien-Re-Import (Härtung Paket 3)
8e4040d Separater Superadmin-Store + eigener Login + MFA (Härtung Paket 2)
8348764 API-seitige Modul-Durchsetzung (§3.4, Härtung Paket 1)
```
---
## 5. Deployment & Verifikation
- **Test-/Coolify-Deploy (Demo):** `dev` deployen; die 7 neuen Migrationen laufen automatisch im Init-Container (`prisma migrate deploy`), Bootstrap-Seed legt Demo-Daten + Demo-Approver an. Die letzte Migration ändert das `AssetType`-Enum und legt zwei RLS-Tabellen an — einmal komplett durchlaufen lassen, bevor die neuen Ansichten getestet werden.
- **Prod-Deploy (ohne Demo-Seed):** Runbook **`docs/DEPLOY-PROD-CONTABO.md`**. Statt Demo-Seed läuft `scripts/bootstrap-admin.ts` (Erst-Superadmin im `platformAdmin`-Store + Mandant/Mandanten-Admin, idempotent), gesteuert per `BOOTSTRAP_*`-Env im `migrate`-Job der `docker-compose.coolify.yml`; Env-Referenz `.env.prod.example`. Fonts sind selbst gehostet (kein Google-Fonts-Fetch zur Build-Zeit → reproduzierbarer Offline-Build).
- **Lokale Verifikation:** `npx tsc --noEmit` → `npm run lint` → `npm run build` (führt `prebuild`-Guard-Check aus); für das Vorlagenpaket `python3 seed/isms-vorlagenpaket-v2/_verify.py` (**OK** = rückstandsfrei, Mapping sauber).
- **Sync-Status (Gitea aktuell offline):** lokaler `dev` ist **27 Commits vor `origin/dev`** (inkl. der beiden Dev-B-Integrations-Merges F3+B2 und B3). Sobald Gitea erreichbar ist: **erst `git fetch`**, prüfen ob `origin/dev` noch `9b479ab` ist (bzw. ob jemand anderes gepusht hat), dann `git push origin dev`. Bei Divergenz nicht blind force-pushen — abstimmen.
- **Merge `dev` → `main`:** liegt beim PM/Lead (Gitea-PR: `…/msolarczek/ISMS-Tool/pulls/new/dev`).
---
## 6. Empfohlene nächste Schritte
1. **ISB-Freigabe** der neuen/angepassten Richtlinien- und VA-Texte (fachlich, inkl. **P01/D01/VA-20** aus B4-3).
2. **Storage-Backend (Epic S1)** — echten Adapter hinter `src/server/storage/adapter.ts` (B4-2-Stub) einhängen (S3/MinIO/Coolify-Volume); danach Datei-Persistenz eigener Richtlinien + Netzplan-Upload REG-NET.
3. **Wizard-Stories vollständig konsolidiert** — Dev A: A1–A8, Dev B: F1–F4, B1–B7 (inkl. B7-2 Export + Prüfziel-Follow-up). **Alle Feature-Branches in `dev`.** Als Nächstes (neue Stories/Epics): SoA-/Reifegrad-Auswertungen auf Basis von A7, Reporting-Ausbau auf Basis des VDA-ISA-Exports, echtes Storage-Backend (Epic S1).
4. **Paket 4** — SMTP + E-Mail-Einladungs-/Aktivierungs-/Reset-Flow (Aktivierung ist bereits gekapselt).
5. **Tenant-weite MFA-Pflicht** scharfschalten (Enrollment-Gate).
6. **Admin Phase 2** (Impersonation, Plan/Limits) oder **NIS2-Modul** — je nach Vertriebs-/Compliance-Priorität.
## Hotfix: Docker-Build brach ohne PASSWORD_PEPPER ab (Deploy-Blocker)
- **Symptom:** Coolify-Deploy „erfolgreich", aber KEINE certvia-Container. Ursache war der Image-Build selbst: `RUN npx prisma generate && npm run build` (Dockerfile:40) exit 1 → kein `up`.
- **Root Cause:** `src/server/auth.ts` erzeugte den Konstant-Zeit-Dummy-Hash auf **Modulebene** (`const DUMMY_HASH_PROMISE = hashPassword(...)`). `hashPassword()` liest den `PASSWORD_PEPPER`, der zur Docker-**Build**-Zeit fehlt (nur `DATABASE_URL`-Platzhalter gesetzt). `next build` (Page-Data-Collection für `/api/auth/[...nextauth]`) lud das Modul → Import-Zeit-Throw (fail-secure) → Build-Abbruch. Lokal grün, weil `.env` den Pepper liefert (im Image per `.dockerignore` ausgeschlossen).
- **Fix:** Dummy-Hash lazy + memoisiert (`dummyHash()`), Aufruf erst zur Laufzeit in `verifyIdentityPassword` — gleiches Muster wie `assertSecureEnv()`/`pepper()`. Konstant-Zeit-Verhalten (F-05) bleibt erhalten.
- **Verifiziert:** `docker build --target builder` grün OHNE gesetzten `PASSWORD_PEPPER` (Coolify-Build-Bedingung); tsc grün.
## Hotfix 2: incident-inbound-worker Crash-Loop riss den Stack ab
- **Symptom (nach Build-Fix):** migrate ✅, app ✅ `Ready`, mail-/backup-worker ✅ — aber Coolify-Status flippte auf „exited" und riss den ganzen Stack ab. Ursache: `incident-inbound-worker` ohne IMAP-Konfig `process.exit(1)` + `restart: unless-stopped` → Crash-Loop → Coolify wertet Deploy als unhealthy → `compose down` (auch der gesunden App).
- **Fix:** `scripts/incident-inbound-worker.ts` idlet jetzt bei fehlender IMAP-Konfig (`idleUntilSignal()`), statt zu exiten. Container bleibt „running", SIGTERM beendet sauber (Exit 0). Bei nachträglicher IMAP-Konfig: Container-Neustart aktiviert den Betrieb.
- **Verifiziert:** tsc grün; Smoke-Test ohne IMAP-Env → Prozess bleibt am Leben, loggt Idle-Hinweis, kein Crash.
## Merge: Objektspeicher MinIO → Garage (self-hosted)
- feature/garage-migration → dev (`--no-ff`). Umsetzung des Konzepts `docs/KONZEPT-garage-migration.md` (4 Lanes), keine Datenmigration (nur Test-Instanzen, Neu-Deploy).
- **Infra:** `garage`-Service ersetzt `minio` in `docker-compose.yml` + `docker-compose.coolify.yml`; `deploy/garage.toml` (Single-Node, `s3_region=us-east-1`, RPC-/Admin-Token); Volumes `garage_meta` (kritisch, ins Backup) + `garage_data`.
- **Provisioning:** `scripts/garage-provision.ts` — idempotenter Init-Job (Layout + Bucket `isms-documents` + Key-Import aus S3_*-Env + Rechte).
- **App-Code:** `ensureBucket()` in `adapter.ts` + `backup-store.ts` nur noch verifizierend (HeadBucket), **kein** S3-`CreateBucket` mehr (Garage kann das nicht); fehlender Bucket → sprechender Konfigfehler statt stiller Heilung. `CreateBucketCommand`-Imports entfernt.
- **Test/Doku:** `scripts/test-garage-storage.ts` (skippt ohne `S3_ENDPOINT`, echter E2E mit gesetzten S3_*), Runbook in `docs/DEPLOY-COOLIFY.md`.
- **Gate grün:** tsc, lint, build, Storage-Smoke-Test.
- **Deploy-Hinweise:** Coolify-Env `S3_ENDPOINT=http://garage:3900`, `S3_REGION=us-east-1`, `GARAGE_RPC_SECRET`/`GARAGE_ADMIN_TOKEN` **literal** setzen (Interpolationsfalle); `minio`-Service erst nach Abnahme entfernen (Rollback-Netz).
## Hotfix Garage-Deploy: garage.toml ins Image backen (Coolify-Bind-Mount-Falle)
- **Symptom:** `garage`-Container crasht ~2s nach Start → unhealthy → Deploy bricht ab. Log: `Error: IO error: Is a directory (os error 21)` nach „Loading configuration…".
- **Ursache:** Coolify legt die Quelle des relativen Bind-Mounts `./deploy/garage.toml` als **Verzeichnis** an (Storage-Behandlung) → Garage bekommt `/etc/garage.toml` als Ordner.
- **Fix:** Neue Dockerfile-Stage `garage` (`FROM dxflrs/garage:v1.2.0` + `COPY deploy/garage.toml /etc/garage.toml`); `docker-compose.coolify.yml` nutzt `build: target garage` statt `image:` + Bind-Mount. Secrets bleiben zur Laufzeit aus `GARAGE_RPC_SECRET`/`GARAGE_ADMIN_TOKEN`. Lokales `docker-compose.yml` behält den Bind-Mount (auf normalem Docker unkritisch).
- **Verifiziert:** `docker build --target garage` + Start mit gültigem Secret → Daemon läuft, `/garage status` Exit 0.
## Merge: Framework-Erweiterung — ISO 27001 neben TISAX (AP1–AP5 + B1)
- Kumulativer Merge von `feature/framework-assessment` (enthielt alle Framework-Lanes: iso27001-framework-mapping, framework-core, ap2, ap3-soa, ap4-mgmt, ap5-doccontrol, assessment) → `dev` (`--no-ff`, a747b57). 150 Dateien.
- **AP1** Framework-Dimension (`TenantFramework`, `PolicyTemplateVersion.framework`), **AP2** Provisionierung & Framework-Flags + framework-fähiger Vorlagen-Editor, **AP3** SoA-Modul (ISO-Anwendbarkeitserklärung, 93 Annex-A-Zeilen), **AP4** Managementklauseln (9.1/9.3/10.2, CAPA + Wirksamkeitsprüfung), **AP5** Dokumentenlenkung (Prüfzyklen, Versionshistorie, Lesebestätigungen), **A1–A4/B2/B3** ISO-Inhalte, **B1** Assessment-/Readiness-Strategie-Schicht.
- **Variante A** (Konzept D4): ein Dokumentensatz, zwei Mappings — `docs/FRAMEWORK-MAPPING-ISO27001.md`, `docs/KONZEPT-framework-iso27001.md`.
- **Gate grün:** tsc, lint, build. Neue Tests grün: `test-framework-{core,templates,provision,assessment,dryrun}`, `test-{assessment-iso,soa,doc-control,review}`. **TISAX-Snapshot-Regression: 0 Abweichungen** (45 Controls, AL2) — TISAX-Verhalten bit-genau erhalten.
## Fix: Normen (Frameworks) je Mandant nachträglich umschaltbar
- `cb2bc0b` Admin-Action + UI (`admin/[id]/page.tsx`, `actions/admin.ts`, i18n de/en): ISO/TISAX pro Mandant nachträglich aktivieren/deaktivieren. `1475193` Abnahmetest-Fix: „wiedererkannt" zählt reaktivierte Dokumente mit.
- Neuer Test `scripts/test-framework-toggle.ts` (Aktivieren/Deaktivieren + Zustand wiederherstellen). Gate grün: tsc/lint/build, test-framework-toggle/-dryrun, TISAX-Snapshot 0 Abweichungen.
## BIA-Prozessübersicht: Prozesshaus-Ansicht (Standard) + Tabellen-Umschalter
- `/processes` (`src/app/(app)/processes/page.tsx`) zeigt statt der flachen Tabelle standardmäßig ein **Prozesshaus**: Bahnen nach Kategorie (Management/Kern/Support), Hauptprozesse als aufklappbare Karten (native `<details>`), Teilprozesse eingerückt (Baumkonnektor). Je Hauptprozess **BIA-Rollup** aus den Teilprozessen: Kritikalität = Maximum, RTO/RPO/MTD = schärfster (kleinster) Wert; Farbcode + Statuspunkt nach `biaStatus`; Schnittstellen (`Process.interfaces`) als Chip. Umschalter „Prozesshaus ⇄ Tabelle“ über `?view=tabelle` (Prozesshaus = Default); Tabellen-Ansicht unverändert.
- **Kein Datenmodell-Umbau** — nutzt vorhandene Felder (`parentId`, `category`, `biaStatus`, `BiaEntry`). KPIs: Prozesse gesamt / im Scope / BIA vollständig / hohe Kritikalität (≥3). Modals (Detail/Anlegen/Bearbeiten) unverändert. Neue i18n-Keys unter `processes` (de/en). Gate grün: tsc/lint/build. Konzept-Mockup: `docs/MOCKUP-bia-prozessuebersicht.html`.
- **Nachtrag — jetzt umgesetzt:** strukturierte Prozess-zu-Prozess-Abhängigkeiten (siehe nächster Abschnitt); der frühere Freitext `interfaces` bleibt zusätzlich erhalten.
## Prozess-Abhängigkeiten (strukturiert: „benötigt" / „wird benötigt von")
- Neues Modell **`ProcessDependency`** (`source` benötigt `target`, optionale `note`), Migration `20260907084110_add_process_dependencies` **inkl. RLS** (tenant_isolation + FORCE), Unique `(source, target)`, FK-Cascade beim Prozess-Löschen; in `TENANT_MODELS` aufgenommen. Auswahl **innerhalb des Mandanten**.
- Server-Actions in `actions/processes.ts`: `addProcessDependency` (idempotenter Upsert, Selbstbezug ausgeschlossen, beide Prozesse müssen existieren) und `removeProcessDependency`; `deleteProcess` löst Kanten in beide Richtungen. Beide `bia:write`-gegated + Audit-Log.
- UI: Bearbeiten-Modal neuer Abschnitt „Abhängigkeiten" (Chips + Entfernen, Auswahl-Form „benötigt: [Prozess] + Notiz", plus read-only „wird benötigt von"); Detail-Modal zeigt beide Richtungen; **Prozesshaus** zeigt „↳ benötigt: …"-Chips und „▲ N Prozesse hängen hiervon ab" (Teilprozesse kompakt inline) + Link zum Abhängigkeits-Graph. Neue i18n-Keys `deps*` (de/en). Gate grün: tsc/lint/build, Modul-Guard-Check 45 Dateien ok.
- **Netz-/Graph-Darstellung:** `buildDependencyGraph` (`src/server/dependency-graph.ts`) um Prozess→Prozess-Kanten (`ProcessDependency`, Label „benötigt von", Flussrichtung Abhängigkeit→Nutzer) erweitert; sie erscheinen im bestehenden Graphen `/dependencies` (React Flow + dagre) und fließen in Analyse/kritischen Pfad ein. SPOF-Erkennung jetzt auch für **Prozesse** (gemeinsam benötigte Prozesse wie „IT-Betrieb", von denen mehrere kritische Prozesse abhängen). Neuer Client-Filter **„Nur Prozesse"** in `dependency-graph.tsx` (blendet Assets aus → reine Prozess-Abhängigkeitskarte). Verifiziert am `demo`-Mandanten: IT-Betrieb als SPOF (3) korrekt erkannt.
## Datenimport (Excel) — zurückgezogen
- Das zuvor auf `dev` ergänzte Excel-Datenimport-Feature (geteilte Logik `src/server/import/`, UI `/settings/import`, Ops-Skript, Tabelle `ImportRef`/Migration `20260904081701_add_import_refs`, Konzept `KONZEPT-datenimport.md`) wurde **vollständig entfernt** (nie deployt, daher ohne DB-Auswirkung). Bestandsdaten werden weiterhin über die Modul-Formulare bzw. den Onboarding-Wizard erfasst.
- `docs/HANDBUCH-KUNDENBETREUUNG.md` bleibt als produkt-/supportorientiertes Einstiegshandbuch für die Kundenbetreuung (Zugang/Rollen, Provisionierung, Framework-Wahl, Module, Onboarding, Support-Fälle, Doku-Verweise) — die Datenimport-Abschnitte wurden herausgenommen.
## Fix: Risikokriterien 5×5 vervollständigt (Matrix ↔ Einstellungen konsistent)
- Bug: Risiko-Bewertung (Formular-Skala 1–5, Matrix 5×5, Score-Schwellen bis 25) war 5×5, aber die Kriterien-Daten nur 4-stufig geseedet (EW_LEVELS=4, Schadensdimensionen 1–4) → 5. Stufe ohne Definition.
- Entscheidung (PO): **5×5** — Kriterien ergänzen (Bewertung/Matrix/Score bleiben unverändert).
- Seed `prisma/import-managed.ts`: 5. EW-Stufe („Nahezu sicher") + 5. Stufe je Schadensdimension.
- Editor `onboarding/steps/criteria/criteria-editor.tsx` (Settings + Wizard geteilt): Typ/Update/Neuanlage/Rendering auf 1–5; `actions.ts` zod-Schema um „5"; `settings/risk-criteria/page.tsx` + `onboarding/steps/criteria/step.tsx` editorDims um „5".
- Backfill `scripts/backfill-risk-5x5.ts` (idempotent): ergänzt EW-Stufe 5 + Schadensstufe 5 für Bestandsmandanten. Für Test/Prod nach Redeploy je Instanz einmal ausführen (demo, gefim).
- Gate grün (tsc/lint/build); Backfill lokal verifiziert.
-328
View File
@@ -1,328 +0,0 @@
# Übergabe: Anwendung auf zwei Frameworks umbauen (ISO 27001 neben TISAX)
**Stand:** 2026-08-21 · **Zielgruppe:** Entwickler:innen · **Vorarbeit:** Branch `feature/iso27001-framework-mapping`, Commit `492d315`
> **Ausgangslage:** Die Inhaltsseite ist fertig. `seed/isms-vorlagenpaket-v2` trägt jetzt **zwei
> Framework-Mappings** auf **einem** Dokumentensatz — `mapping.json` (VDA ISA, 321 Anforderungen) und
> `mapping-iso.json` (ISO/IEC 27001:2022, 120 Anforderungen). Die Anwendung kennt das zweite Mapping
> noch nicht: `parsePackageFiles` liest `mapping.json` fest verdrahtet.
>
> **Auftrag:** Framework als erste Klasse im Datenmodell und in der Paketauflösung, damit ein Mandant
> ISO, TISAX oder beides führen kann. Danach die drei ISO-Artefakte, die es im Tool noch nicht gibt:
> SoA, Kennzahlen, Managementbewertung/Korrekturmaßnahmen.
Fachlicher Hintergrund und Begründung der Entscheidung: `docs/FRAMEWORK-MAPPING-ISO27001.md`.
Gesamtarchitektur und die übrigen Lanes: `docs/KONZEPT-framework-iso27001.md`.
---
## 0. Was **nicht** angefasst werden muss
Die Paketinhalte sind generiert. Wer ISO-Texte oder Zuordnungen ändern will, ändert
`_iso_crosswalk.json` bzw. `_iso_sections.json` und lässt `python3 _generate_iso.py` laufen —
**nie direkt die Markdown-Dateien**, der Generator überschreibt sentinel-begrenzte Blöcke.
Danach müssen beide Prüfskripte grün sein:
```bash
cd seed/isms-vorlagenpaket-v2
python3 _generate_iso.py # idempotent, zweiter Lauf ändert nichts
python3 _verify.py # TISAX-Sicht
python3 _verify_iso.py # ISO-Sicht
python3 _render_diff.py HEAD # TISAX-Regression gegen den aktuellen Stand
```
Die Sichtbarkeit im Dokument steuern zwei Variablen aus `variables.schema.json`:
`FLAG_FW_TISAX` (Default `true`) und `FLAG_FW_ISO27001` (Default `false`).
### Parallelbetrieb ist vorgesehen
Sind **beide** Flags gesetzt, rendert das Dokument beide Anforderungssichten untereinander — über
**einem** gemeinsamen Umsetzungstext. Genau dafür ist die Bibliothek gebaut:
```
3.2 Sichere Anmeldung
*Anforderungsbezug:* VDA ISA 4.1.2 · ISO/IEC 27001 A.8.5
**Anforderung**
*Anforderungen nach VDA ISA 2027:*
- **[MUSS]** Verfahren zur Benutzerauthentifizierung nach dem Stand der Technik werden angewandt.
- **[SOLL]** Für privilegierte Benutzerkonten werden höherwertige Verfahren genutzt …
*Anforderungen nach ISO/IEC 27001:*
- **[ISO A.8.5]** Sichere Authentisierungstechnologien und -verfahren sind einzusetzen.
**Umsetzung bei {{ORG_NAME}}**
Die Authentifizierungsverfahren sind risikobasiert ausgewählt … Passwortvorgaben nach BL-IAM-01 …
```
Der Anforderungsbezug steht in einer eigenen Zeile unter der Überschrift und nennt je nach
Betriebsart eine oder beide Normen. Die beiden Zwischenüberschriften erscheinen **nur**, wenn
tatsächlich beide Frameworks aktiv sind.
Die 19 ISO-only-Abschnitte kommen bei einem Doppel-Mandanten additiv hinzu.
Auf der Datenseite ist der Parallelbetrieb erst nach AP1 möglich: `PolicyRequirement` trägt dann
beide ID-Namensräume (`4.1.2-M1` und `A.5.15-1`) nebeneinander — vorausgesetzt, Falle 1.1 ist gelöst.
---
## 1. Die vier Fallen — bitte zuerst lesen
### 1.1 `reconcilePackage` archiviert die Anforderungen des jeweils anderen Frameworks
**Das ist der kritische Punkt.** `prisma/import-policies.ts:368-371` archiviert jede
`PolicyRequirement`, deren `reqId` nicht im importierten Paket steht:
```ts
for (const ex of existingReqs) {
if (desiredReqIds.has(ex.reqId) || ex.archivedAt) continue;
report.requirements.archived++;
await prisma.policyRequirement.update({ where: { id: ex.id }, data: { archivedAt: now } });
}
```
Ein ISO-Import in einen Mandanten mit TISAX archiviert damit **alle 321 VDA-ISA-Anforderungen** —
und umgekehrt. Die ID-Namensräume kollidieren zwar nicht (`4.1.2-M1` vs. `A.5.15-1`, `@@unique([tenantId, reqId])`
bleibt heil), aber der Abgleich muss **framework-scoped** werden:
- `PolicyRequirement.framework Framework` ergänzen (Backfill `TISAX`),
- den Archivierungslauf auf `where: { tenantId, framework }` einschränken.
Für **Dokumente** gilt das nicht: Beide Mappings lesen dieselben `richtlinien/`- und `verfahren/`-Dateien,
`pkg.documents` ist identisch. Ebenso Variablen, Baseline-Parameter und Nachweisregister — die sind geteilt
und dürfen genau einmal je Mandant abgeglichen werden.
### 1.2 Zwei Unique-Constraints brechen bei zwei Frameworks
| Modell | heute | muss werden |
|---|---|---|
| `PolicyTemplateVersion` (`prisma/schema.prisma:1639`) | `version String @unique` | `@@unique([framework, version])` — sonst kollidieren ISO 2.1 und TISAX 2.1 |
| `PolicyPackageState` (`prisma/schema.prisma:1614`) | `tenantId String @unique` | `@@unique([tenantId, framework])` — sonst merkt sich ein Mandant nur eine Paketversion |
### 1.3 TISAX darf sich nicht verändern
Bestandsmandanten müssen bitgenau dasselbe sehen wie heute. Der Renderdiff über alle 17 Richtlinien
(TISAX-Kontext vorher/nachher) war bei der Paketumstellung **0 Abweichungen** — dieser Wert ist die
Messlatte. `src/lib/policy-render.ts` belegt fehlende Framework-Flags bereits vor
(`FLAG_FW_TISAX = true`, `FLAG_FW_ISO27001 = false`), damit ein Bestandsmandant vor dem Paket-Re-Import
keine leeren Anforderungsblöcke sieht. **Diesen Fail-Safe nicht entfernen**, auch nicht wenn die Flags
später über `TenantFramework` gesetzt werden.
Nebenbei: `applyProtection` (`src/lib/policy-render.ts:54`) setzt `FLAG_HIGH_PROTECTION` bedingungslos auf `true`
mit der Begründung „im TISAX-Modell stets aktiv". Für ISO-Mandanten ist das derzeit folgenlos (die
ISO-Blöcke nutzen die Schutzbedarf-Flags nicht), sollte aber beim Bau der `IsoStrategy` bewusst
entschieden werden.
### 1.4 Migrations- und Build-Konventionen
- Migrationsflow Prisma 7 wie in `docs/HANDOVER-DEV.md:100`: `migrate diff --from-config-datasource … --to-schema … --script`,
danach den **RLS-DO-Block manuell** an die `migration.sql` anhängen, dann `migrate deploy`.
- Jedes neue mandantengebundene Modell gehört in **`TENANT_MODELS`** (`src/server/db.ts:81`) **und**
braucht eine RLS-Policy in seiner Migration.
- `scripts/check-module-guards.ts` läuft als `prebuild`-Gate: **jede neue Datei** unter
`src/server/actions/` muss dort eingetragen sein (Modul-Key oder `EXEMPT`), sonst schlägt der Build fehl.
- Bei paralleler Lane-Entwicklung teilen sich die Worktrees dieselbe lokale Postgres-DB. Beim Erzeugen
einer Migration nur die **eigenen** DDL-Blöcke übernehmen und Fremd-Drops von Hand entfernen.
- `npm run lint` und `npm run build` müssen vor jedem Commit grün sein (`AGENTS.md:25`).
---
## 2. Arbeitspakete
### AP1 — Framework-Dimension *(Fundament, blockiert alles Weitere)*
**Schema**
```prisma
enum Framework { ISO_27001 TISAX }
model TenantFramework {
id String @id @default(cuid())
tenantId String @map("tenant_id")
framework Framework
isPrimary Boolean @default(false) @map("is_primary")
config Json? // z. B. { tisaxLevel: "AL3" } bzw. { certScope, certBodyTarget }
createdAt DateTime @default(now()) @map("created_at")
@@unique([tenantId, framework])
@@index([tenantId])
@@map("tenant_frameworks")
}
```
Zusätzlich: `PolicyRequirement.framework`, `PolicyTemplateVersion.framework`,
`PolicyPackageState.framework` (siehe Fallen 1.1 und 1.2). Alles additiv, Backfill der Bestandsdaten auf
`TISAX`.
**Paketauflösung**
| Datei | Änderung |
|---|---|
| `prisma/import-policies.ts` | `parsePackageFiles(seedDir, mappingFile = "mapping.json")`; Requirements framework-scoped reconcilen |
| `prisma/template-store.ts:147` | `resolvePackageForTenant(prisma, tenantId, seedDir, framework)`; `loadPublishedPackage(prisma, locale, framework)`; `getAvailableVersion` ebenso |
| `scripts/sync-policy-templates.ts:23` | über **Frameworks × Sprachen** iterieren statt nur über Sprachen |
Die vier Aufrufer von `resolvePackageForTenant` bekommen den Framework-Parameter durchgereicht:
`src/server/provision.ts:139`, `src/server/actions/policy-package.ts:28`, `src/server/actions/admin.ts`,
`src/app/(app)/policies/updates/page.tsx:44`.
**Das Seed-Verzeichnis bleibt für beide Frameworks dasselbe** — die fünf `SEED_DIR`-Konstanten ändern
sich nicht, nur der Mapping-Dateiname. Das ist der Vorteil von Variante A.
**DoD:** Bestandsmandanten laufen unverändert als TISAX; ein Mandant kann mit
`frameworks: ["ISO_27001"]`, `["TISAX"]` oder beiden provisioniert werden; bei Doppel-Framework
koexistieren 321 + 120 Anforderungen und **keine** ist fälschlich archiviert.
---
### AP2 — Provisionierung und Flags *(klein, direkt nach AP1)*
`ProvisionOpts` (`src/server/provision.ts:37`) um `frameworks: Framework[]` erweitern.
`provisionTenant` schreibt die `TenantFramework`-Zeilen, importiert **je Framework** das passende
Mapping und setzt die Sichtbarkeits-Flags als `PolicyVariable`:
| Mandant führt | `FLAG_FW_TISAX` | `FLAG_FW_ISO27001` |
|---|:--:|:--:|
| nur TISAX | `true` | `false` |
| nur ISO | `false` | `true` |
| beides | `true` | `true` |
**Achtung Reihenfolge:** `reconcilePackage` erhält nutzergepflegte Variablenwerte und überschreibt sie
nicht. Die Flags müssen also **nach** dem Import gesetzt werden, sonst bleibt der Schema-Default stehen
und ein ISO-Mandant sieht die VDA-ISA-Sicht.
`tisaxLevel` bleibt vorerst auf `TenantSettings`, wird aber als TISAX-scoped dokumentiert (Entscheidung D2).
Die AL-Flags dürfen bei einem reinen ISO-Mandanten nicht gesetzt werden.
**DoD:** Ein frisch provisionierter ISO-Mandant öffnet `/policies` und sieht in jedem Dokument die
ISO-Anforderungssicht plus die 19 ISO-only-Abschnitte; keine „ISA"-Klammern in den Überschriften.
---
### AP3 — SoA-Modul *(das fehlende ISO-Kernartefakt)*
Heute ist der Modul-Key `soa` (`src/lib/modules.ts:19`) mit `href: "/soa"` registriert, **die Route
existiert aber nicht** — die Logik liegt als Wizard-Schritt 7 in `src/server/actions/soa.ts` und ist ein
VDA-ISA-Reifegrad-Assessment (0–3), nicht die ISO-Anwendbarkeitserklärung.
```prisma
model SoaEntry {
id String @id @default(cuid())
tenantId String @map("tenant_id")
framework Framework
control String // "A.5.15"
applicable Boolean @default(true)
justification String // Begründung Einbeziehung ODER Ausschluss
source String? // Risiko-ID / gesetzliche / vertragliche Anforderung
implementationStatus String @default("geplant") // umgesetzt | teilweise | geplant
ownerId String? @map("owner_id")
policyCode String? @map("policy_code")
evidenceId String? @map("evidence_id")
@@unique([tenantId, framework, control])
@@index([tenantId])
@@map("soa_entries")
}
```
Die vier Felder `applicable`, `justification`, `implementationStatus` und die Ausschlussbegründung sind
**normative Pflichtangaben** (ISO/IEC 27001:2022, 6.1.3 d) — ohne sie ist die SoA im Zertifizierungsaudit
angreifbar.
Vorbefüllung aus `mapping-iso.json`: 93 Controls; das Feld `condition` (z. B. `FLAG_DEV_INHOUSE`) steuert
die Default-Anwendbarkeit. Als fachliche Vorlage für Aufbau und Spalten dient
`seed/isms-vorlagenpaket-v2/Statement-of-Applicability-ISO.md`.
**DoD:** Ein ISO-Mandant kann die SoA vollständig pflegen und als PDF/XLSX exportieren; ein Control ohne
Begründung wird als unvollständig markiert.
---
### AP4 — Managementklauseln: Kennzahlen, Bewertung, Korrekturmaßnahmen
Drei Modelle fehlen; alle drei sind ISO-Pflichtthemen und heute nicht abbildbar:
| Klausel | Modell | Inhalt |
|---|---|---|
| 9.1 | `Kpi` / `KpiValue` | Kennzahl, Datenquelle, Zielwert, Turnus, Verantwortlicher, Messwerte je Periode |
| 9.3 | `ManagementReview` | Datum, Eingaben nach 9.3.2, Ergebnisse nach 9.3.3, Beschlüsse mit Verantwortlichem und Termin |
| 10.2 | `Nonconformity` + `CorrectiveAction` | Herkunft, Sofortkorrektur, Ursachenanalyse, Maßnahme, Wirksamkeitsbewertung |
Vorhandene Bausteine, auf denen das aufsetzen kann: `Task.recurrence` (RRULE), `Task.remindAt`,
`Task.effectiveUntil` (Wirksamkeitsintervall, gekoppelt an `Evidence.validUntil`), `TaskParticipant` (RACI)
und `AuditLog`. Die Datenquellen für die Kennzahlen liegen bereits im Tool: Aufgabenfristen und
Überfälligkeit, Incident-SLA und Meldefristen (`src/lib/incident-deadlines.ts`), Reifegrade je Control,
Maßnahmenstatus.
Fachlicher Inhalt der Abschnitte steht in R03 der Bibliothek (`ISO-MS-MESSUNG`, `ISO-MS-MGMTREVIEW`,
`ISO-MS-CAPA`) — die Modelle sollten die dort beschriebenen Felder tragen.
**DoD:** Kennzahlenblatt mit Zielwerten pflegbar und über zwei Perioden auswertbar; Management-Review
entlang der 9.3.2-Agenda protokollierbar; ein Maßnahmenfall inklusive dokumentierter Wirksamkeitsprüfung
abschließbar.
---
### AP5 — Dokumentenlenkung *(klein, hohe Auditwirkung)*
- `PolicyDocument.reviewCycle` und `nextReviewAt` — heute führt nur `ManagedRegister` einen
`reviewCycle`; A.5.1 verlangt die Überprüfung „in geplanten Abständen". Behelfsweise über
`Task.recurrence` möglich, sauberer am Dokument.
- `PolicyAcknowledgement { tenantId, policyDocumentId, version, userId, acknowledgedAt }` — die
Lesebestätigung ist in `SPEC.md` §4.6 vorgesehen und fehlt. Sie ist zugleich der einfachste Nachweis
für Klausel 7.3 und Control A.6.3.
- Änderungshistorie je Dokumentversion — im Entwicklungsstand als offen geführt. Kann aus `AuditLog`
(Vorher/Nachher) abgeleitet oder als eigene Tabelle geführt werden.
**DoD:** Übersicht „Prüfung fällig" im Tool; Auswertung der Lesebestätigungen je Richtlinienversion;
Historie eines Dokuments über mindestens zwei Versionen sichtbar.
---
## 3. Reihenfolge und Aufwand
```
AP1 Framework-Dimension ██████ 4–6 PT ← blockiert alles
├─ AP2 Provisionierung ██ 1–2 PT
├─ AP3 SoA-Modul ██████ 5–8 PT
├─ AP4 Managementkl. ██████ 5–8 PT
└─ AP5 Dok.-Lenkung ███ 2–3 PT
```
AP3, AP4 und AP5 sind nach AP1 parallelisierbar. Die Schätzung entspricht den Lanes 1, 4 und 5 aus
`KONZEPT-framework-iso27001.md`; der inhaltliche Teil von Lane 2 (ISO-Mapping und -Texte) ist erledigt und
entfällt.
**Feature-Flag:** ISO bleibt laut Entscheidung D7 hinter einem Plattform-Schalter, bis AP3 abgenommen ist.
---
## 4. Abnahme
| Prüfung | Erwartung |
|---|---|
| `python3 _verify.py` und `_verify_iso.py` | beide `OK` |
| `python3 _render_diff.py <rev>` (TISAX-Sicht) | 0 Abweichungen — Skript liegt im Paket bei. **Vergleichsstand ist der Kopf dieses Branches, nicht `a9649b3`**: der Anforderungsbezug ist dort bewusst aus der Überschrift in eine eigene Zeile gewandert (14 von 17 Richtlinien betroffen, ausschließlich diese Zeile — der Anforderungs- und Umsetzungstext ist unverändert). |
| `python3 _render_diff.py <rev> --framework BEIDE` | Parallelbetrieb prüfbar |
| `npx tsx scripts/test-framework-dryrun.ts` | alle Prüfungen bestanden — Trockenlauf ohne Schreibzugriff über Paketebene und alle Mandanten der lokalen DB |
| Bestandsmandant (TISAX) nach Deploy | Readiness und Exporte identisch zum Snapshot vor dem Umbau |
| Import ISO in Mandant mit TISAX | 120 neue Anforderungen, **0 archivierte** ISA-Anforderungen |
| Import ISO, Umsetzungstexte | 120 von 120 gefüllt (0 leer) |
| Dokumente bei Doppel-Framework | 39 Dokumente, **nicht** doppelt |
| `npm run lint`, `npx tsc --noEmit`, `npm run build` | grün |
Der Importer lässt sich ohne Datenbank gegen beide Mappings prüfen — `parsePackageFiles` ist reine
Dateiarbeit und liefert `documents`, `requirements`, `variables`, `baseline`, `evidence` als
`ParsedPackage`. Ein Trockenlauf des Mandanten-Imports geht über `reconcilePackage(..., { dryRun: true })`;
er erzeugt den Änderungsreport ohne Schreibzugriff und eignet sich als Freigabebedingung.
---
## 5. Offene Punkte auf der Paketseite (nicht Entwicklung)
Diese Punkte gehören dem ISB bzw. der Redaktion, nicht dem Entwicklungsteam — hier nur zur Abgrenzung:
- Nachweisregister um Zeilen für Kennzahlenblatt, Management-Review-Protokoll und Maßnahmenregister ergänzen.
- VA-15 trennen (internes Audit vs. Managementbewertung) und ein Verfahren für Korrekturmaßnahmen
ergänzen — **Nummer ab VA-21**, VA-20 ist belegt.
- `REVIEW_CYCLE` wird an 33 Bestandsstellen für fünf verschiedene Zyklen verwendet; die getrennten
Variablen (`POLICY_REVIEW_CYCLE`, `MGMT_REVIEW_CYCLE`, `RISK_REVIEW_CYCLE`) greifen bisher nur in den
neuen ISO-Abschnitten.
- ISB-Freigabe der 19 neuen Abschnittstexte und Review des Crosswalks.
+41
View File
@@ -0,0 +1,41 @@
# Certvia-Archiv
Craftvia ist aus dem Fundament des ISMS-Produkts **Certvia** hervorgegangen (Auth.js mit
Identity/Mitgliedschaften, RLS, Mail-/Backup-Worker, Garage, Härtung). Dieser Ordner hält
die dabei übernommenen Dokumente **unverändert** vor. Sie sind **nicht maßgeblich** für Craftvia.
## Warum archiviert
- Produkt-, Domain- und Personenbezug auf Certvia/ISMS (`app.certvia.de`, Gitea-/Coolify-Hosts,
Rollen ISB/DSB, Vorfall-Mail-Eingang, Risiko-Backfill), veraltete Namen (`isms_app`,
`isms-documents`, MinIO).
- Konzepte und Umsetzungs-Prompts sind umgesetzt. Der Ist-Stand steht im Code und in der
Craftvia-Doku.
- Der betriebsrelevante Inhalt (Coolify-Deploy, Prebuilt-Images, RLS-Aktivierung, Secrets,
Backup/Restore, Garage) ist in **[docs/craftvia/DEPLOY.md](../craftvia/DEPLOY.md)**
zusammengeführt und auf Craftvia umgeschrieben.
Maßgeblich sind [AGENTS.md](../../AGENTS.md), [docs/craftvia/SPEC-CRAFTVIA.md](../craftvia/SPEC-CRAFTVIA.md),
[docs/craftvia/ARCHITEKTUR.md](../craftvia/ARCHITEKTUR.md) und [docs/craftvia/DEPLOY.md](../craftvia/DEPLOY.md).
## Inhalt
| Datei | Thema | Noch als Hintergrund nützlich für |
|---|---|---|
| `DEPLOY-COOLIFY.md` | Testserver via Coolify (Certvia) | – (ersetzt durch DEPLOY.md) |
| `DEPLOY-PROD-CONTABO.md` | Prod-VPS, LUKS, pgBackRest/age/restic, PITR, Vorfall-Mail-Eingang | Host-Encryption- und PITR-Details |
| `DEPLOY-PROD-PREBUILT.md` | Prebuilt-Images über die Registry | – (ersetzt durch DEPLOY.md) |
| `HANDOVER-DEVOPS.md` | frühe DevOps-Übergabe (Stand Juli 2026) | – |
| `DEVOPS-INTEGRATION-RUNBOOK.md` | Branch-Integration im Certvia-Team | – |
| `SECRETS-REGISTER.md` | Secrets-Register (Certvia) | Rotationsregeln (in DEPLOY.md übernommen) |
| `KONZEPT-backup-restore.md` | Backup-/Restore-/DSGVO-Engine | Designbegründung von `src/server/backup/**` |
| `KONZEPT-backup-target.md` | konfigurierbarer Backup-Zielspeicher | Designbegründung `/admin/backup` |
| `KONZEPT-garage-migration.md` | MinIO → Garage | Designbegründung Garage/`garage-provision` |
| `KONZEPT-haertung.md` | Pepper, Host-Encryption, Secrets | Designbegründung `PASSWORD_PEPPER` |
| `KONZEPT-identity-mandanten.md`, `FEINDESIGN-identity-mandanten.md`, `UEBERGABE-identity-mandanten.md` | zentrale Identity + Mandanten-Mitgliedschaften | Designbegründung Two-Step-Login/Mandantenwechsel |
| `KONZEPT-ui-i18n.md` | Betreiber-Konsole-UX, i18n | – |
| `SEC1-MAIL.md`, `SEC2-AUTH-SELFSERVICE.md` | Mail-Fundament, Passwort-Self-Service | Hintergrund zu `src/server/mail/**`, `scripts/test-mail.ts`, `scripts/test-auth-selfservice.ts` |
| `sicherheit/` | PO-Konzept und Claude-Code-Prompts SEC1–SEC6 (Certvia) | – |
Die Querverweise **innerhalb** dieser Dokumente (`docs/…`) zeigen noch auf die alten Pfade.
Sie werden bewusst nicht nachgezogen.
@@ -1,107 +0,0 @@
# Umsetzungspaket — Vollständige Branding-Umstellung auf **Certvia**
> Claude-Code-Prompt für **einen** Entwickler. Basis-Branch **`dev`**, Feature-Branch **`dev/branding-certvia`** (PR-Ziel `dev`).
> Quelle der Wahrheit: **Certvia Brand-Board** (`Certvia-Brand-Board-final.html`) + Logo-SVGs (`certvia-mark-positiv/negativ/grau/anthrazit.svg`, `certvia-favicon.svg`, `certvia-lockup-positiv.svg`) aus `Certvia-Design-Uebergabe.zip`.
## 0. Ziel & Leitplanken
Die App vollständig auf den **Certvia-Styleguide** umstellen — Logo, Wortmarke, App-Name, Favicon/Icons, Typografie, Marken-/UI-Farben, Dokument-/Export-Design und alle Texte.
**Wichtige Leitplanken:**
- **GEFIM bleibt Dachmarke.** „**Ein Produkt von GEFIM**" (inkl. GEFIM-Logo) bleibt im Footer/Impressum/Export-Fußzeile erhalten. Nur die **Produktmarke** wird Certvia.
- **UI-Farben bleiben wie initial festgelegt** (Dark-Theme-Primär `#7d6fd6` etc.) — sie **sind** die Certvia-UI-Palette. Nicht „korrigieren". Marken-Violett `#5d52a3` gilt für Logo/Print/Verläufe, UI-Interaktiv bleibt `#7d6fd6`.
- **Keine Funktionsänderung** — reines Branding/Design. Kein Umbau von Logik/Datenmodell.
---
## 1. Design-Tokens (verbindliche Referenz)
**Markenfarben (Logo, Print, Verläufe, Akzent):**
| Rolle | Hex |
|---|---|
| Violett (Marke) | `#5d52a3` |
| Magenta (Akzent: „via", Haken, Highlights) | `#812d80` |
| Hellblau | `#8dc4e0` |
| Anthrazit (Text/Headlines) | `#3b3b3a` |
**UI Dark-Theme (unverändert, = Certvia-Produktpalette):**
`--violet:#7d6fd6` · `--violet-deep:#5d52a3` · `--blue:#8dc4e0` · `--magenta:#b45bb0` · `--bg0:#0e1220` · `--bg1:#141a2e` · `--panel:rgba(30,38,64,.72)` · `--panel-b:rgba(120,135,180,.18)` · `--elev:#1a2138` · `--txt:#e8ecf7` · `--muted:#8b93ad` · `--ok:#39c07f` · `--warn:#f0ad4e` · `--risk:#ff6b6b` · Primär-Verlauf `linear-gradient(135deg,#5d52a3,#7d6fd6)` · Fokus-Ring `rgba(125,111,214,.35)`.
**Typografie:** Headlines/Wortmarke **Poppins**, Fließtext **Open Sans** (bereits im Einsatz — bestätigen, **selbst hosten**, kein ungefragtes Google-Fonts-CDN).
**Logo/Marke:** C-Monogramm (offenes „C" Violett `#5d52a3` + Haken Magenta `#812d80`, Haken schließt bündig an die C-Öffnung). Wortmarke „Cert" (Poppins Light, Anthrazit) + „**via**" (Poppins Bold, Magenta). Auf dunklem Grund: C `#7d6fd6`, Haken `#d17bcf`, Text weiß.
---
## 2. Story B0 — Bestandsaufnahme / Branding-Inventar (zuerst!) (S)
**Als** Entwickler **möchte ich** alle Branding-Berührungspunkte finden, **damit** nichts übersehen wird.
- Repo-weit suchen und als kurze Inventarliste (Datei → Fundstelle → Maßnahme) dokumentieren:
```bash
rg -i "gefim" -l # Produktname vs. Dachmarke unterscheiden
rg -i "shield|logo|brand|favicon|wordmark|app[_-]?name|siteName|title" src public
rg -i "#5d52a3|#7d6fd6|#812d80|#8dc4e0|#b45bb0|#0e1220" src # hartkodierte Farben außerhalb der Tokens
rg -i "poppins|open sans|fonts.googleapis" src public
```
- **AK:** Inventarliste liegt vor; jede Fundstelle ist einer der Stories S1–S9 zugeordnet; klar getrennt „Produktmarke → Certvia" vs. „Dachmarke → GEFIM bleibt".
---
## 3. Stories
### S1 — Design-Tokens zentralisieren & Certvia-Palette verankern (M)
- Eine **zentrale Token-Quelle** (CSS-Variablen/`theme`/Tailwind-Config) als Single Source of Truth; hartkodierte Farben aus S0 durch Tokens ersetzen.
- Marken- vs. UI-Tokens klar benennen (`--brand-violet` `#5d52a3` vs. `--ui-primary` `#7d6fd6`), damit Logo/Print und UI nicht vermischt werden.
- **AK:** keine hartkodierten Markenfarben mehr in Komponenten; Umschalten zentral möglich; UI optisch unverändert (nur konsolidiert).
### S2 — Logo, Wortmarke & Icons (M)
- **App-Header/Sidebar**, **Login** (`/login`) und **Plattform-Login** (`/platform/login`): GEFIM-Schild → **Certvia-Logo** (Lockup C-Monogramm + „Certvia"). Dark-Variante nutzen.
- **Favicon** + **PWA/Manifest-Icons** (`public/`, `manifest.webmanifest`, `apple-touch-icon`) auf `certvia-favicon.svg` (C auf Violett-Tile) und generierte PNGs (32/180/192/512).
- `meta-theme-color` = `#5d52a3` (schon so auf der Website).
- SVG-Assets aus dem Design-Paket in `public/assets/logo/` bzw. `src/components/brand/` einbinden; **Logo als React-Komponente** `<CertviaLogo variant="lockup|mark" theme="light|dark" />`.
- **AK:** in allen App-Bereichen erscheint das Certvia-Logo; Favicon/Tab-Icon = Certvia; keine GEFIM-Schild-Reste in Produkt-UI.
### S3 — App-Name, Titel, Meta & i18n-Texte (S–M)
- App-Name/`<title>`/OG-/Twitter-Meta, PWA-Name, Sidebar-Kopf, „Über"/Version-Info → **Certvia**.
- i18n (`de`/`en`): alle Produktname-Strings auf „Certvia"; Tagline „Informationssicherheit. Endlich einfach." wo passend.
- **AK:** Browser-Tab, Meta, Manifest und sichtbare Produktnamen zeigen „Certvia"; „ein Produkt von GEFIM" bleibt erhalten.
### S4 — Typografie bestätigen & selbst hosten (S)
- Poppins (300/400/500/600/700) + Open Sans (400/600/700) **self-hosted** (woff2) statt CDN; Fallback-Stack; `font-display: swap`.
- **AK:** keine externen Font-Requests; Wortmarke „Cert**via**" korrekt (Light + Bold), Headlines Poppins, Fließtext Open Sans.
### S5 — Dokument-/Export-Corporate-Design (M–L)
- **DOCX/PDF/Print-Export** (Richtlinien, VDA-ISA-Katalog, Reports) auf **Certvia-Corporate-Design**: Deckblatt/Kopf mit Certvia-Logo + Farben, Fußzeile „Certvia — ein Produkt von GEFIM" + GEFIM-Logo, Poppins/Open Sans.
- Betrifft den (offenen) DOCX/PDF-Export — Branding-Layer so bauen, dass er beim Export-Feature andockt.
- **AK:** exportierte Dokumente tragen Certvia-Branding; GEFIM als Dachmarke in der Fußzeile.
### S6 — E-Mail-/Benachrichtigungs-Templates (S–M, falls vorhanden)
- Sobald SMTP/E-Mail (Paket 4) existiert bzw. für Einladungs-/Reset-Mails: Header/Footer/Farben/Logo auf Certvia; Absender-/Signatur-Text „Certvia — ein Produkt von GEFIM".
- Falls E-Mail noch nicht aktiv: nur die **Template-Basis** brandfähig vorbereiten.
- **AK:** vorhandene Mail-Templates gebranded oder brandfähige Basis vorbereitet.
### S7 — Auth-, Fehler- & Systemseiten (S)
- Login, Change-Password, deaktiviertes Konto, 404/500, Wartungs-/Status-Banner, Loading-/Empty-States: Certvia-Logo + Palette.
- **AK:** keine ungebrandeten/gemischten Screens mehr.
### S8 — Mandanten-Branding-Default = Certvia (S–M)
- Der Whitelabel-/Branding-Fallback je Mandant (Admin Phase 2 „Logo-Upload") ist **Certvia** als Default; Custom-Logo überschreibt nur, wenn gesetzt.
- **AK:** ohne Custom-Logo zeigt jeder Mandant Certvia; „ein Produkt von GEFIM" bleibt unabhängig davon.
### S9 — Restliche GEFIM-Referenzen bereinigen (S)
- Alle in S0 gefundenen **Produkt**-Referenzen „GEFIM" → „Certvia" (Kommentare, Alt-Texte, aria-labels, Klassennamen unkritisch). **Dachmarken-Hinweise bewusst belassen.**
- **AK:** `rg -i "gefim"` liefert nur noch Dachmarken-Kontext (Footer/Impressum/Export-Fußzeile/Assets `gefim/`).
---
## 4. QA / Definition of Done
- **Visuelles Review** der Kernscreens (Dashboard, Richtlinien, Assets, Risiken, Lieferanten, Aufgaben, Admin, beide Logins, Export, Fehlerseiten) hell **und** dunkel — Screenshots im PR.
- **Kontrast prüfen:** Magenta `#812d80` auf Weiß und `#d17bcf` auf Dunkel (WCAG AA für Text/aktive Elemente).
- `rg -i "gefim"` = nur Dachmarken-Kontext; keine externen Font-/Logo-Requests; keine hartkodierten Markenfarben.
- `npx tsc --noEmit` → `npm run lint` → `npm run build` grün; Demo-Umgebung lauffähig.
- **Reines Branding-PR** — keine funktionalen Diffs.
## 5. Reihenfolge
S0 (Inventar) → S1 (Tokens) → S2 (Logo/Icons) → S3 (Name/Meta/i18n) → S4 (Fonts) → S7 (Auth/Fehler) → S9 (Cleanup) → S5 (Export) → S8 (Mandanten-Default) → S6 (Mail, falls vorhanden).
## 6. Assets (mitliefern an den Entwickler)
Aus `Certvia-Design-Uebergabe.zip`: `logo/certvia-mark-positiv.svg`, `-negativ.svg`, `-grau.svg`, `-anthrazit.svg`, `certvia-favicon.svg`, `certvia-lockup-positiv.svg`, `logo/README-Logo.md`, `Brand-Board.html` (Farb-/Typo-Referenz) sowie `gefim-dachmarke/` (GEFIM-Logo für den „ein Produkt von GEFIM"-Hinweis).
> Hinweis: Falls PNG-Rastergrößen (Favicon/Apple-Touch/PWA 32/180/192/512) benötigt werden, kann ich sie aus dem SVG erzeugen und beilegen — sag kurz Bescheid.
-29
View File
@@ -1,29 +0,0 @@
# Certvia — Logo-Dateien
Logo = **C-Monogramm + Haken** (Entwurf C) in GEFIM-Farben. Alle Marken (außer Lockup) sind quadratisch (viewBox 64×64), vektoriell und frei skalierbar.
## Dateien
| Datei | Verwendung |
|---|---|
| `certvia-mark-positiv.svg` | Standard auf hellem Grund (C Violett, Haken Magenta) |
| `certvia-mark-negativ.svg` | Auf farbigem/dunklem Grund (weiß) |
| `certvia-mark-grau.svg` | Graustufen |
| `certvia-mark-anthrazit.svg` | Einfarbig Anthrazit (Dokumente/Druck s/w) |
| `certvia-favicon.svg` | App-Icon/Favicon (Monogramm auf violettem Tile) |
| `certvia-lockup-positiv.svg` | Wortmarke horizontal (Zeichen + „Certvia") |
## Farben
- **Violett (C):** `#5d52a3`
- **Magenta (Haken & „via"):** `#812d80`
- **Anthrazit (Wortmarke „Cert"):** `#3b3b3a`
- Auf dunklem Grund/UI: C `#7d6fd6`, Haken `#d17bcf`.
## Konstruktion / Regeln
- „C" ist **offen nach rechts**; der **Haken** liegt in der Öffnung („cert").
- **Kein Verlauf** im Zeichen. „C" nicht schließen, nicht verzerren, Seitenverhältnis wahren.
- **Schutzraum:** mind. Höhe des Haken-Elements rundum frei.
- Wortmarke „Cert" (Poppins Light/300, Anthrazit) + „**via**" (Poppins Bold/700, Magenta).
- Hinweis: SVG-Lockup nutzt Poppins per `font-family` — beim finalen Export ggf. **Text in Pfade wandeln**, damit die Schrift überall korrekt erscheint.
## Schrift
- **Poppins** (Wortmarke/Headlines), **Open Sans** (Fließtext).
+109
View File
@@ -0,0 +1,109 @@
# Craftvia MVP – Abnahme-Matrix
> Stand: 2026-09-15 · Branch `feature/craftvia-mvp` @ `ef2f3f1` · Gate: tsc · lint · build · **65/65 Testskripte grün**; komplette Suite zusätzlich mit `RLS_ENFORCED=true` **65/65 grün**
> Quelle der Anforderungen: `docs/craftvia/SPEC-CRAFTVIA.md` §40 (Gesamt-Abnahme), §37 (User Stories), §44 (offene fachliche Entscheidungen).
> Status: **erfüllt** = umgesetzt und durch automatisierten Test belegt · **teilweise** = umgesetzt mit dokumentierter Lücke · **offen** = nicht umgesetzt.
> Nachweise verweisen auf Testskripte unter `scripts/` (Lauf: `npm run test`) und Lane-Berichte unter `docs/craftvia/lanes/`.
## 1. Gesamt-Abnahmekriterien (Spec §40)
| # | Kriterium | Status | Nachweis | Offene Punkte |
|---|---|---|---|---|
| 1 | Mehrere Mandanten mit sicher getrennter Datenhaltung | erfüllt | Tenant-Guard `src/server/db.ts` + RLS `enable_tenant_rls` für alle Fachtabellen; `test-tenant-isolation`, `test-rls-enforcement` (35+ Tabellen FORCE RLS), Mandantentrennungs-Fälle in allen Lane-Tests; `test-tenant-transaction` (Owner + RLS); **komplette Testsuite zusätzlich mit `RLS_ENFORCED=true` grün (52/52)** ; `test-e2e-*` Mandantentrennung über alle 35 Tenant-Modelle (direkt in Postgres + 67 Service-Aufrufe), `test-security-http` (Produktions-Build: fremde IDs in allen `/api/v1`- und Datei-Routen → 404) | RLS in Prod aktivieren (`RLS_ENFORCED=true`, Rolle `craftvia_app` LOGIN) – Betriebsschritt |
| 2 | Benutzer, Rollen und Montageteams verwaltbar | erfüllt | Fundament Nutzer/Rollen/Einladung (`test-invitation`, `test-tenant-users-authz`); Rollen tenant-admin/backoffice/team-lead/technician in `rbac.ts`; Teams L1 (`test-stammdaten-teams`) | |
| 3 | Auftragsbestätigungen als PDF importierbar | erfüllt | L3 `/imports`, `POST /api/v1/work-orders/import`; `test-import-flow`, `test-import-rules` | Live-Extraktion gegen Claude nur mit `ANTHROPIC_API_KEY` (`test-import-live`) |
| 4 | Kunden-, Objekt- und Auftragsdaten automatisiert extrahiert | teilweise | L3 Claude-Provider (PDF-/Bild-Block, JSON-Schema, Konfidenz je Feld), Plausibilitätsprüfung, Dubletten-/Objektkandidaten; Fake-Provider-Tests | Umgesetzt und mit Fake-Provider getestet; **Live-Extraktion mit Claude mangels `ANTHROPIC_API_KEY` nicht verifiziert** – an echten Pilotdokumenten validieren |
| 5 | Backoffice prüft und korrigiert erkannte Daten | erfüllt | Prüfmaske `/imports/[id]` (unsichere Felder markiert, Korrektur-Diff, keine Auftragsanlage ohne Bestätigung); `test-import-flow` K-Fälle | |
| 6 | Aufträge Teams zuweisbar | erfüllt | L2 `assignWorkOrder` + Event `work_order.assigned` → L6 Empfänger; `test-auftraege-core`, `test-benachrichtigungen-events` | |
| 7 | Monteure bearbeiten Aufträge auf Tablet/Smartphone | erfüllt | L4 `/m/**` (eigene Shell, Bottom-Nav, Touch ≥ 48 px); `test-einsatz-field`; authentifizierter Smoke `scripts/smoke-auth.ts` | `smoke-auth.ts` 94/94 Seiten-Prüfungen für alle Rollen + zweiten Mandanten; visuelle Prüfung 375/768/1024 px im Browser offen |
| 8 | Wesentliche Daten offline erfassbar | erfüllt | L7 Service Worker, IndexedDB-Outbox, `/m/offline`, `/m/sync`; `test-offline-core`, `test-offline-sync-e2e` (20 Ops → 1 Batch, Replay → duplicate) | Manueller DevTools-Offline-Test; Abschließen offline bewusst gesperrt (Pflichtprüfung server-seitig) |
| 9 | Arbeitszeiten, Tätigkeiten, Material, Fotos dokumentierbar | erfüllt | L4 Sessions/TimeEntry, Notizen, Material (Abweichungsgrund Pflicht), Fotos (Kompression, Pflichtfotos); `test-einsatz-field`, `test-einsatz-sync` | |
| 10 | Tagesberichte erzeugbar | erfüllt | L5 Tagesbericht (Content-Snapshot je Tag, Auftrag bleibt offen); `test-berichte-flow` | |
| 11 | Abschlussberichte inkl. Kundenunterschrift | erfüllt | L5 Abschlussbericht, Unterschrift inkl. Abwesend/Verweigert/Später mit Begründung; `test-berichte-flow` | |
| 12 | PDF-Arbeitsnachweis erstellt | erfüllt | L5 HTML→PDF (Playwright/Chromium im Worker), Checksumme, unveränderliche Version; `test-berichte-pdf` | Worker-Image `craftvia-worker` mit Chromium lokal gebaut und PDF erzeugt (L10b); Registry-Push/Prod-Deploy offen |
| 13 | Backoffice gibt abgeschlossene Aufträge zur Abrechnung frei | erfüllt | L2 `releaseForBilling` nur mit freigegebenem Abschlussbericht; `test-auftraege-status` | |
| 14 | Monteure legen selbstständig Notdiensteinsätze an | erfüllt | L8 `/m/emergency` (vorläufiger Kunde/Objekt in einer Transaktion, offline als Sync-Op); `test-notdienst-flow` | |
| 15 | Backoffice wird über Notdiensteinsätze informiert | erfüllt | L6 Pflichtmail + In-App bei `emergency.created/completed` (Format §19.4); `test-notdienst-flow`, `test-benachrichtigungen-events` | Echter SMTP-Versand in Prod konfigurieren |
| 16 | Frühere Einsätze und technische Dokumente pro Objekt abrufbar | erfüllt | L1 Objekt-Historie + Dokumente (Sichtbarkeit/Scope), L4 mobile Historie; `test-stammdaten-sites`, `test-stammdaten-documents` | |
| 17 | Relevante Änderungen revisionsfähig protokolliert | erfüllt | `writeAuditLog` (before/after, IP, User-Agent) in allen Mutationen, Audit-Viewer `/settings/audit`; `test-audit-mail-context`, `test-benachrichtigungen-inbox-audit` | Audit-Einträge in Transaktionen erst nach Commit (L10b); Tamper-Schutz/Log-Aggregation (Betrieb) offen |
## 2. User Stories (Spec §37)
| US | Titel | Status | Nachweis | Offene Punkte |
|---|---|---|---|---|
| US-001 | Mandant anlegen | erfüllt | Plattform-Konsole `/admin` (Fundament), `provisionTenant`; Isolationstests | |
| US-002 | Auftragsbestätigung importieren | teilweise | L3 (s. §40 #3–5); `test-import-flow`, `test-e2e-*` (Import → Abrechnung) | Live-Texterkennung/Extraktion mit Claude nicht verifiziert (kein API-Schlüssel) |
| US-003 | Bestehenden Kunden erkennen | erfüllt | L1 `findDuplicateCustomers` (keine Auto-Zusammenführung), L3 Kandidaten in Prüfmaske; `test-stammdaten-duplicates` | |
| US-004 | Auftrag einem Team zuweisen | erfüllt | L2 + L6 | |
| US-005 | Technische Unterlagen ansehen | erfüllt | L1/L4 Dokumente mit Sichtbarkeit (backoffice_only verborgen), freigegebene Historie; L7 Vorab-Download offline | |
| US-006 | Einsatz dokumentieren | erfüllt | L4 | |
| US-007 | Tagesbericht erstellen | erfüllt | L5 | |
| US-008 | Abschlussbericht erstellen | erfüllt | L5 (Pflichtangaben/-fotos geprüft, Status „Zur Prüfung") | |
| US-009 | Auftrag zur Abrechnung freigeben | erfüllt | L2 + L5 + L6 | |
| US-010 | Notdiensteinsatz anlegen | erfüllt | L8 + L6 | |
| US-011 | Objekt-Historie aufrufen | erfüllt | L1 `getSiteHistory` (offene Folgearbeiten hervorgehoben) | |
| US-012 | Offline arbeiten | erfüllt | L7 + L4 Sync-Server (Idempotenz, Konflikte nicht still überschrieben) | |
## 3. Soll-/Kann-Funktionen (Spec §36.2/§36.3)
| Funktion | Status | Nachweis / Anmerkung |
|---|---|---|
| Sprachnotizen + Transkription | erfüllt | L4 Aufnahme, L9 Whisper-kompatibler Provider (`test-lotse-transcription`); ohne `TRANSCRIPTION_API_KEY` Status `disabled` |
| KI-Berichtsentwurf (Lotse) | erfüllt | L9 Vorschlag in `content.lotse`, Prüfbestätigung serverseitig Pflicht, Datenminimierung (`test-lotse-draft`) |
| Konfigurierbare Vorlagen | erfüllt | L2 `/settings/order-types`, `/settings/checklists` |
| E-Mail-Versand von Berichten | erfüllt | L6 interne Benachrichtigungen; L11 „An Kunden senden“ auf `/reports/[id]` und `POST /api/v1/reports/{id}/send`: freigegebenes PDF als Anhang (Queue enthält nur Document-Referenz; Zustellung lädt Bytes mandantengebunden mit Prüfsummenprüfung), Recht `report:approve`, Audit, Dedupe je Adresse+Version; `test-report-customer-mail` (41 Prüfungen), realer Versand an Mailhog geprüft. Offen: kein erneutes Senden an dieselbe Adresse, ein Empfänger ohne CC, Sprache = Mandant |
| Berichtsversionierung | erfüllt | L5 `lineageId`/`version`, alte Version erst bei Freigabe der neuen `superseded` |
| MFA | erfüllt | Fundament TOTP + Passkeys, Mandanten-MFA-Pflicht |
| Kartenlink zur Einsatzadresse | erfüllt | L1 OpenStreetMap-Link, L4 Routenlink |
| Push-Benachrichtigungen | offen | nach MVP (Spec §20.2 „später") |
| Kalenderansicht | erfüllt (Backoffice) | L13 Menüpunkt „Planung“ → `/planning`: Standard „Heute + nächste 4 Werktage“ (Teams = Kolonnen als Zeilen, heutige Spalte mit Live-Status je Kolonne), Umschalter Heute (Stundenraster 6–20 Uhr, Kolonnen als parallele Spalten) · 5 Tage · Woche · Nächste Woche; Auslastung der Kolonnenkapazität je Tag, Konflikte (überbucht, Überschneidung, Person doppelt eingeplant, außerhalb der Arbeitstage) mit roter Kante + Icon + Text, Hinweis „Kolonne unvollständig“, Drag & Drop mit Bestätigung und Tastatur-Alternative, ungeplante Aufträge in der Seitenleiste; Dashboard-Kacheln „Planung heute“ und „Konflikte diese Woche“; Teamleiter lesend für eigene Kolonnen; `test-planung-core`. Mobile Kalenderansicht für Monteure weiterhin offen (§11.2 optional) |
| Live-Lage der Monteure | erfüllt (ohne GPS) | L13 `/planning/live`: ein Marker/Eintrag je Kolonne am Objekt des laufenden Auftrags, Mitglieder mit individuellem Status aus ihrer WorkSession, Monteure ohne Team einzeln; Karte (OpenStreetMap) + Liste, 30-s-Aktualisierung; keine Geräte-Koordinaten; `test-planung-live` |
| Verzugswarnungen / früher fertig | erfüllt (Hinweis) | L13: erfasste Kolonnenzeit (vereinigte Arbeitssegmente) ≥ 80 % → „Verzug droht“, ≥ 100 % → Meldung `planning.overrun` (erneut je +50 %), gefährdeter Folgeauftrag (Restzeit + geschätzte Fahrzeit bzw. Tageskapazität) → Markierung + `planning.followup_at_risk`, früher fertig → „früher fertig“ + Vorzieh-/Umkreis-Vorschläge + `planning.capacity_freed`; Meldungen nur In-App an Backoffice + Teamleiter, dedupliziert, aus dem 5-Minuten-Job `planning-watch`; nie automatische Umplanung; `test-planung-watch` |
| Einsatz-Empfehlungen | erfüllt (Vorschlag) | L13: Teams mit Auftrag in der Nähe (Luftlinie) und freier Kapazität, Begründung als Text, Übernehmen nur mit Bestätigung; nahe ungeplante Aufträge; Geocoding über Nominatim-Job mit Cache; `test-planung-recommend`, `test-planung-geocode`. Keine Routen-/Fahrzeitberechnung (Spec §35 „automatische Tourenoptimierung“ bleibt außerhalb) |
| Standorterfassung beim Einsatzstart | erfüllt | L4 optional beim Session-Start |
| Barcode/QR-Code | offen | Version 2 |
| Kundenspezifische Statusmodelle | offen | Statusmodell global, Labels gruppiert (Brandbook §12.3) |
| Digitale Einsatzfreigabe durch Teamleiter | erfüllt | L5 `report:approve_team` |
| Abrechnungsübersicht | erfüllt | L14 `/billing` (Tabs Offen · Abgerechnet · Storniert, Filter, Mehrfachdruck), Detail `/billing/[id]` mit Abrechnungsblatt (Auftragsdaten inkl. Abrechnungshinweisen des Kunden, freigegebene Zeiten je Person/Art mit Fahrzeit getrennt und Pausen nur informativ, Anzahl Anfahrten mit Datumsliste, Material verwendet/zusätzlich/nicht verwendet, Zusatzleistungen/Abweichungen/Restarbeiten, Berichts- und Unterschriftsreferenzen), PDF-Abrechnungsblatt (Worker, `backoffice_only`), „Abgerechnet“ mit optionaler Rechnungsnummer (Aufstellung eingefroren, Zeiten/Material nie doppelt abrechenbar), Stornierung mit Grund; Einträge je Auftragsabschluss, bestätigtem Meilenstein bzw. – ohne Meilensteine – freigegebenem Tagesbericht; Meilensteine im Auftragsdetail und mobil (offline `milestone.reach`); Rechte `billing:read`/`billing:write` (Backoffice, Mandantenadmin); `test-abrechnung-service`, `test-abrechnung-sync`. **Abgrenzung: keine Buchhaltung** – keine Preise, Beträge, Steuern, Rechnungserstellung, Zahlungen und kein Export in Buchhaltungsformate (Spec §35); die Rechnung stellt die Buchhaltung außerhalb von Craftvia |
## 4. Offene fachliche Entscheidungen (Spec §44) – getroffene MVP-Annahmen
| Thema | MVP-Annahme | Zu klären mit Pilotkunde |
|---|---|---|
| Berichtsvorlage | generische Vorlage L5 mit Mandantenlogo/-namen | Layout/Pflichtinhalte des Pilotkunden |
| Pflichtfelder je Auftragsart | über Checklisten-Vorlagen je Auftragsart konfigurierbar | konkrete Pflichtfelder |
| Fotoarten | Standard-Pflichtfotos §14.2 als Vorschläge | verbindliche Liste |
| Arbeitszeitkorrektur | **Entschieden (L12 Zeiterfassung, 2026-09-15):** Monteure tragen eigene Zeiten der letzten 7 Tage nach bzw. schlagen Korrekturen vor (`field:record_own_time`), Begründung Pflicht, Kennzeichen „manuell“; Freigabe durch Teamleiter (eigene Teams) oder Backoffice (`time:approve`), nie durch die erfassende Person selbst; bis zur Freigabe zählen Nachträge nicht für Bericht/Summen, Korrekturvorschläge lassen den alten Wert gelten; Abrechnungsfreigabe gesperrt, solange Zeiten offen sind. Direktkorrektur/Nachtrag für Teammitglieder weiterhin mit `field:correct_time` (sofort freigegeben). Grund, Audit before/after, Events `time.approval_requested/approved/rejected` | Freigabefrist/Eskalation, Export an Lohnabrechnung |
| Aufbewahrungsfristen | Soft Delete, keine automatische Löschung; KI-Protokoll: `AI_GENERATION_RETENTION_DAYS` (Default 180): täglicher Worker-Job leert Ein-/Ausgaben und Personenbezug im KI-Protokoll; monatliches Token-Kontingent je Mandant (`/settings/lotse`) | Fristen je Dokumentart |
| Empfänger Abrechnungsbenachrichtigung | `/settings/email` Abrechnungsempfänger je Mandant | Adressen |
| OCR-/KI-Anbieter | Claude (Anthropic) für Extraktion + Lotse, Whisper-kompatibel für Transkription (Entscheidung 2026-09-14) | Vertrag/AVV, Region |
| E-Mail-Dienst | SMTP (Mail-Worker, Queue) | Anbieter, SPF/DKIM/DMARC |
| Hostingstandort | Coolify/Docker (aus Certvia übernommen) | Rechenzentrum/Region |
| MFA-Regelung | optional je Nutzer, Mandanten-Pflicht einschaltbar | Pflicht für Admins? |
| Standortdaten | optional beim Einsatzstart/Foto, kein Dauertracking. **Live-Lage (L13) nutzt keine Geräte-Standorte:** angezeigt wird der Einsatzort (Objektadresse) des laufenden Auftrags, Status aus der Zeiterfassung; Start-Koordinaten der WorkSession werden weder gelesen noch ausgeliefert. Objektadressen werden serverseitig über OpenStreetMap Nominatim verortet (nur Adresse, keine Personendaten, max. 1 Anfrage/s, Ergebnis am Objekt gespeichert); Kartenkacheln lädt der Browser von `tile.openstreetmap.org` (IP-Adresse des Nutzers wird dabei an OSM übertragen) | Einwilligung/Betriebsvereinbarung für die Anzeige „wer arbeitet wo“; Datenschutzhinweis zur Kachel-/Geocoding-Nutzung bzw. eigener Karten-/Geocoding-Dienst (EU-Hosting) für Produktion |
| Maximale Offline-Dauer | `OFFLINE_MAX_DAYS=7` | Wert |
| Maximale Dateigrößen | Bild 15 MB, PDF 25 MB, Audio 20 MB | Werte |
| Buchhaltungssoftware | keine Schnittstelle (MVP-Abgrenzung §35) | Zielsystem für V2 |
| Spätere Schnittstellen | versionierte API `/api/v1` + OpenAPI | Prioritäten |
| Branding der PWA | Craftvia (Brandbook), Mandantenlogo in Berichten | Mandanten-Branding in App? |
| Berichtssprache | Deutsch (EN-Kataloge vorbereitet) | |
| Lösch-/Archivierungskonzept | DSGVO-Export/-Löschung aus Fundament, PII-Liste um Fachfelder erweitert | Löschfristen |
## 5. Bekannte Einschränkungen vor Produktivbetrieb
- **Login-Drosselung:** Konto-Lockout nach 5 Fehlversuchen vorhanden; zusätzlich Drosselung je IP und je Konto (Scope `login`, 20 Versuche / 15 min, `LOGIN_RATE_LIMIT_PER_15_MIN`; `test-login-rate-limit`). Zähler im Speicher je App-Instanz.
- **Polyglot-Uploads:** gültiger Bildkopf mit angehängtem Inhalt passiert die Typprüfung (Auslieferung als Download mit erkanntem Typ + `nosniff`, keine Ausführung); echte Inhaltsprüfung nur mit ClamAV (`CLAMAV_HOST`).
- **Rate-Limit** `/api/v1` je App-Instanz im Speicher (bei mehreren Instanzen Redis-Zähler nötig); API nur mit Session-Cookie (kein Token-Zugang für Integrationen).
- **Mobile Listen** ohne Paginierung (max. 200 bzw. 100 heute) – für MVP-Größen ausreichend (Performance-Messung 5 020 Aufträge: Dashboard/Liste 30–53 ms, mobil ~110 ms im Produktions-Build).
- **`emitEvent` in Transaktionen** feuert sofort (bei späterem Rollback evtl. Benachrichtigung ohne Datenänderung).
- **Unterschrift offline:** Sync-Op `signature.capture` nicht registriert (Unterschrift erfordert Verbindung).
- **Live-KI** (Claude-Extraktion, Lotse, Transkription) ohne API-Schlüssel nicht verifiziert.
- **Deployment:** Images lokal gebaut (App 400 MB, Worker 2,68 GB inkl. Chromium), kein Registry-Push, CI-Testjob nicht auf Server ausgeführt.
- **Demo-Seed:** Daten relativ zum Seed-Zeitpunkt.
## 6. Nicht verifiziert in dieser Umgebung
- Live-Aufrufe gegen Claude (Extraktion, Lotse) und Transkription – keine API-Schlüssel gesetzt.
- Visuelle Prüfung mit echter Passwort-Anmeldung im Browser (Agenten geben keine Passwörter ein; angemeldete Prüfungen per `scripts/smoke-auth.ts`).
- Manueller Offline-Test mit Browser-DevTools.
- Deployment auf Zielumgebung (nur lokaler Docker-Build).
+104
View File
@@ -0,0 +1,104 @@
# Craftvia API (`/api/v1`)
Versionierte JSON-API für Backoffice-Formulare, die Mobile-App/PWA (Offline-Sync) und künftige Integrationen. Die maschinenlesbare Spezifikation (OpenAPI 3.1) liefert `GET /api/v1/openapi.json` (gepflegt in `src/lib/api/openapi.ts`, `API_ROUTES` listet alle dokumentierten Pfade). Jede neue oder geänderte `src/app/api/v1/**/route.ts` muss dort nachgetragen werden.
## Authentifizierung und CSRF
- **Session-Cookie** von Auth.js: `authjs.session-token` (unter HTTPS `__Secure-authjs.session-token`). Ohne Cookie antwortet bereits der Proxy (`src/proxy.ts`) mit `401`.
- **Rechte** werden bei jedem Request aus der Datenbank gelesen (Mitgliedschaft, Identitätsstatus, Session-Kill-Switch, Passwortwechsel, effektive Rechte), nie aus dem JWT. Fehlt ein Recht oder ist das Modul des Mandanten deaktiviert, kommt `403`.
- **Sichtbarkeit:** Objekte eines fremden Mandanten oder außerhalb des eigenen Scopes (z. B. Monteur ↔ fremder Auftrag) liefern `404`, nicht `403`.
- **CSRF:** Schreibende Methoden (POST/PATCH) nur Same-Origin: Der `Origin`-Header muss zum Host passen, `Sec-Fetch-Site` muss `same-origin` oder `none` sein. Sonst `403 forbidden`.
## Fehlerformat
Alle Routen antworten im Fehlerfall mit `Cache-Control: no-store` und
```json
{ "error": { "code": "invalid", "message": "validation failed", "details": [{ "path": "customerId", "code": "too_small" }] } }
```
| Code | HTTP | Bedeutung / `details` |
|---|---|---|
| `unauthorized` | 401 | nicht angemeldet, Konto inaktiv, Sitzung invalidiert |
| `forbidden` | 403 | Recht fehlt, Modul deaktiviert, Passwortwechsel nötig, Cross-Site-Request |
| `not_found` | 404 | unbekannt, fremder Mandant oder außerhalb des Scopes |
| `conflict` | 409 | Versionskonflikt (`baseVersion`), Doppelbestätigung/unzulässiger Zustand, mögliche Dubletten (`details.reason = "possible_duplicates"`, `details.candidates`) |
| `invalid` | 422 | Validierung (Zod: `details = [{ path, code }]`), fehlerhaftes JSON/Multipart |
| `blocked` | 422 | fachlich gesperrt, z. B. `details = CompletionBlocker[]` |
| `payload_too_large` | 413 | Datei/Body zu groß |
| `rate_limited` | 429 | Header `Retry-After` (Sekunden), `details.retryAfterSeconds` |
| `internal` | 500 | unerwarteter Fehler, keine internen Details |
## Pagination
`GET /customers`, `GET /sites` und `GET /sites/{id}/history` verwenden `?page` (≥ 1) und `?pageSize` (1–100, Standard 25, bei der Historie 50). Antwort: `{ "data": [...], "pagination": { "page", "pageSize", "total" } }` (Historie zusätzlich `meta.onlyApproved`).
`GET /work-orders` hat ein eigenes Format: `{ items, total, page, pageSize, groupCounts }` (`groupCounts` = Anzahl je Statusgruppe ohne Status-/Gruppenfilter).
## Idempotenz und Konflikte (Sync)
- `POST /sync` nimmt `{ deviceId, operations[] }` mit 1–100 Operationen an (die PWA-Outbox schickt Batches ≤ 50). Jede Operation hat eine `clientOpId` (UUID) und wird einzeln angewendet. Die HTTP-Antwort ist `200`, das Ergebnis steht je Operation in `results[]`: `applied` | `duplicate` | `conflict` | `rejected` (mit `errorCode`, `message`, `idMap`, `entityVersion`).
- **Idempotenz:** Eine wiederholte `clientOpId` (je Mandant) liefert `duplicate` mit dem gespeicherten Ergebnis. Ist die ID bereits durch einen anderen Nutzer belegt, wird die Operation `rejected`.
- **Konflikte:** `work_order.transition` und `report.submit` verlangen `baseVersion`. Weicht sie von `WorkOrder.version` ab, lautet das Ergebnis `conflict`, `entityVersion` ist dann die aktuelle Version. Alle anderen Operationen sind additiv (Client-IDs in den Payloads, z. B. `clientId`, werden über `idMap` auf Server-IDs abgebildet).
- Den opType-Katalog mit den Payload-Schemas enthält `src/lib/sync/ops.ts` (Spec: Komponenten `SyncPayload*`).
- REST-Schreibrouten für Aufträge (`PATCH /work-orders/{id}`, `/assign`, `/transition`) akzeptieren optional `baseVersion` und antworten bei Abweichung mit `409`.
## Uploads
- `POST /uploads` (Einsatz): multipart mit `file`, `clientId` (UUID), `workOrderId`, `kind` (`photo` | `voice_note`) und optional `preview` (Thumbnail ≤ 2 MB). Maximal 25 MB, der Inhalt wird per Magic Bytes geprüft. Idempotent über `clientId`: dieselbe clientId liefert `200 { documentId, duplicate: true }`, ein neuer Upload `201 { documentId, duplicate: false }`. Die `documentId` wird danach in `photo.attach`/`voice.attach` referenziert.
- `POST /work-orders/{id}/documents`: multipart mit `file`, `category`, `visibility`, `title?`. Antwort `201`. Mit `Accept: text/html` kommt stattdessen ein `303`-Redirect (Backoffice-Formular).
- `POST /work-orders/import`: multipart mit `file` (PDF/JPEG/PNG, ≤ 25 MB), Antwort `201 { id, status }`. Die Extraktion läuft asynchron.
## Rate Limits
Die Zählung erfolgt je Nutzer in einem Fenster von einer Minute, im Speicher je App-Instanz (bei mehreren Instanzen also pro Instanz).
- Standard: `API_RATE_LIMIT_PER_MINUTE` (Default 300)
- Einsatz-Endpunkte `/sync`, `/uploads`, `/field/**`: `API_FIELD_RATE_LIMIT_PER_MINUTE` (Default 1200)
Bei Überschreitung kommt `429` mit `Retry-After`.
## Endpunkte
Die Pfade sind relativ zu `/api/v1`. „Recht“ nennt das Gate der Route. Mit „Service“ markierte Rechte prüft der Service (zusätzlich zum Scope).
| Methode | Pfad | Modul | Recht | Beschreibung |
|---|---|---|---|---|
| GET | `/customers` | customers | `customer:read` | Kunden suchen (`q`, `status`, paginiert) |
| POST | `/customers` | customers | `customer:write` | Kunde anlegen (409 bei möglichen Dubletten ohne `acknowledgeDuplicates`) |
| GET | `/customers/{id}` | customers | `customer:read` | Kunde inkl. Ansprechpartner |
| PATCH | `/customers/{id}` | customers | `customer:write` | Kunde ändern (fehlt = unverändert, `null` = leeren) |
| GET | `/sites` | sites | `site:read` | Standorte suchen (`q`, `customerId`, `status`, paginiert) |
| POST | `/sites` | sites | `site:write` | Standort anlegen |
| GET | `/sites/{id}/history` | sites | `site:read` | Einsatzhistorie (Außendienst: nur freigegebene Einsätze) |
| GET | `/work-orders` | work_orders | Scope (`work_order:read_all`/`read_team`) | Auftragsliste mit Filtern/Presets |
| POST | `/work-orders` | work_orders | Service: `work_order:write` (Notfall: `emergency:create`) | Auftrag anlegen |
| GET | `/work-orders/{id}` | work_orders | Scope | Detail + `availableTransitions` + `completionBlockers` |
| PATCH | `/work-orders/{id}` | work_orders | Service: `work_order:write` | Stammdaten ändern (`baseVersion`) |
| POST | `/work-orders/{id}/assign` | work_orders | `work_order:assign` | Team/Monteure zuweisen |
| POST | `/work-orders/{id}/transition` | work_orders | je Übergang (`requiredPermission`) | Statuswechsel (422 `blocked` mit Blockern) |
| GET | `/work-orders/{id}/materials` | work_orders | Scope | Material Soll/Ist |
| POST | `/work-orders/{id}/materials` | work_orders | `work_order:write` | Materialvorgabe hinzufügen |
| POST | `/work-orders/{id}/documents` | work_orders | `document:write` | Dokument hochladen (multipart) |
| POST | `/work-orders/{id}/daily-report` | reports | `report:write` | Tagesbericht-Entwurf anlegen/holen (201/200) |
| POST | `/work-orders/{id}/completion-report` | reports | `report:write` | Abschlussbericht-Entwurf anlegen/holen (422 bei Blockern) |
| POST | `/work-orders/import` | imports | `import:write` | Auftragsdokument importieren (multipart) |
| GET | `/imports/{id}` | imports | `import:write` | Importstatus, Extraktion, Kandidaten |
| POST | `/imports/{id}/confirm` | imports | `import:write`, `work_order:write` | Prüfformular bestätigen → Auftrag |
| POST | `/reports/{id}/approve` | reports | `report:read` + Service: `report:approve_team`/`report:approve` | Bericht freigeben |
| GET | `/reports/{id}/pdf` | reports | `report:read` | PDF des freigegebenen Berichts (`?download=1`) |
| GET | `/reports/{id}/files/{documentId}` | reports | `report:read` | Foto/Unterschrift/Logo aus dem Bericht |
| POST | `/sync` | field | Service je opType (`field:execute`, `emergency:create`, …) | Batch-Operationen (offline/online) |
| POST | `/uploads` | field | `field:execute` | Foto/Sprachnotiz hochladen → `documentId` |
| GET | `/field/bundle` | field | `field:execute` | Offline-Pull (`?since=<ISO>`, max. 200 Aufträge) |
| GET | `/field/documents/{id}` | field | Service: `document:read` + Sichtbarkeit/Scope | Dokument für die Mobile-App (`?variant=preview`) |
| GET | `/planning/board` | work_orders | Service: `work_order:read_all` oder Teamleiter (eigene Teams) | Plantafel: Aufträge je Team/Tag, Kapazität, Auslastung, Konflikte (`from`, `to` ≤ 42 Tage, `teamId`, `orderTypeId`, `priority`) |
| POST | `/planning/schedule` | work_orders | `work_order:assign` + Service: `work_order:write` | Einplanen (Team + Termin + Dauer, atomar, `baseVersion` Pflicht, 409 „Auftrag wurde zwischenzeitlich geändert“) |
| GET | `/planning/recommendations` | work_orders | `work_order:assign` | Einsatz-Empfehlungen (Luftlinie + freie Kapazität, Top 5) + nahe ungeplante Aufträge; nur Vorschläge |
| GET | `/planning/live` | work_orders | Service: `work_order:read_all` oder Teamleiter | Live-Lage: Status aus aktiver WorkSession, Standort = Objekt des Auftrags, keine Geräte-Koordinaten |
| GET | `/openapi.json` | – | angemeldet | OpenAPI-3.1-Dokument |
## Testphase: Nur-Lesen nach Ablauf (L15)
Für abgelaufene Testmandanten beantworten alle nicht lesenden Anfragen (POST/PUT/PATCH/DELETE, inkl.
`/sync` und `/uploads`) mit **422** `{ "error": { "code": "blocked", "message": "trial_expired", "details": { "readOnly": true, "message": "…", "deletionDueAt": "…" } } }`.
GET-Anfragen, Datei-Downloads und PDFs bleiben erlaubt. Siehe [TESTPHASE.md](TESTPHASE.md).
+159
View File
@@ -0,0 +1,159 @@
# Craftvia – Architektur & Team-Verträge (MVP)
> Verbindlich für alle Lanes. Fachliche Quelle: `docs/craftvia/SPEC-CRAFTVIA.md` (Spec) und `docs/craftvia/BRANDBOOK.md`. Technische Regeln: `AGENTS.md`.
> Bei Widerspruch gilt: AGENTS.md (Sicherheit) > dieses Dokument (Verträge) > Spec (Fachlichkeit).
## 1. Architekturentscheidungen
| Thema | Entscheidung | Abweichung von Spec |
|---|---|---|
| Stack | Next.js 16 App Router + Server Actions + Route Handlers, Prisma 7, Postgres 16 mit RLS, BullMQ/Redis, Garage (S3), Auth.js v5 | Spec §30 empfiehlt NestJS – bewusst nicht, einheitliche Codebasis mit Certvia-Fundament |
| API | Backoffice: Server Actions. Mobile/PWA + Integrationen: versionierte REST-Route-Handler unter `src/app/api/v1/**` mit denselben Service-Funktionen | – |
| Fachlogik | **Service-Schicht** `src/server/services/<modul>/*.ts`: reine Funktionen `(ctx, input) → result`, `ctx = { db: TenantDb, session, tenantId, userId }`. Server Actions und `/api/v1` sind dünne Adapter (Guard → Zod → Service → Audit/Revalidate). | – |
| Mandanten | `tenantId` auf jeder Fachtabelle, `dbForTenant` + RLS (`enable_tenant_rls`) | – |
| KI | Claude (Anthropic SDK) für Extraktion/Lotse; Whisper-kompatible Transkription; Provider-Interfaces | – |
| PDF | HTML-Template → PDF via Playwright/Chromium **im Worker** (nicht im App-Container) | – |
| Offline | Service Worker + IndexedDB-Outbox, Operation-basierter Sync `/api/v1/sync` mit Idempotenz-Keys und Versionsprüfung | – |
| Tests | tsx-Skripte `scripts/test-*.ts`, Runner `npm run test`; Gate `npm run gate` | Spec nennt Unit/E2E-Frameworks – MVP nutzt bestehenden Skript-Stil |
## 2. Rollen & Rechte
Siehe `src/server/rbac.ts` (vom Fundament angelegt). Rollen: `tenant-admin`, `backoffice`, `team-lead`, `technician`. Plattform-Admin = separater Store (kein Fachdatenzugriff).
**Sichtbarkeit von Aufträgen (Pflicht, serverseitig, zentral):** `src/server/services/work-orders/visibility.ts#workOrderScope(ctx): Prisma.WorkOrderWhereInput`
- `work_order:read_all` → alle (nicht gelöscht)
- sonst `work_order:read_team` → `assignedTeamId ∈ aktive Teams des Users` ODER User ist `WorkOrderAssignee` ODER `teamLeadUserId = userId` ODER (Notdienst) `createdById = userId`
- Kunden/Objekte für Monteure: nur lesbar, wenn über einen sichtbaren Auftrag erreichbar (`customerScope`, `siteScope` in derselben Datei).
Jede Query auf WorkOrder und abhängige Entitäten (Photos, Reports, Documents …) für Nicht-Backoffice-Rollen MUSS diesen Scope verwenden.
**Objekt-Historie für Feldrollen (Entscheidung, US-005/US-011):** Monteure/Teamleiter sehen an einem Objekt, das sie über einen eigenen sichtbaren Auftrag erreichen (`siteScope`), die FREIGEGEBENEN Einsätze aller Teams (Aufträge mit freigegebenem Bericht; `getSiteHistory` in `services/sites/history.ts`). Interne Hinweise sind nie Teil der Historie.
**Dokument-Sichtbarkeit:** `documentVisibilityFilter(ctx)`: `backoffice_only` nur mit `document:read_internal`; `team_lead` nur Teamleiter/Backoffice; `team`/`customer_report` für berechtigte Auftragsbeteiligte.
## 3. Statusmodell Auftrag
Quelle: `src/lib/work-orders/status.ts` (client-safe, reine Daten + Funktionen). Server prüft jeden Übergang über `assertTransition(from, to, ctx)`; kein direktes `status`-Update außerhalb von `services/work-orders/transition.ts#transitionWorkOrder`.
```
draft ─► review_required ─► planned ─► assigned ─► accepted ─► en_route ─► in_progress
│ ▲ │ │ │ ▲
└──────► assigned ────┘ └────────────┴──► in_progress
in_progress ⇄ paused, in_progress ⇄ waiting_material
in_progress ─► daily_report_created ─► in_progress (nächster Tag) | en_route
in_progress ─► technically_completed ─► signature_pending ─► in_review
technically_completed ─► in_review (Unterschrift erfasst/nicht erforderlich)
in_review ─► released_for_billing ─► billed
in_review ─► in_progress (Korrektur angefordert)
* (außer billed) ─► cancelled [work_order:cancel]
```
Rechte je Übergang:
| Übergang | Recht |
|---|---|
| draft/review_required → planned/assigned, Zuweisung | `work_order:write` / `work_order:assign` |
| assigned → accepted → en_route → in_progress, pause/resume, waiting_material, daily_report_created, technically_completed, signature_pending, → in_review | `field:execute` + Auftrag im Scope |
| in_review → in_progress (Korrektur) | `report:approve_team` oder `report:approve` |
| in_review → released_for_billing | `work_order:release_billing` (+ freigegebener Abschlussbericht) |
| released_for_billing → billed | `work_order:release_billing` |
| → cancelled | `work_order:cancel` |
**Guards vor Abschluss** (`technically_completed`): alle `required` Checklistenpunkte erledigt, alle `PhotoRequirement` mit ≥1 Foto, keine laufende `WorkSession`. Fehlende Punkte werden als strukturierte Liste zurückgegeben (`CompletionBlocker[]`), UI zeigt sie.
UI-Label-Gruppen (Brandbook §12.3) in `status.ts#STATUS_GROUP`:
Neu = draft, review_required · Geplant = planned, assigned, accepted · Unterwegs = en_route · In Arbeit = in_progress, paused, waiting_material, daily_report_created · Dokumentation unvollständig = technically_completed, signature_pending · Zur Prüfung = in_review · Bereit zur Abrechnung = released_for_billing · Abgerechnet = billed · Storniert = cancelled.
Jede Statusänderung: `WorkOrderStatusChange` + Audit + Event.
## 4. Querschnitts-Verträge (vom Architekten angelegt, Lanes nutzen sie)
### 4.1 Events → Benachrichtigungen
`src/lib/events.ts`: `EVENT_TYPES` (const) + Payload-Typen.
`src/server/events.ts#emitEvent(ctx, event)`: schreibt nach erfolgreicher Mutation. Lanes rufen **nur** `emitEvent` auf – nie direkt `Notification`/Mail. Die Lane „Benachrichtigungen" implementiert Empfängerauflösung, In-App-Notification, E-Mail-Queue.
Events: `work_order.assigned`, `work_order.changed`, `work_order.cancelled`, `work_order.started`, `work_order.daily_report_created`, `work_order.technically_completed`, `work_order.signature_missing`, `report.submitted`, `report.approved`, `report.rejected`, `work_order.released_for_billing`, `emergency.created`, `emergency.completed`, `work_order.missing_required`, `sync.failed`, `import.ready_for_review`, `import.failed`.
### 4.2 Nummernkreise
`src/server/services/numbering.ts#nextNumber(db, tenantId, key)` – atomar (`UPDATE … RETURNING` in Transaktion). Keys: `customer` (K-), `work_order` (A-), `emergency` (N-), `report` (B-).
### 4.3 Dateien
`src/server/services/documents/store.ts`:
- `storeFile(ctx, { bytes, fileName, declaredMime, category, visibility, links:{customerId?,siteId?,workOrderId?}, lineageId? }) → Document` – validiert (Allowlist, Größenlimit je Kategorie, Magic Bytes, Dateiname normalisiert), SHA-256, `storage.put`, Versionierung über `lineageId`.
- `getDownloadUrl(ctx, documentId)` → interne Route `/files/<documentId>` (Autorisierung über Document + Sichtbarkeit + Auftrags-Scope; keine öffentlichen Links). Die bestehende `files/[...key]`-Route wird auf `documentId` umgestellt.
- Limits: Bilder 15 MB (Client komprimiert vorher auf max. 2560 px / JPEG 0.82 + Thumbnail 400 px), PDF 25 MB, Audio 20 MB.
- Malware-Scan: Interface `FileScanner` (`src/server/services/documents/scanner.ts`), MVP-Implementierung = Magic-Byte/Typprüfung + Hook für ClamAV (`CLAMAV_HOST` optional).
### 4.4 Jobs (BullMQ)
Queue-Namen in `src/server/jobs/queues.ts`: `import-extraction`, `transcription`, `report-pdf`, `image-derivatives`, `notifications` (Mail nutzt bestehende Mail-Queue). Worker-Einstieg `scripts/craftvia-worker.ts` (`npm run worker:craftvia`), jede Lane registriert ihren Processor in `src/server/jobs/processors/<name>.ts` und in `processors/index.ts` (eine Zeile je Lane).
### 4.5 KI-Provider
`src/server/ai/providers.ts`:
```ts
interface DocumentExtractionProvider { name; model; extract(input: { bytes: Buffer; mimeType: string }): Promise<WorkOrderExtraction> }
interface TranscriptionProvider { name; model; transcribe(input: { bytes: Buffer; mimeType: string; language: "de" }): Promise<{ text: string }> }
interface LotseProvider { name; model; draftReport(input: ReportDraftInput): Promise<ReportDraftOutput> }
```
Konfiguration per Env (`AI_EXTRACTION_PROVIDER=anthropic`, `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`, `TRANSCRIPTION_PROVIDER=openai-compatible`, `TRANSCRIPTION_API_URL`, `TRANSCRIPTION_API_KEY`, `TRANSCRIPTION_MODEL=whisper-1`). Ohne Key: graceful degradation (Status `disabled`, manuelle Eingabe). Jede Nutzung → `AiGeneration`. Tests nutzen `FakeProvider`.
### 4.6 Offline-Sync
- Mobile Mutationen sind **Operationen**: `{ clientOpId: uuid, opType, entityType, entityId?, baseVersion?, payload, clientCreatedAt }`.
- `opType`-Katalog (`src/lib/sync/ops.ts`, Zod-Schemas): `session.start`, `session.pause`, `session.resume`, `session.end`, `work_order.transition`, `note.create`, `checklist.toggle`, `material.upsert`, `photo.attach` (nach Blob-Upload), `voice.attach`, `report.save_draft`, `report.submit`, `signature.capture`, `emergency.create`.
- Online und offline identischer Pfad: Client-Outbox → `POST /api/v1/sync` (Batch) → `services/sync/apply.ts` → dispatcht auf dieselben Services. Idempotent über `SyncOperation(tenantId, clientOpId)`.
- Binärdaten: `POST /api/v1/uploads` (multipart, `clientId`) liefert `documentId`; die Op referenziert `documentId`.
- Konflikte: `WorkOrder.version` ≠ `baseVersion` bei konfliktbehafteten Ops (`work_order.transition`, `report.submit`) → `status=conflict`, nichts überschrieben, Event `sync.failed`, Backoffice-Liste. Additive Ops (Notiz, Foto, Material, Zeiten) sind konfliktfrei.
- Pull: `GET /api/v1/field/bundle?since=` liefert Aufträge im Scope inkl. Kunde, Objekt, Kontakte, Checkliste, Material, Pflichtfotos, Dokument-Metadaten (Blobs werden vom SW gecacht), letzte freigegebene Berichte am Objekt.
### 4.7 Berichtsinhalt
`src/lib/reports/content.ts#ReportContent` (Zod): Snapshot aller Berichtsdaten (Spec §16.2/§17.2) – wird beim Erstellen/Freigeben aus DB gebaut (`services/reports/build-content.ts`) und ist nach `approved` unveränderlich. Änderungen nach Freigabe = neue Version (`lineageId`, `version+1`, alte → `superseded`).
### 4.8 Transaktionen, Header, Uploads (Fundament-Nachträge)
- **Mehrschritt-Schreibvorgänge nur über `inTransaction(ctx, fn)`** (`src/server/services/context.ts`, basiert auf `tenantTransaction` in `db.ts`). Direktes `ctx.db.$transaction(...)` ist bei `RLS_ENFORCED=true` nicht atomar.
- Datei-Routen, die im eigenen iframe angezeigt werden dürfen, stehen in `EMBEDDABLE_FILE_ROUTES` (`next.config.ts`, `frame-ancestors 'self'` / `SAMEORIGIN`); alles andere bleibt `DENY`.
- Request-Bodies über den Proxy: `experimental.proxyClientMaxBodySize = 26mb` (größter Upload 25 MB).
- Berichtsversionen (§4.7): Eine freigegebene Version wird erst `superseded`, wenn die NEUE Version freigegeben wird – so existiert immer ein gültiges freigegebenes PDF.
- Neue Personenreferenz-Felder (`…ById`, `userId`) in `src/server/dsgvo/pii-fields.ts` eintragen.
## 5. Routen
| Bereich | Route | Modul |
|---|---|---|
| Backoffice Dashboard | `/dashboard` | – |
| Aufträge | `/work-orders`, `/work-orders/[id]` | work_orders |
| Import | `/imports`, `/imports/[id]` (Prüfmaske) | imports |
| Kunden | `/customers`, `/customers/[id]` | customers |
| Objekte | `/sites`, `/sites/[id]` (inkl. Historie) | sites |
| Teams | `/teams` | teams |
| Berichte | `/reports`, `/reports/[id]` | reports |
| Dokumente | `/documents` | documents |
| Benachrichtigungen | `/notifications` | notifications |
| Einstellungen | `/settings/order-types`, `/settings/checklists`, `/settings/numbering`, `/settings/email` | – (settings:templates / tenant:manage) |
| Suche | `/search?q=` | – |
| Sync-Konflikte | `/work-orders/conflicts` | work_orders |
| Mobile (PWA) | `/m` (Heute), `/m/orders`, `/m/orders/[id]`, `/m/orders/[id]/{time,materials,photos,notes,checklist,report,sign}`, `/m/emergency`, `/m/sync`, `/m/profile` | field / emergency |
| API | `/api/v1/**` | je Modul |
Mobile-Layout `src/app/(app)/m/(field)/layout.tsx` (L4 darf `/m` in eine eigene Route-Group `src/app/(field)/m/**` verschieben, damit die Backoffice-Sidebar entfällt, und dafür die Zugriffsprüfungen aus `(app)/layout.tsx` in `src/server/app-access.ts` extrahieren – erlaubter Fundament-Eingriff): eigene Shell mit Bottom-Navigation (Heute · Aufträge · Notdienst · Sync · Profil), große Touch-Ziele (≥ 48 px), Offline-/Sync-Badge. Rollen `team-lead`/`technician` landen nach Login auf `/m`; Backoffice auf `/dashboard`.
## 6. Lanes & Datei-Ownership
Jede Lane arbeitet in eigenem Worktree/Branch `lane/<name>` von `feature/craftvia-mvp`, besitzt ihre Pfade exklusiv und fasst fremde Pfade nur über die in §4 genannten Einzeiler-Registrierungen an.
| Lane | Besitzt | Liefert |
|---|---|---|
| **L1 Stammdaten** | `services/{customers,sites,teams}`, `actions/{customers,sites,teams}`, `app/(app)/{customers,sites,teams}`, `components/{customers,sites,teams}`, `lib/customers/duplicates.ts`, `api/v1/{customers,sites,teams}`, messages `customers/sites/teams` | CRUD, Ansprechpartner, Dublettenprüfung + Merge (mit Bestätigung), Objekt-Detail inkl. Dokumente-Tab (nutzt §4.3) und **Objekt-Historie** (§8.3), Teams + Mitglieder, `customerScope/siteScope` |
| **L2 Aufträge** | `services/work-orders/**`, `lib/work-orders/**`, `actions/work_orders`, `app/(app)/work-orders`, `components/work-orders`, `api/v1/work-orders`, `app/(app)/settings/{order-types,checklists,numbering}`, `app/(app)/dashboard`, `app/(app)/search`, messages `workOrders/dashboard/search/settingsTemplates` | Auftrag CRUD, Statusmaschine + Guards, Zuweisung, Checklisten/Pflichtfotos/Materialvorgabe, Dokumente am Auftrag, Backoffice-Dashboard mit Filtern (§21), Abrechnungsfreigabe, Suche (§25), Konfliktliste |
| **L3 Import** | `services/imports/**`, `lib/imports/**`, `actions/imports`, `app/(app)/imports`, `components/imports`, `jobs/processors/import-extraction.ts`, `ai/extraction/**` | Upload, Validierung, Claude-Extraktion (PDF nativ + Bild), Konfidenzen, Prüfmaske, Dublettenkandidaten (nutzt `lib/customers/duplicates.ts` von L1 – Vertrag: `findDuplicateCustomers(db, candidate) → Candidate[]`, L3 nutzt Stub bis Merge), Bestätigung → Kunde/Objekt/Auftrag (über L2-Service `createWorkOrder`), Musterdokumente + Tests |
| **L4 Einsatz mobil** | `app/(app)/m/(field)/ bzw. nach Umzug app/(field)/m/**` (außer emergency, sync), `services/field/**`, `lib/sync/ops.ts`, `services/sync/**`, `api/v1/{sync,uploads,field}`, `components/field/**`, messages `field` | Mobile Shell, Heute/Aufträge, Auftragsdetail (Infos, Dokumente, Objekt-Historie read-only), Zeiten (Start/Pause/Stopp + Korrektur mit Grund), Checkliste, Material (bestätigen/abweichen/zusätzlich), Fotos (Kamera, Kompression, Kategorie/Phase/Kommentar), Notizen, Sprachnotiz-Aufnahme, Sync-API serverseitig |
| **L5 Berichte** | `services/reports/**`, `lib/reports/**`, `actions/reports`, `app/(app)/reports`, `components/reports/**` (inkl. `signature-pad.tsx`), `jobs/processors/report-pdf.ts`, `server/pdf/**`, `api/v1/reports`, messages `reports` | Tages-/Abschlussbericht (Content-Builder, Validierung Pflichtangaben), Unterschrift inkl. Ablehnungsgründe, PDF (HTML-Template, Mandantenlogo, Fotos, Unterschrift, Checksumme), Versionierung, Teamleiter-/Backoffice-Freigabe/Zurückweisung; mobile Seiten `/m/orders/[id]/{report,sign}` als Komponenten, die L4 einbindet |
| **L6 Benachrichtigungen & Audit** | `server/events.ts` (Implementierung), `services/notifications/**`, `app/(app)/notifications`, `components/notifications/**` (Glocke), `mail/templates` Craftvia-Events, `app/(app)/settings/email`, `app/(app)/settings/audit`, messages `notifications` | Empfängerregeln je Event, In-App + E-Mail, Mandanten-Mailkonfiguration (§33.2), Audit-Viewer mit Filter |
| **L7 Offline/PWA** *(Welle 2)* | `public/sw.js` bzw. `src/app/sw.ts`-Build, `lib/offline/**` (IndexedDB, Outbox, Blob-Queue), `app/(app)/m/(field)/ bzw. nach Umzug app/(field)/m/sync`, `components/offline/**` | Service Worker (App-Shell + Bundle-Cache + Dokument-Cache), Outbox mit Retry, Upload-Queue, Sync-Status/Fehler, Konfliktanzeige, Installierbarkeit |
| **L8 Notdienst** *(Welle 2)* | `app/(app)/m/(field)/ bzw. nach Umzug app/(field)/m/emergency`, `services/emergency/**`, `actions/emergency`, Backoffice-Review `app/(app)/work-orders/emergency-review` | Notdienst-Erfassung (bestehender/vorläufiger Kunde + Objekt), Start, Abschluss → Events; Backoffice: bestätigen/zuordnen/zusammenführen/abrechnen |
| **L9 Lotse** *(Welle 2)* | `services/lotse/**`, `ai/lotse/**`, `jobs/processors/transcription.ts`, `components/lotse/**`, messages `lotse` | Transkription, „Bericht mit Lotse vorbereiten" (Entwurf gekennzeichnet, editierbar), Vollständigkeitsprüfung „3 Angaben fehlen" |
| **L10 Stabilisierung** *(Welle 3)* | `scripts/test-e2e-*.ts`, `scripts/test-security-*.ts`, `prisma/seed.ts` Demo-Daten, Docs | E2E-Prozessskripte (§43.3), Sicherheitstests (§43.4), Demo-Seed, Deploy-Doku, OpenAPI (`/api/v1/openapi.json`) |
Gemeinsame Einzeiler-Registrierungen (Konflikte trivial auflösbar): `src/lib/nav.ts`, `src/server/jobs/processors/index.ts`, `scripts/check-module-guards.ts` (nur falls Top-Level-Actions), `src/server/audit` Entity-Labels.
## 7. Definition of Done je Lane
1. `npm run gate` grün (generate, tsc, lint, build inkl. Guard-Check, alle Tests).
2. Neue Tests: mind. Service-Logik + **Mandantentrennung** (Mandant B sieht/ändert nichts von A) + **Rollen** (Monteur außerhalb Scope → Fehler) je Lane.
3. Jede Mutation: Guard mit Permission, Zod, Audit (`before/after`), ggf. `emitEvent`.
4. Keine hartkodierten UI-Texte (messages/de/<ns>.json; en mindestens Schlüssel mit DE-Fallback-Text).
5. Responsive geprüft (Backoffice ≥ 1024 px und 768 px; Mobile 375 px).
6. Lane-Bericht `docs/craftvia/lanes/<lane>.md`: Umfang, Dateien, Tests, offene Punkte.
+68
View File
@@ -0,0 +1,68 @@
# Craftvia — Branding in der Anwendung
Maßgeblich ist das Brandbook: [BRANDBOOK.md](BRANDBOOK.md) (§11.4 Farben, §11.5 Typografie).
Referenzbild: [assets/craftvia-brand-identity-v1.png](assets/craftvia-brand-identity-v1.png).
Dieses Dokument beschreibt, **wo** die Marke im Code verankert ist.
## Quellen der Wahrheit
| Was | Datei |
|---|---|
| CSS-Tokens (Browser) | `src/app/globals.css` — `--brand-*` (Kernfarben), `--ui-*` (UI-Rollen), shadcn-Tokens abgeleitet |
| JS-Konstanten (Metadaten, Mail, Dokumente) | `src/lib/brand.ts` — `BRAND`, `BRAND_COLORS`, `STATUS_COLORS`, `BRAND_FONTS` |
| Logo (Inline-SVG) | `src/components/brand/craftvia-logo.tsx` |
| Mandanten-Logo-Override | `src/components/brand/tenant-brand.tsx` + `resolveTenantBranding()` |
| E-Mail-Layout | `src/lib/email-brand.ts` |
| Dokument-/Druck-CD | `src/lib/document-brand.ts`, `@media print` in `globals.css` |
| Favicon/PWA-Icons | `scripts/generate-brand-icons.ts` → `public/favicon/*`, `public/favicon.ico` |
| Manifest | `public/site.webmanifest` (`theme_color` #082E5B) |
## Farben
| Rolle | Name | Hex | Token |
|---|---|---|---|
| Primär | Lotsenblau | `#082E5B` | `--brand-lotsenblau` → `--primary` |
| Akzent/CTA | Signalorange | `#FF6A00` | `--brand-signalorange` → `--ui-accent` (`bg-cta`) |
| Text | Graphit | `#34383D` | `--foreground` |
| Fläche | Hafengrau | `#F3F5F7` | `--background` |
| Sekundärtext | Stahlgrau | `#66727D` | `--muted-foreground` |
| Linien | Zink | `#D9E0E5` | `--border`, `--input` |
| Erfolg / Warnung / Fehler / Info | | `#23845D` / `#C87912` / `#C64242` / `#2F6FA3` | `--ok` / `--warn` / `--risk` / `--info` |
Regeln: Signalorange sparsam (eine Hauptaktion je Ansicht, aktive Elemente). Farbe nie
als einziges Statussignal — immer mit Text oder Icon. Keine Verläufe, keine Schlagschatten
(`--shadow-card` ist eine feine Kontur); die Utility-Namen `bg-grad*` zeigen Vollfarbe.
## Logo
`<CraftviaLogo />` — Props:
- `variant`: `"horizontal"` (Signet + Wortmarke, Default) | `"signet"`
- `tone`: `"color"` (Default) | `"mono"` (einfarbig Lotsenblau) | `"reversed"` (weiß, für dunkle Flächen)
- `tile` (nur Signet): App-Icon-Kachel in Lotsenblau mit weißem C und orangem Haken
- `tagline`: „Handwerk. Digital auf Kurs." unter der Wortmarke
- `height`: Renderhöhe in px
Konstruktion (viewBox 64×64, exportiert als `SIGNET`): kräftiges C (Kreisbogen r=19,
Strichstärke 11) mit Öffnung rechts oben; oranger Haken startet links außerhalb, durchquert
das C (dort per Maske freigestellt) und endet als Pfeil nach rechts oben. Wortmarke
„Craft" Lotsenblau + „via" Signalorange, Gewicht 800.
Nach Änderungen an der Geometrie Icons neu erzeugen:
```bash
npx tsx scripts/generate-brand-icons.ts
```
## Typografie
Inter (Brandbook §11.5), selbst gehostet über `next/font/local`
(`src/app/fonts/inter-variable.ttf`, SIL Open Font License) — kein Laden aus dem Netz.
TODO: auf woff2-Subsets umstellen (Dateigröße).
## Produktidentität
- Name: **Craftvia**, Tagline: **Handwerk. Digital auf Kurs.**
- In-Product-Assistent: **Lotse** (eigene Figur, getrennt vom Logo; Modul `lotse`)
- TOTP-Issuer: `Craftvia · Plattform` (`src/server/mfa.ts`), WebAuthn-RP-Name: `Craftvia`
- Mail-Absender-Default: `MAIL_FROM_NAME` → `Craftvia`
+344
View File
@@ -0,0 +1,344 @@
# Craftvia – Betrieb & Deployment
> Maßgeblich für Test- und Produktivbetrieb. Abgeleitet aus den Fundament-Runbooks (archiviert
> unter `docs/_certvia-archiv/`) und auf Craftvia umgeschrieben. Domains in diesem Dokument sind
> Platzhalter (`app.craftvia.example`).
> Deploy-Dateien: `docker-compose.coolify.yml` (Build auf dem Host) bzw.
> `docker-compose.coolify.prebuilt.yml` (fertige Images aus der Registry).
> Env-Referenzen: `.env.coolify.example` (Testserver), `.env.prod.example` (Produktion),
> `.env.example` (lokal).
## 1. Architekturüberblick
```
Internet ──► Coolify-Proxy (Traefik, TLS) ──► app:3000
│
┌─────────────── Netz "backend" (internal: true, kein Egress) ─┼──────────────────────────┐
│ postgres (pgvector/pg16, RLS) redis (requirepass) garage (S3 :3900, Admin :3903) │
│ ▲ ▲ ▲ ▲ ▲ ▲ ▲ │
│ migrate app craftvia-worker worker backup-worker garage-provision │
└──────────────────────────────────────────────────────────────────────────────────────────┘
app, craftvia-worker, worker, backup-worker zusätzlich im Netz "default" (Egress/Proxy)
```
| Dienst | Dockerfile-Target / Image | Aufgabe | Lebensdauer |
|---|---|---|---|
| `migrate` | `migrate` / `craftvia-migrate` | `prisma migrate deploy` → Rollen-Rechte-Sync (`scripts/sync-role-permissions.ts`) → optional Demo-Seed (`RUN_DEMO_SEED`) bzw. Erst-Admin (`BOOTSTRAP_ADMIN`) | Init-Job, `restart: "no"` |
| `app` | `runner` / `craftvia-app` | Next.js standalone (Backoffice, PWA `/m`, REST `/api/v1/**`, Betreiber-Portal `/admin`) | dauerhaft, Healthcheck auf `/` |
| `craftvia-worker` | `worker` / `craftvia-worker` | BullMQ-Queues `import-extraction`, `transcription`, `report-pdf`, `image-derivatives` (`scripts/craftvia-worker.ts`); enthält Chromium + Schriften | dauerhaft |
| `worker` | `migrate` / `craftvia-migrate` | Mail-Worker (`scripts/mail-worker.ts`): Zustellung mit Retry/DLQ, täglicher Erinnerungslauf | dauerhaft |
| `backup-worker` | `migrate` / `craftvia-migrate` | Queue `backup-ops` (`scripts/backup-worker.ts`): Mandanten-Export/-Restore, DSGVO-Export, seriell | dauerhaft |
| `postgres` | `pgvector/pgvector:0.8.0-pg16` | Primärdatenbank, RLS-Policies `tenant_isolation` | Volume `pgdata` |
| `redis` | `redis:7.4.2-alpine` | Queues (BullMQ) | Volume `redisdata` |
| `garage` | `garage` / `craftvia-garage` | Objektspeicher (Dokumente, Fotos, PDFs, Backups), Config eingebacken aus `deploy/garage.toml` | Volumes `garage_meta`, `garage_data` |
| `garage-provision` | `migrate` / `craftvia-migrate` | Layout, Bucket, Access-Key, Rechte über die Admin-API (idempotent) | Init-Job |
Startreihenfolge: `postgres` (healthy) → `migrate`; `garage` (healthy) → `garage-provision`; danach
`app`, `craftvia-worker`, `worker`, `backup-worker`.
**Ohne laufende Worker** reiht die App bei gesetztem `REDIS_URL` Jobs nur ein: Import-Extraktion,
Transkription, Berichts-PDFs und Bild-Derivate bleiben dann liegen (`craftvia-worker`), Mails bleiben
`pending` (`worker`), Restore/Export bleiben `queued` (`backup-worker`). Ohne Redis laufen die
Craftvia-Processors inline in der App. Das ist nur für Dev/Demo gedacht: der PDF-Processor ist absichtlich nicht im
App-Bundle, Freigaben bleiben gültig, „PDF erzeugen" auf `/reports/[id]` stößt den Job erneut an.
**Härtung (alle Dienste):** `no-new-privileges`, `cap_drop: ALL` (gezielte `cap_add` nur für die
Entrypoints von postgres/redis), CPU-/RAM-Limits, gepinnte Image-Tags, non-root-User `app` (UID
1001) in allen Node-Images, Redis mit Passwort.
## 2. Domains, Proxy, TLS
- In Coolify beim Service **`app`** die Domain setzen, z. B. `https://app.craftvia.example:3000`
(Port 3000 = Container-Port, Schema `https`). Coolify/Traefik stellt das Let's-Encrypt-Zertifikat aus.
Kein Host-Port wird exponiert. `garage`, `postgres`, `redis` und die Worker erhalten **keine**
Domain.
- `AUTH_URL` = exakt die öffentliche App-URL (`https://app.craftvia.example`), `AUTH_TRUST_HOST=true`
(im Compose Default). `APP_BASE_URL` für absolute Links in Mails/PDFs (leer = `AUTH_URL`).
- Passkeys: `WEBAUTHN_ORIGIN`/`WEBAUTHN_RP_ID` nur setzen, wenn sie von `AUTH_URL` abweichen.
- Uploads bis 25 MB laufen über den Proxy (`experimental.proxyClientMaxBodySize = 26mb`). Vorgelagerte
Proxies dürfen kein niedrigeres Body-Limit haben.
- DNS: A/AAAA-Record `app.craftvia.example` → Server-IP; Firewall nur 80/443 + SSH.
## 3. Ersteinrichtung (Coolify)
1. **Ressource:** Git-Repo (Deploy-Key, nur lesend), Build Pack **Docker Compose**, Compose Location
`docker-compose.coolify.yml` (oder `…prebuilt.yml`, siehe §9).
2. **Environment-Variablen** aus `.env.prod.example` bzw. `.env.coolify.example` eintragen (§4). Secrets
**literal** setzen, nicht über `${…}` referenzieren (Coolify-Interpolation).
3. **Persistent Storage prüfen:** `pgdata`, `garage_meta`, `garage_data`, `redisdata`, `backups`.
4. **Erster Deploy Produktion:** `RUN_DEMO_SEED=false`, `BOOTSTRAP_ADMIN=true` + `BOOTSTRAP_ADMIN_*` +
`BOOTSTRAP_TENANT_*`. Der `migrate`-Job legt über `scripts/bootstrap-admin.ts` den ersten Mandanten mit Mandanten-Admin
(`/login`) **und** einen Plattform-Admin (`/platform/login`, MFA-Einrichtung beim ersten Login)
an. Das Skript ist idempotent und überschreibt kein Passwort. Log im `migrate`-Container prüfen, danach Passwort ändern und
`BOOTSTRAP_ADMIN=false`.
5. **Testserver:** `RUN_DEMO_SEED=true` legt die Demo-Mandanten an (Logins siehe `AGENTS.md`). Nie in Produktion.
6. **RLS scharfschalten** (§6), **Smoke** (§10).
## 4. Konfiguration & Secrets
### 4.1 Variablen
| Variable | Dienste | Pflicht | Bedeutung |
|---|---|---|---|
| `POSTGRES_USER/PASSWORD/DB`, `DATABASE_URL` | postgres, alle Node-Dienste | ja | Owner-Verbindung (Superuser/BYPASSRLS), Host `postgres` |
| `RLS_ENFORCED`, `RLS_DATABASE_URL` | app, craftvia-worker | Prod ja | scharfe RLS über Rolle `craftvia_app` (§6) |
| `REDIS_PASSWORD` | redis + alle Queue-Nutzer | ja | `REDIS_URL` wird im Compose daraus gebildet, nicht separat setzen |
| `S3_ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET/REGION` | app, craftvia-worker, backup-worker, garage-provision | ja | Garage: `http://garage:3900`, Key `GK`+24 Hex, Secret 64 Hex, Region `us-east-1`. Ohne S3 speichert der Adapter nur Metadaten (Stub), in Prod also unbrauchbar |
| `GARAGE_RPC_SECRET`, `GARAGE_ADMIN_TOKEN` | garage, garage-provision | ja | je `openssl rand -hex 32` |
| `AUTH_SECRET` | app, Worker | ja | ≥ 32 Zeichen, sonst Fail-Secure-Abbruch (`src/server/env.ts`) |
| `PASSWORD_PEPPER` | app, migrate, backup-worker | ja | 64 Hex, **nicht rotierbar** |
| `MFA_ENC_KEY` | app, craftvia-worker | empfohlen | TOTP-Secrets at-rest (leer = aus `AUTH_SECRET`), nach dem Setzen nicht ändern |
| `BACKUP_ENC_KEY` | backup-worker | empfohlen | AES-256-GCM der Backup-Artefakte (leer = `AUTH_SECRET`) |
| `AUTH_URL`, `APP_BASE_URL` | app, Worker | ja | öffentliche URL |
| `SMTP_HOST/PORT/SECURE/USER/PASSWORD/FROM`, `MAIL_FROM_NAME`, `MAIL_REPLY_TO` | app, worker, craftvia-worker | für Mailversand | ohne vollständige Konfiguration bleiben Mails `pending` mit Begründung |
| `AI_EXTRACTION_PROVIDER`, `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` | app, craftvia-worker | optional | §7 |
| `TRANSCRIPTION_PROVIDER/API_URL/API_KEY/MODEL` | app, craftvia-worker | optional | §7 |
| `AI_MONTHLY_TOKEN_LIMIT` | app, craftvia-worker | optional | §7.2, Default 0 = unbegrenzt |
| `AI_GENERATION_RETENTION_DAYS` | craftvia-worker | optional | §7.3, Default 180 |
| `API_RATE_LIMIT_PER_MINUTE`, `API_FIELD_RATE_LIMIT_PER_MINUTE` | app | optional | §8, Default 300 / 1200 |
| `OFFLINE_MAX_DAYS` | app | optional | Offline-Bundle gilt nach N Tagen als veraltet (1–365, Default 7) |
| `CLAMAV_HOST`, `CLAMAV_PORT` | app | optional | zusätzlicher Malware-Scan per clamd INSTREAM (sonst Allowlist + Magic Bytes) |
| `PDF_CHROMIUM_PATH` | craftvia-worker | – | im Image/Compose fest `/usr/bin/chromium` |
| `BACKUP_LOCAL_DIR`, `BACKUP_S3_BUCKET` | app, backup-worker, garage-provision | optional | §11 |
| `RUN_DEMO_SEED`, `BOOTSTRAP_ADMIN*`, `BOOTSTRAP_TENANT_*` | migrate | – | §3 |
### 4.2 Secrets-Register (Grundregeln)
Werte liegen ausschließlich im Passwortmanager (je Umgebung eigener Ordner) plus versiegelter
Offline-Kopie und werden nur als Coolify-Env verteilt, **nie** im Repo, Image oder Backup-Bucket.
Test, Staging und Prod haben unterschiedliche Werte.
| Secret | Rotierbar? | Folge einer Rotation |
|---|---|---|
| `AUTH_SECRET` | ja | alle Sessions ungültig, Nutzer loggen neu ein |
| `PASSWORD_PEPPER` | **nein** | erzwungener Passwort-Reset aller Konten |
| `MFA_ENC_KEY` | **nein** | alle Nutzer müssen MFA neu einrichten |
| `BACKUP_ENC_KEY` | bedingt | gilt nur für neue Artefakte, Altschlüssel bis Retention-Ende aufbewahren |
| `craftvia_app`-Passwort (`RLS_DATABASE_URL`) | ja | `ALTER ROLE … PASSWORD`, danach Env setzen und app + craftvia-worker neu starten |
| `POSTGRES_PASSWORD`, `REDIS_PASSWORD` | ja | koordiniert mit allen Diensten neu deployen |
| `GARAGE_*`, `S3_ACCESS_KEY/SECRET_KEY` | ja | neuen Key provisionieren (`garage-provision`), Env tauschen, alten Key entfernen |
| `ANTHROPIC_API_KEY`, `TRANSCRIPTION_API_KEY`, `SMTP_PASSWORD` | ja | Env tauschen, Dienste neu starten |
## 5. Migrationen
- Der `migrate`-Job führt bei **jedem** Deploy `npx prisma migrate deploy` aus. Das ist idempotent: nur neue
Migrationen werden angewandt. app und Worker starten erst nach erfolgreichem Abschluss
(`service_completed_successfully`). Der App-Container migriert nie selbst.
- Danach läuft `scripts/sync-role-permissions.ts`: additiv, zieht neu eingeführte Rechte für bestehende
Mandanten nach. Betroffene Nutzer sehen neue Rechte nach erneutem Login (JWT).
- Regeln für neue Migrationen (RLS für Tenant-Tabellen usw.): [MIGRATIONS.md](MIGRATIONS.md).
- Manuell (Coolify-Terminal des `migrate`-Containers oder `docker exec`):
`npx prisma migrate status` / `npx prisma migrate deploy`.
- **Vor** Migrationen mit Datenumbau: Cluster-Backup ziehen (§11.1).
## 6. Row Level Security aktivieren
Die Baseline-Migration legt die Rolle `craftvia_app` **NOLOGIN NOBYPASSRLS** an, vergibt die
Tabellenrechte und aktiviert je Tenant-Tabelle `ENABLE` + `FORCE ROW LEVEL SECURITY` mit Policy
`tenant_isolation` (`USING` + `WITH CHECK` auf `current_setting('app.tenant_id', true)`).
Mit `RLS_ENFORCED=true` verbinden sich app und craftvia-worker über `RLS_DATABASE_URL` als
`craftvia_app` und setzen `app.tenant_id` transaktionslokal (`src/server/db.ts`, `dbForTenant`,
`tenantTransaction`). Migrationen, Seed/Bootstrap, Login-Lookup, Mail- und Backup-Worker laufen weiter über die
Owner-`DATABASE_URL`.
1. Passwort für die App-Rolle setzen (einmalig je Umgebung, Wert in den Passwortmanager):
```bash
docker exec -it <postgres-container> psql -U craftvia -d craftvia \
-c "ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<STARKES_PASSWORT>';"
```
2. Env setzen:
`RLS_ENFORCED=true`,
`RLS_DATABASE_URL=postgresql://craftvia_app:<STARKES_PASSWORT>@postgres:5432/craftvia?schema=public`
3. Neu deployen. Fehlt `RLS_DATABASE_URL` bei aktivem Flag, bricht der Prozess beim Start ab
(fail secure).
4. Nachweis: `npx tsx scripts/test-rls-enforcement.ts`. Der Test prüft Owner-Sicht, Isolation A/B, 0 Zeilen
ohne Kontext, `WITH CHECK` und `dbForTenant` end-to-end. Er gehört auch zum CI-Gate.
**Warnungen:**
- Die Owner-Rolle in `DATABASE_URL` **muss** Superuser oder BYPASSRLS sein. Sonst sieht der Login keine
Nutzer, und Restore (`session_replication_role`) scheitert.
- Mehrschritt-Schreibvorgänge nur über `inTransaction(ctx, fn)`, direktes `ctx.db.$transaction` ist
unter `RLS_ENFORCED=true` nicht atomar (ARCHITEKTUR §4.8).
- Zeilen mit `tenant_id = NULL` (Plattform-Audit, Mail-Logs, Auth-Tokens) sind für `craftvia_app`
unsichtbar. Sie werden nur über den Owner-Client geschrieben.
## 7. Worker, Chromium und KI-Provider
### 7.1 craftvia-worker und Chromium
- Image-Stage `worker` (`Dockerfile`): `node:22.14.0-slim` + Debian-Pakete `chromium`, `fonts-dejavu-core`,
`fonts-liberation`; `node_modules` inkl. `tsx`, generierter Prisma-Client, `scripts/`, `src/`,
`messages/`, `prisma/`. Start `npx tsx scripts/craftvia-worker.ts`.
- PDF-Rendering (`src/server/pdf/render.ts`): `playwright-core` startet `PDF_CHROMIUM_PATH` mit
`--no-sandbox --disable-dev-shm-usage`. Der Container braucht deshalb keine zusätzlichen Capabilities.
`shm_size: 1gb` ist als Reserve für fotoreiche Berichte gesetzt. Seiten laden keine Netzressourcen, alle Assets sind als `data:`
eingebettet.
- Concurrency: `report-pdf` 2, übrige Queues 4 je Worker-Prozess. Horizontal skalieren = weitere
`craftvia-worker`-Replicas (BullMQ verteilt). RAM-Limit im Compose 1,5 GB.
- Diagnose im Container: `chromium --version`; Logs zeigen `[worker] listening on <queue>` bzw.
`[worker] <queue> job <id> failed: …`.
### 7.2 KI-Provider
| Zweck | Env | Verhalten ohne Konfiguration |
|---|---|---|
| Auftragsimport-Extraktion (PDF/Bild) | `AI_EXTRACTION_PROVIDER=anthropic`, `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` (leer = `claude-opus-5`) | Import bleibt manuell erfassbar |
| Lotse (Berichtsentwurf, Vollständigkeitsprüfung) | `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` | kein Entwurf, UI funktioniert weiter |
| Transkription von Sprachnotizen | `TRANSCRIPTION_PROVIDER=openai-compatible`, `TRANSCRIPTION_API_URL` (Default OpenAI `/v1/audio/transcriptions`), `TRANSCRIPTION_API_KEY`, `TRANSCRIPTION_MODEL` (Default `whisper-1`) | Status `disabled` |
- Jede KI-Nutzung wird in `AiGeneration` protokolliert (Art, Provider, Modell, Bezug, Tokens ein/aus,
auslösender Nutzer, Ein-/Ausgabe).
- **Kostenbremse:** `AI_MONTHLY_TOKEN_LIMIT` ist die Plattform-Vorgabe für Tokens (ein + aus) je Mandant je
Kalendermonat (UTC), `0` = unbegrenzt. Mandantenadministratoren können unter `/settings/lotse` einen
eigenen Wert setzen (`TenantSettings.aiMonthlyTokenLimit`; leer = Plattform-Vorgabe, `0` = unbegrenzt).
Ist das Kontingent aufgebraucht, lehnt der Lotse neue Entwürfe/Zusammenfassungen ab („Kontingent
aufgebraucht“), die Import-Extraktion fällt auf manuelle Erfassung zurück. Geprüft wird vor jedem
Aufruf – ein laufender Aufruf kann das Limit einmalig überschreiten. Transkription (Audio) liefert keine
Tokens und wird nicht gezählt.
- Datenschutz: Anbieter (Anthropic, Transkriptions-API) sind Auftragsverarbeiter, daher AVV und
Drittlandbewertung vor Aktivierung klären. Die Worker brauchen Egress (Netz `default`).
### 7.3 Aufbewahrung KI-Protokoll
`AI_GENERATION_RETENTION_DAYS` (Default 180): Der Job `ai-retention` (Queue gleichen Namens) läuft
täglich im `craftvia-worker` (BullMQ-Job-Scheduler `ai-retention-daily`, beim Worker-Start registriert,
idempotent auch bei mehreren Replikas). Er leert Ein- und Ausgaben (`input`/`output`) von
`AiGeneration`-Einträgen, die älter als die Frist sind, und entfernt den Personenbezug (`createdById`).
Metadaten (Art, Modell, Tokens, Zeitpunkt, Bezug) bleiben für Kosten- und Nachvollziehbarkeit erhalten;
je Mandant wird ein Audit-Eintrag `ai_generation_retention` geschrieben. Die Frist mit dem DSB abstimmen.
Die Variable muss im `craftvia-worker` gesetzt sein.
### 7.4 Testphase (L15)
Öffentliche Selbstanmeldung unter `/testen`, Plattform-Wizard unter `/admin/trial`. Variablen
`TRIAL_MAX_DAYS` (Default 30) und `TRIAL_CONTACT_EMAIL`. Der `craftvia-worker` legt beim Start den
täglichen Job-Scheduler `trial-lifecycle-daily` an (Erinnerungen, Ablauf, Löschung nach 30 Tagen) und
verarbeitet `tenant-export` (Datenexport). Details, Schreibsperre und manueller Lauf:
[TESTPHASE.md](TESTPHASE.md). Vor dem Go-live die Platzhaltertexte für Nutzungsbedingungen und
Datenschutz (`messages/*/trial.json` → `legal.*`) ersetzen.
## 8. Rate Limits
| Bereich | Env | Default | Zählung |
|---|---|---|---|
| REST-API `/api/v1/**` | `API_RATE_LIMIT_PER_MINUTE` | 300 | je Nutzer pro Minute |
| Einsatz/Sync: `/api/v1/sync`, `/api/v1/uploads`, `/api/v1/field/**` | `API_FIELD_RATE_LIMIT_PER_MINUTE` | 1200 | je Nutzer pro Minute |
| Passwort-Reset, Alt-Passwort-Prüfung, E-Mail-Änderung | fest (`src/server/rate-limit.ts`) | 5–10 je Fenster | je IP und je Konto |
Das Field-Limit ist höher, weil die PWA nach Offline-Phasen Outbox-Batches (≤ 50 Ops) und Fotos in
Schüben nachsendet. Wird es zu knapp gewählt, laufen die Clients in Retry/Backoff, und die Sync-Seite zeigt
Fehler. Limits je Prozess gelten pro App-Instanz. Bei mehreren Replicas multipliziert sich das
effektive Limit.
## 9. Prebuilt-Images (Registry)
Wenn der Host-Build in Coolify zu lange dauert, die Images auf einem Build-Host (amd64) bauen, in die
Registry pushen und in Coolify `docker-compose.coolify.prebuilt.yml` verwenden.
| Image | Target | Dienste |
|---|---|---|
| `${REGISTRY}/craftvia-app:${IMAGE_TAG}` | `runner` | app |
| `${REGISTRY}/craftvia-migrate:${IMAGE_TAG}` | `migrate` | migrate, worker, backup-worker, garage-provision |
| `${REGISTRY}/craftvia-worker:${IMAGE_TAG}` | `worker` | craftvia-worker |
| `${REGISTRY}/craftvia-garage:${IMAGE_TAG}` | `garage` | garage |
```bash
docker login <registry-host>
REGISTRY=registry.example.com/craftvia ALSO_MAIN=true ./scripts/build-and-push-images.sh
# worker-Image (bis das Skript es mitbaut):
docker build --platform linux/amd64 --target worker -t registry.example.com/craftvia/craftvia-worker:main .
docker push registry.example.com/craftvia/craftvia-worker:main
```
**Gotchas:** Coolify reicht `IMAGE_TAG` nicht zuverlässig in die Compose-Interpolation, daher immer auch
`:main` pushen. Coolify entfernt alte Container **vor** dem Pull: erst alle Images pushen, dann
Redeploy, sonst ist die Umgebung unten. Registry-Token mit Minimalrechten (`read/write:package`)
verwenden und nach Klartext-Nutzung widerrufen.
## 10. Smoke nach Deploy
1. `migrate` und `garage-provision` mit Exit 0 beendet. `app` ist healthy, `craftvia-worker`, `worker` und
`backup-worker` laufen, im Log steht `[worker] listening on report-pdf` usw.
2. `https://app.craftvia.example/login` lädt mit gültigem Zertifikat. `/sw.js` und `/site.webmanifest` sind ohne
Session erreichbar (PWA).
3. Login Backoffice → `/dashboard`, `/work-orders`, `/customers`, `/reports`. Login Monteur → `/m`.
4. Datei-Upload an einem Auftrag und Download über `/files/<documentId>` (prüft Garage + S3-Keys).
5. Bericht freigeben → PDF erscheint am Auftrag (prüft Queue, craftvia-worker, Chromium).
6. Optional: Import-PDF hochladen → Extraktion (bei gesetztem API-Key). Sprachnotiz → Transkription.
7. Test-Mail (z. B. Passwort-Reset) kommt an bzw. steht nachvollziehbar auf `pending`.
8. Plattform-Login `/platform/login` → `/admin`, `/admin/backup` zeigt das Backup-Ziel.
9. Bei aktiver RLS: Login + Auftragsliste funktionieren (sonst prüfen: `RLS_DATABASE_URL`, Rolle hat LOGIN).
Automatisierter HTTP-Smoke mit Session-Cookie (ohne Passworteingabe, braucht DB-Zugriff und
`AUTH_SECRET`): `BASE=https://app.craftvia.example npx tsx scripts/smoke-auth.ts` (z. B. im
`migrate`-Container oder von einem Admin-Host mit Tunnel zur DB).
## 11. Backup & Restore
Zwei Ebenen, **beide** sind nötig: DB **und** Objektspeicher.
### 11.1 Ebene A: Cluster (gesamte Datenbank + Volumes)
- **Postgres:** mindestens täglich `pg_dump -Fc` (Coolify Scheduled Task) in einen **separaten**,
verschlüsselten Speicher. Für PITR pgBackRest/wal-g mit WAL-Archiving und `repo-cipher-type=aes-256-cbc`.
Deckt auch die globalen Tabellen (Identity, Plattform-Admins, Kataloge) ab.
```bash
docker exec <postgres-container> pg_dump -U craftvia -d craftvia -Fc > craftvia-$(date +%F).dump
# Restore in leere DB (App + Worker gestoppt):
docker exec -i <postgres-container> pg_restore -U craftvia -d craftvia --clean --if-exists < craftvia-YYYY-MM-DD.dump
```
- **Garage:** `garage_meta` (Bucket-/Key-/Layout-Definitionen, **kritisch**) und `garage_data`
sichern, z. B. mit restic (eigenes Repo, eigenes Passwort) oder als Volume-Snapshot bei gestopptem
`garage`. Ohne `garage_meta` sind die Objektdaten nicht adressierbar.
- **Volume `backups`:** enthält lokale App-Backup-Artefakte (Ebene B), mitsichern.
- **Host-Encryption:** Daten-Volumes auf LUKS bzw. provider-verschlüsseltem Block-Storage. Das
Boot-Unlock-Verfahren dokumentieren.
- **Restore-Test** mindestens quartalsweise in eine Wegwerf-Umgebung, Ergebnis protokollieren.
### 11.2 Ebene B: Mandanten-Export/-Restore und DSGVO (Betreiber-Portal)
- Ziel der Artefakte in `/admin/backup` wählbar (Lokal = Volume `/app/.backups` oder S3). Die
Konfiguration liegt verschlüsselt in der DB. Präzedenz: DB-Config → `S3_*`/`BACKUP_LOCAL_DIR` → lokaler Default.
- Export, Restore und DSGVO-Export je Mandant unter `/admin/[id]` (Plattform-Full-Admin + MFA-Step-up), ausgeführt
vom `backup-worker`. Die Artefakte sind mit `BACKUP_ENC_KEY` (AES-256-GCM) verschlüsselt, der Restore arbeitet
nur innerhalb von `tenant_id` und betrifft keine anderen Mandanten. `TENANT_MODELS` in `src/server/db.ts` und
`src/server/backup/topology.ts` müssen jede Tenant-Tabelle enthalten.
### 11.3 Restore-Kohärenz (Vorbedingung)
`PASSWORD_PEPPER`, `MFA_ENC_KEY` und `BACKUP_ENC_KEY` stehen **nicht** im Backup. Ein Restore in eine
Umgebung mit anderen Werten macht Logins (Pepper), MFA (`MFA_ENC_KEY`) bzw. das Entschlüsseln
der Artefakte (`BACKUP_ENC_KEY`) unmöglich. Vor jedem Restore die Secrets der Quellumgebung
bereitstellen oder einen Passwort-/MFA-Reset einplanen. Cross-Environment-Restores (prod → staging)
sind nur so lauffähig.
## 12. Update & Rollback
**Update (Standard):**
1. CI grün (Gate-Job: migrate, seed, tsc, lint, build, Tests).
2. Migrationen der Release sichten. Bei destruktiven Änderungen vorher `pg_dump` (§11.1).
3. Coolify-Redeploy (bzw. Images pushen, dann Redeploy). `migrate` läuft vor app und Workern.
4. Smoke (§10). Neue Env-Variablen aus den `.env.*.example`-Dateien vorher eintragen.
**Rollback:**
- **Ohne Schemaänderung:** vorheriges Image-Tag als `:main` retaggen und pushen (Prebuilt) bzw. vorherigen Commit
deployen. Die Worker ziehen dasselbe Tag mit.
- **Mit Schemaänderung:** Prisma-Migrationen haben kein automatisches Down. Entweder Vorwärts-Fix
(neue Migration), oder App + Worker stoppen, DB aus dem Pre-Deploy-Dump wiederherstellen (§11.1),
dann den alten Stand deployen. `prisma migrate deploy` toleriert in der DB angewandte Migrationen,
die im alten Code fehlen.
- **Queues:** Beim Rollback können Jobs eines neueren Payload-Formats in Redis liegen. Vor dem Rollback
Worker-Logs prüfen, fehlgeschlagene Jobs nach dem Fix erneut anstoßen (z. B. „PDF erzeugen").
## 13. Go-Live-Checkliste
- [ ] Frische, starke Secrets je Umgebung, im Passwortmanager + versiegelte Offline-Kopie
- [ ] `RUN_DEMO_SEED=false`, Bootstrap-Admin-Passwort geändert, `BOOTSTRAP_ADMIN=false`
- [ ] HTTPS aktiv, `AUTH_URL`/`APP_BASE_URL` korrekt
- [ ] `RLS_ENFORCED=true` + `RLS_DATABASE_URL`, Smoke mit aktiver RLS bestanden
- [ ] `craftvia-worker` läuft, Test-PDF erzeugt
- [ ] SMTP mit SPF/DKIM/DMARC der Absenderdomain
- [ ] KI: AVV geklärt, `AI_MONTHLY_TOKEN_LIMIT` und `AI_GENERATION_RETENTION_DAYS` festgelegt
- [ ] Backups Ebene A (Postgres + `garage_meta`/`garage_data` + `backups`) eingerichtet, Restore-Test dokumentiert
- [ ] Monitoring: Uptime-Check auf die App-URL, Log-Aggregation, Alarm bei Worker-Neustarts
- [ ] Firewall (80/443/SSH), SSH-Key-Login, unattended-upgrades
+71
View File
@@ -0,0 +1,71 @@
# Craftvia — Migrationen & Row Level Security
## Ausgangslage
Die Certvia-Historie (67 Migrationen) wurde beim Rückbau zu **einer** Baseline gesquasht:
- `prisma/migrations/0001_baseline/migration.sql`
- DDL aus `npx prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script`
- angehängter RLS-Block (Rolle `craftvia_app`, GRANTs, Funktion `enable_tenant_rls`)
Bestehende Datenbanken aus der Certvia-Zeit sind **nicht** migrierbar — lokal neu aufsetzen:
```bash
docker compose exec postgres psql -U craftvia -d postgres -c 'DROP DATABASE IF EXISTS craftvia WITH (FORCE);' -c 'CREATE DATABASE craftvia;'
npx prisma migrate deploy
npx prisma db seed
```
## Mandantentrennung — drei Stellen, die immer zusammen gepflegt werden
Jede neue Tabelle mit `tenant_id` (z. B. `customers`, `work_orders`) braucht:
1. **Migration**: nach dem `CREATE TABLE` im selben Migrationsfile
```sql
SELECT enable_tenant_rls('customers');
```
Die Funktion ist idempotent und setzt `ENABLE ROW LEVEL SECURITY`, die Policy
`tenant_isolation` (`USING` **und** `WITH CHECK` auf
`tenant_id = current_setting('app.tenant_id', true)`), `FORCE ROW LEVEL SECURITY`
und die GRANTs für `craftvia_app`.
2. **`TENANT_MODELS` in `src/server/db.ts`** — aktiviert den Tenant-Guard
(`dbForTenant` filtert/injiziert `tenantId`, prüft Ownership bei Mutationen).
3. **`TENANT_MODELS` in `src/server/backup/topology.ts`** — Backup/Restore/DSGVO-Export.
Die Liste ist bewusst dupliziert; `scripts/test-backup-isolation.ts` bzw.
`scripts/test-rls-enforcement.ts` prüfen Synchronität und RLS-Abdeckung gegen die DB.
Zusätzlich: Felder, die eine `User.id` referenzieren (Zuweisung, Freigabe, Ersteller),
in `src/server/dsgvo/pii-fields.ts` eintragen.
Globale Tabellen (Kataloge ohne `tenant_id`, z. B. künftige Materialstammdaten des
Plattformbetreibers) bekommen **keine** Policy und stehen in keiner TENANT_MODELS-Liste.
## Nullable `tenant_id`
`audit_logs`, `mail_logs` und `auth_tokens` tragen eine nullable `tenant_id`
(Plattform-Ereignisse). Die Policy ist identisch; Zeilen mit `NULL` sind für
`craftvia_app` unsichtbar und werden ausschließlich über den Owner-Client geschrieben.
## Neue Migration anlegen
```bash
# Schema ändern, dann:
npx prisma migrate dev --name <kurzname> --create-only
# migration.sql prüfen und ggf. `SELECT enable_tenant_rls('<tabelle>');` anhängen
npx prisma migrate dev
```
## RLS scharf testen (lokal)
```bash
docker compose exec postgres psql -U craftvia -d craftvia \
-c "ALTER ROLE craftvia_app WITH LOGIN PASSWORD 'craftvia_app_local';"
npx tsx scripts/test-rls-enforcement.ts
```
Für den Betrieb mit scharfer RLS: `RLS_ENFORCED=true` und
`RLS_DATABASE_URL=postgresql://craftvia_app:<pw>@<host>:5432/craftvia?schema=public`.
+67
View File
@@ -0,0 +1,67 @@
# Craftvia – Testphase & Onboarding (Betrieb)
Stand: 2026-09-16 · Lane L15 · Lane-Bericht: [lanes/testphase.md](lanes/testphase.md)
Craftvia lässt sich als zeitlich begrenzte Testversion anbieten – per öffentlicher Selbstanmeldung
(`/testen`) oder durch den Plattform-Admin (`/admin/trial`).
## 1. Lebenszyklus
| Phase | Zustand | Was gilt |
|---|---|---|
| Anmeldung | `TrialSignup.status = pending` | Nichts provisioniert. Bestätigungslink 24 h gültig, einmal verwendbar. |
| Testphase | `Tenant.plan = TRIAL`, `now < trialEndsAt` | Voll nutzbar. Banner „Testphase endet in X Tagen“ ab 7 Tagen vor Ende. |
| Abgelaufen | `now ≥ trialEndsAt` | **Nur lesen + Export.** Login, Lesen, Datei-Downloads, PDFs, Export bleiben möglich. |
| Löschung | `now ≥ deletionDueAt` (Standard: Ende + 30 Tage) | Täglicher Job löscht den Mandanten (Offboarding aller Mandanten-Tabellen + Speicher `<tenantId>/`), Mandant `ARCHIVED`. |
| Umgewandelt | `plan = FULL`, `convertedAt` | Nie Nur-Lesen, nie automatische Löschung. |
`trialEndsAt` ist das **exklusive** Ende: Beginn des Folgetags des gewählten Datums in Europe/Berlin.
Die Sperre hängt nur an diesem Zeitpunkt, nicht am Job – sie greift sekundengenau.
Plattform-Aktionen (Mandantendetail, Karte „Testphase“, jeweils mit Bestätigung und Plattform-Audit):
Enddatum ändern/verlängern (auch nach Ablauf → sofort wieder schreibbar, Erinnerungen neu geplant),
in Vollversion umwandeln, Testphase sofort beenden, Löschung vormerken (Ende + 30 Tage, frühestens in
7 Tagen) bzw. abbrechen. Nur Plattform-**Voll**-Admins; Mandanten-Admins haben keinen Weg dorthin.
## 2. Konfiguration
| Variable | Default | Bedeutung |
|---|---|---|
| `TRIAL_MAX_DAYS` | `30` | Längste selbst gewählte Testphase (1–365). Vorbelegung im Wizard: heute + 14 (höchstens `TRIAL_MAX_DAYS`). Der Plattform-Admin darf bis 365 Tage setzen. |
| `TRIAL_CONTACT_EMAIL` | leer | Kontakt in Banner und Testphasen-Mails („Vollversion oder Verlängerung: …“). Leer = allgemeiner Hinweis. |
Weitere Voraussetzungen: `APP_BASE_URL` (Links in Mails), funktionierender Mailversand (Double-Opt-in),
S3/Garage (Export-Dateien), `REDIS_URL` + `craftvia-worker` (Jobs). Rate-Limits der öffentlichen
Endpunkte sind fest (je App-Instanz, siehe `src/server/rate-limit.ts`): Anmeldung 5/h je IP und je
E-Mail, Bestätigung 20/15 min je IP, Schrittprüfung 120/15 min je IP.
## 3. Jobs (`npm run worker:craftvia`)
| Queue | Auslöser | Inhalt |
|---|---|---|
| `trial-lifecycle` | Job-Scheduler `trial-lifecycle-daily` (alle 24 h, beim Worker-Start angelegt) | Erinnerungen 7/3/1 Tage vorher, Ablaufmail, Löschhinweis 7 Tage vor Löschung, Löschung, Aufräumen alter Anmeldungen (7 Tage nach Link-Ablauf). Idempotent: jede Mail wird vor dem Versand über eine bedingte Aktualisierung „beansprucht“. |
| `tenant-export` | „Export erstellen“ unter `/settings/export` | ZIP (CSV + JSON + Dateien) nach `<tenantId>/uploads/…`, Download 7 Tage über `/settings/export/<id>` (Sitzung + `tenant:manage`). Ohne Redis läuft der Export inline. |
Manueller Lauf (z. B. nach Ausfall des Workers), im App- oder Worker-Container:
```bash
node --import tsx -e "import('./src/server/services/trial/lifecycle.ts').then(m => m.runTrialLifecycle()).then(console.log)"
```
Im Worker werden Jobs, die im Namen von Nutzern schreiben (`import-extraction`, `transcription`), für
abgelaufene Testmandanten übersprungen. PDFs/Vorschaubilder bereits gespeicherter Daten laufen weiter.
## 4. Schreibsperre – wo sie greift
- Server Actions der Module: `moduleGuard` (`src/server/action-guard.ts`) → `ServiceError("blocked", "trial_expired")`.
- `/api/v1`: `requireApiContext` sperrt jede nicht lesende Anfrage, die über `withApi` läuft (Sync, Uploads, alle Mutationen) → HTTP 422 `{ error: { code: "blocked", message: "trial_expired", details: { readOnly: true, message, deletionDueAt } } }`. Die Offline-Outbox behandelt 422 beim Sync-Batch als vorübergehend und wiederholt später.
- Routen außerhalb von `withApi`: Backoffice-Upload `/documents/upload`, `POST /api/v1/work-orders/[id]/documents` (explizit).
- Einstellungen, Nutzer-/Rollenverwaltung, Lotse-Einstellungen (EXEMPT-Actions, explizit).
- Nicht gesperrt: Login, Konto (eigenes Passwort, MFA, Sprache), Mandantenwechsel, Lesen, `/files/<id>`, Export, Plattform-Aktionen.
## 5. Datenschutz
- Anmeldung speichert Passwort nur als Argon2id-Hash (mit Pepper), Link-Token nur als SHA-256, IP nur als HMAC. Der Passwort-Hash wird nach Bestätigung/Ablauf aus der Anmeldung entfernt; Anmeldungen werden 7 Tage nach Link-Ablauf gelöscht.
- Enumeration-Schutz: Für bereits registrierte Adressen gleiche Antwort, aber Hinweis-Mail statt Link.
- Nutzungsbedingungen und Datenschutzhinweise unter `/testen/nutzungsbedingungen` bzw. `/testen/datenschutz` sind **Platzhalter** (Texte in `messages/<locale>/trial.json` → `legal.*`) und vor dem Go-live vom Betreiber zu ersetzen.
- Löschung über das bestehende DSGVO-Offboarding (Löschnachweis `DeletionCertificate`), zusätzlich Objektspeicher-Präfix `<tenantId>/`.
+88
View File
@@ -0,0 +1,88 @@
# Lane L14 – Abrechnungsübersicht (`lane/abrechnung`)
Stand: 2026-09-15 · Basis `5ea23a1` (`feature/craftvia-mvp`, L1–L12 integriert) · Spec §10.3, §13, §16/§17, §35
Ziel: Abgeschlossene Aufträge und erreichte Meilensteine laufen in einer **Abrechnungsübersicht** auf, damit die Buchhaltung **außerhalb von Craftvia** die Rechnungen stellt. **Keine Buchhaltung:** keine Preise, Beträge, Steuern, Rechnungserstellung, Zahlungen, kein Export in Buchhaltungsformate (kein CSV).
## 1. Umfang
| Punkt | Umsetzung |
|---|---|
| Datenmodell | Migration `20260915150000_abrechnung_uebersicht`: Tabellen `work_order_milestones` (`WorkOrderMilestone`: Titel, Beschreibung, Reihenfolge, Status `open/reached/confirmed/billed`, gemeldet/bestätigt/abgelehnt von–am, Notiz, Ablehnungsgrund, `billingRecordId`, Soft Delete) und `billing_records` (`BillingRecord`: Art `order_completion/milestone/daily_report`, Quelle `milestoneId`/`reportId`, Zeitraum, Status `open/billed/voided`, `snapshot` Json, PDF-Dokument + Prüfsumme, Rechnungsnummer, abgerechnet/storniert von–am, Stornogrund). Beide mit `enable_tenant_rls`, in beiden `TENANT_MODELS`-Listen, Personenfelder in `pii-fields.ts`. **Partial-Unique-Indizes** (SQL): höchstens ein nicht stornierter Eintrag je Auftragsabschluss / Meilenstein / Tagesbericht. L12-Modelle: je **eine** nullable Spalte `billing_record_id` + Index an `time_entries` und `material_usages` (keine FK). |
| Entstehung (`services/billing/candidates.ts#syncBillingCandidates`) | Idempotent, `INSERT … ON CONFLICT DO NOTHING` (hält auch eine umgebende Transaktion intakt). **Event-Weg:** `emitEvent` ruft nach der Benachrichtigung einen Billing-Hook für `work_order.released_for_billing` und `report.approved` auf – Abrechnungsfreigabe (L2) und Berichtsfreigabe (L5) bleiben unverändert. Meilenstein-Bestätigung ruft den Sync in derselben Transaktion. `scripts/billing-backfill.ts` für Bestandsdaten (abgerechnete Aufträge bekommen bewusst keinen offenen Eintrag). |
| Zeitraum-Regeln | **Auftragsabschluss:** vom Ende des letzten abgerechneten Abschnitts (sonst erste dokumentierte Tätigkeit/Auftragsanlage) bis zur Freigabe; enthält **alles noch nicht Abgerechnete** des Auftrags. **Meilenstein:** gleicher Beginn bis **Bestätigungszeitpunkt**; enthält alle noch nicht abgerechneten Positionen, die vor der Bestätigung erfasst wurden. **Tagesbericht** (nur Aufträge **ohne** definierte Meilensteine): Kalendertag des Berichts in der Mandanten-Zeitzone; eine neue Berichtsversion ersetzt den Bericht eines noch offenen Eintrags. Positionen = Zeiteinträge (nach `startedAt`) und Materialerfassungen (nach `createdAt`) mit `billingRecordId = null`. |
| Aufstellung (`services/billing/statement.ts`) | Kopf: Mandant (Name/Adresse wie Bericht; Logo wie im Bericht noch ohne Dokument), Kunde (Nr., Name, Rechnungsadresse = Kundenadresse, **Abrechnungshinweise** `billingNotes`), Objekt, Auftrag (Nr., externe Nr., Angebotsnr., Titel, Auftragsart, Abrechnungsart, Team), Abschnitt (Art, Meilenstein-Titel/Berichtsdatum, Zeitraum, Einsatztage). **Zeiten** je Person: Arbeit, Fahrzeit (Anfahrt + Rückfahrt), Materialbeschaffung, Summe; Pausen und Unterbrechungen nur informativ, nicht summiert; Kennzeichen „manuell nachgetragen (freigegeben)“. Nur `approved` + beendete Einträge; offene Nachträge als Hinweis „x h offen, nicht enthalten“, laufende Einträge als Hinweis. **Anfahrten:** Anzahl + Datumsliste. **Material:** verwendet, zusätzlich (hervorgehoben), nicht verwendete Planpositionen (informativ), Abweichungsgründe. **Texte** Zusatzleistungen/Abweichungen/offene Restarbeiten aus freigegebenen Berichten des Abschnitts; **Berichtsreferenzen** (Nr./Version/Datum/Freigabe) mit Unterschrift. |
| Anfahrten-Regel | `lib/billing/statement.ts#countTrips`: je Kalendertag bilden Anfahrts-Segmente (`travel`), die sich zeitlich überschneiden **oder innerhalb von 15 min beginnen**, **eine** Fahrt (Kolonne fährt gemeinsam); eine spätere, getrennte Anfahrt am selben Tag zählt erneut. Rückfahrten zählen nicht als Anfahrt. Keine km. |
| Aktionen (`services/billing/records.ts`) | `markBilled` (`billing:write`, Transaktion): Aufstellung neu berechnen → offene Zeiten ohne `confirmPendingExcluded` → `blocked pending_time_entries`; **Snapshot eingefroren**, Rechnungsnummer optional, Positionen per bedingtem `updateMany` zugeordnet (Mengenabgleich → sonst `conflict positions_already_billed`), Meilenstein → `billed`, Auftragsabschluss → Auftrag `billed` über `applyTransition`; Audit; danach PDF-Job. `voidBilling` (Grund Pflicht, nur `billed`): `voided`, Positionen frei, Meilenstein zurück auf `confirmed`, **neuer offener Eintrag** derselben Quelle, Auftrag `billed → released_for_billing`. `updateInvoiceNumber` (nur `billed`, Audit), `requestBillingPdf`. |
| Statusmodell | `status.ts` **unverändert** (`billed` bleibt Endstatus in der Übergangstabelle). `transition.ts#applyTransition` hat die interne Option `billingVoid`: nur damit ist `billed → released_for_billing` erlaubt (Recht `work_order:release_billing`, Grund Pflicht, Guards der Freigabe, Statushistorie, Audit, Event). Nicht über `transitionSchema`/API/UI erreichbar. |
| PDF | Job `billing-pdf` (Queue in `queues.ts`, Processor-Einzeiler mit `turbopackIgnore`), `services/billing/pdf.ts#generateBillingPdf`: aus dem Snapshot, HTML (`server/pdf/templates/billing.tsx`, gleiche Komponente wie Detail/Druck) → `pdf/render.ts` → `storeFile` Kategorie **`other`**, Titel „Abrechnungsblatt A-… – Art“, Sichtbarkeit `backoffice_only`, am Auftrag/Kunde/Objekt; Prüfsumme am Eintrag; nie ersetzt. Kopf mit Mandant, Fuß mit Eintrag-ID, Erstellt-Datum, Seite x von y. |
| Meilensteine (`services/billing/milestones.ts`) | `createMilestone`/`updateMilestone`/`deleteMilestone` (Soft Delete)/`moveMilestone` (`work_order:write`, Auftrag nicht abgerechnet/storniert; ändern/löschen nur `open`), `markMilestoneReached` (`field:execute` + Auftrag im Scope **oder** `work_order:write`; idempotent), `confirmMilestone` / `rejectMilestone` (Grund Pflicht) mit `billing:write`. Events `milestone.reached` → Nutzer mit `billing:write` (In-App), `milestone.confirmed`/`milestone.rejected` → meldende Person (In-App, Grund im Text); Links Backoffice `/work-orders/[id]?tab=billing`, mobil `/m/orders/[id]`. |
| Rechte / Modul | `billing:read`, `billing:write` → Backoffice (über den bestehenden Rollenfilter) und Mandantenadmin; keine Feldrolle. Modul `billing` in `lib/modules.ts` (Layout `requireModule`, Actions `server/actions/billing/*` mit `moduleGuard("billing")`). `scripts/sync-role-permissions.ts` zieht Bestandsmandanten nach (lokal +8 Zuordnungen). |
| Sync | Op `milestone.reach` (Envelope, `ops.ts` passthrough, Registry `external-ops.ts` → `services/billing/sync-ops.ts`, Zod `lib/billing/schemas.ts#milestoneReachPayload`), additiv/konfliktfrei, idempotent über `clientOpId` und fachlich. |
| API | `GET /api/v1/billing`, `GET /api/v1/billing/{id}`, `POST /api/v1/billing/{id}/billed`, `POST /api/v1/billing/{id}/void`, `GET`/`POST /api/v1/work-orders/{id}/milestones`, `POST /api/v1/milestones/{id}/reach|confirm|reject` – `requireApiContext("billing", …)` + `withApi`; OpenAPI-Tag „Abrechnung“. |
## 2. Screens / Routen
| Route | Inhalt |
|---|---|
| `/billing` | Nav „Abrechnung“ (Modul `billing`, `billing:read`). Tabs **Offen** (Standard) · Abgerechnet · Storniert mit Zählern; Filter Zeitraum, Kunde, Team, Art, Suche Auftragsnr./Kunde; Tabelle Auftrag, Kunde/Objekt, Art + Abschnitt, Zeitraum, Arbeitszeit, Fahrzeit, Anfahrten, Materialpositionen, Warnbadge „x h offene Zeiten“ (Text + Icon), „Bereit seit“ (bzw. abgerechnet am + Rechnungsnr. / storniert am + Grund); Mehrfachauswahl → „Abrechnungsblätter drucken“; Paginierung. Ohne Recht: Hinweis. |
| `/billing/[id]` | Kopf mit Status/Art, Aktionen „Als abgerechnet markieren“ (Popup: Rechnungsnummer optional, bei offenen Zeiten Warnung + Checkbox), „PDF“ (sobald erzeugt, `/files/<id>`), „Druckansicht“, „Rechnungsnummer ändern“, „Stornieren“ (Popup, Grund Pflicht); Links Auftrag, Fotos, Berichte; Aufstellung als Abrechnungsblatt. |
| `/billing/print?ids=…` | Druckansicht (bis 50 Blätter, Seitenumbruch je Blatt, Navigation/Buttons per Print-CSS ausgeblendet), Button „Drucken“. Kein Export. |
| `/work-orders/[id]?tab=billing` | Tab „Meilensteine & Abrechnung“: Meilensteine anlegen, nach oben/unten, löschen (offen), Status (Text + Icon), gemeldet/bestätigt von–am, Notiz, letzter Ablehnungsgrund; Bestätigen/Ablehnen (Grund); Liste der Abrechnungseinträge mit Status und Link. |
| `/m/orders/[id]` | Abschnitt „Meilensteine“ (nur wenn vorhanden): Status-Badges, großer Button „Erreicht melden“ (optionale Notiz, 48 px), offline über die Outbox („wird übertragen“), Ablehnungsgrund, „wartet auf Bestätigung“. |
| `/dashboard` | Kachel „Bereit zur Abrechnung“ (offene Einträge, nur mit `billing:read`) → `/billing`. |
## 3. Dateien
**Neu**
- `prisma/migrations/20260915150000_abrechnung_uebersicht/migration.sql`
- `src/lib/billing/{schemas,statement}.ts`
- `src/server/services/billing/{candidates,statement,records,milestones,queries,pdf,events,sync-ops,common}.ts`
- `src/server/actions/billing/{billing,milestones}.ts`
- `src/server/jobs/processors/billing-pdf.ts`, `src/server/pdf/templates/billing.tsx`
- `src/app/(app)/billing/{layout,page}.tsx`, `src/app/(app)/billing/[id]/page.tsx`, `src/app/(app)/billing/print/page.tsx`
- `src/app/api/v1/billing/route.ts`, `billing/[id]/route.ts`, `billing/[id]/billed/route.ts`, `billing/[id]/void/route.ts`, `work-orders/[id]/milestones/route.ts`, `milestones/[id]/{reach,confirm,reject}/route.ts`
- `src/components/billing/{statement-document,work-order-billing-tab,action-form,print-button,ui}.tsx`, `src/components/field/{milestones,milestone-reach-button}.tsx`
- `messages/{de,en}/billing.json`
- `scripts/test-abrechnung-service.ts`, `scripts/test-abrechnung-sync.ts`, `scripts/billing-backfill.ts`
**Eingriffe außerhalb von `billing/` (alle additiv, klein)**
- `prisma/schema.prisma` (2 Enums-Blöcke + 2 Modelle am Ende, je 1 Spalte + Index an `TimeEntry`/`MaterialUsage`, 2 Relationslisten am Ende von `WorkOrder` – `plannedDurationMinutes`/L13-Felder unberührt)
- `src/server/db.ts`, `src/server/backup/topology.ts` (TENANT_MODELS), `src/server/dsgvo/pii-fields.ts`, `src/server/rbac.ts` (2 Rechte), `src/lib/modules.ts` (1 Modul), `src/lib/nav.ts` (1 Eintrag), `src/components/audit-trail.tsx` (2 Labels)
- `src/lib/events.ts` (3 Events, entityType `milestone`), `src/server/events.ts` (Billing-Hook nach `handleEvent`, eigener try/catch), `src/server/services/notifications/recipients.ts` (1 Case), `handle-event.ts` (Link `milestone`)
- `src/server/services/work-orders/transition.ts` (Option `billingVoid`, s. o.), `src/server/services/work-orders/dashboard.ts` (Zähler `billingReady`), `src/app/(app)/dashboard/page.tsx` (Kachel), `src/app/(app)/work-orders/[id]/page.tsx` (Tab `billing`)
- `src/app/(field)/m/(core)/orders/[id]/page.tsx` (1 Import + 1 Zeile `MilestonesSection`)
- `src/lib/sync/envelope.ts` (Op-Typ), `src/lib/sync/ops.ts` (passthrough), `src/server/services/sync/external-ops.ts` (Registry), `src/server/jobs/queues.ts` (Queue `billing-pdf`), `src/server/jobs/processors/index.ts` (1 Zeile)
- `src/lib/api/openapi.ts` (Tag + 8 Pfade)
- `messages/{de,en}/{nav,modules,dashboard,workOrders,notifications}.json` (additive Schlüssel)
- Tests: `scripts/test-e2e-tenant-isolation.ts` (Fixture-Zeilen für die 2 neuen Tenant-Modelle), `scripts/test-security-roles.ts` (Proben `billing:read`/`billing:write`), `scripts/smoke-auth.ts` (Abrechnung, Tab, mobil, Mandant 2)
- `scripts/lib/demo-seed.ts`: **Seed-Fix** – auf frischer DB brach `prisma db seed` seit L12 ab (`other_session_running`: DEMO-07/DEMO-06 hatten laufende Uhren, spätere vergangene Einsätze derselben Monteure kollidierten). DEMO-06/DEMO-07 werden jetzt nach DEMO-19 angelegt (Inhalt unverändert).
Keine neuen npm-Abhängigkeiten, keine Stubs.
## 4. Tests
- `scripts/test-abrechnung-service.ts` – **118 Prüfungen**: Kandidaten je Art (Auftragsabschluss idempotent, über das echte `releaseForBilling`-Event, Partial-Unique-Index, Tagesbericht über `report.approved` ohne Doppel, Zeitraum = Kalendertag, neue Berichtsversion ersetzt, Abschlussbericht erzeugt keinen Abschnitt, Auftrag mit Meilensteinen ohne Tagesbericht-Abschnitt); Anfahrten-Zählung (Kolonne 2 Personen gleichzeitig = 1, zwei Tage = 2, getrennt am selben Tag = 2); Aufstellung (Arbeit/Fahrzeit/Materialbeschaffung je Person, Pausen nicht summiert, manuell-Kennzeichen, Summen, offener Nachtrag nur als Hinweis, abgelehnte/laufende nicht enthalten, Material verwendet/zusätzlich/nicht verwendet, Abrechnungshinweise, Nummern, Berichtstexte, keine Preis-/Betragsfelder); markBilled (Teamleiter/Monteur forbidden, offene Zeiten blocked, Rechnungsnummer, PDF-Job, Auftrag → billed, Positionen zugeordnet, Audit, doppelt → conflict, **Snapshot eingefroren** trotz späterer Änderung, **zweiter Abschnitt enthält abgerechnete Positionen nicht**, Rechnungsnummer ändern); voidBilling (Grund Pflicht, Rechte, nur billed, Positionen frei, neuer offener Eintrag, Auftrag zurück + Statushistorie, genau ein offener Eintrag, zweites Storno conflict); Meilenstein-Flow (Anlegen nur Backoffice, Sortieren, Monteur ohne Scope not_found, melden + Event an Backoffice, idempotent, gemeldet nicht änder-/löschbar, Monteur/Teamleiter können nicht bestätigen, Ablehnen mit Grund + Event/Link an Meldenden, Bestätigen → Eintrag + Event, Zeiten nach Bestätigung nicht enthalten, abgerechnet → Meilenstein billed, nächster Meilenstein ohne abgerechnete Zeiten, Soft Delete, Sichtbarkeit mobil); Liste/Filter/Sortierung/Summen, Detail, `billing:read` für Monteur/Teamleiter forbidden, Dashboard-Kachel; **Mandantentrennung** (Liste, Detail, Aufstellung, abrechnen, stornieren, Rechnungsnummer, Meilenstein anlegen/melden/bestätigen, PDF, Kandidaten-Sync); **PDF-Render-Smoke** (Dokument `other`/`backoffice_only`/Titel, Prüfsumme, Bytes `%PDF`, nicht ersetzt; Skip ohne Chromium – lokal über Google Chrome gerendert).
- `scripts/test-abrechnung-sync.ts` – **21 Prüfungen**: Registry/Envelope, Payload-Schema, `milestone.reach` applied + Event, gleiche clientOpId → duplicate, neue clientOpId für gemeldeten Meilenstein → applied ohne Änderung, ungültige Payload → rejected invalid, Meilenstein eines anderen Auftrags → not_found, Monteur ohne Zuweisung / Mandant B → not_found ohne Wirkung, Teamleiter des Teams darf melden; alle 8 API-Operationen dokumentiert und als Route vorhanden.
- Angepasst: `test-e2e-tenant-isolation.ts` (163 ✓, inkl. RLS je neuer Tabelle), `test-security-roles.ts` (129 ✓, Matrix-Abdeckung der neuen Rechte).
**Gate** `npm run gate` (Vordergrund): prisma generate, tsc, lint (0 Fehler, 3 bestehende Warnungen außerhalb L14), build inkl. Modul-Guard-Check (35 Action-Dateien), **69/69 Testskripte grün**.
**RLS-Lauf** `RLS_ENFORCED=true npm run test` (`RLS_DATABASE_URL` auf `craftvia_abrechnung`): **69/69 grün**.
**HTTP-Smoke** (`scripts/smoke-auth.ts`, Dev-Server :3115, frischer Demo-Seed, `sync-role-permissions`, `billing-backfill`): 120 Prüfungen, alle grün (eine Monteur-Prüfung war zunächst falsch formuliert – Seitentexte stehen über die serialisierten Messages immer im HTML – und wurde auf den datengebundenen Eintrag-Link umgestellt, Monteur-Lauf danach 31/31). Neu: Backoffice `/billing`, `?status=billed`, Suche, `/billing/[id]` (Abrechnungsblatt, Anfahrten, Aktion), `/billing/print`, Dashboard-Kachel, Auftrags-Tab mit Meilenstein; Monteur mobil „Meilensteine / Erreicht melden“, `/billing` ohne Recht (Hinweis, kein Eintrag), Detail 404; Mandant 2: Liste ohne Demo-Einträge, Detail 404, Druckansicht leer, Auftrags-Tab 404. Das Abrechnungsblatt wurde zusätzlich als PDF/PNG gerendert und visuell geprüft. Browser-Prüfung mit Anmeldung nicht durchgeführt (keine Passwort-/Token-Eingabe durch den Agenten).
## 5. Bekannte Lücken / Hinweise
1. **Bestehender Button „Abgerechnet“ (L2)** im Auftragsdetail (`released_for_billing → billed`) bleibt erhalten; wird er statt der Abrechnungsübersicht benutzt, bleibt der offene Auftragsabschluss-Eintrag stehen (kein Snapshot, keine Positionszuordnung). Empfehlung: Button ausblenden, sobald das Modul `billing` aktiv ist (L2-Datei, hier nicht geändert).
2. **Events in Transaktionen:** Der Billing-Hook läuft wie `handleEvent` im Kontext des Aufrufers (bei `inTransaction` innerhalb der Transaktion); Anlage per `ON CONFLICT DO NOTHING`, Fehler werden geloggt. Ein unerwarteter SQL-Fehler im Hook würde eine umgebende Postgres-Transaktion dennoch abbrechen.
3. **Partial-Unique-Indizes** stehen nur in der Migration (Prisma kennt sie nicht); ein späteres `prisma migrate diff` zeigt sie nicht an – bei Schema-Squash erhalten.
4. **Rechnungsnummer nachträglich:** Spalte + Anzeige aktualisiert; ein bereits erzeugtes PDF bleibt unverändert (unveränderliches Dokument).
5. **Recht für Auftragsstatus:** `markBilled`/`voidBilling` eines Auftragsabschlusses ändern den Auftragsstatus und brauchen zusätzlich `work_order:release_billing` (Standardrollen mit `billing:write` haben es; eigene Rollen ohne das Recht erhalten `forbidden`, nichts wird gespeichert).
6. **Sync-Op `milestone.reach`** prüft das Modul `billing` nicht (läuft über `/api/v1/sync`, Modul `field`); die mobile Anzeige erscheint nur, wenn Meilensteine existieren.
7. **Zeitpunkt der Positionen:** Material wird nach Erfassungszeitpunkt (`createdAt`), Zeiten nach Beginn (`startedAt`) einem Tages-/Meilenstein-Abschnitt zugeordnet; Nachträge nach der Bestätigung eines Meilensteins fallen in den nächsten Abschnitt bzw. den Auftragsabschluss.
8. **Mandantenlogo** im Blatt wie im Bericht noch nicht (kein Logo-Dokument).
9. **Umgebung:** Demo-Seed auf frischer DB brauchte den Seed-Fix (s. §3). `docs/craftvia/API.md` nicht ergänzt (OpenAPI ist maßgeblich).
## 6. Migration / Deploy
`20260915150000_abrechnung_uebersicht` – additiv (3 Enums, 2 Tabellen mit RLS, 2 nullable Spalten + Indizes, 3 Partial-Unique-Indizes, 2 FKs mit `ON DELETE CASCADE` auf `work_orders`). Deploy: `prisma migrate deploy` → `npx tsx scripts/sync-role-permissions.ts` (Nutzer neu anmelden) → `npx tsx scripts/billing-backfill.ts` → Worker neu starten (Queue `billing-pdf`).
+115
View File
@@ -0,0 +1,115 @@
# Lane L2 – Aufträge & Backoffice
Branch `lane/auftraege` (Basis `feature/craftvia-mvp` @ bf44567). Keine Schemaänderung, **keine neue Migration**, keine neuen npm-Abhängigkeiten.
## 1. Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §10.1 Auftragsdaten | Anlage/Bearbeitung aller Felder (Kunde, Objekt, Ansprechpartner, Auftragsart, Priorität, Zeitraum, Beschreibung, Leistungsumfang, Hinweise intern/Monteure, Unterschrift erforderlich, Abrechnungsart, externe Auftrags-/Angebotsnummer). Soft Delete nur für Entwürfe. |
| §10.2 Auftragsarten | `/settings/order-types`; `ensureDefaultOrderTypes` legt die 8 Standardarten beim ersten Zugriff je Mandant an (idempotent). |
| §10.3 Statusmodell / ARCHITEKTUR §3 | `transition.ts#transitionWorkOrder` ist der einzige Status-Schreibpfad: Übergangstabelle (`status.ts`), Rechte je Übergang, Scope, Begründungspflicht (Storno, Korrektur, Freigabe zurücknehmen), Guards → `ServiceError("blocked", …, CompletionBlocker[])`, optimistische Versionsprüfung (`baseVersion`), `WorkOrderStatusChange`, Audit, Event. |
| §10.4 / US-004 Teamzuweisung | `assign.ts#assignWorkOrder`: Team + optionale Einzelpersonen + Teamleiter (Default: Teamleiter des Teams), draft/review_required/planned → assigned, Teamwechsel nach „angenommen“ → assigned, Event `work_order.assigned`, Audit. Popup im Detail. |
| §12.4 Checklisten, §14.2 Pflichtfotos | Vorlagen je Auftragsart (`/settings/checklists`, Standardvorschlag aus §12.4/§14.2); Übernahme beim Anlegen; Pflege am Auftrag (Punkte/Pflichtfotos hinzufügen/entfernen, Vorlage nachträglich übernehmen). Erledigte Punkte / Pflichtfotos mit Fotos bleiben als Nachweis. |
| Completion-Guards | `completion.ts#getCompletionBlockers`: offene Pflichtpunkte (inkl. „mit Foto“ ohne Foto), Pflichtfotos ohne Foto, offene WorkSession. Zusätzlich: `→ in_review` nur mit erfasster Unterschrift (jede Outcome-Art, §18.2) wenn `signatureRequired`; `→ released_for_billing` nur mit freigegebenem Abschlussbericht. |
| §13.1 Materialvorgabe | CRUD (`materials.ts`), Tab „Material“ mit Soll/Ist inkl. Abweichung, Begründung und Zusatzmaterial. |
| §21 Dashboard | `/dashboard`: 11 Kacheln (offen, heute, laufend, nicht angenommen, überfällig, Berichte zur Prüfung, abgeschlossen, zur Abrechnung, neue Notdienste, fehlende Unterschriften, Sync-Konflikte), Filter Zeitraum/Kunde/Team/Monteur/Auftragsart/Priorität; jede Kachel verlinkt auf `/work-orders?preset=…` (bzw. Konfliktliste). Feldrollen (ohne `work_order:read_all`) → Redirect `/m`. „Heute“ rechnet in der Mandanten-Zeitzone. |
| §21 Liste | `/work-orders`: Statusgruppen-Tabs mit Zählern, Filterleiste (Zeitraum, Kunde, Objekt, Team, Monteur, Status, Auftragsart, Priorität, Freitext), Sortierung, Paginierung, Karten- (Statuskante) und Tabellenansicht, Anlage-Popup mit Kundensuche → Objekt/Kontakt-Auswahl. |
| Detail | `/work-orders/[id]`: Kopf mit Status (Text + Icon), nächster primärer Aktion (CTA), weiteren Übergängen, Bearbeiten/Zuweisen/Stornieren; Tabs Übersicht · Checkliste & Pflichtfotos · Material · Zeiten (Lesesicht) · Fotos (Galerie) · Notizen · Berichte (Liste + Abrechnungsaktionen) · Dokumente (Upload) · Verlauf (Statusänderungen + Audit). |
| US-009 Abrechnung | Freigabe, Zurückweisung zur Korrektur (mit Grund), Freigabe zurücknehmen, „abgerechnet“ – protokolliert, Event `work_order.released_for_billing`. |
| §23.5 Konflikte | `/work-orders/conflicts`: offene `SyncOperation(status=conflict)` mit Payload-Vorschau; „Übernehmen“ (Op erneut gegen aktuellen Stand als ursprünglicher Gerätenutzer) / „Verwerfen“ (resolvedAt/By + Audit). |
| §25 Suche | `/search?q=` + Header-Suchfeld: Aufträge (Nummer, externe/Angebotsnummer, Titel, Beschreibung, Leistungsumfang, Objektadresse), Kunden (inkl. Adresse), Objekte, Ansprechpartner, Dokumentnamen, Notizen; ILIKE mit `workOrderScope/customerScope/siteScope` und Dokument-Sichtbarkeit; Filter Zeitraum/Status/Team. |
| Nummernkreise | `/settings/numbering` (Präfix/Stellen; Zähler wird nie zurückgesetzt). |
| API | `GET/POST /api/v1/work-orders`, `GET/PATCH /api/v1/work-orders/[id]`, `POST …/assign`, `POST …/transition`, `GET/POST …/materials`, zusätzlich `POST …/documents` (multipart). Fehler: 401/403/404/409/422 (blocked mit `details: CompletionBlocker[]`). |
Jede Mutation: Permission (Guard + Service) · Scope (`workOrderScope`) · Zod · `writeAuditLog` (before/after) · `version + 1` · ggf. `emitEvent`. Fachdaten ausschließlich über `ctx.db`.
## 2. Verträge für andere Lanes
Exakte Signaturen (Stubs von L3/L5 darauf umstellen):
```ts
// src/server/services/work-orders/create.ts
createWorkOrder(ctx: ServiceCtx, raw: CreateWorkOrderInput): Promise<{ id: string; number: string; status: string; version: number }>
// CreateWorkOrderInput (src/lib/work-orders/schemas.ts): title, customerId, siteId?, contactId?, orderTypeId?, priority?,
// status?: "draft" | "review_required" | "planned" | "in_progress" (Default draft; in_progress nur mit isEmergency),
// description?, scope?, plannedStart?, plannedEnd?, signatureRequired?, billingType?, internalNotes?, technicianNotes?,
// externalOrderNumber?, offerNumber?, isEmergency?, emergencyReason?, sourceImportId?, numberKey?: "work_order" | "emergency",
// applyTemplate? (Default true), materials?, checklistItems?, photoRequirements?
// src/server/services/work-orders/transition.ts
transitionWorkOrder(ctx: ServiceCtx, raw: { workOrderId: string; to: WorkOrderStatus; reason?: string | null; baseVersion?: number;
eventData?: Record<string, string | number | boolean | null> }): Promise<{ id: string; status: WorkOrderStatus; version: number; from: WorkOrderStatus }>
// eventData wird in event.data gemischt; number/from/to werden nie überschrieben.
// src/server/services/work-orders/completion.ts
getCompletionBlockers(ctx: ServiceCtx, workOrderId: string): Promise<CompletionBlocker[]> // mit Scope-Prüfung (not_found)
computeCompletionBlockers(ctx: ServiceCtx, workOrderId: string): Promise<CompletionBlocker[]> // ohne Scope-Prüfung, für bereits geladene Aufträge
```
- **Transaktionen:** L2 verwendet **kein `$transaction`**. Die Services arbeiten ausschließlich über `ctx.db`. Ein `ctx`, dessen `db` ein Transaktions-Client ist (künftig `inTransaction(ctx, fn)`), wird unverändert unterstützt. Ausnahme: `writeAuditLog` schreibt über den Owner-Client (Fundament) außerhalb der Transaktion. `createWorkOrder` legt Auftrag, Checkliste, Pflichtfotos, Material und die erste Statushistorie in **einem** verschachtelten `create` an (atomar). Die Nummernvergabe (`nextNumber`) läuft davor. Nicht atomare Mehrschritt-Schreibvorgänge, die nach dem Fundament-Merge in `inTransaction` gehören: `applyTransition` (Versions-Update → StatusChange → Audit), `assignWorkOrder` (Auftrag → Assignees löschen/anlegen → StatusChange), Material-/Checklisten-Mutationen (Zeile → `touchWorkOrder`), `applySyncConflict` (Re-Apply → SyncOperation-Update).
- L3 Import: `status: "review_required"` oder `"planned"`, `sourceImportId`, optional `materials`, `checklistItems`, `photoRequirements`.
- L8 Notdienst: `isEmergency: true`, `numberKey: "emergency"` (N-…), `status: "in_progress"` erlaubt nur mit `isEmergency`; Recht `work_order:write` **oder** `emergency:create`.
- `transitionWorkOrder(ctx, { workOrderId, to, reason?, baseVersion? })` – für L4 (Sync `work_order.transition`) und L5 (Berichte).
- `getCompletionBlockers(ctx, id)`, `assignWorkOrder`, `cancelWorkOrder`, `releaseForBilling` / `rejectForCorrection` / `revokeBillingRelease` / `markBilled`.
- `ensureDefaultOrderTypes(db, tenantId)` – kann in `provision.ts` aufgerufen werden (Fundament, nicht angefasst).
## 3. Dateien
**Service/Lib (neu):** `src/lib/work-orders/{schemas,defaults,filters,time,action-state}.ts`; `src/server/services/work-orders/{_shared,create,update,transition,completion,assign,cancel,release-billing,materials,checklist,list,dashboard,detail,search,conflicts,settings,options,documents,sync-reapply}.ts`
**Actions:** `src/server/actions/work_orders/{_form,work-orders,settings}.ts`
**UI:** `src/app/(app)/work-orders/{page.tsx,[id]/page.tsx,conflicts/page.tsx}`, `src/app/(app)/dashboard/page.tsx`, `src/app/(app)/search/page.tsx`, `src/app/(app)/settings/{order-types,checklists,numbering}/page.tsx`, `src/components/work-orders/{action-form,ui,page-context,work-order-list,work-order-fields,detail-tabs,settings-nav}.tsx|ts`
**API:** `src/app/api/v1/work-orders/{_http.ts,route.ts,[id]/route.ts,[id]/assign,[id]/transition,[id]/materials,[id]/documents}`
**Texte:** `messages/{de,en}/{workOrders,dashboard,search,settingsTemplates}.json`
**Tests:** `scripts/test-auftraege-{status,core,scope,numbering}.ts`, Fixtures `scripts/lib-auftraege-fixtures.ts` (kein `test-`-Präfix → nicht vom Runner ausgeführt)
**Erlaubte Fremd-Einzeiler:**
- `src/lib/nav.ts`: Eintrag „Auftragsvorlagen“ → `/settings/order-types` (`settings:templates`) + Label `templates` in `messages/{de,en}/nav.json`
- `src/app/(app)/layout.tsx`: Header-Suchfeld → `GET /search`
- `src/components/audit-trail.tsx`: Entity-Labels `order_type`, `checklist_template`, `number_sequence`, `sync_operation`
## 4. Tests
`npm run gate` **grün**: prisma generate, tsc, lint (0 Fehler; 2 Warnungen im Fundament-Platzhalter `services/notifications/handle-event.ts`), build inkl. Modul-Guard-Check (15 Action-Dateien), **26/26 Testskripte grün**, davon 4 neue L2-Skripte (alle Nachweise erfüllt).
Hinweis Lane-DB: `scripts/test-rls-enforcement.ts` nutzt ohne `RLS_DATABASE_URL` die Standard-DB `craftvia`. In der lokalen `.env` des Worktrees (nicht eingecheckt) ist `RLS_DATABASE_URL` auf `craftvia_auftraege` gesetzt (Rolle `craftvia_app`, lokales Testpasswort laut Testskript).
| Skript | Inhalt |
|---|---|
| `test-auftraege-status.ts` | alle 256 Statuspaare gegen den Vertrag; **180 Rolle×Übergang-Kombinationen** (tenant-admin, backoffice, team-lead, technician) mit unabhängig aus der Rechte-Tabelle abgeleiteter Erwartung (Erfolg inkl. Version/Historie/Audit bzw. forbidden ohne Schreibwirkung); verbotene Paare → invalid; Begründungspflicht; Versionskonflikt |
| `test-auftraege-core.ts` | Anlage (Nummer, Vorlage/Unterschrift aus Auftragsart, Notdienst N-), Update (Version, Konflikt, Kunde/Objekt-Konsistenz, Audit before/after, Soft Delete), Zuweisung, Completion-Blocker (Checkliste/Pflichtfoto/laufende Session), Unterschrift vor in_review, Freigabe nur mit freigegebenem Abschlussbericht, Material-CRUD + Soll/Ist, Checklistenpflege, Liste (Presets, Filter, Gruppen, Paginierung), Dashboard-Kacheln, Konflikte verwerfen/übernehmen, Auftragsarten/Vorlagen/Nummernkreis |
| `test-auftraege-scope.ts` | Monteur fremdes Team → not_found (Liste/Detail/Übergang/Blocker); Team-Mitglied/Teamleiter/Einzelzuweisung sehen; Monteur ohne Backoffice-Rechte → forbidden; **Mandant B**: Liste/Detail/Update/Übergang/Zuweisung/Material/Konflikt/Dashboard/Suche ohne Zugriff auf A, Tenant-Guard direkt; Suche respektiert Scope und Filter |
| `test-auftraege-numbering.ts` | 20 parallele `createWorkOrder` auf frischem Mandanten → eindeutig und lückenlos A-00001…A-00020; zweiter Mandant eigener Kreis |
## 5. Stubs / Abhängigkeiten
| Stub (in L2-Pfad) | Ersetzt durch | Vorgehen nach Merge |
|---|---|---|
| `services/work-orders/documents.ts#uploadWorkOrderDocument` (Allowlist PDF/JPEG/PNG/WebP, Magic Bytes, Größenlimit, SHA-256, `storage.put`, `Document`) | L1 `services/documents/store.ts#storeFile` | Rumpf durch `storeFile(ctx, { …, links: { workOrderId, customerId, siteId } })` ersetzen |
| `services/work-orders/sync-reapply.ts#reapplySyncOperation` (nur `work_order.transition`) | L4 `services/sync/apply.ts` | an L4-Dispatcher delegieren; Signatur bleibt |
| Tab „Berichte“: einfache Liste mit Link `/reports/[id]` | L5-Komponente | optional L5-Komponente einbinden |
| Dateilinks `/files/<storageKey>` | Umstellung auf `/files/<documentId>` (ARCHITEKTUR §4.3, L1) | Links in `detail-tabs.tsx` anpassen |
Events werden über `emitEvent` gesendet (`work_order.assigned|changed|cancelled|started|daily_report_created|technically_completed|signature_missing|released_for_billing`); Empfängerauflösung liefert L6.
## 6. Bekannte Lücken / Hinweise an Fundament/Architekt
- **`requireApiContext` existiert nicht** (in `context.ts` referenziert). `/api/v1/work-orders` nutzt `moduleGuard("work_orders")` (Session-Cookie, DB-autoritative Rechte). Token-Auth für Mobile/Integrationen fehlt; `src/proxy.ts` leitet `/api/v1/**` ohne Cookie per 307 auf `/login` statt 401 → Fundament-Bedarf.
- Upload-Größe: Dokument-Upload läuft über Route-Handler (kein 1-MB-Server-Action-Limit), aber `proxyClientMaxBodySize` (Default 10 MB) begrenzt – für PDFs bis 25 MB in `next.config.ts` erhöhen (Fundament).
- Einstellungsseiten liegen ohne Modul-Layout unter `/settings/*`; die Actions laufen über `moduleGuard("work_orders")` (bei deaktiviertem Modul nicht speicherbar).
- `PII_REFERENCE_FIELDS` (`src/server/dsgvo/pii-fields.ts`, Fundament) enthält die Personen-Referenzen des Auftragsmodells noch nicht (`WorkOrder.createdById/teamLeadUserId`, `WorkOrderAssignee.userId`, `WorkOrderStatusChange.actorId`, `SyncOperation.resolvedById`) → Architekt/Fundament.
- Suche in Berichtsinhalten (JSON) nicht umgesetzt (Berichte gehören L5).
- Dashboard „Berichte zur Prüfung“ zählt Berichte (`submitted`/`team_approved`); die Kachel verlinkt auf Aufträge mit solchen Berichten.
- Bearbeiten-Popup ändert den Kunden nicht (API `PATCH` kann es, inkl. Konsistenzprüfung).
- Browser-Smoke mit Anmeldung nicht automatisiert (siehe §7).
## 7. Screens / Routen
`/dashboard` · `/work-orders` (`?group=`, `?preset=`, `?view=table`, `?new=1`) · `/work-orders/[id]` (`?tab=overview|checklist|material|times|photos|notes|reports|documents|history`, `?assign=1`, `?edit=1`, `?transition=<status>`) · `/work-orders/conflicts` · `/search?q=` · `/settings/order-types` · `/settings/checklists` · `/settings/numbering`
**Smoke (Dev-Server Port 3102, curl):** alle Seiten oben sowie `GET /api/v1/work-orders`, `GET /api/v1/work-orders/[id]` und `POST …/transition` antworten ohne Session mit 307 → `/login?callbackUrl=…` (Proxy-Gate greift, Server startet fehlerfrei). Alle Seiten und Route-Handler werden im Production-Build fehlerfrei kompiliert. Die i18n-Schlüssel sind statisch geprüft: 808 Verwendungen, alle in de und en vorhanden. **Nicht durchgeführt:** Smoke mit Anmeldung / visuelle Prüfung (Responsive 1024/768 px). Der Agent gibt keine Passwörter in Login-Abläufe ein; bitte manuell mit einem Seed-Nutzer prüfen (`backoffice@demo.example` → `/dashboard`, `/work-orders`; `monteur@demo.example` → Redirect `/m`).
+108
View File
@@ -0,0 +1,108 @@
# Lane L6 – Benachrichtigungen & Audit
Branch `lane/benachrichtigungen` (Basis `bf44567`, `feature/craftvia-mvp`). Spec §19.4, §20, §26, §33, ARCHITEKTUR §4.1.
## Umfang / erfüllte Spec-Punkte
| Punkt | Umsetzung |
|---|---|
| §20.1 Ereignisse, ARCHITEKTUR §4.1 | `handleEvent` (Signatur unverändert) löst alle 17 `EVENT_TYPES` auf: In-App-`Notification` über `ctx.db` + E-Mail über `enqueueMail` |
| §20.2 Kanäle | In-App (Glocke, `/notifications`) + E-Mail (Mail-Queue, MailLog) |
| §19.4 Notdienst | Pflichtmail an Backoffice + feste Notdienst-Empfänger, Format „Neuer Notdiensteinsatz abgeschlossen / Monteur / Kunde / Einsatzbeginn / Einsatzende / Status" |
| §33.1 E-Mail-Ereignisse | Templates `craftvia_team_assigned`, `craftvia_report_review`, `craftvia_billing_release`, `craftvia_emergency`, `craftvia_document_failed`, `craftvia_notification` (de/en, Link in die App über `APP_BASE_URL`, Craftvia-Fußzeile mit Grund des Empfangs) |
| §33.2 Konfiguration | `/settings/email` (tenant:manage): Absendername, Antwortadresse, Empfänger Notdienst, Empfänger Abrechnung (je max. 20). Absenderadresse bleibt Plattform-Domain (SPF/DKIM) – nur Hinweis. Vorlagen je Mandant: nicht im MVP |
| §26 Audit | `/settings/audit` (audit:read): Filter Zeitraum/Benutzer/Aktion/Objektart/Objekt-ID, Pagination, Detail-Popup mit before/after-Diff, Ergebnis (erfolgreich/abgelehnt), IP/User-Agent (derzeit „nicht erfasst", s. Fundament-Bedarf) |
| Nutzer-Einstellungen | `/account` Abschnitt „Benachrichtigungen": E-Mail-Opt-out je Typ, Notdienst als Pflicht (nicht abwählbar) |
### Empfängerregeln (`src/server/services/notifications/recipients.ts`)
| Event | In-App | E-Mail |
|---|---|---|
| `work_order.assigned` / `changed` / `cancelled` | aktive Teammitglieder (validFrom/validTo) + Teamleiter des Teams + `teamLeadUserId` + Einzel-Assignees | dieselben |
| `work_order.started` / `daily_report_created` / `technically_completed` / `signature_missing` / `missing_required` | Backoffice = Nutzer mit `work_order:read_all` **und** `report:approve` | dieselben |
| `report.submitted` | `data.approvalStage="team"` → Teamleiter des Auftrags mit `report:approve_team`; `"backoffice"` → Backoffice; ohne Angabe → beide | dieselben |
| `report.approved` / `rejected` | Ersteller (`createdById`) + Team des Auftrags | dieselben |
| `work_order.released_for_billing` | Nutzer mit `work_order:release_billing` | feste Abrechnungsempfänger; sind keine hinterlegt, die Nutzer selbst |
| `emergency.created` / `completed` | Backoffice | Backoffice + feste Notdienst-Empfänger, **Pflicht** (Opt-out wirkungslos) |
| `import.ready_for_review` / `import.failed` | importierender Nutzer (`importedById`) | derselbe |
| `sync.failed` | betroffener Nutzer (`SyncOperation.userId`) + Backoffice | dieselben |
Allgemein: Akteur (`ctx.userId`) ausgenommen – außer bei System-Ergebnis-Events (`import.*`, `sync.failed`), deren Betroffener sonst nie informiert würde. Alle IDs werden über `ctx.db.user` (Status ACTIVE) neu aufgelöst → nie Nutzer anderer Mandanten. Feste Adressen, die zugleich einem Nutzer-Empfänger gehören, erhalten keine Doppelmail. Sprache je Empfänger: `Identity.uiLocale` → Mandanten-Locale → `de`. Link je Empfänger: Backoffice (`work_order:read_all`) → `/work-orders/…`, `/reports/…`; sonst mobile Routen `/m/orders/…`, `/m/sync`.
### Verträge für andere Lanes (über `DomainEvent.data`)
- `occurrenceId` (string/number): für wiederholbare Events (z. B. Tagesbericht je Tag, mehrere `changed`) – wird an den Mail-`dedupeKey` gehängt. Ohne `occurrenceId` gilt `event:entity:user` (gleiches Event zweimal → eine Mail).
- `approvalStage` (`"team"` | `"backoffice"`) bei `report.submitted` (L5).
- `startedAt` / `endedAt` (ISO) und optional `technician` bei `emergency.*` (L8); Fallback: geplanter Beginn bzw. Erstellzeit, Ende = Eventzeit, Monteur = Akteur.
- `reason` bei `sync.failed` (Fallback, falls `SyncOperation.errorCode` leer).
- In-App-Dedupe: existiert eine **ungelesene** Benachrichtigung gleichen Typs zur selben Entität, wird sie aufgefrischt statt verdoppelt.
## Dateien
Neu:
- `src/server/services/notifications/{recipients,texts,inbox,preferences,mail-settings,page-ctx}.ts`, `handle-event.ts` (Platzhalter ersetzt)
- `src/server/services/audit/viewer.ts` (Audit-Viewer-Service; neuer Pfad ohne Owner, fachlich Teil dieser Lane)
- `src/server/actions/notifications/{inbox,preferences,mail-settings}.ts` (je `moduleGuard("notifications")` + `await guard(...)`)
- `src/components/notifications/{bell,preferences-section,preferences-form}.tsx`
- `src/app/(app)/notifications/page.tsx` (Platzhalter ersetzt), `src/app/(app)/settings/email/{layout,page}.tsx`, `src/app/(app)/settings/audit/page.tsx`
- `messages/{de,en}/notifications.json`
- `prisma/migrations/20260914120000_benachrichtigungen_tenant_mail_settings/migration.sql`
- `scripts/test-benachrichtigungen-events.ts`, `scripts/test-benachrichtigungen-inbox-audit.ts`
Erlaubte Fremd-Eingriffe:
- `src/server/mail/templates.ts`: nur neue Template-Keys + `CRAFTVIA_TEMPLATE_KEYS` + Craftvia-Fußzeilen (SEC1-`TEMPLATE_KEYS` unverändert, damit `test-mail.ts` gleich bleibt)
- `src/app/(app)/layout.tsx`: `<NotificationBell />` im Header (Import + 1 Zeile)
- `src/app/(app)/account/page.tsx`: `<NotificationPreferencesSection />` (Import + neuer Abschnitt)
- `src/lib/nav.ts`: Einträge `/notifications`, `/settings/email`, `/settings/audit` (+ Icons) und passende Labels in `messages/{de,en}/nav.json`
- `src/components/audit-trail.tsx`: Entity-Labels der Craftvia-Entitäten
- `prisma/schema.prisma`: 4 Felder an `TenantSettings`
## Migration (begründet)
`TenantSettings` hatte keine Mailfelder (nur Fundament-`smtp Json`, das für SMTP-Zugangsdaten gedacht ist). Neue Spalten `mail_from_name`, `mail_reply_to`, `emergency_recipients TEXT[]`, `billing_recipients TEXT[]`. Keine neue Tabelle → kein `enable_tenant_rls`, keine TENANT_MODELS-Änderung (tenant_settings ist bereits mandantengebunden).
## Tests
| Skript | Prüfungen | Ergebnis |
|---|---|---|
| `test-benachrichtigungen-events.ts` | 63: Empfänger je Eventgruppe (assigned inkl. Einzel-Assignee, cancelled, started, report.submitted team/ohne Stufe, report.approved, released_for_billing, emergency.completed, import.failed, sync.failed), Akteur ausgenommen, Mandantentrennung (fremder Kontext, fremde Entität, fremde User-ID als Ersteller), Dedupe, occurrenceId, Opt-out, Pflichtmail Notdienst trotz Opt-out, feste Empfänger ohne Doppelmail, handleEvent/emitEvent werfen nie, Templates de/en + §19.4-Format | grün |
| `test-benachrichtigungen-inbox-audit.ts` | 46: Posteingang nur eigene + Filter, Monteur/Mandant B → `not_found` bei fremder Benachrichtigung, ohne `notification:read` → `forbidden`, markAllRead mandantengetrennt, Audit bei markRead, Open-Redirect-Schutz, Präferenz-Defaults, Mailkonfiguration nur `tenant:manage` + Validierung (Header-Injection, ungültige/zu viele Adressen) + Mandantentrennung + Audit, Audit-Viewer nur mit `audit:read`, nur eigener Mandant, Filter, Detail-Diff | grün |
**Gate (`npm run gate`): grün** – prisma generate, tsc, lint, build inkl. Modul-Guard-Check (16 Action-Dateien), 24/24 Testskripte.
Hinweis Umgebung: Mit eigener Lane-Datenbank muss `RLS_DATABASE_URL` auf dieselbe DB zeigen
(`postgresql://craftvia_app:craftvia_app_local@localhost:5432/craftvia_benachrichtigungen?schema=public`),
sonst schlägt `test-rls-enforcement.ts` fehl: Owner-Client und `craftvia_app`-Client landen dann in verschiedenen Datenbanken. Das hat nichts mit dem Code dieser Lane zu tun; in der kopierten `.env` ist die Zeile auskommentiert.
**Smoke (Dev-Server :3106, Server-Rendering per HTTP, Seed-Mandant „demo")**, 13 Prüfungen grün:
`/notifications` inkl. Filter Status/Typ, `/settings/email`, `/settings/audit` inkl. Detail-Popup, `/account` (Abschnitt Benachrichtigungen), Glocke und Navigation im Header/Sidebar (Admin);
Backoffice sieht das Audit-Protokoll, wird von `/settings/email` umgeleitet; Monteur wird von `/settings/audit` umgeleitet und sieht keine Admin-Navigation.
Sitzungen wurden lokal ohne Passworteingabe erzeugt (`finalizeIdentityLogin` + `AUTH_SECRET`); die Smoke-Daten wurden danach entfernt. Visuelle Prüfung (Responsive 1024/768/375 px) steht noch aus.
## Stubs / Abhängigkeiten
- Keine Stubs nötig: Empfängerauflösung liest direkt die Domänentabellen (WorkOrder, Team, TeamMember, WorkOrderAssignee, Report, ImportJob, Document, SyncOperation).
- **L4 (Mobile-Header):** Glocke einbinden mit `import { NotificationBell } from "@/components/notifications/bell";` und `<NotificationBell variant="mobile" />` (48-px-Touchziel). Server-Komponente, blendet sich ohne `notification:read`/bei deaktiviertem Modul selbst aus.
- **L2/L4/L5/L8:** Links zeigen auf `/work-orders/[id]`, `/work-orders/conflicts`, `/reports/[id]`, `/m/orders/[id]`, `/m/orders/[id]/report`, `/m/sync`, `/imports/[id]` (Routen laut ARCHITEKTUR §5).
- Settings-Übersicht (`/settings/page.tsx`, Fundament) verlinkt die neuen Seiten noch nicht; erreichbar über die Sidebar.
## Fundament-Bedarf (nicht selbst geändert)
1. **IP/User-Agent im Audit-Log (Spec §26):** `writeAuditLog` erfasst weder IP noch User-Agent, `AuditLog` hat keine Spalten dafür. Vorschlag: Spalten `ip`, `user_agent` + Ermittlung aus `headers()` im action-guard. Der Viewer zeigt die Werte bereits an, sobald `after.ip`/`after.userAgent` bzw. künftige Spalten gefüllt sind (derzeit „nicht erfasst").
2. **Absendername/Reply-To je Mandant im Mail-Kern:** `deliverMail` nutzt nur globale `MAIL_FROM_NAME`/`MAIL_REPLY_TO`. Benötigt: optionale `fromName`/`replyTo` in `EnqueueInput`/`MailJob` und deren Verwendung in `deliver.ts`. Die gespeicherten Werte liefert `tenantMailSender(ctx)` (`services/notifications/mail-settings.ts`); bis dahin werden sie gespeichert, aber beim Versand noch nicht angewendet.
3. `src/server/mail/notifications.ts#notifyUser` (SEC1) bleibt bestehen; Craftvia-Fachmodule nutzen ausschließlich `emitEvent`. AGENTS.md-Andockpunkt „Benachrichtigungen" sollte auf `emitEvent` zeigen.
## Bekannte Lücken
- Mandanteneigene E-Mail-Vorlagen (§33.2 „E-Mail-Vorlagen") nicht umgesetzt.
- Glocke aktualisiert sich bei Navigation/Aktion (Server-Rendering), kein Live-Push/Polling.
- Kein Aufräumen alter Benachrichtigungen (Löschfrist) – Kandidat für einen Job.
- In-App-Opt-out gibt es bewusst nicht (nur E-Mail).
## Screens / Routen
- `/notifications` – Liste mit Filter Status/Typ, „Öffnen" (markiert gelesen + springt zur Entität), „Als gelesen markieren", „Alle als gelesen markieren"
- Glocke im Backoffice-Header – Zähler ungelesen, Dropdown letzte 10, „Alle gelesen", „Alle anzeigen"
- `/account` – Abschnitt „Benachrichtigungen"
- `/settings/email` – Mandanten-Mailkonfiguration
- `/settings/audit` – Audit-Protokoll mit Filter und Detail-Popup (`?detail=<id>`)
+80
View File
@@ -0,0 +1,80 @@
# Lane L5 – Berichte & Unterschrift
Branch `lane/berichte` (Basis `bf44567`, `feature/craftvia-mvp`). Spec §16, §17, §18, §32, US-007/008/009, ARCHITEKTUR §4.7.
## Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §16 Tagesbericht | `createDailyReport`: Entwurf je Auftrag + Kalendertag (Mandanten-Zeitzone), Inhalte nur des Tages (Zeiten, Notizen, Fotos, Material). Auftrag → `daily_report_created` über `transitionWorkOrder` (aus `paused`/`waiting_material` über `in_progress`), Auftrag bleibt offen. Idempotent je Tag. Event `work_order.daily_report_created`. |
| §17.1/17.2 Abschlussbericht | `createCompletionReport` prüft `getCompletionBlockers` (Checkliste, Pflichtfotos, laufende Zeiten) → `ServiceError("blocked", details: CompletionBlocker[])`. Snapshot mit allen Inhalten aus §17.2 (`ReportContent`). |
| Pflichtangaben | Absenden verlangt „Ausgeführte Leistungen“ (`REPORT_REQUIRED_TEXTS`) + erneut Blocker-Prüfung → strukturierte Liste. |
| §17.3 PDF | Nach Freigabe (`report:approve`) Job `report-pdf` → `generateReportPdf`: HTML-Template (React SSR) → Chromium/Playwright → Dokument (Kategorie `daily_report`/`completion_report`, Sichtbarkeit `customer_report`, an Auftrag/Kunde/Objekt → erscheint in Objekt-Historie), `pdfDocumentId` + SHA-256 `pdfChecksum`. Bestehendes PDF wird nie ersetzt. |
| §17.4 Versionierung | `createNewVersion` (nur `report:approve`): neue Version gleiche `lineageId`, `version+1`, Entwurf aus freigegebenem Snapshot. Die freigegebene Version bleibt unverändert und wird erst bei Freigabe der Nachfolgerin `superseded` (so existiert immer ein gültiges freigegebenes PDF). |
| §18.1 Unterschrift | `captureSignature`: Name, Funktion, Datum/Uhrzeit (`signedAt`), Bestätigungstext (messages, mit Berichts-/Auftragsnummer + Datum), Bezug Bericht, erfassender Nutzer. Canvas-Pad mit Pointer Events (Maus/Touch/Stift), Löschen, PNG-Export → Server Action → Dokument `signature`. |
| §18.2 Ausnahmen | `customer_absent`/`refused`/`later` → Begründung Pflicht; `not_required` nur bei `signatureRequired=false` oder `report:approve`. Erfasste Unterschrift (`signed`) wird nie überschrieben; `later` kann nachgereicht werden (Auftrag `signature_pending` → `in_review`). |
| §32 PDF-Merkmale | Bericht-ID, Versionsnummer, Erstellungs-/Freigabedatum, Freigabestatus, Prüfsumme (Fuß: SHA-256 des Inhalts-Snapshots; SHA-256 der PDF-Datei am Bericht/Dokument), A4, Seitenumbrüche, Fotoraster 2-spaltig, Kopf-/Fußzeile mit Seitenzahl, Mandantenlogo sonst Firmenname, Inter eingebettet. |
| Freigabe | Teamleiter (`report:approve_team`) → `team_approved`; Backoffice (`report:approve`) → `approved` (Inhalt final eingefroren, Event `report.approved`, PDF-Job). Zurückweisen mit Pflichtgrund → `rejected`, Abschluss-Auftrag `in_review` → `in_progress`, Event `report.rejected`. |
| Auftragsstatus bei Absenden | Abschlussbericht v1: … → `in_progress` → `technically_completed` → `signature_pending` (keine Unterschrift/`later`) bzw. `in_review` (`signed`, `not_required`, `refused`, `customer_absent`; bei den letzten beiden zusätzlich Event `work_order.signature_missing`). Folgeversionen ändern den Auftragsstatus nicht. |
| US-009 Berichtsseite | `/reports` (zur Prüfung zuerst, Filter Typ/Status/Team/Zeitraum), `/reports/[id]` (strukturierte Ansicht, PDF-Link, Versionen, Aktionen, Badge „Lotse-Entwurf“ bei `aiDrafted`). |
## Dateien
- Vertrag/Client-safe: `src/lib/reports/content.ts` (Zod `ReportContent`, Status-/Outcome-Konstanten), `src/lib/reports/dates.ts` (Tagesfenster Zeitzone), `src/lib/reports/action-state.ts`
- Services `src/server/services/reports/`: `build-content.ts`, `common.ts` (Scope `reportScope`/`requireVisibleReport`, Audit, Refresh), `create.ts`, `edit.ts`, `submit.ts`, `approve.ts`, `reject.ts`, `new-version.ts`, `signature.ts`, `pdf.ts`, `files.ts` (Datei-Auslieferung nur für im Snapshot referenzierte Dokumente), `queries.ts` (Liste/Detail/Mobil), `read-ctx.ts`, `http.ts` (API-Adapter), `_stubs/{work-orders,documents}.ts`
- PDF: `src/server/pdf/render.ts`, `src/server/pdf/templates/report.tsx`, Processor `src/server/jobs/processors/report-pdf.ts`
- Actions `src/server/actions/reports/`: `workflow.ts` (create/save/submit/approve/reject/newVersion/regeneratePdf), `signature.ts`, `_state.ts`
- API: `POST /api/v1/work-orders/[id]/daily-report`, `POST /api/v1/work-orders/[id]/completion-report`, `POST /api/v1/reports/[id]/approve`, `GET /api/v1/reports/[id]/pdf`, zusätzlich `GET /api/v1/reports/[id]/files/[documentId]` (Fotos/Unterschrift in Ansicht)
- UI Backoffice: `src/app/(app)/reports/page.tsx`, `src/app/(app)/reports/[id]/page.tsx`; Komponenten `src/components/reports/{report-view,review-actions,reject-form,status-badge,blocker-list,action-message,step-indicator,signature-pad}.tsx`
- UI Mobil: `src/components/reports/mobile/{report-editor,report-review,sign-flow,report-screen,sign-screen,create-report-form}.tsx`; dünne Seiten `src/app/(app)/m/(field)/orders/[id]/{report,sign}/page.tsx`
- Texte: `messages/de/reports.json`, `messages/en/reports.json`
- Tests: `scripts/test-berichte-flow.ts`, `scripts/test-berichte-pdf.ts`
Fremd-Einzeiler/erlaubte Eingriffe: `src/server/jobs/processors/index.ts` (Registrierung `report-pdf`, mit `turbopackIgnore`), `Dockerfile` (neue Stage `worker` am Ende). `src/lib/nav.ts` war bereits eingetragen. `package.json`/`package-lock.json`: neue Abhängigkeit `playwright-core`.
## Abhängigkeit `playwright-core`
`playwright-core@^1.63` (Apache-2.0, ~8 MB entpackt, kein postinstall-Download, keine transitiven Laufzeit-Abhängigkeiten). Vom Architektur-Vertrag vorgegeben (§1 PDF). Wird nur im Worker geladen (`turbopackIgnore` im Processor-Registry-Eintrag), nicht im App-Bundle. Browser-Auflösung: `PDF_CHROMIUM_PATH` → Playwright-Chromium → lokal installiertes Google Chrome (`channel: "chrome"`, Entwicklerrechner).
**Worker-Image:** braucht Chromium + Schriften. Vorschlag als Stage `worker` im `Dockerfile` (Debian-Paket `chromium`, `fonts-dejavu-core`, `fonts-liberation`, `PDF_CHROMIUM_PATH=/usr/bin/chromium`, Start `npx tsx scripts/craftvia-worker.ts`). Compose-Service (`target: worker`, `REDIS_URL`, `DATABASE_URL`, `S3_*`) ist noch einzutragen (nicht Lane-Ownership).
## Tests
| Skript | Prüfungen | Ergebnis |
|---|---|---|
| `scripts/test-berichte-flow.ts` | 70: Content-Builder (Tagesfilter Zeiten/Notizen/Fotos/Material, Materialabweichungen Menge/nicht verwendet/zusätzlich/undokumentiert, Kopfdaten), Tagesbericht + Auftragsstatus + Idempotenz + Nummernkreis, Abschluss-Blocker, Pflichtangaben, Unterschrift-Validierung je outcome, Statusfolgen submit/team_approve/reject/resubmit/approve inkl. Auftragsstatus, Einfrieren nach Freigabe, Versionierung (v1 nie überschrieben, superseded erst bei Freigabe v2), Mandantentrennung (lesen/ändern/freigeben/Liste/direkter DB-Update), Rollen/Scope (Monteur ohne Zuweisung, Teamleiter anderes Team, Monteur ohne Freigaberecht), Audit before/after | grün |
| `scripts/test-berichte-pdf.ts` | 11: PDF gerendert, Dokument > 0 Bytes, beginnt mit `%PDF`, Kategorie/Sichtbarkeit/Auftragsbezug, SHA-256 gespeichert und = Bytes, zweiter Lauf überschreibt nicht, Download eigener Mandant, Mandant B → not_found, nicht referenziertes Dokument → not_found. Ohne startbaren Browser: Skip mit Meldung (Exit 0) | grün (lokal über Google Chrome) |
Gate: siehe Rückmeldung an den Architekten (`npm run gate` grün).
## Stubs / Abhängigkeiten zu anderen Lanes
| Stub | Vertrag | Ersetzen durch |
|---|---|---|
| `services/reports/_stubs/work-orders.ts#transitionWorkOrder` | ARCHITEKTUR §3 (canTransition + requiredPermission, Scope, `WorkOrderStatusChange`, Version+1, Audit, Event) | L2 `services/work-orders/transition.ts` |
| `services/reports/_stubs/work-orders.ts#getCompletionBlockers` | §3 Guards vor Abschluss → `CompletionBlocker[]` | L2 (Datei gemäß L2, z. B. `services/work-orders/guards.ts`) |
| `services/reports/_stubs/documents.ts#storeFile/readFileBytes` | §4.3 `services/documents/store.ts` (Allowlist, Magic Bytes, Größenlimit, SHA-256, Lineage) | Architekt/Dokumente (`services/documents/store.ts`) |
| Unterschrift-Upload über Server Action (Data-URL) | §4.6 `POST /api/v1/uploads` | L4 – für Offline-Sync (`signature.capture` referenziert `documentId`) |
Imports sind mit `TODO(merge …)` markiert. `/api/v1/reports/...` nutzt einen eigenen Adapter (`http.ts`, `moduleGuard("reports")`), da noch kein gemeinsames `requireApiContext` existiert.
L4 bindet die mobilen Seiten ein: Link „Bericht“ im Auftragsdetail → `/m/orders/[id]/report` (Tabs Abschluss/Tag), Abschluss → `/m/orders/[id]/sign`. Sync-Ops `report.save_draft`/`report.submit`/`signature.capture` können direkt `updateReportTexts`/`submitReport` (`expectedWorkOrderVersion`)/`captureSignature` (`clientId`) aufrufen.
## Bekannte Lücken / offene Punkte
- **Schema unverändert** (keine Migration). Mandantenlogo: `TenantSettings.logoKey` ist kein `Document` → `logoDocumentId` bleibt `null`, PDF zeigt Firmennamen, bis ein Logo-Upload als Dokument existiert.
- Berichtsnummer (`B-…`) liegt im Snapshot (`content.reportNumber`), nicht als Spalte – Suche nach Nummer braucht ggf. eine Spalte (Architekt).
- PDF wird ohne laufenden Worker nicht erzeugt, solange `REDIS_URL` gesetzt ist (Job bleibt in der Queue). Ohne Redis läuft `dispatchJob` inline, der Processor ist im App-Bundle aber absichtlich nicht enthalten → Fehler wird geloggt, Freigabe bleibt gültig, „PDF erzeugen“ auf der Detailseite stößt den Job erneut an.
- `generateReportPdf` rendert Fotos in Originalgröße (Data-URI); bei vielen großen Fotos ggf. auf Derivate (L4 `image-derivatives`) umstellen.
- E-Mail-Versand des PDF (§17.3 „optional“) und Empfänger der Events liegen bei L6.
- Mobile-Seiten sind ohne L4-Shell nur über direkte URL erreichbar; Offline-Fähigkeit (L7) nicht Teil dieser Lane.
- `src/server/dsgvo/pii-fields.ts`: `Report.createdById/approvedById/teamApprovedById`, `Signature.capturedById`, `signerName` sollten vom Architekten eingetragen werden (Fundament-Datei, nicht geändert).
## Screens / Routen
| Route | Rolle | Inhalt |
|---|---|---|
| `/reports` | Backoffice, Teamleiter (Scope) | Liste, zur Prüfung zuerst, Filter Typ/Status/Team/Von–Bis |
| `/reports/[id]` | Backoffice, Teamleiter (Scope) | Strukturierte Ansicht, Metadaten, PDF-Link, Versionen, Freigeben/Als Teamleiter prüfen/Zurückweisen (Popup `?reject=1`)/Neue Version/PDF erzeugen |
| `/m/orders/[id]/report?type=completion\|daily` | Monteur, Teamleiter | Blocker → Bericht erstellen → prüfen/ergänzen → (Tag) absenden / (Abschluss) weiter zur Unterschrift |
| `/m/orders/[id]/sign` | Monteur, Teamleiter | Unterschrift (Pad) oder Grund, dann Bericht absenden |
+97
View File
@@ -0,0 +1,97 @@
# Lane L10b – Betrieb & Aufräumen (`lane/betrieb`)
Stand: 2026-09-15 · Basis `a7d4b02` (`feature/craftvia-mvp`, L1–L9 integriert) · Spec §27, §31, §34, §42, §43 · ARCHITEKTUR §4.6, §4.8
## 1. Umfang / erfüllte Punkte
| Punkt | Umsetzung |
|---|---|
| **API-Doku** | `src/lib/api/openapi.ts` (OpenAPI 3.1, statisch gepflegt): alle 23 v1-Pfade mit 29 Operationen, `cookieAuth`, einheitliches Fehlerformat, Pagination, Idempotenz (`clientOpId`, Upload-`clientId`), Konflikte, Rate Limits, Recht/Modul je Operation (`x-craftvia-module`, `x-craftvia-permissions`). `GET /api/v1/openapi.json` (angemeldet). Kurzdoku `docs/craftvia/API.md`. Test prüft, dass jede `route.ts` dokumentiert ist. |
| **Rate Limiting** | `requireApiContext` zählt je Nutzer über `rate-limit.ts`: Bucket `api` (`API_RATE_LIMIT_PER_MINUTE`, Default 300/min) für alle Module, `apiField` (`API_FIELD_RATE_LIMIT_PER_MINUTE`, Default 1200/min) für `field` (sync, uploads, bundle, Dokument-Cache). Überschritten → 429 `rate_limited` + `Retry-After`. Offline-Outbox behandelt 429 als transient (Backoff). |
| **Deploy/Betrieb** | Compose (Coolify + prebuilt): Service `craftvia-worker` (Target `worker`, Chromium, `shm_size 1gb`, Härtung wie übrige Worker). `Dockerfile`: worker-Stage mit `HOME=/home/app` (Chromium-Profil als non-root, vorher startete Chromium nicht). Env-Beispiele mit allen Craftvia-Variablen. `docs/craftvia/DEPLOY.md` (Architektur, Domains, Secrets, Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke, Update/Rollback, Go-Live-Checkliste). CI (`.github`, `.gitea`): Job `gate` mit Postgres (pgvector) + Redis-Service. `build-and-push-images.sh` baut `craftvia-worker`. Certvia-/ISMS-Dokumente → `docs/_certvia-archiv/` (mit README). |
| **a) API-Kontexte** | `imports/_context.ts`, `sync/api-context.ts`, `reports/http.ts`, `work-orders/_http.ts` entfernt; alle 22 Fachrouten nutzen `requireApiContext` + `withApi`/`toErrorResponse`. `withApi` prüft Same-Origin für jede Mutation vor der Anmeldung (fehlte vorher bei imports, reports, work-orders). |
| **b) Konflikt übernehmen** | Entscheidung: **`report.submit` wird unterstützt.** `sync-reapply.ts` delegiert an `apply.ts#reapplyOperation` (gleiche Payload-Validierung und Services wie der Sync, ohne `baseVersion`, als Gerätenutzer). Erlaubt: `work_order.transition`, `report.submit` (mit gespeichertem `aiReviewed`); sonst `invalid reapply_unsupported`. Hinweistext der Konfliktliste angepasst. Ältere gespeicherte Ops ohne `payload.workOrderId` nutzen `entityId`. |
| **c) Bundle + Offline** | `getFieldBundle` liefert je Auftrag `mySession` (`{ id, status, startedAt }` der eigenen aktiven Session oder `null`). `bundle-core.ts#initialSession` nutzt es; nur für Bundles ohne Feld (vor L10b gespeichert) weiter Näherung über den Auftragsstatus. |
| **d) mergeCustomers** | über `inTransaction` (sequenziell, geschützter Statuswechsel `status ≠ merged`), einbettbar in äußere Transaktionen. |
| **e) Audit `read`** | `AuditAction` + Label „Lesezugriff"/„Read access" im Audit-Viewer; Notdienst-Kunden-/Objektsuche protokolliert `read`. |
| **f) clientId je Mandant** | Migration `20260915090000_betrieb_client_id_per_tenant`: 8 Tabellen `@@unique([tenantId, clientId])`. Kein Code nutzte `findUnique` über `clientId` (Replays laufen über `findFirst` im Mandanten-Client). |
| **g) Backoffice mobil** | `components/backoffice-frame.tsx`: unter **1024 px** Sidebar als Drawer hinter Menü-Button (44 px, `aria-expanded`/`aria-controls`, schließt bei Navigation, Hintergrund, Escape; geschlossen `invisible` → nicht fokussierbar). Damit sind 768 px und 375 px abgedeckt; ab 1024 px unverändert statisch. Header kompakter (Name/Mandant ab `sm`). |
| **h) Audit nach Commit** | **Umgesetzt, nicht zu invasiv:** `writeAuditLog` puffert innerhalb `inTransaction` (AsyncLocalStorage in `audit.ts`, `withDeferredAudit`) und schreibt nach dem Commit; Rollback verwirft die Einträge, `denied` bleibt. Verschachtelte Transaktionen teilen den äußeren Puffer. Schreibfehler beim Flush werden geloggt (die Fachänderung ist schon committet). |
| **i) Berichtseditor** | mobiler `ReportEditor` hält Eingaben mit `useOfflineDraft("report:<reportId>")`; Wiederherstellung nur, solange die Servertexte die Basis des Entwurfs sind (sonst gewinnt der Server, z. B. nach Übernahme eines Lotse-Vorschlags); nach Speichern/Absenden gelöscht; Hinweis „Entwurf wiederhergestellt." |
| **j) report.submit offline** | `lib/sync/ops.ts`: Zod-Schemas `report.save_draft` `{ workOrderId, reportId, texts }` und `report.submit` `{ workOrderId, reportId, aiReviewed? }`; Registry-Einträge → `services/reports/sync-ops.ts`. `baseVersion` → `expectedWorkOrderVersion`, `aiReviewed` wird durchgereicht (ohne → `rejected invalid`, `field aiReviewed`). Bericht muss zum Auftrag der Op gehören. `signature.capture` bleibt unregistriert (s. Lücken). |
| **k) Lotse-Betrieb** | **Aufbewahrung:** `services/lotse/retention.ts` leert `input`/`output` und `createdById` älter als `AI_GENERATION_RETENTION_DAYS` (Default 180), Metadaten bleiben, Audit `ai_generation_retention` je Mandant; Queue/Processor `ai-retention`, täglicher BullMQ-Job-Scheduler beim Start von `craftvia-worker`. **Kontingent:** `services/lotse/budget.ts`, Tokens ein+aus je Kalendermonat (UTC); `TenantSettings.aiMonthlyTokenLimit` (Migration `20260915091000_betrieb_ai_token_limit`, nur Spalte) vor Env `AI_MONTHLY_TOKEN_LIMIT` (Default 0 = unbegrenzt). Lotse-Entwurf/Zusammenfassung → `blocked budget_exceeded` mit Klartext; Import-Extraktion → manuelle Erfassung + Hinweis `ai_budget_exceeded`. `/settings/lotse`: Kontingent setzen (leer = Plattform, 0 = unbegrenzt), Verbrauch + „Kontingent aufgebraucht" (Text + Icon). |
### Verhaltensänderungen (bewusst, dokumentiert in API.md/OpenAPI)
- Fehlerformat überall `{ error: { code, message, details? } }` (vorher bei imports/sync/reports teils `{ error: "code" }`).
- `invalid` → **422** (vorher 400 bei imports, sync, uploads, reports); `blocked` → **422** (vorher 409 bei L1-Routen und im Import-Adapter). Clients angepasst: Import-Uploader (neues Format), `lib/field/upload.ts` und Outbox (422 = endgültig ungültig).
- POST-Routen von imports/reports/work-orders verlangen jetzt Same-Origin (Server-zu-Server-Aufrufe ohne `Origin`/`Sec-Fetch-Site` sind weiter möglich).
- `test-einsatz-sync.ts`: Prüfung „nicht verfügbare Op" nutzt `signature.capture`, weil `report.save_draft` jetzt registriert ist (Begründung j).
## 2. Dateien
**Neu:** `src/lib/api/openapi.ts`, `src/app/api/v1/openapi.json/route.ts`, `src/server/services/reports/{dto,sync-ops}.ts`, `src/server/services/lotse/{retention,budget}.ts`, `src/server/jobs/processors/ai-retention.ts`, `src/components/backoffice-frame.tsx`, `prisma/migrations/20260915090000_betrieb_client_id_per_tenant/`, `prisma/migrations/20260915091000_betrieb_ai_token_limit/`, `docs/craftvia/{API,DEPLOY}.md`, `docs/_certvia-archiv/README.md`, `scripts/test-betrieb-{api,sync,audit}.ts`, `scripts/smoke-betrieb.ts`, dieser Bericht.
**Entfernt:** `src/app/api/v1/imports/_context.ts`, `src/app/api/v1/work-orders/_http.ts`, `src/server/services/sync/api-context.ts`, `src/server/services/reports/http.ts`.
**Geändert (Aufräumpunkte a–k):** `src/server/api/{respond,context}.ts`, `src/server/rate-limit.ts`, alle `src/app/api/v1/**/route.ts` außer customers/sites, `src/components/imports/uploader.tsx`, `src/lib/field/upload.ts`, `src/lib/offline/{outbox,bundle-core,types}.ts`, `src/lib/sync/ops.ts`, `src/server/services/sync/{apply,external-ops}.ts`, `src/server/services/work-orders/sync-reapply.ts`, `src/server/services/field/queries.ts`, `src/server/audit.ts`, `src/server/services/context.ts`, `src/server/services/customers/merge.ts`, `src/server/services/emergency/lookup.ts`, `src/server/services/lotse/{settings,draft-report,voice}.ts`, `src/server/services/imports/process.ts`, `src/lib/imports/extraction.ts`, `src/lib/lotse/action-state.ts`, `src/server/actions/lotse-settings.ts`, `src/app/(app)/settings/lotse/page.tsx`, `src/app/(app)/layout.tsx`, `src/components/reports/mobile/report-editor.tsx`, `src/server/jobs/{queues,processors/index}.ts`, `scripts/craftvia-worker.ts`, `prisma/schema.prisma`, `messages/{de,en}/{nav,notifications,workOrders,lotse,imports}.json`, `scripts/test-einsatz-sync.ts`.
**Deploy/Doku:** `Dockerfile`, `docker-compose.coolify.yml`, `docker-compose.coolify.prebuilt.yml`, `.env.example`, `.env.prod.example`, `.env.coolify.example`, `.github/workflows/ci.yml`, `.gitea/workflows/ci.yml`, `scripts/build-and-push-images.sh`, `docs/**` (Archiv-Verschiebung).
**Minimale Eingriffe außerhalb der Ownership (je 1–3 Zeilen):**
- `README.md` Abschnitt „Betrieb": Verweise auf `docs/craftvia/DEPLOY.md`/`API.md` statt archivierter Certvia-Docs, Target `worker`.
- Kommentar-Pfade auf `docs/_certvia-archiv/…`: `scripts/garage-provision.ts`, `scripts/test-auth-selfservice.ts`, `scripts/bootstrap-admin.ts`, `scripts/test-garage-storage.ts`, `deploy/garage.toml`.
- `src/server/jobs/processors/index.ts` (erlaubter Einzeiler `ai-retention`), `messages/{de,en}/nav.json` (Menü-Labels).
- `src/server/audit.ts` (Fundament) – ausdrücklich Aufräumpunkte e/h.
## 3. Tests
| Skript | Prüfungen | Inhalt |
|---|---|---|
| `test-betrieb-api.ts` | 150 | jede v1-Route: `requireApiContext`, keine lane-lokalen Kontexte, Fehler über respond.ts; ohne Sitzung 401 im einheitlichen Format (alle Methoden); jede Mutation mit fremdem Origin bzw. `Sec-Fetch-Site: cross-site` → 403 vor der Anmeldung; Fehler-Mapping (404/403/422/409/422 blocked mit details, ZodError mit Feldpfaden, 500 ohne interne Details); `readJsonObject`; Rate Limit je Nutzer (Standard/Einsatz getrennt, 429 + Retry-After, anderer Nutzer unabhängig); OpenAPI 3.1 deckt jede `route.ts` ab, keine veralteten Pfade, Route liefert das Dokument |
| `test-betrieb-sync.ts` | 36 | c) Bundle `mySession` (Monteur laufend/pausiert, Teamleiter ohne eigene Session → `null`, Offline-Ableitung inkl. altem Bundle), Mandant B und Monteur ohne Zuweisung sehen den Auftrag nicht; f) gleiche Session-/Notiz-`clientId` in A und B, Idempotenz je Mandant, DB-Unique im selben Mandanten; j) `report.save_draft`, `report.submit` ohne reportId/ohne `aiReviewed` → invalid, Mandant B/Monteur ohne Zuweisung → not_found, fremder Bericht über eigenen Auftrag → not_found, veraltete Version → conflict; b) Übernehmen: Mandant B → not_found, Monteur → forbidden, nicht konfliktbehaftete Op → invalid, Backoffice → Bericht submitted + resolved, zweites Übernehmen → not_found, unzulässiger Übergang → abgelehnt ohne Änderung |
| `test-betrieb-audit.ts` | 48 | h) aufgeschoben/nach Commit/Rollback (nur `denied` bleibt)/verschachtelt/direkt; d) Merge: Monteur forbidden, Mandant B not_found, Rollback in äußerer Transaktion (nicht zusammengeführt, Objekt nicht umgehängt, kein Audit), Erfolg + Audit Quelle/Ziel, doppelt → conflict; e) Suche → Audit `read`, Mandant B findet nichts, ohne `emergency:create` → forbidden; k) Aufbewahrung (Default/Env, Processor registriert, Inhalte + Personenbezug entfernt, Metadaten bleiben, junge Einträge unverändert, Mandant B unberührt, Audit, idempotent, längere Frist) und Kontingent (unbegrenzt, Monteur forbidden, Mandanten-Limit → blocked `budget_exceeded`, Audit, Einstellungsseite, Mandant B unabhängig, Env-Default, Vormonat zählt nicht, 0 = unbegrenzt, Feld weggelassen = unverändert, null = Plattform) |
**Gate (`npm run gate`) grün:** prisma generate, tsc, lint (0 Fehler, 3 Warnungen in fremden Dateien: `layout.tsx` ungenutzter Import `CraftviaLogo` – vorbestehend, `services/field/mime.ts`, u. a.), build inkl. Modul-Guard-Check, **52/52 Testskripte**. Lane-DB `craftvia_betrieb`, `RLS_DATABASE_URL` auf dieselbe DB.
**Lauf mit `RLS_ENFORCED=true npm run test`:** 50/52 grün. Die zwei Abweichungen betreffen Dateien, die L10b nicht verändert hat (Diff zu `a7d4b02` nur ein Kommentar in `test-garage-storage.ts`; `db.ts`/`storage` unverändert):
- `test-tenant-isolation.ts` erwartet für `findUnique` über einen fremden Compound-Key (Role `tenantId_key`) einen Throw des Owner-Guards; mit scharfer RLS liefert die Abfrage `null` (kein Datenabfluss, aber andere Semantik).
- `test-garage-storage.ts` lädt `storage/backup-store.ts` → `db.ts` bricht fail-secure ab („RLS_ENFORCED=true, aber RLS_DATABASE_URL fehlt") – der Test läuft ohne die RLS-Umgebung.
→ Beide Tests sind auf den Owner-Betrieb ausgelegt; Anpassung für einen RLS-Modus gehört ins Fundament/L10a.
**HTTP-Smoke** (Dev-Server :3111, Session-Cookies ohne Passworteingabe): `scripts/smoke-betrieb.ts` **18/18 grün** – anonym 401 JSON (`openapi.json`, `sync`); Admin: OpenAPI 3.1 (23 Pfade), customers mit Pagination, 404 `not_found` (work-orders, reports/pdf) im einheitlichen Format, fremder Origin → 403, Array-Body → 422, kaputtes JSON bei imports/confirm → 422, `/settings/lotse` mit KI-Kontingent, `/dashboard` mit Menü-Button, `/settings/audit?action=read`, Konfliktliste mit neuem Hinweis; Monteur: Bundle mit `mySession`, leerer Sync-Batch → 422, `report.submit` ohne reportId → `rejected invalid`, `/m` 200, `/dashboard` → 307. Mandantentest demo2 übersprungen (aktueller Seed enthält keine Aufträge; L10a liefert Demo-Daten). Zusätzlich `scripts/smoke-auth.ts` (Architekt) gegen :3111 **19/19 grün** (Backoffice-Seiten mit neuem Layout, Monteur-Seiten).
**Docker:** `docker build --target runner` (400 MB) und `--target worker` (2,68 GB) lokal erfolgreich; im Worker-Image startet Chromium mit `cap_drop ALL`/`no-new-privileges`/1 GB shm und erzeugt ein PDF. Kein Push.
## 4. Stubs / Abhängigkeiten
- Keine neuen Stubs. Der L2-Stub `sync-reapply.ts` ist durch den L4-Dispatcher ersetzt.
- Genutzt: L1 `requireApiContext`/respond.ts, L4 `applyOperations`/Registry, L5 `submitReport`/`updateReportTexts`/`requireVisibleReport`, L7 `useOfflineDraft`, L9 `applyLotseReview`, L2 `applySyncConflict`.
## 5. Bekannte Lücken / offene Punkte
1. **Rate Limit je App-Instanz** (In-Memory wie SEC2); bei mehreren Replikas zählt jede Instanz getrennt. Geteilter Redis-Zähler = SEC5.
2. **`/api/v1` nur mit Session-Cookie** – kein Token für Integrationen (unverändert).
3. **`signature.capture` offline** nicht registriert: `/api/v1/uploads` kennt keine Upload-Art für das Unterschriftsbild (`kind: signature`, PNG). Die mobile Berichts-/Unterschrift-UI nutzt weiterhin Server Actions; die neuen Ops `report.save_draft`/`report.submit` stehen der Outbox bereit, die UI ist aber noch nicht auf `submitOp` umgestellt (L5/L7).
4. **Events in Transaktionen:** `emitEvent` innerhalb von `inTransaction` wird weiterhin sofort ausgeführt (Benachrichtigung bei späterem Rollback möglich) – analog zu h lösbar.
5. **Kontingent:** Monatsgrenze UTC; ein laufender Aufruf kann das Limit einmalig überschreiten; Transkription (Audio) zählt nicht; keine Warnung vor Erreichen.
6. **Aufbewahrung:** Sprachnotiz-Zusammenfassungen älter als die Frist können nicht mehr übernommen werden (Ausgabe geleert); das KI-Protokoll zeigt dann `null`.
7. **Sidebar** bewusst ab < 1024 px eingeklappt (Anforderung ≤ 768 px ist enthalten); **visuelle Browser-Prüfung nicht durchgeführt** (Login im Browser hätte eine Sitzung/Credential-Eingabe erfordert) – geprüft per Server-Rendering (Markup, Labels). Bitte manuell bei 375/768/1024 px ansehen.
8. **`POST /api/v1/work-orders/{id}/documents`** gibt das Dokument inkl. `storageKey` zurück (L2-Verhalten, unverändert) – interner Schlüssel sollte nicht nach außen.
9. **413 vs. 422:** Größenprüfungen in Import-/Dokument-Services melden `invalid file_too_large` (422); nur die Route-eigenen Vorprüfungen (`/uploads`) antworten 413.
10. **CI-Job nicht ausgeführt** (kein Runner): offen, ob `prisma db execute --stdin` für das `craftvia_app`-Passwort und Chromium im Gitea-Runner verfügbar sind (PDF-Test skippt sonst).
11. **Prebuilt-Deploy** braucht ein gepushtes `craftvia-worker`-Image (Skript baut es jetzt, Push nicht ausgeführt).
12. **RLS-Modus-Tests** (`test-tenant-isolation`, `test-garage-storage`) s. §3.
## 6. Screens / Routen
| Route | Änderung |
|---|---|
| `GET /api/v1/openapi.json` | neu – OpenAPI 3.1 |
| alle `/api/v1/**` | einheitliches Fehlerformat, Same-Origin für Mutationen, Rate Limit |
| `POST /api/v1/sync` | Ops `report.save_draft`, `report.submit` |
| `GET /api/v1/field/bundle` | `orders[].mySession` |
| Backoffice-Layout (alle `(app)`-Seiten) | Menü-Button + Drawer unter 1024 px |
| `/settings/lotse` | monatliches KI-Kontingent + Verbrauch |
| `/settings/audit` | Aktion „Lesezugriff" |
| `/work-orders/conflicts` | Übernehmen auch für abgesendete Berichte |
| `/m/orders/[id]/report` | Offline-Entwurf im Berichtseditor |
+110
View File
@@ -0,0 +1,110 @@
# Lane L4 – Einsatz mobil (`lane/einsatz`)
Stand: 2026-09-14 · Basis `bf44567` (`feature/craftvia-mvp`)
## 1. Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §11.2 Auftragsansicht | `/m` Heute (heute geplant + laufend + pausiert), `/m/orders` Tabs Kommend · Laufend · Abzuschließen · Vergangen; große Karten mit Nummer, Kunde, Objektadresse + Routenlink, Zeitfenster, Statusgruppe (Text + Icon + Statuskante), Primärbutton |
| §12.1 Einsatzstart | `session.start` speichert Startzeit, Benutzer, Team, Auftrag, optional Standort, Gerätestatus (User-Agent), Offline-Flag; optional „Losfahren“ (Anfahrt) vor Arbeitsbeginn |
| §12.2 Arbeitszeiten | Session aus TimeEntry-Segmenten (Anfahrt → Arbeit ⇄ Pause), Start/Pause/Weiter/Ende, Dauerberechnung; Korrektur nur mit `field:correct_time`, Grund Pflicht, `corrected`-Flag, Audit before/after |
| §12.3 Tätigkeitsdokumentation | Notizen mit Art (9 große Chips) + Text; Entwurf bleibt bei Verbindungsabbruch lokal erhalten (US-006); Sprachnotiz |
| §12.4 Checklisten | Abhaken mit großem Schalter + Kommentar |
| §13.2/§13.3 Material | Planposition vollständig/teilweise/nicht verwendet mit Stepper-Menge; Begründung bei Abweichung Pflicht (UI + Server, gemeinsame Regeln); Zusatzmaterial mit Bezeichnung, Artikelnummer, Menge, Einheiten-Vorschlägen, Grund |
| §14.1/§14.3/§14.4 Fotos | Kamera (`capture="environment"`) + Galerie; Phase Vorher/Während/Nachher, Pflichtfoto (Kategorie), Checklistenpunkt, Kommentar, optional Standort; Kompression im Browser (max. 2560 px, JPEG 0.82, Vorschaubild 400 px, EXIF-Orientierung über `imageOrientation: "from-image"`), Upload-Fortschritt; Metadaten: Auftrag, Einsatz, Benutzer, Zeit, MIME (Magic Bytes), Größe, Prüfsumme, Uploadstatus; serverseitiges Vorschaubild per Job, falls der Client keines liefert |
| §15.1 Sprachnotizen | MediaRecorder (webm/opus, mp4 auf iOS), max. 5 min, Upload → VoiceNote `pending` → `dispatchJob("transcription")`; ohne registrierten Processor (L9) Status `disabled` |
| §22 Mobile Ansicht | eigene Shell ohne Sidebar, Bottom-Nav Heute · Aufträge · Notdienst · Sync · Profil, Online/Offline-Badge, Touch-Ziele ≥ 48 px, eine Primäraktion je Zustand, Kamera in 2 Taps (Karte → Foto) |
| US-005 | Dokumente am Auftrag + Objekt (nur erlaubte Sichtbarkeit, `backoffice_only` verborgen), freigegebene Berichte der Objekt-Historie; Auslieferung über autorisierte Route |
| US-006 | Einsatz starten/beenden, Fotos, Material bestätigen/ändern, Zusatzmaterial, Notizen mit lokalem Entwurf |
| ARCHITEKTUR §4.6 (Server) | `src/lib/sync/ops.ts`, `applyOperations`, `POST /api/v1/sync`, `POST /api/v1/uploads`, `GET /api/v1/field/bundle?since=` |
Primäraktion je Zustand (`components/field/primary-action.tsx`): `assigned` → Annehmen · `accepted` → Losfahren (sekundär: Arbeit starten) · `en_route` → Arbeit starten · in Arbeit mit laufender eigener Session → Abschließen (sekundär Pause) · pausiert → Weiter · `technically_completed`/`signature_pending` → Hinweis „Bericht und Unterschrift folgen“. Abschließen beendet die eigene Session und ruft `work_order.transition → technically_completed`; offene Pflichtpunkte werden vorher als Liste angezeigt und der Button gesperrt.
## 2. Routen / Screens
| Route | Inhalt |
|---|---|
| `/m` | Heute |
| `/m/orders?tab=` | Auftragsliste mit Tabs |
| `/m/orders/[id]` | Kopf (Status, Zeitfenster, Primäraktion), Schnellaktionen (Foto direkt · Notiz · Material · Checkliste · Zeiten), Hinweise für Monteure, Objekt (Zugang/Parken/Sicherheit/Technik hervorgehoben, Route), Kunde/Ansprechpartner (`tel:`/`mailto:`), Auftrag, Dokumente, Objekt-Historie, Zusammenfassungen |
| `/m/orders/[id]/photos` · `/notes` · `/materials` · `/checklist` · `/time` | Unterseiten (max. 2 Ebenen unter der Liste) |
| `/m/profile` | Name, Betrieb, Team, Rolle, Sprache, Abmelden (Link ins Büro für Teamleiter/Backoffice) |
| `/m/sync` | **Platzhalter** für L7 (Verbindungsstatus, „Änderungen werden sofort übertragen“) |
| `/m/emergency` | unverändert Platzhalter von L8 (nur verschoben) |
| `POST /api/v1/sync` | Batch-Ops → `SyncResponse` |
| `POST /api/v1/uploads` | multipart `file`, `clientId`, `workOrderId`, `kind` (`photo`/`voice_note`), optional `preview` → `{ documentId, duplicate }` |
| `GET /api/v1/field/bundle?since=` | Offline-Pull |
| `GET /api/v1/field/documents/[id]?variant=preview` | autorisierte Dokument-Auslieferung (Sichtbarkeit + Scope) |
Route-Group-Umzug: `src/app/(app)/m/**` → `src/app/(field)/m/**`. `(field)/m/layout.tsx` = Shell (Zugriffsprüfung), `(field)/m/(core)/layout.tsx` = Modul-Gate `field`, `emergency/layout.tsx` = Modul-Gate `emergency` (unverändert). Die Zugriffsprüfungen aus `(app)/layout.tsx` sind nach `src/server/app-access.ts#requireAppAccess` extrahiert und werden von beiden Shells genutzt (erlaubter Fundament-Eingriff, ARCHITEKTUR §5).
Header-Slot für L6: im Mobile-Header (`(field)/m/layout.tsx`) ist links neben dem Online-Badge Platz für `<NotificationBell variant="mobile" />` markiert.
## 3. Dateien
**Neu (Ownership L4)**
- `src/app/(field)/m/layout.tsx`, `(core)/layout.tsx` (verschoben), `(core)/page.tsx`, `(core)/orders/page.tsx`, `(core)/orders/[id]/{page.tsx,load.ts}`, `(core)/orders/[id]/{photos,notes,materials,checklist,time}/page.tsx`, `(core)/profile/page.tsx`, `sync/page.tsx` (Platzhalter L7)
- `src/app/api/v1/sync/route.ts`, `src/app/api/v1/uploads/route.ts`, `src/app/api/v1/field/bundle/route.ts`, `src/app/api/v1/field/documents/[id]/route.ts`
- `src/server/services/field/`: `common.ts`, `sessions.ts`, `time-correction.ts`, `checklist.ts`, `materials.ts`, `notes.ts`, `photos.ts`, `voice.ts`, `uploads.ts`, `documents.ts`, `queries.ts`, `page-context.ts`, `stubs/{work-order-transition,documents-store,site-history}.ts`
- `src/server/services/sync/apply.ts`, `src/server/services/sync/api-context.ts`, `src/server/services/sync/external-ops.ts` (Registry für Ops anderer Lanes)
- `src/server/actions/field/time.ts` (Zeitkorrektur, `moduleGuard("field")`)
- `src/server/jobs/processors/image-derivatives.ts`
- `src/lib/sync/ops.ts`, `src/lib/field/{client-ops,upload,image,format,material-rules}.ts`
- `src/components/field/*` (Shell-Navigation, Online-Badge, Statusbadge, Auftragskarte, Primäraktion, Foto, Sprachnotiz, Notiz, Material, Stepper, Checkliste, Zeitkorrektur, UI-Klassen)
- `messages/de/field.json`, `messages/en/field.json`
- `scripts/test-einsatz-field.ts`, `scripts/test-einsatz-sync.ts`, `scripts/lib/einsatz-fixture.ts`
**Fundament / Fremd-Einzeiler**
- `src/server/app-access.ts` (neu, extrahiert) + `src/components/account-inactive-notice.tsx` (Hinweis „Konto deaktiviert“ aus dem Layout herausgelöst, Text unverändert) + `src/app/(app)/layout.tsx` nutzt beides
- `src/app/page.tsx`: rollenabhängige Startseite (`landingPath`: `field:execute` ohne `work_order:read_all` → `/m`, sonst `/dashboard`)
- `src/app/login/page.tsx`, `src/app/login/mfa/page.tsx`: Default-Redirect nach Login `/dashboard` → `/` (je 1 Zeile, + „bereits angemeldet“-Redirect)
- `src/server/jobs/processors/index.ts`: Registrierung `image-derivatives` (1 Zeile)
- `src/components/audit-trail.tsx`: Entity-Labels für `work_session`, `time_entry`, `checklist_item`, `material_usage`, `photo`, `voice_note`, `activity_note`, `sync_operation` (1 Zeile)
Keine Schemaänderung, keine neue Migration, keine neuen npm-Abhängigkeiten (`sharp` ist über Next vorhanden).
## 4. Tests
- `scripts/test-einsatz-field.ts` – Session-Zustände (keine doppelte laufende Session, Pause-Segmente, Ende berechnet Dauer), Abschluss-Guards, Zeitkorrektur (Recht, Grund, Ende ≥ Beginn, Audit before/after), Material (Abweichung ohne Grund → invalid, Zusatzmaterial, Idempotenz), Checkliste/Notiz, Scope (Monteur ohne Zuweisung → not_found; ohne `field:execute` → forbidden), Sichtbarkeit/Bundle, Mandantentrennung.
- `scripts/test-einsatz-sync.ts` – Idempotenz (gleiche clientOpId → duplicate, keine Doppelanlage, je Mandant), Konflikt bei veralteter baseVersion (nichts überschrieben, SyncOperation `conflict`), Scope über Sync (fremder Auftrag → not_found, kein Versions-Leak), Ops fremder Lanes (rejected invalid, nicht gespeichert), Uploads (Idempotenz, Magic Bytes, Mandantentrennung beim Hochladen/Öffnen/Anhängen, `backoffice_only` verborgen), Foto/Sprachnotiz (Status `disabled`), image-derivatives-Processor inkl. Fremdmandant.
Ergebnis: siehe Abschnitt 7 (Gate).
## 5. Stubs & Abhängigkeiten zu anderen Lanes
| Stub (in L4-Pfad) | Vertrag | Ersetzen durch |
|---|---|---|
| `services/field/stubs/work-order-transition.ts` | `transitionWorkOrder(ctx, { workOrderId, to, reason?, baseVersion? }) → { id, from, status, version }`; `completionBlockers(ctx, workOrderId) → CompletionBlocker[]`; wirft `ServiceError` inkl. `blocked` mit Blockern | L2 `services/work-orders/transition.ts` (Importe in `sessions.ts`, `queries.ts`, `sync/apply.ts`, Test) |
| `services/field/stubs/documents-store.ts` | `storeFile(ctx, { bytes, fileName, declaredMime, category, visibility, links, lineageId? }) → Document` (§4.3) inkl. Magic-Byte-Prüfung/Limits/SHA-256 | gemeinsames `services/documents/store.ts` (Import in `uploads.ts`) |
| `services/field/stubs/site-history.ts` | `getSiteHistory(ctx, siteId, { onlyApproved, limit? }) → SiteHistoryEntry[]` | L1 (Importe in `queries.ts`) |
| `app/(field)/m/sync/page.tsx` | Platzhalter | L7 ersetzt die Seite und `lib/field/client-ops.ts#submitOp` (Signatur stabil) |
| Sync-Ops `report.save_draft`, `report.submit`, `signature.capture` | Lazy-Import-Registry `services/sync/external-ops.ts` (auskommentierte Einzeiler) → `services/reports/sync-ops.ts#applySyncOp(ctx, op) → { idMap?, entityVersion? }` | L5 liefert das Modul und aktiviert je Op eine Zeile in `external-ops.ts`; bis dahin `rejected invalid` („not available yet“), nicht gespeichert |
| Sync-Op `emergency.create` | Registry-Einzeiler in `services/sync/external-ops.ts` → `services/emergency/sync-ops.ts#applySyncOp` | L8 |
| Transkription | Processor `transcription` in `jobs/processors/index.ts` | L9; bis dahin VoiceNote `disabled` |
| `/m/orders/[id]/{report,sign}` | noch nicht angelegt | Komponenten von L5, Einbindung durch L4 nach Merge |
## 6. Bekannte Lücken / Hinweise an den Architekten
0. **Registry statt berechnetem `import()`**: Ein Import mit berechnetem Pfad (`import(`../${dir}/sync-ops`)`) kann Turbopack nicht auflösen (Build-Warnung, Modul wäre nach Merge nicht ladbar). Daher explizite Registry `services/sync/external-ops.ts` – L5/L8 brauchen dort je Op einen Einzeiler.
1. **Offline**: Ops werden sofort gesendet; ohne Verbindung wird nichts zwischengespeichert (außer Notiz-Entwurf). Outbox/Service Worker = L7.
2. **Bundle-Löschungen**: `bundle?since=` liefert nur geänderte/offene Aufträge, keine Tombstones für entzogene/abgeschlossene Aufträge (Client muss Vollabgleich ohne `since` machen).
3. **Jobs (Fundament-Bug)**: `enqueueJob` erzeugt `jobId` mit `:` – BullMQ lehnt das ab („Custom Id cannot contain :“), `dispatchJob` fällt deshalb immer auf Inline-Ausführung zurück. Fix in `src/server/jobs/queues.ts` (z. B. `-` statt `:`) nötig.
4. **`requireApiContext`** wird in `services/context.ts` erwähnt, existiert im Fundament aber nicht → lane-lokal in `services/sync/api-context.ts` (nutzt `moduleGuard`, Same-Origin-Prüfung, Fehler-Mapping). Kandidat für das Fundament.
5. **DSGVO `pii-fields.ts`** (Fundament): Personenreferenzen der Einsatzmodelle sind noch nicht eingetragen: `WorkSession.userId`, `TimeEntry.userId`, `TimeEntry.correctedById`, `ActivityNote.authorId`, `Photo.takenById`, `VoiceNote.recordedById`, `MaterialUsage.recordedById`, `ChecklistItem.checkedById`, `Document.uploadedById`, `SyncOperation.userId`/`resolvedById`.
6. **`/files/[...key]`** prüft weiterhin nur das Mandantenpräfix (TODO documents); die Mobile-App nutzt deshalb `/api/v1/field/documents/[id]` mit Sichtbarkeits- und Scope-Prüfung.
7. **Transaktionen**: Session-Operationen (Segment schließen/öffnen + Statuswechsel) laufen sequenziell ohne DB-Transaktion; bei parallelen Doppelstarts desselben Users ist eine zweite aktive Session theoretisch möglich (kein Unique-Index). Idempotenz über `clientId`/`clientOpId` deckt Wiederholungen ab.
8. **„Heute“** wird in der Server-Zeitzone berechnet, Anzeige in `Europe/Berlin` (`lib/field/format.ts`); Mandanten-Zeitzone fehlt im Modell.
9. **Passkey-Login** und Mandantenwechsel (`tenant-switch.ts`, `/select-tenant`) leiten weiter fest auf `/dashboard` – Feldrollen landen dort erst über die Startseite `/`, nicht direkt auf `/m`.
10. **HEIC** ohne Browser-Dekodierung: Upload scheitert mit Hinweis (keine serverseitige Konvertierung).
## 7. Gate & Smoke
**Tests:** `test-einsatz-field.ts` 48 Prüfungen, `test-einsatz-sync.ts` 38 Prüfungen – alle grün (S3/Garage-Pfad aktiv; ohne `S3_ENDPOINT` werden nur die Byte-Abrufe übersprungen).
**Gate:** `npm run gate` grün – prisma generate, tsc, lint (0 Fehler, 2 Warnungen im Fundament-Platzhalter `handle-event.ts`), build inkl. Modul-Guard-Check (14 Action-Dateien), 24/24 Testskripte.
**HTTP-Smoke** (Dev-Server :3104, Demo-Mandant mit Smoke-Auftrag, Session-Cookies für `monteur@`, `multi@` (Monteur ohne Zuweisung) und `admin2@` (Mandant demo2)): 32/32 Prüfungen grün – Startseite `/` → `/m` (Monteur) bzw. `/dashboard` (Admin), alle Mobile-Seiten 200 mit erwarteten Inhalten, fremder/zugriffsloser Auftrag → 404, Backoffice-Dashboard rendert weiterhin, Sync (not_found für fremd, applied+duplicate, conflict bei veralteter Version, 400 bei leerem Batch, 403 bei Cross-Origin, ohne Session → Login-Redirect), Bundle, Upload 201/200 (idempotent)/404 (fremder Mandant), Dokument-Original + Vorschau 200 bzw. 404 für fremd, `photo.attach` applied.
**Mobile-UX 375×812** (Browser-Pane): große Karten mit Statuskante, Status als Text + Icon, eine orange Primäraktion, Schnellaktionen mit Kamera direkt (Heute → Karte → Foto = 2 Taps), Bottom-Nav 64 px, Buttons ≥ 48 px, max. 2 Ebenen unter der Liste; Klickpfad Annehmen → Losfahren über die UI geprüft.
+106
View File
@@ -0,0 +1,106 @@
# Lane L3 – Auftragsimport
Branch `lane/import` (von `feature/craftvia-mvp` @ `bf44567`). Spec §9 komplett, §7.3, §31, US-002, US-003, ARCHITEKTUR §4.4/§4.5.
## Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §9.2 Dateien | PDF (Text und Scan: Claude liest PDF nativ als `document`-Block, inkl. Seitenbild → kein separates OCR), JPG, PNG. Allowlist + Magic Bytes + Größenlimit (PDF 25 MB, Bild 15 MB) |
| §9.3 1–2 Upload, Typprüfung | `services/imports/upload.ts#createImport` → Dokument (Kategorie `order_confirmation`, Sichtbarkeit `backoffice_only`, SHA-256) → `ImportJob uploaded` → `dispatchJob("import-extraction")` |
| §9.3 3–5 Texterkennung, Analyse, Extraktion | `ai/extraction/anthropic.ts` (Anthropic SDK 0.115, Modell `ANTHROPIC_MODEL`, Default `claude-opus-5`): Streaming + `finalMessage()`, adaptive thinking, Structured Output (`output_config.format` JSON-Schema aus `lib/imports/extraction.ts`), Volltext `text` + 22 Felder mit `value/confidence/source`. Prompt: nichts erfinden, unsichere Felder < 0.8, deutsche Datums-/Zahlenformate normalisieren, Kunde ≠ Briefkopf, Objekt ≠ Kundenadresse. Refusal/`max_tokens` → Fehler; bei Opus 5 serverseitiger Fallback (`fallbacks: "default"`, Beta `server-side-fallback-2026-07-01`) |
| §9.3 6 Plausibilität | `lib/imports/plausibility.ts`: Datum gültig und plausibel (Auftrags-/Dokumentdatum ≤ +1 Monat, Ausführung −2…+3 Jahre), PLZ 5-stellig (DE), E-Mail-/Telefonformat, Ende ≥ Beginn. Verstoß: Konfidenz ≤ 0.4 + Hinweis |
| §9.3 7 / §7.3 / US-003 Dubletten | Kandidaten über Kundennummer, Firmenname (Rechtsform-normalisiert), Personenname, E-Mail, Telefon, Adresse (Straße/Str./Strasse) mit Score + Gründen; Objekt-Kandidaten = Objekte der Kandidaten an gleicher Adresse. Keine automatische Zuordnung/Zusammenführung |
| §9.5 Vertrauenswerte | je Feld gespeichert (`ImportJob.extraction.fields`), Prüfmaske markiert < 0.8 mit Warnfarbe + Icon + Text „unsicher · Sicherheit n %“, Quelle als Tooltip und Hinweistext |
| §9.6 Manuelle Prüfung | `/imports/[id]`: links Original (PDF-iframe / Bild), rechts Formular Kunde · Objekt · Ansprechpartner · Auftrag · Positionen/Material; Kundenentscheidung (Kandidat mit Score/Gründen · Suche · neu anlegen · Link „Dubletten im Kundenstamm zusammenführen“ → L1), Objekt analog (keins/bestehend/neu), Positionen editierbar mit „als Materialvorgabe übernehmen“. **Kein Auftrag ohne Bestätigung** |
| §9.3 10 Bestätigung | `services/imports/confirm.ts#confirmImport`: eine Transaktion – atomarer Statuswechsel `review_required → confirmed` (verhindert Doppelaufträge), Kunde/Kontakt/Objekt anlegen oder zuordnen, `createWorkOrder` (Status-Historie `review_required → planned`, `sourceImportId`, Materialvorgabe), Originaldokument an Auftrag/Kunde/Objekt verknüpft, `corrections` (Diff Extraktion ↔ bestätigt + Entscheidungen). Audit: `import_job` (import), `work_order`/`customer`/`site`/`contact` (create) |
| §9.7 Originaldokument | bleibt dauerhaft (auch bei Verwerfen); gespeichert: Importdatum, importierender Nutzer, erkannter Text, Extraktion, Extraktionsversion, Modell, Provider, Korrekturen, `AiGeneration` |
| §31 Provider-Abstraktion | `DocumentExtractionProvider` (ARCHITEKTUR §4.5), `getExtractionProvider()` → `null` ohne Key bzw. bei `AI_EXTRACTION_PROVIDER≠anthropic`; `FakeExtractionProvider` für Tests |
| Graceful degradation | ohne Provider: `review_required` mit leerer Extraktion + Hinweis „manuell erfassen“ |
| Fehler | Provider-/Dateifehler → `failed` + `errorMessage` + Audit + Event `import.failed`; „Neu verarbeiten“ (`failed → uploaded` + Dispatch). Erfolg → Event `import.ready_for_review` |
## Routen / Screens
| Route | Inhalt |
|---|---|
| `/imports` | Upload (Drag & Drop + Dateiauswahl, Fortschrittsbalken per XHR), Liste mit Status (Pill = Farbe + Icon + Text), Aktionen Prüfen / Neu verarbeiten / Auftrag, Auto-Refresh während Verarbeitung |
| `/imports/[id]` | Prüfmaske bzw. Statusansicht (Verarbeitung, fehlgeschlagen + Neu verarbeiten/Verwerfen, bestätigt + Link zum Auftrag, verworfen), erkannter Text aufklappbar |
| `/imports/[id]/file` | Original für die Vorschau (inline, nur eigener Origin als Frame; `?download=1`) – Session + `import:write` (DB-autoritativ) + Modul + Dokument-Sichtbarkeit |
| `POST /api/v1/work-orders/import` | multipart `file` → `201 { id, status }` |
| `GET /api/v1/imports/[id]` | Status, Extraktion inkl. Konfidenzen, Kandidaten |
| `POST /api/v1/imports/[id]/confirm` | JSON = Prüfformular (`lib/imports/review.ts#reviewFormSchema`) → `{ workOrderId, workOrderNumber, customerId, siteId, contactId }` |
Fehler-Mapping API: `not_found` 404, `forbidden` 403, `invalid` 400 (mit Feldpfaden), `conflict` 409, ohne Session 401.
## Dateien
- `src/lib/imports/` – `extraction.ts` (Felder, Zod, JSON-Schema, gespeicherte Form), `plausibility.ts`, `review.ts` (Formularschema, Mapping, Feld-Konfidenzen, Korrektur-Diff), `status.ts`
- `src/server/ai/extraction/` – `anthropic.ts` (Provider + `getExtractionProvider`), `fake.ts`
- `src/server/services/imports/` – `upload.ts`, `process.ts`, `confirm.ts` (inkl. Verwerfen/Neu verarbeiten), `queries.ts`, Stubs `document-store-stub.ts`, `duplicates-stub.ts`, `work-orders-stub.ts`
- `src/server/jobs/processors/import-extraction.ts`
- `src/server/actions/imports/imports.ts` (confirm, discard, retry, Kundensuche – je `moduleGuard("imports")` + `await guard(...)`)
- `src/app/(app)/imports/page.tsx`, `src/app/(app)/imports/[id]/page.tsx`, `src/app/(app)/imports/[id]/file/route.ts`
- `src/app/api/v1/imports/_context.ts`, `src/app/api/v1/imports/[id]/route.ts`, `src/app/api/v1/imports/[id]/confirm/route.ts`, `src/app/api/v1/work-orders/import/route.ts`
- `src/components/imports/` – `uploader.tsx`, `review-form.tsx`, `status-pill.tsx`, `job-actions.tsx`, `auto-refresh.tsx`
- `messages/de/imports.json`, `messages/en/imports.json`
- `scripts/make-sample-pdfs.ts` → `docs/craftvia/samples/01-musterbau-auftragsbestaetigung.pdf`, `02-elbblick-wartungsauftrag.pdf`, `03-privatkunde-reparatur.pdf` (fiktiv; Nr. 3 enthält absichtlich PLZ „2148“, E-Mail „(at)“ und Ende vor Beginn für die Plausibilitätshinweise)
- Tests: `scripts/test-import-rules.ts`, `scripts/test-import-flow.ts`, `scripts/test-import-live.ts`
Fremd-Einzeiler: `src/server/jobs/processors/index.ts` (Processor-Registrierung). `src/lib/nav.ts` enthielt `/imports` bereits.
## Tests
| Skript | Prüfungen | Inhalt |
|---|---|---|
| `test-import-rules.ts` | 55 | Plausibilitätsregeln, Parsing der Provider-Ausgabe, JSON-Schema (strict-tauglich), Mapping Extraktion → Formular, Formularvalidierung, Korrektur-Diff, Normalisierung Dubletten, Magic Bytes/Dateiname, PDF-Generator |
| `test-import-flow.ts` | 75 | Upload/Validierung, Verarbeitung mit Fake-Provider, Dubletten-/Objektkandidaten, AiGeneration, kein Auftrag ohne Bestätigung, Idempotenz; **Rollen** (Monteur/Teamleiter → `forbidden`); **Mandantentrennung** (B kann A weder lesen, laden, bestätigen, verwerfen, neu verarbeiten noch A-Kunden zuordnen; Kandidaten/Suche nur im Mandanten); Bestätigung bestehender vs. neuer Kunde inkl. Dokumentverknüpfung, Korrekturen, Audit; Doppelbestätigung → conflict; vergebene Kundennummer → conflict ohne Teilanlage; Verwerfen; Provider-Fehler/Datei fehlt/Dispatch-Fehler → failed + Neu verarbeiten; ohne Provider → manuell |
| `test-import-live.ts` | 6 | nur mit `ANTHROPIC_API_KEY` (sonst übersprungen): echte Extraktion einer generierten Auftragsbestätigung |
## Gate
`npm run gate` grün (Lane-DB `craftvia_import`, `RLS_DATABASE_URL` auf dieselbe DB): prisma generate, `tsc` ohne Fehler, Lint 0 Fehler (2 Warnungen im Fundament-Platzhalter `services/notifications/handle-event.ts`), Build inkl. Modul-Guard-Check (14 Action-Dateien), **25/25 Testskripte grün**. `test-import-live.ts` ohne `ANTHROPIC_API_KEY` übersprungen (Exit 0) – die Live-Extraktion gegen Claude ist damit **nicht** verifiziert.
## Smoke (Dev-Server :3103, curl mit Seed-Logins)
| Prüfung | Ergebnis |
|---|---|
| ohne Session: `/imports`, `/imports/[id]`, `/imports/[id]/file`, `/api/v1/**` | 307 → `/login` |
| Backoffice `GET /imports` | 200, Upload-Bereich gerendert |
| Backoffice `POST /api/v1/work-orders/import` (Beispiel-PDF 01) | 201, ohne API-Key direkt `review_required` mit Hinweis „manuell erfassen“ |
| Upload `package.json` | 400 |
| `GET /api/v1/imports/[id]` / unbekannte ID | 200 / 404 |
| `GET /imports/[id]` | 200, Prüfmaske + Originaldokument gerendert |
| `GET /imports/[id]/file` | 200, `inline`, **aber `X-Frame-Options: DENY`** (siehe Bedarf Punkt 2) |
| `POST /api/v1/imports/[id]/confirm` mit `{}` | 400 mit Feldpfaden |
| Monteur: API / Datei / Upload | 403 / 404 / 403 |
Keine visuelle Browser-Prüfung (Login-Maske würde Passworteingabe durch den Agenten erfordern) – Layout 1024/768/375 px noch manuell prüfen. Der Smoke hinterlässt einen Import im Mandanten `demo` der Lane-DB.
## Stubs / Abhängigkeiten zu anderen Lanes
| Stub (L3-Pfad) | Vertrag | Ablösen |
|---|---|---|
| `services/imports/document-store-stub.ts#storeFile` | ARCHITEKTUR §4.3 `services/documents/store.ts#storeFile` (auf dem Basis-Commit nicht vorhanden; keiner Lane zugeordnet) | Import in `upload.ts` umstellen; `readDocumentBytes` durch Service-Funktion ersetzen |
| `services/imports/duplicates-stub.ts#findDuplicateCustomers` | L1 `lib/customers/duplicates.ts#findDuplicateCustomers(db, candidate) → { customerId, score, reasons[] }[]` | Import in `process.ts` umstellen (`findSiteCandidates` bleibt L3) |
| `services/imports/work-orders-stub.ts#createWorkOrder` + `CreateWorkOrderInput` | L2 `createWorkOrder(ctx, input)`; Stub schreibt Status-Historie direkt statt über `transitionWorkOrder` | Import in `confirm.ts` umstellen; L2-Service muss einen Transaktions-Client in `ctx.db` akzeptieren und `sourceImportId`, Status `planned` (Historie ab `review_required`) und Materialvorgabe unterstützen |
| `app/api/v1/imports/_context.ts#importsApiContext` | gemeinsames `requireApiContext` (in `services/context.ts` erwähnt, nicht vorhanden) | durch zentrale Funktion ersetzen |
Links in andere Lanes: `/work-orders/[id]` (L2), `/customers/[id]` für das Zusammenführen (L1, Merge mit Bestätigung).
Hinweis Pfad: `src/app/api/v1/work-orders/import/route.ts` liegt im L2-Ordner `api/v1/work-orders` (vom Auftrag so gefordert) – neue Datei, kein Konflikt mit L2-Dateien zu erwarten.
## Bedarf an Fundament / Architektur (nicht geändert)
1. **Upload > 10 MB:** `src/proxy.ts` puffert Request-Bodies standardmäßig nur bis 10 MB (`proxyClientMaxBodySize`, danach wird der Body still abgeschnitten). Für die geforderten 25 MB in `next.config.ts` `experimental.proxyClientMaxBodySize: "26mb"` setzen. Server Actions werden für Uploads bewusst nicht genutzt (1-MB-Limit).
2. **PDF-Vorschau im iframe:** globale Header in `next.config.ts` setzen `X-Frame-Options: DENY` und `frame-ancestors 'none'` für alle Pfade. Die Datei-Route setzt `SAMEORIGIN`/`frame-ancestors 'self'`, **im Smoke verifiziert: der globale Header gewinnt (`X-Frame-Options: DENY`) → die iframe-Vorschau bleibt im Browser leer**; „In neuem Tab öffnen“ funktioniert. `object`/`embed` unterliegen derselben Regel, daher kein Umweg in L3. Fix im Fundament: in `next.config.ts` für `/imports/:id/file` `X-Frame-Options: SAMEORIGIN` und `frame-ancestors 'self'` statt `DENY`/`'none'` ausliefern. Außerdem braucht die Vorschau echte Bytes → `S3_*` muss gesetzt sein (ohne S3 speichert der Storage-Stub keine Bytes; Verarbeitung mit Provider endet dann mit `file_unavailable`).
3. **RLS_ENFORCED=true:** `dbForTenant` öffnet je Operation eine eigene Transaktion; innerhalb von `ctx.db.$transaction` (Bestätigung) ist das nicht atomar. Betrifft alle Lanes mit Transaktionen – zentral in `db.ts` lösen.
4. `documents/store.ts` (§4.3) ist keiner Lane zugeordnet – Zuständigkeit klären.
5. **Eigene Lane-Datenbank und RLS-Test:** `scripts/test-rls-enforcement.ts` legt Fixtures über `DATABASE_URL` an, verbindet `craftvia_app` aber ohne `RLS_DATABASE_URL` fest auf die DB `craftvia`. Mit `DATABASE_URL=…/craftvia_import` schlägt der Test deshalb fehl (0 Zeilen, FK-Verletzung) – kein Codefehler. Gate daher mit `RLS_DATABASE_URL=postgresql://craftvia_app:craftvia_app_local@localhost:5432/craftvia_import?schema=public` ausgeführt; der Test ist damit grün. Vorschlag Fundament: Default-URL aus `DATABASE_URL` ableiten.
## Bekannte Lücken
- Keine Migration nötig: Hinweise und Objektkandidaten liegen in `ImportJob.extraction` (`{ fields, hints, siteCandidates }`), Dublettenkandidaten in `duplicate_candidates`.
- Malware-Scan nur Magic-Byte-/Typprüfung (ClamAV-Hook gehört zum Dokumentservice §4.3).
- Kostenlimit/Region/Aufbewahrung je Mandant (§31) nicht umgesetzt; Konfiguration nur per Env.
- Kundensuche in der Prüfmaske: Top 10 nach Name/Nummer/Ort/E-Mail; kein Paging.
- Keine Teamzuweisung/Auftragsart in der Prüfmaske (erfolgt danach im Auftrag, L2).
- Worker: mit `REDIS_URL` läuft die Extraktion in `npm run worker:craftvia`; ohne laufenden Worker bleibt ein Import auf „Hochgeladen“.
+52
View File
@@ -0,0 +1,52 @@
# Lane L11 – Kundenversand (`lane/kundenversand`)
Stand: 2026-09-14 · Basis `bc58738` (`feature/craftvia-mvp`, L1–L9 + L10b integriert) · Spec §17.3 („optional per E-Mail versendet"), §36.2 (Soll)
## 1. Umfang / erfüllte Punkte
| Punkt | Umsetzung |
|---|---|
| **Mail-Fundament: Anhänge per Referenz** | `MailJob.attachments?: MailAttachmentRef[]` (`{ documentId }`) – in Redis landen nur IDs, nie Bytes. `EnqueueInput.attachments` nur mit `tenantId` (sonst Fehler vor dem MailLog-Insert). `OutgoingMail.attachments` → nodemailer. `MailLog` unverändert (keine Spalte). |
| **Zustellung (deliver.ts)** | `loadMailAttachments(mailLogId, refs)` vor `provider.send`: Mandant **ausschließlich aus der MailLog-Zeile**, Document mit `tenantId = MailLog.tenantId`, `deletedAt: null`, Storage-Key mit Mandanten-Präfix, Bytes über `readStoredBytes` (Lazy-Import), SHA-256 = `checksum`, Summe ≤ `MAIL_MAX_ATTACHMENT_BYTES` (Default 10 MB), max. 10 Anhänge. Fehler → `MailAttachmentError` → MailLog `failed` mit klarer Meldung, **keine** Mail (auch nicht ohne Anhang). Speicher nicht erreichbar → `TransientMailError` (Retry). `deliverMail(job, { provider? })` für Tests (additiv). |
| **Template** | `craftvia_report_customer` (de/en), eigene Liste `CUSTOMER_TEMPLATE_KEYS` (SEC1-/L6-Listen und deren Tests unverändert). Vars `{ customerName, tenantName, reportTitle, reportDate, message? }`. Kein App-Link, Hinweis „Der Arbeitsnachweis ist als PDF angehängt.", Fußzeile „…von <Betrieb> über Craftvia versendet". Betreff einzeilig (CR/LF entfernt), Freitext HTML-escaped (email-brand). Absendername/Reply-To des Mandanten über bestehendes `tenantSender`. |
| **Service** | `services/reports/send-to-customer.ts#sendReportToCustomer(ctx, { reportId, to?, message? }, deps?)`: `report:approve`, `requireVisibleReport`, Status `approved` sonst `blocked report_not_approved`, PDF-Dokument vorhanden (nicht gelöscht) sonst `blocked pdf_missing`, Empfänger `to` (Zod-E-Mail, getrimmt, klein) oder Ansprechpartner des Auftrags (nicht gelöscht) → Kunde, sonst `invalid recipient_missing` (`details.field = "to"`). `dedupeKey report-customer:<reportId>:<to>:<version>` → `duplicate`. Audit `export`/`report` mit `{ op: "send_to_customer", to, version, mailLogId, delivery, withMessage }`. Sprache = Mandanten-Locale (Default de). Zusätzlich `defaultReportRecipient`, `listCustomerMailings`. |
| **UI** | `/reports/[id]`: neuer Abschnitt „An Kunden senden" (nur `approved` + PDF + `report:approve`): Empfänger vorbelegt (Hinweis, wenn keiner hinterlegt), optionale Nachricht, Button (44 px); Rückmeldung versendet / wird versendet / bereits an … gesendet / fehlgeschlagen (Text + Icon); Liste „Bisherige Versände" aus `MailLog` (Adresse, Zeitpunkt, Version, Status mit Icon + Text). |
| **Server Action** | `actions/reports/send-to-customer.ts#sendReportToCustomerAction` (`moduleGuard("reports")`, `await guard("report:approve")`), State-Typ in `components/reports/send-to-customer-state.ts`. |
| **API** | `POST /api/v1/reports/{id}/send` (`requireApiContext("reports", "report:approve")`, `withApi` → Same-Origin), Body optional `{ to?, message? }`; 202 `queued`, 200 `sent`/`duplicate`/`failed`; Fehler 404/422 im Einheitsformat. OpenAPI-Eintrag `sendReportToCustomer`. |
## 2. Dateien
- **Neu:** `src/server/services/reports/send-to-customer.ts`, `src/server/actions/reports/send-to-customer.ts`, `src/components/reports/send-to-customer.tsx`, `src/components/reports/send-to-customer-state.ts`, `src/app/api/v1/reports/[id]/send/route.ts`, `scripts/test-report-customer-mail.ts`, dieser Bericht.
- **Geändert (additiv):** `src/server/mail/{job,service,provider,provider-smtp,deliver,templates}.ts`, `src/app/(app)/reports/[id]/page.tsx` (Imports + neuer Abschnitt), `src/lib/api/openapi.ts` (ein Pfad), `messages/{de,en}/reports.json` (`customerMail`).
- **Fremd-Eingriffe:** keine. **Migrationen:** keine (MailLog/Document reichen). **Neue Abhängigkeiten:** keine.
## 3. Tests
| Skript | Prüfungen | Inhalt |
|---|---|---|
| `test-report-customer-mail.ts` | 41 | Monteur/Teamleiter → forbidden, Mandant B (Senden, Versandliste) → not_found; nicht freigegeben / ohne PDF / PDF gelöscht → blocked mit reason; ohne Empfänger → invalid `recipient_missing`; Header-Injection-Adresse → invalid; Default-Empfänger Kontakt → Kunde bei gelöschtem Kontakt; MailLog pending + Template + Mandant + dedupeKey; Nutzlast nur Dokument-Referenz; Audit export; `enqueueMail` mit Anhang ohne tenantId → Fehler ohne MailLog; Dedupe (Groß-/Kleinschreibung) und erneuter Versand an andere Adresse; Zustellung mit Fake-Provider (Bytes = gespeichertes PDF, SHA-256 = checksum, `.pdf`/`application/pdf`, Betreff, Text ohne App-Link, Reply-To/Absendername, HTML-Escaping, MailLog sent); **Mandantentrennung beim Zustellen** (Dokument von B, Plattform-MailLog, unbekannte ID) → failed, nichts gesendet; Prüfsummen-Manipulation, soft-gelöschtes Dokument, Größenlimit → failed; Template de/en; SMTP-Durchstich gegen Mailhog (Anhang im Rohtext, MailLog sent; Skip ohne Mailhog) |
| `test-mail.ts`, `test-audit-mail-context.ts` | unverändert | grün |
**Gate (`npm run gate`) grün:** prisma generate, tsc, lint (0 Fehler, 3 vorbestehende Warnungen in fremden Dateien: `scripts/test-betrieb-api.ts`, `src/app/(app)/layout.tsx`, `src/server/services/field/mime.ts`), build inkl. Modul-Guard-Check (32 Action-Dateien), **53/53 Testskripte**. Lane-DB `craftvia_kundenversand`, `RLS_DATABASE_URL` auf dieselbe DB.
**HTTP-Smoke** (Dev-Server :3112 ohne `REDIS_URL` → Inline-Versand an Mailhog, Session-Cookies ohne Passworteingabe nach `scripts/smoke-auth.ts`, Fixtures im Mandanten `demo`, danach entfernt): **13/13 grün** – Backoffice sieht den Abschnitt mit vorbelegtem Empfänger bei freigegebenem Bericht, nicht bei eingereichtem; Monteur 404; API anonym 401, fremder Origin 403, Monteur 403, nicht freigegeben 422 `blocked`, ungültige Adresse 422, Backoffice 200 `sent`, zweiter Aufruf 200 `duplicate`; Seite zeigt den Versand mit Status; Mailhog enthält die Mail mit PDF-Anhang.
## 4. Stubs / Abhängigkeiten
Keine Stubs. Genutzt: L5 `requireVisibleReport`/`contentOf`/PDF-Dokument, L1 `readStoredBytes`, `requireApiContext`/`withApi`, SEC1/L6 Mail-Kern inkl. `tenantSender`.
## 5. Bekannte Lücken / offene Punkte
1. **Worker-Retries bei permanentem Anhangsfehler:** `worker.ts` (nicht Lane-Ownership) wiederholt jeden Nicht-`MailNotConfiguredError` bis zu 5-mal. `MailAttachmentError` ist permanent – das MailLog steht sofort auf `failed`, die Retries laufen aber ins Leere und enden in der Dead-Letter-Queue. Vorschlag (Einzeiler im Worker): `MailAttachmentError` wie `MailNotConfiguredError` in `UnrecoverableError` umwandeln.
2. **Ergebnis `failed` bei fehlender SMTP-Konfiguration:** Das MailLog bleibt `pending` (SEC1-Verhalten), die UI meldet „fehlgeschlagen"; ein erneuter Versand an dieselbe Adresse ist wegen Dedupe dann `duplicate`. Erneutes Zustellen ausstehender MailLogs ist Betriebsthema (kein Resend-Knopf im MVP).
3. Versandliste zeigt nur die aktuelle Berichtsversion (neue Version = neuer Bericht = neue Liste); keine Anzeige von Fehlertexten (bewusst, keine internen Details).
4. Keine Mehrfach-Empfänger/CC; Sprache folgt der Mandanten-Locale, nicht dem Kunden.
5. Umgebung: Das Kopieren der Haupt-`.env` wurde vom Berechtigungssystem blockiert; die Lane-`.env` wurde aus `.env.example` (lokale Docker-Defaults, eigene `AUTH_SECRET`/`PASSWORD_PEPPER`) mit DB `craftvia_kundenversand` und `RLS_DATABASE_URL` auf dieselbe DB erzeugt.
6. Visuelle Browser-Prüfung nicht durchgeführt (nur Server-Rendering/HTTP).
## 6. Screens / Routen
| Route | Änderung |
|---|---|
| `/reports/[id]` | Abschnitt „An Kunden senden" + „Bisherige Versände" |
| `POST /api/v1/reports/{id}/send` | neu |
+103
View File
@@ -0,0 +1,103 @@
# Lane L16 – Lotse-Chat für Monteure (`lane/lotse-chat`)
Stand: 2026-09-15 · Basis `6b8cdf5` (`feature/craftvia-mvp`, L1–L14 integriert) · Spec §15, §27, §31 · Brandbook §4.3, §9, §11.4, §12 · ARCHITEKTUR §4.5, §4.8
Ziel: In der mobilen Web-App schreibt oder spricht der Monteur wie in einem Chat („Auftrag fertig, Speicher installiert, 2 Std., 1 Filter verbaut“). Der Lotse ordnet den Auftrag zu, fragt fehlende Angaben nach und schlägt konkrete Aktionen vor. **Er führt nichts selbst aus** – jede Änderung braucht den Tipp des Monteurs auf „Bestätigen“.
## 1. Umfang / erfüllte Auftragspunkte
| Auftrag | Umsetzung |
|---|---|
| Datenmodell | `LotseConversation` (Mandant, Nutzer, optionaler Auftragskontext, `lastMessageAt`), `LotseMessage` (Rolle `user`/`assistant`/`tool`, Text, `content` JSON mit Chips, Karten-IDs, Sprungzielen, Modell-Fassung), `LotseActionProposal` (Art, Nutzlast, HMAC-`payloadHash`, Status `proposed/confirmed/discarded/expired/failed`, `expiresAt` = +30 min, Ergebnis/Fehlercode, `confirmedAt/By`, `decidedAt`). `TenantSettings.lotseChatEnabled` (Default an). Alle Tabellen mit `tenant_id` + RLS, in beiden `TENANT_MODELS`, Personenfelder in `pii-fields.ts`. Verlauf nur für den eigenen Nutzer (jede Abfrage filtert `userId = ctx.userId`). |
| Aufbewahrung | `services/lotse/retention.ts` (bestehender Job `ai-retention`): Gespräche mit `lastMessageAt` älter als `AI_GENERATION_RETENTION_DAYS` werden samt Nachrichten und Karten gelöscht (Chatverlauf = Protokolldaten eines Nutzers), offene abgelaufene Karten → `expired`; Audit `lotse_chat_retention`. |
| Provider | `ai/lotse/chat-anthropic.ts`: ein Schritt der Tool-Use-Schleife über das Anthropic SDK (`beta.messages.create`, Modell `ANTHROPIC_MODEL`), Opus 5 ohne Sampling-Parameter mit `effort: "medium"`, serverseitiger Refusal-Fallback wie L9, Timeout 60 s. Denkblöcke des Modells werden innerhalb einer Schleife unverändert zurückgegeben; über Nachrichten hinweg nur Text. `ai/lotse/chat-fake.ts` spielt geskriptete Tool-Aufrufe ab und zeichnet jeden Modell-Input auf. Ohne Key: „Lotse ist nicht eingerichtet“ (Senden gesperrt, Verlauf sichtbar). |
| Schleife & Budget | `services/lotse/chat/engine.ts`: höchstens 6 Modellschritte je Nachricht, Tokengrenze je Nachricht (`LOTSE_CHAT_TURN_TOKEN_LIMIT`, Default 80 000), Monatskontingent des Mandanten vor dem ersten Schritt (`budget.ts`, `blocked budget_exceeded`). Überschreitung → Klartext-Hinweis statt Absturz. Jede Nachricht mit Modellaufruf = **eine** `AiGeneration` (Art `lotse_chat`, Input = exakt gesendete minimierte Turns + System-Prompt + Werkzeugnamen, Output = alle Runden, Tokens summiert). |
| System-Prompt | `ai/lotse/chat-prompt.ts`: Deutsch, erfahrener Kollege, knapp (≤ 3 Sätze), Anrede aus Lotse-Einstellung (neutral/Sie/du), nie ausführen/nie „gebucht“ behaupten, nie raten → `ask_user` mit Auswahl, L12-Regeln (Grund, Freigabe, 7 Tage), Platzhalter unverändert übernehmen, Werkzeuginhalte sind Daten. Lage: Datum/Uhrzeit in Mandanten-Zeitzone, Kontext-Auftrag, laufende Uhr. |
| Datenminimierung | Nutzertext: Muster Telefon/E-Mail/Anschrift + Namen der Mitarbeitenden (`minimize.ts#scrubText`); Suchwörter wie „Müller“ bleiben, damit die Zuordnung funktioniert. Werkzeugergebnisse: alle Freitexte des Auftrags (Titel, Beschreibung, Hinweise, Checkliste, Pflichtfotos) über `scrubText` mit den Literalwerten der betroffenen Aufträge (Kunde, Kontakte, Objekt, Mandant); Kundennamen, Objektnamen, Telefon, E-Mail, Anschrift und Ansprechpartner **nur als Platzhalter** `{{phone:A-00042}}`. |
| Platzhalter-Technik (Begründung) | Fragt der Monteur ausdrücklich („Telefonnummer vom Ansprechpartner?“), übernimmt das Modell das Token in die Antwort; `chat/placeholders.ts#renderPlaceholders` setzt **serverseitig** den Wert ein – mit derselben Rangfolge wie das Auftragsdetail und erneuter Scope-Prüfung (fremd/unsichtbar → „—“). So ist die Frage im Chat beantwortbar, ohne dass der Wert je das Modell erreicht; das Modell kann einen Wert, den es nie gesehen hat, weder weitergeben noch verfälschen. Für den nächsten Turn wird die Token-Fassung (`content.modelText`) gesendet. |
| Lese-Werkzeuge | `list_my_orders` (heute + bis 7 Tage, laufende), `get_order_details` (Status, Zeitfenster, Beschreibung, Hinweise, Objekt-Hinweise inkl. Zugang, Checkliste, Plan-/Zusatzmaterial, Pflichtfotos, eigene Uhr, verfügbare Platzhalter), `get_running_clock`, `check_completeness` (L9 `completeness.ts`, Sprungziele), `search_material` (Planpositionen + bisher verwendete Bezeichnungen im Scope), `ask_user` (Rückfrage mit Chips, beendet die Schleife). Alles mit Monteur-ctx im Scope von `visibility.ts`. |
| Vorschlags-Werkzeuge | `transition_work_order` (annehmen, Anfahrt, Arbeit starten, Pause, fortsetzen, technisch abschließen; Abschluss prüft Blocker vorab → keine Karte, Sprungziele), `book_time` (work/travel/return_travel/material_procurement, Dauer bis jetzt oder von–bis, Datum; Grund Pflicht; 7-Tage-/Zukunft-/16-h-/Überschneidungsprüfung vorab), `record_material` (Planposition per Name, Status aus Planmenge, Abweichung/Zusatzmaterial mit Grund; `validateMaterialUsage` von L4), `add_note` (Art + Text), `suggest_report_fields` (nur mit offenem Bericht). Fehlende Pflichtwerte → `missing_values`, **keine Karte**. |
| Auftragszuordnung | `chat/orders.ts#resolveOrder`: Suchbegriff (Nummer auch als Ziffern, Kunde, Objekt, Ort, Titel) > Kontext-Auftrag > Auftrag der laufenden Uhr > einziger heutiger Auftrag; mehrdeutig → `order_ambiguous` + Auswahl-Chips (Beschriftung mit Kunde nur für den Monteur, Wert „Auftrag A-00042“). |
| Bestätigen | `chat/confirm.ts`: Eigentümer (sonst `not_found`), Status `proposed`, Ablauf (→ `expired`), HMAC-Hash (→ `failed tampered` + Audit `denied`), Bearbeiten nur für freigegebene Felder (Auftrag/IDs fix, Hash neu). Ein atomares Row-Update beansprucht die Karte (Doppeltipp/parallel → `idempotent`), danach Ausführung **über die bestehenden Services** mit Monteur-ctx: `transitionWorkOrder`, `startSession/pauseSession/resumeSession/endSession` (inkl. L12-Auto-Wechsel, auf der Karte angekündigt), `addManualTimeEntry` (eigene Zeit → `pending`, Freigabepflicht), `upsertMaterialUsage`, `createNote`, Berichtsvorschlag in `content.lotse` (L9-Mechanismus: im Bericht übernehmen/verwerfen, Prüfbestätigung beim Absenden). Service-Fehler → Karte `failed` mit Klartext-Code + Lotse-Hinweis mit Sprungzielen (Pflichtfoto, Checkliste, Unterschrift, Meine Zeiten, Material, Bericht). Audit `lotse_action_proposal` before/after mit `source: "lotse_chat"`. „Alle bestätigen“: feste Reihenfolge Zeit → Material → Notiz → Bericht → Status, Stopp beim ersten Fehler mit Hinweis „N Karten noch offen“. |
| Keine äußere Transaktion (Entscheidung) | Die Services öffnen eigene Transaktionen und senden Benachrichtigungen (Mail). Eine umschließende Transaktion hätte die DB-Transaktion über den Mailversand offen gehalten (5-s-Timeout, im Gate beobachtet). Deshalb: Karte atomar beanspruchen, Abschluss-Blocker vor jeder Änderung prüfen, dann Services einzeln. |
| UI mobil | `/m/lotse` (Verlauf, Eingabe, Senden, „Neuer Chat“, Mikrofon-Taste: Aufnahme ≤ 2 min → `POST /api/v1/lotse/transcribe` → Transkript im Eingabefeld editierbar; ohne Transkriptionsanbieter deaktiviert mit Hinweis). Aktionskarten mit klaren Werten (z. B. „Arbeitszeit · Auftrag A-00042 · Di., 15.09. · 13:05–15:05 Uhr · 2:00 Std. · Grund … · Zählt nach Freigabe“), Bestätigen/Bearbeiten/Verwerfen, Status als Text + Icon, „Gültig bis“. Chips nur an der letzten Antwort, Sprungziele als Liste. Offline: Hinweis, Eingabe bleibt (auch `localStorage`), nichts wird gesendet/ausgeführt. Einstieg: Bottom-Navigation „Lotse“ (nur wenn nutzbar) und „Lotse fragen“ im Auftragsdetail (Kontext). Touch ≥ 48 px, Farben nur Tokens, Lotse-Mark in Signalorange, Signalorange sonst nur an Primärknöpfen/offenen Karten. |
| Einstellungen | `/settings/lotse`: Schalter „Lotse-Chat für Monteure“ (wirkt nur bei eingeschaltetem Lotse), Datenfluss um Chat und Spracheingabe ergänzt, KI-Protokoll zeigt Art „Lotse-Chat“ (Transkriptionen der Spracheingabe: Art `transcription`, `entityType lotse_chat`, ohne Inhalt). |
## 2. Routen / Screens
| Route | Rolle | Inhalt |
|---|---|---|
| `/m/lotse` | Monteur, Teamleiter (`lotse:use` + `field:execute`) | Chat ohne Auftragsbezug; ausgeschaltet → Hinweisseite |
| `/m/lotse?order=<id>` | dito | Chat mit Auftragskontext; fremder/unsichtbarer Auftrag → 404 |
| `/m/orders/[id]` | dito | Button „Lotse fragen“ unter der Vollständigkeits-Karte |
| Bottom-Navigation | dito | Eintrag „Lotse“ (6 Einträge, nur wenn Chat nutzbar) |
| `/settings/lotse` | Mandantenadministrator | Schalter + Datenfluss |
| `POST /api/v1/lotse/transcribe` | dito | multipart `file` → `{ text }` (Audio wird nicht gespeichert), OpenAPI ergänzt |
Server Actions (`actions/lotse/chat.ts`, `moduleGuard("lotse")` + `lotse:use`, `field:execute`): senden, bestätigen (optional mit Änderungen), verwerfen, alle bestätigen, neuer Chat – jeweils mit frischer Chat-Ansicht als Ergebnis.
## 3. Dateien
**Neu (Ownership L16 / Lotse-Pfade)**
- `prisma/migrations/20260916200000_lotse_chat/migration.sql`
- `src/lib/lotse/chat.ts` (client-safe: Kartenarten, Zod-Schemas, Ansichtstypen)
- `src/server/ai/lotse/{chat-types,chat-prompt,chat-fake,chat-anthropic}.ts`
- `src/server/services/lotse/chat/{access,orders,placeholders,proposals,tools,engine,conversations,confirm,transcribe}.ts`
- `src/server/actions/lotse/{chat,_chat-state}.ts`, `src/app/api/v1/lotse/transcribe/route.ts`
- `src/app/(field)/m/(core)/lotse/{layout,page}.tsx`
- `src/components/lotse/chat/{lotse-chat,proposal-card,mic-button,ask-button}.tsx`
- `scripts/test-lotse-chat-engine.ts`, `scripts/test-lotse-chat-confirm.ts`, `scripts/test-lotse-chat-live.ts`, `scripts/lib/lotse-chat-fixture.ts`
**Geändert in Lotse-Pfaden (erlaubt laut Auftrag)**
- `src/server/services/lotse/{settings,retention}.ts`, `src/server/actions/lotse-settings.ts`, `src/app/(app)/settings/lotse/page.tsx`, `messages/{de,en}/lotse.json`
**Fremdeingriffe (je klein, markiert „L16“)**
- `prisma/schema.prisma` (3 Modelle, 2 Enums, 1 Spalte) · `src/server/db.ts` + `src/server/backup/topology.ts` (TENANT_MODELS) · `src/server/dsgvo/pii-fields.ts` (3 Felder)
- `src/components/field/bottom-nav.tsx` (Eintrag + Prop `lotse`), `src/app/(field)/m/layout.tsx` (Flag `lotseChat`), `messages/{de,en}/field.json` (`nav.lotse`)
- `src/app/(field)/m/(core)/orders/[id]/page.tsx` (Import + 1 Zeile Button)
- `src/lib/api/openapi.ts` (Pfad `/lotse/transcribe` – `test-betrieb-api` verlangt jede Route im Dokument)
- `src/components/audit-trail.tsx` (Entity-Labels)
- `scripts/test-e2e-tenant-isolation.ts` (je eine Zeile für die 3 neuen Tenant-Modelle – der Test verlangt vollständige Abdeckung aller TENANT_MODELS)
- `scripts/smoke-auth.ts` (Monteur-/Admin-Prüfungen)
Keine Änderung an `rbac.ts` (bestehende Rechte `lotse:use` + `field:execute` + `field:record_own_time`/`report:write` genügen), keine neuen npm-Abhängigkeiten, keine Stubs.
## 4. Schema / Migration
`20260916200000_lotse_chat` (additiv): Enums `LotseMessageRole`, `LotseProposalStatus`; Tabellen `lotse_conversations`, `lotse_messages`, `lotse_action_proposals` (Kaskade von Gespräch zu Nachrichten/Karten), `tenant_settings.lotse_chat_enabled BOOLEAN DEFAULT true`; `SELECT enable_tenant_rls(...)` für alle drei Tabellen. **Begründung:** Chatverlauf und bestätigungspflichtige Vorschläge brauchen persistente, nutzerbezogene Datensätze mit Ablauf, Ergebnis und Audit-Bezug – weder `AiGeneration` (Protokoll, wird pseudonymisiert) noch Report-`content` passen. `workOrderId` ohne Fremdschlüssel (weiche Referenz, Auftrag kann soft-gelöscht werden; Sichtbarkeit wird bei jedem Lesen neu geprüft). Deploy: `prisma migrate deploy` (keine Rechte-Synchronisation nötig).
## 5. Tests
| Skript | Prüfungen | Ergebnis |
|---|---|---|
| `test-lotse-chat-engine.ts` | 75: System-Prompt, Suchwörter; Zuordnung Kontext > laufende Uhr, Suchbegriff Kunde/Objekt/Nummer, Mehrdeutigkeit → Chips (Beschriftung mit Kunde, Wert ohne Namen), `ask_user` beendet Schleife; vier Karten aus einer Nachricht ohne jede Änderung an Zeiten/Material/Notizen/Status; fehlende Pflichtwerte (Grund, Dauer, Zusatzmaterial-/Mindermengen-Grund) → keine Karte; Überschneidung mit laufender Uhr, Pflichtfoto-Blocker mit Sprungziel, kein offener Bericht → keine Karte; **Datenminimierung**: Modell-Input ohne Telefon/E-Mail/Anschrift/PLZ/Kunden-, Kontakt-, Firmen-, Mitarbeiternamen/Mandantenkontakt, Platzhalter-Token vorhanden, fachliche Hinweise erhalten, Wert serverseitig eingesetzt, AiGeneration minimiert; Scope (Monteur ohne Zuweisung → not_found / Auftrag nicht gefunden), fremder Verlauf im selben Mandanten und aus Mandant B → not_found, B findet A-Aufträge nicht, ohne `lotse:use` → forbidden; Chat-Schalter aus / Modul aus → `blocked`, kein Modellaufruf, Audit; ohne Anbieter → not_configured; Monatskontingent → `budget_exceeded`; Rundengrenze; Tokengrenze; Anbieterfehler → Hinweis; Protokoll zeigt `lotse_chat`; Aufbewahrung löscht Verläufe nur des Mandanten | grün |
| `test-lotse-chat-confirm.ts` | 52: Annehmen/Arbeit starten über Services (Statuswechsel, Session, Akteur), Doppeltipp idempotent, Audit mit Quelle; Zeitnachtrag `pending` mit Grund, Bearbeiten (nur erlaubte Felder, Hash aktualisiert, Audit `edited`), Überschneidung → `failed overlap`; Material, Notiz (paralleler Doppeltipp → genau eine Notiz); Abschluss mit fehlendem Pflichtfoto → `failed blocked`, Auftrag + Session unverändert, Sprungziel; erneutes Bestätigen → `already_decided`; Ablauf nach 30 min; fremder Monteur/Admin/Mandant B → not_found; manipulierte Nutzlast → `tampered`; Verwerfen (idempotent, danach nicht bestätigbar); Monteur ohne Zuweisung → Service verweigert; „Alle bestätigen“ Reihenfolge + Stopp + zweiter Lauf; Berichtsvorschlag in `content.lotse` und Übernahme per L9; Chat aus / Modul aus → blocked | grün |
| `test-lotse-chat-live.ts` | optional gegen Claude, nur mit `ANTHROPIC_API_KEY` | übersprungen (kein Key) |
Angepasst: `test-e2e-tenant-isolation.ts` (Fixture um die 3 Tenant-Modelle ergänzt) – grün.
**Gate:** `npm run gate` grün – prisma generate, tsc, lint (0 Fehler, 3 bestehende Warnungen außerhalb L16), build inkl. Modul-Guard-Check (37 Action-Dateien), **79/79 Testskripte**. Im ersten Lauf schlugen `test-e2e-tenant-isolation` (fehlende Fixture-Zeilen, behoben) und `test-einsatz-field` fehl (Transaktions-Timeout 5 s beim Mailversand innerhalb einer L12-Session-Transaktion unter Last paralleler Lanes; einzeln grün, im zweiten Gate grün – bestehendes Verhalten, siehe L12-Bericht §5.5).
**RLS-Lauf** `RLS_ENFORCED=true` (Lane-DB `craftvia_lotsechat`, Rolle `craftvia_app`): `test-lotse-chat-engine`, `test-lotse-chat-confirm`, `test-e2e-tenant-isolation`, `test-tenant-isolation`, `test-rls-enforcement` – 5/5 grün.
**HTTP-Smoke** (`npx next start -p 3116`, `scripts/smoke-auth.ts`, Demo-Seed): Monteur **37/37** grün (neu: `/m/lotse` mit „Lotse-Chat“, „Neuer Chat“, „Sprechen“; `/m/lotse?order=<DEMO-07>` mit Auftragsnummer; Auftragsdetail mit „Lotse fragen“; `/m/lotse?order=<fremder Auftrag DEMO-08>` → 404), Admin **12/12** grün (`/settings/lotse` mit „Lotse-Chat für Monteure“, Protokoll). Server danach beendet.
Visuelle Browserprüfung nicht durchgeführt (Anmeldung hätte Passwort- oder Token-Eingabe durch den Agenten erfordert).
## 6. Stubs & Abhängigkeiten
Keine Stubs. Genutzt: L2 `transitionWorkOrder`/`computeCompletionBlockers`/`workOrderScope`, L4 `requireFieldOrder`/`createNote`/`upsertMaterialUsage`/`validateMaterialUsage`/`sniffMime`, L12 `startSession`/`pauseSession`/`resumeSession`/`endSession`/`getMyActiveSession`/`addManualTimeEntry`/`earliestStart`, L5 `requireVisibleReport`/`contentOf`, L9 `checkCompleteness`/`scrubText`/`lotseVoice`/`isLotseEnabled`/Transkriptionsanbieter, L10b `assertTokenBudget`/`requireApiContext`/Aufbewahrungsjob.
## 7. Bekannte Lücken / Hinweise an den Architekten
1. **Live-Verhalten mit Claude ungeprüft** (kein Key): Tool-Definitionen, Prompt und Denkblock-Rückgabe sind typgeprüft und gegen den Fake getestet; `scripts/test-lotse-chat-live.ts` läuft, sobald ein Key gesetzt ist. Prompt-Feinschliff (Tonalität, Rückfragen) nach ersten echten Gesprächen empfohlen.
2. **Kein `/api/v1` für Senden/Bestätigen** – die mobile UI nutzt Server Actions (wie L9); nur die Transkription ist eine API-Route (Upload > Server-Action-Limit). Für native Clients ggf. nachziehen.
3. **Doppeltipp während der Ausführung:** Der zweite Tipp erhält sofort `confirmed (idempotent)`, auch wenn die Ausführung danach noch scheitert; die Karte zeigt nach dem Neuladen `failed`.
4. **Abschluss nicht vollständig atomar:** Blocker werden vorab geprüft; scheitert der Statuswechsel danach aus anderem Grund (z. B. parallele Änderung), bleibt die eigene Uhr beendet – wie beim bestehenden Zwei-Schritt-Flow der Primäraktion.
5. **Audit der Fachentitäten ohne Quelle:** Die Services schreiben ihre Audits unverändert; die Verbindung zur Chat-Karte steht im Audit `lotse_action_proposal` (Ergebnis-IDs, `source: "lotse_chat"`). Ein `source`-Feld im Fundament-Audit wäre sauberer.
6. **Gespeicherte Antworten enthalten eingesetzte Werte** (z. B. Telefonnummer), sichtbar nur für den Nutzer, gelöscht mit der Aufbewahrung. Das Modell sieht sie nie.
7. **Namen im Freitext des Monteurs** werden (außer Mitarbeitenden) nicht ersetzt, damit die Suche „Objekt Müller“ funktioniert; Telefon/E-Mail/Anschrift schon.
8. **Zeit „2 Std.“ über Mitternacht** → Rückfrage nach der Uhrzeit (kein Raten des Vortags).
9. **Verlauf ans Modell:** letzte 20 Nachrichten als Text + Kartenstatus; Werkzeugergebnisse früherer Nachrichten werden nicht erneut gesendet (Kosten, Datenminimierung).
10. **Bottom-Navigation mit 6 Einträgen** (~62 px je Eintrag bei 375 px) – bei weiteren Einträgen Profil/Sync zusammenlegen.
11. Offline gibt es bewusst keine Warteschlange für Chat-Nachrichten (Eingabe bleibt lokal erhalten).
+96
View File
@@ -0,0 +1,96 @@
# Lane L9 – Lotse (KI-Assistent) (`lane/lotse`)
Stand: 2026-09-14 · Basis `d5c1221` (`feature/craftvia-mvp`, Welle 1 integriert) · Spec §15, §36.2 · Brandbook §4.3, §9, §12.4 · ARCHITEKTUR §4.5
## 1. Umfang / erfüllte Spec-Punkte
| Spec / Auftrag | Umsetzung |
|---|---|
| §15.1 Transkription | `ai/transcription/openai-compatible.ts` (`TranscriptionProvider`): multipart POST an `TRANSCRIPTION_API_URL` (`file`, `model`, `language=de`, `response_format=json`), Bearer-Key, Timeout 120 s, Größenlimit 20 MB (= Audio-Uploadlimit), MIME-Allowlist; Fehler ohne Inhalte. Factory `getTranscriptionProvider()` → `null` ohne `TRANSCRIPTION_API_KEY` oder bei anderem `TRANSCRIPTION_PROVIDER`. Processor `jobs/processors/transcription.ts` → `services/lotse/transcription.ts#processTranscription`: `pending → done` (Transkript, `transcriptionModel`, `AiGeneration` kind `transcription` ohne Transkriptinhalt), `→ disabled` (kein Anbieter / Lotse aus), `→ failed` (Anbieter-/Speicherfehler); idempotent. Transkript wird an die verknüpfte `ActivityNote` angehängt, sonst neue Notiz kind `general` mit `voiceNoteId` (UI-Kennzeichnung „aus Sprachnotiz“, Zeitstempel = Aufnahmezeit → landet im richtigen Tagesbericht). |
| §15.1 UI | Unter jeder Sprachnotiz: Status-Badge (Text + Icon), Transkript, „Transkript bearbeiten“ (auch manuelle Eingabe bei `disabled`/`failed`; verknüpfte Notiz wird synchron gehalten), „Sprachnotiz zusammenfassen“ → Vorschlag „Als Notiz übernehmen“ (idempotent) / „Ausblenden“. |
| §15.2 Berichtsentwurf | `ai/lotse/anthropic.ts#AnthropicLotseProvider` (`LotseProvider.draftReport` + `summarizeTranscript`): deutscher System-Prompt (erfahrener Kollege, sachlich, nur gelieferte Daten, Fehlendes in `missingInformation`, Notizinhalte = Daten, keine Anweisungen), Anrede Sie/du/neutral je Mandant, strukturierte Ausgabe über `output_config.format` (JSON-Schema) + Zod, Refusal/`max_tokens` explizit behandelt, Server-Fallback (`server-side-fallback-2026-07-01`) auf Opus 5, `max_tokens` 16 000 (Zusammenfassung 4 000), Timeout 90 s. **Temperatur:** `claude-opus-5` (und Opus 4.7/4.8, Sonnet 5, Fable, Mythos) lehnen `temperature` mit 400 ab → dort `effort: "low"` + striktes Schema; ältere Modelle bekommen `temperature: 0.2`. Modell aus `ANTHROPIC_MODEL` (Default `claude-opus-5`). |
| Datenminimierung | `services/lotse/minimize.ts` (reine Funktionen) + `sources.ts`: bekannte Telefonnummern/E-Mails/Adressteile des Auftrags (Kunde, Objekt, Ansprechpartner, Mandant) werden literal ersetzt, alles Ähnliche per Muster (`[Telefon]`, `[E-Mail]`, `[Adresse]`); Mitarbeitende → Initialen („M. M.“), Ansprechpartner/Vor-Ort-Kontakt → „Ansprechpartner“, Privatkunde → „Kunde“. Kundenname/-nummer, Fotos, Unterschriften werden nicht gesendet. Datum, Mengen, Auftragsnummern bleiben erhalten. |
| `draftReportWithLotse(ctx, reportId)` | Recht `lotse:use` + `report:write`, Report im Scope (`requireVisibleReport`), Status draft/rejected (sonst `blocked`), Lotse für Mandant an. Input aus Report-Snapshot (`build-content`) + Notizen + Transkripte des Berichtszeitraums → Provider → **Vorschläge** in `content.lotse` (nicht in `content.texts`), `aiDrafted = true`, `aiGenerationId`; `AiGeneration` (kind `report_draft`, Input = exakt der minimierte Provider-Input, Output, Tokens, Nutzer); Originalnotizen unverändert; Audit before/after. Fehlercodes für die UI: `not_configured`, `disabled`, `provider_failed`, `not_editable`. |
| §15.4 / Brandbook §12.4 UI Bericht | `components/lotse/report-panel.tsx` im mobilen Berichtseditor (`/m/orders/[id]/report`) und Backoffice `/reports/[id]`: Wortmarke „Lotse“ (Kompass-Icon + Signalorange, kein Bitmap), Button „Bericht mit Lotse vorbereiten“ / „Neu vorbereiten“ (Ladezustand, Klartextfehler, Hinweis „Lotse ist nicht eingerichtet“), je Feld Karte „Vorschlag vom Lotsen – bitte prüfen“ mit bisherigem Text, editierbarem Vorschlag, Übernehmen/Verwerfen; Liste „Das fehlt dem Lotsen noch“; Prüfnachweis „Vorschlag geprüft und bestätigt am …“. Kein dauerhafter Chatbot. |
| Vollständigkeitsprüfung | `services/lotse/completeness.ts#checkCompleteness(ctx, workOrderId)`: deterministische Regeln zuerst – Pflichtfoto fehlt, Pflicht-Checklistenpunkt offen, Materialabweichung ohne Grund, geplantes Material nicht bestätigt, keine Arbeitszeit, keine Tätigkeitsbeschreibung (weder Notiz noch Berichtstext), Unterschrift fehlt bei `signatureRequired` – danach KI-Hinweise aus `missingInformation` des offenen Lotse-Entwurfs. Jeder Punkt mit Klartext-Label und Deep-Link (`/m/orders/[id]/{photos,checklist,materials,time,notes,sign,report}`). UI `completeness-card.tsx` im mobilen Auftragsdetail: „3 Angaben fehlen“ + „Lotse prüfen lassen“ (aufklappbare Liste). |
| Freigabeprinzip §15.3 | `services/lotse/review.ts#applyLotseReview`, aufgerufen in `submitReport` **vor** jedem Schreibzugriff: Report mit `aiDrafted` ohne `aiReviewed: true` → `invalid` (`details.field = "aiReviewed"`). Bei Bestätigung wird `content.lotse.reviewedAt/reviewedById` gesetzt und mit der Freigabe eingefroren. UI: Pflicht-Checkbox „Ich habe den Vorschlag vom Lotsen geprüft.“ im Tagesbericht-Editor und im Unterschrift-/Absende-Schritt des Abschlussberichts; Fehlertext direkt am Formular. |
| Transparenz & Datenschutz | `/settings/lotse` (tenant:manage, bewusst **nicht** modulgegatet, damit Wiedereinschalten möglich ist): Lotse an/aus (= Modul-Toggle `lotse`, `TenantModule`), Anrede neutral/Sie/du, Anbieter + Modell + Einrichtungsstatus für Entwurf und Transkription (Host, keine Secrets), Liste „Gesendet wird / Nicht gesendet wird“, Freigabeprinzip, Link zum KI-Protokoll. `/settings/lotse/protocol`: `AiGeneration`-Liste (Zeitpunkt, Art, Modell, Tokens ein/aus, Nutzer) für `tenant:manage` oder `audit:read`; gesendete Daten/Antwort (Popup) nur für `tenant:manage`. Sidebar-Eintrag „Lotse (KI)“. |
**Entscheidung `aiReviewedAt` im content statt eigener Spalte:** Der Prüfnachweis gehört fachlich zum Berichtssnapshot – er wird mit der Freigabe unveränderlich eingefroren und steht damit neben den übernommenen Texten. Keine zweite Quelle, keine Migration an `reports`. Voraussetzung war ein optionaler Block `lotse` in `ReportContent` (siehe §3), den `refreshContent` beim Neuaufbau des Snapshots erhält.
**Entscheidung Vorschläge getrennt von `texts`:** KI-Text erreicht den Bericht nur über eine menschliche Aktion (Übernehmen, ggf. bearbeitet). Ein Report mit `aiDrafted` braucht zusätzlich die Prüfbestätigung beim Absenden – beide Sicherungen sind serverseitig.
## 2. Routen / Screens
| Route | Rolle | Inhalt |
|---|---|---|
| `/m/orders/[id]` | Monteur, Teamleiter | + Hinweis-Karte „N Angaben fehlen – Lotse prüfen lassen“ (nur mit `field:execute` + `lotse:use`, Lotse an, mind. 1 Punkt) |
| `/m/orders/[id]/notes` | Monteur, Teamleiter | Sprachnotiz: Status-Badge, Transkript (bearbeitbar), „Sprachnotiz zusammenfassen“, Notizen mit „aus Sprachnotiz“ |
| `/m/orders/[id]/report` | Monteur, Teamleiter | Lotse-Panel oberhalb des Editors, Prüfbestätigung beim Absenden (Tagesbericht) |
| `/m/orders/[id]/sign` | Monteur, Teamleiter | Prüfbestätigung beim Absenden (Abschlussbericht) |
| `/reports/[id]` | Backoffice, Teamleiter | Lotse-Panel (Entwurf/Vorschläge bei draft/rejected; Prüfnachweis bei eingereichten) |
| `/settings/lotse` | Mandantenadministrator | Ein/aus, Anrede, Datenfluss-Transparenz |
| `/settings/lotse/protocol` | Admin (mit Inhalten), `audit:read` (ohne Inhalte) | KI-Protokoll |
## 3. Dateien
**Neu (Ownership L9)**
- `src/server/ai/lotse/{anthropic,prompt,types,fake}.ts`, `src/server/ai/transcription/{openai-compatible,fake}.ts`
- `src/server/services/lotse/{draft-report,suggestions,review,completeness,transcription,voice,settings,sources,minimize,protocol,state}.ts`
- `src/server/jobs/processors/transcription.ts`
- `src/server/actions/lotse/{assist,_state}.ts` (`moduleGuard("lotse")`), `src/server/actions/lotse-settings.ts` (Top-Level, EXEMPT – Modul-Toggle)
- `src/lib/lotse/{content,completeness,action-state}.ts` (client-safe; Pfad analog `lib/reports`, in §6 nicht ausdrücklich gelistet)
- `src/components/lotse/{lotse-mark,draft-button,suggestion-card,review-confirm,report-panel,completeness-card,voice-note-panel,voice-note-client}.tsx`
- `src/app/(app)/settings/lotse/page.tsx`, `src/app/(app)/settings/lotse/protocol/page.tsx`
- `messages/de/lotse.json`, `messages/en/lotse.json`
- `prisma/migrations/20260914180000_lotse_address_form/migration.sql`
- `scripts/test-lotse-draft.ts`, `scripts/test-lotse-transcription.ts`, `scripts/test-lotse-live.ts`
**Fremd-Einzeiler / Integrationspunkte (je 1–3 Zeilen, markiert mit „L9“)**
- `src/server/jobs/processors/index.ts`: Registrierung `transcription`
- `src/lib/nav.ts` + `messages/{de,en}/nav.json`: Eintrag „Lotse (KI)“ → `/settings/lotse`
- `scripts/check-module-guards.ts`: `"lotse-settings.ts": "EXEMPT"`
- `src/components/audit-trail.tsx`: Entity-Labels `ai_generation`, `lotse_settings`
- `src/server/ai/providers.ts` (Vertrag): `ReportDraftInput.addressForm` um `"neutral"` erweitert (abwärtskompatibel; Anforderung „ohne Einstellung neutral ohne Pronomen“)
- L5 `src/lib/reports/content.ts`: optionales Feld `lotse: lotseBlockSchema.optional()` (alte Snapshots bleiben gültig)
- L5 `src/server/services/reports/common.ts#refreshContent`: Lotse-Block beim Neuaufbau erhalten
- L5 `src/server/services/reports/submit.ts`: Schemafeld `aiReviewed` + Aufruf `applyLotseReview`
- L5 `src/server/actions/reports/workflow.ts#submitReportAction`: Checkbox `aiReviewed` durchreichen
- L5 `src/components/reports/action-message.tsx` + `messages/{de,en}/reports.json`: Fehlertext `errors.aiReviewRequired`
- L5 `src/components/reports/mobile/{report-screen,report-editor,sign-flow,sign-screen}.tsx`: Panel + Prüfbestätigung einbinden, `key` am Editor (neu laden nach Übernahme)
- L5 `src/app/(app)/reports/[id]/page.tsx`: Lotse-Panel
- L4 `src/app/(field)/m/(core)/orders/[id]/page.tsx`: Hinweis-Karte; `.../notes/page.tsx`: Sprachnotiz-Panel + „aus Sprachnotiz“
- L4 `src/server/services/field/queries.ts`: `voiceNoteId` im Notiz-Select
- L4 `src/server/services/field/voice.ts`: ohne konfigurierten Transkriptionsanbieter direkt `disabled` statt Job einreihen (sonst bliebe die Sprachnotiz bei Redis ohne Worker `pending`; hält außerdem L4-Test „Sprachnotiz ohne Transkriptions-Processor → disabled“ grün)
## 4. Schema / Migration
`20260914180000_lotse_address_form`: `ALTER TABLE tenant_settings ADD COLUMN lotse_address_form TEXT` (`TenantSettings.lotseAddressForm`, `"sie" | "du" | NULL`). **Begründung:** Der Auftrag nennt TenantSettings als Speicherort; es gab kein passendes Feld. Die Tabelle ist bereits mandantengebunden (RLS, TENANT_MODELS) → keine neue Tenant-Tabelle, keine RLS-/Topologie-Änderung. Keine weiteren Schemaänderungen (Lotse ein/aus = bestehender `TenantModule`-Toggle, Vorschläge/Prüfnachweis im Report-`content`). Keine neuen npm-Abhängigkeiten (`@anthropic-ai/sdk` war vorhanden; Transkription nutzt `fetch`/`FormData` von Node).
## 5. Tests
| Skript | Prüfungen | Ergebnis |
|---|---|---|
| `scripts/test-lotse-draft.ts` | 71: Muster der Datenminimierung (Telefon, E-Mail, Adresse; Datum/Mengen/Auftragsnummer bleiben), **Snapshot-Assertion** des Provider-Inputs (keine Telefon/E-Mail/Adresse/Namen von Kunde, Kontakt, Mandant, Mitarbeitenden; exakte minimierte Beschreibung und Notiz), Prompt je Anrede, Einstellungen (nur `tenant:manage`, Lotse aus sperrt Entwurf/Prüfung, Mandant B unberührt, Audit), Rechte/Scope/Status (ohne `lotse:use` → forbidden, Monteur ohne Zuweisung → not_found, Mandant B → not_found, approved → blocked, ohne Anbieter → not_configured, Anbieterfehler → provider_failed ohne Änderungen, Provider bei Fehlern nie aufgerufen), Fake-Mapping Output → Vorschläge, Texte/Notizen unverändert, AiGeneration = gesendeter Input, Audit; Vollständigkeitsregeln + missingInformation → Liste mit Deep-Links; Übernehmen (bearbeitet)/Verwerfen, Mandant B/Fremder kann nicht entscheiden; Submit ohne/mit `aiReviewed=false` → invalid, mit Bestätigung → submitted + Prüfnachweis; Protokoll (Monteur forbidden, Backoffice ohne Inhalte, Admin mit Inhalten, Mandant B sieht nichts) | grün |
| `scripts/test-lotse-transcription.ts` | 45: OpenAI-kompatibler Provider gegen lokalen HTTP-Server (Bearer, multipart `model`/`language=de`/`file`, HTTP 500 ohne Antwortinhalt, Größenlimit, MIME, Timeout), Factory ohne Key/fremder Provider → null, Processor-Registrierung; Processor Fake → done (Transkript, Modell, Notiz „aus Sprachnotiz“, AiGeneration ohne Inhalt), idempotent, Anhängen an verknüpfte Notiz, Provider null → disabled, Fehler → failed, Speicherfehler → failed, Lotse aus → disabled ohne Übertragung, Job mit Mandant B findet A nicht; Transkript bearbeiten (Notiz synchron, manuell bei disabled, pending → conflict, Fremder/Mandant B → not_found, ohne `field:execute` → forbidden, Audit); Zusammenfassen (Transkript minimiert gesendet, AiGeneration, Übernahme idempotent, Scope/Mandant, ohne Transkript/Anbieter/Recht) | grün |
| `scripts/test-lotse-live.ts` | optional gegen Claude, nur mit `ANTHROPIC_API_KEY` (sonst Skip, Exit 0) | übersprungen (kein Key) |
**Gate:** `npm run gate` grün – prisma generate, tsc, lint (0 Fehler, 2 Warnungen außerhalb L9: `services/field/mime.ts` u. a.), build inkl. Modul-Guard-Check (29 Action-Dateien), **45/45 Testskripte**.
**HTTP-Smoke** (Dev-Server :3109, Lane-DB `craftvia_lotse`, Seed-Logins über Auth.js-Credentials, temporärer Smoke-Auftrag im Demo-Mandanten – wieder entfernt): Monteur `/m/orders/[id]` 200 mit „Angaben fehlen“, „Lotse prüfen lassen“, Pflichtfoto-, Arbeitszeit- und Lotse-Hinweis; `/notes` 200 mit Status „Transkribiert“, Transkript, „aus Sprachnotiz“, „Transkript bearbeiten“; `/report?type=daily` 200 mit „Vorschlag vom Lotsen – bitte prüfen“, Prüf-Checkbox, „Neu vorbereiten“, „Lotse ist nicht eingerichtet“, fehlende Angaben, Übernehmen/Verwerfen; Monteur `/settings/lotse` → Redirect. Admin `/settings/lotse`, `/settings/lotse/protocol` (+ Inhalt-Popup) und `/reports/[id]` 200 mit erwarteten Inhalten. Backoffice: Protokoll 200 ohne Inhaltslink, `/settings/lotse` → Redirect. Mandant B (`admin2`): `/reports/[A]` 404, Protokoll ohne A-Einträge. Visuelle Prüfung im Browser nicht durchgeführt (Login würde Passworteingabe im Browser erfordern); Layouts nutzen die L4/L5-Klassen (Touch-Ziele ≥ 48 px mobil, ≥ 44 px Backoffice, Status immer mit Text + Icon, Farben nur über Tokens).
## 6. Stubs & Abhängigkeiten
Keine neuen Stubs. Genutzt: L5 `requireVisibleReport`/`contentOf`/`buildReportContent`/`submitReport`, L4 `requireFieldOrder`/`createNote`, L2 `workOrderScope`/`requireVisibleWorkOrder`, `services/documents/read.ts#readDocumentBytes`, `api/context.ts#requireApiContext`.
## 7. Bekannte Lücken / Hinweise an den Architekten
1. **Offline-Submit (L7):** Die Sync-Op `report.submit` ist noch nicht registriert (`services/sync/external-ops.ts`). Sobald sie kommt, muss `aiReviewed` im Payload an `submitReport` durchgereicht werden – sonst werden Lotse-Entwürfe offline korrekt, aber mit `invalid` abgelehnt.
2. **`/dashboard` 500 (L2, nicht L9):** `src/app/(app)/dashboard/page.tsx:134` ruft die Client-Funktion `buttonCls()` im Server-Component auf („Attempted to call buttonCls() from the server“). Tritt für Admin und Backoffice auf; Build/Gate bleiben grün, weil es ein Laufzeitfehler ist.
3. **DSGVO `pii-fields.ts` (Fundament):** `AiGeneration.createdById` eintragen; `AiGeneration.input/output` enthalten minimierte Einsatzdaten → beim DSGVO-Export/-Löschen mitberücksichtigen.
4. **Aufbewahrung `AiGeneration`:** Kein Löschkonzept für KI-Protokolleinträge (Spec §31 „Aufbewahrung“); Vorschlag: Inhalte nach X Monaten auf `null` setzen, Metadaten behalten.
5. **Mustererkennung** der Datenminimierung ist bewusst konservativ (Telefon nur mit führender 0/+, ≥ 7 Ziffern; Straßen über gängige Suffixe). Unbekannte Personennamen im Freitext (z. B. Nachbarn) werden nicht erkannt; alle Mitarbeitenden des Mandanten, Kontakte und Privatkunden des Auftrags schon.
6. **Unterbrechung beim Übernehmen:** Ungespeicherte Eingaben im Berichtseditor gehen verloren, wenn ein Vorschlag übernommen wird (Editor wird neu geladen); Hinweis steht am Button.
7. **`lotse-settings.ts`** liegt als Top-Level-Action (EXEMPT), weil ein Modul-Guard auf `lotse` das Wiedereinschalten verhindern würde; Auth = `requireSession` + `requirePermission` + DB-autoritativ `requireApiContext(null, "tenant:manage")`.
8. **Kosten-/Mengenlimit je Mandant** (Spec §31) nicht umgesetzt; `AiGeneration` liefert die Datenbasis (Tokens je Nutzung).
9. Kein `/api/v1`-Endpunkt für Lotse (mobile UI nutzt Server Actions); für Offline/Integrationen ggf. nachziehen.
+98
View File
@@ -0,0 +1,98 @@
# Lane L8 – Notdienst
Branch `lane/notdienst` (Basis `d5c1221` auf `feature/craftvia-mvp`, Welle 1 L1–L6 integriert). Spec §19 komplett, §39, US-010, §7.3, ARCHITEKTUR §3/§4.1/§4.6.
**Keine Schemaänderung, keine neue Migration, keine neuen npm-Abhängigkeiten.**
## 1. Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §19.1 / US-010 Monteur legt Notdienst selbst an | `/m/emergency` (Modul `emergency`, Recht `emergency:create` + `field:execute`), jederzeit anlegbar (keine Geschäftszeitenprüfung, §19 Punkt 5 optional nicht umgesetzt) |
| §19.2 Erfassungsmaske | 3 Schritte mit Schrittanzeige: **1 Kunde** – Suche bestehender Kunden (Name/Ort/Kundennummer, Treffer zeigen nur Name · Nummer · Ort · „Vorläufig") oder „Neuer Kunde" (Firma/Name, Telefon Pflicht, E-Mail, Adresse); **2 Einsatzort** – Objekt des Kunden oder Einsatzadresse (Straße + Ort Pflicht, „Kundenadresse übernehmen"), Ansprechpartner vor Ort + Telefon Pflicht; **3 Grund** (Pflicht, Textfeld) + Sprachnotiz optional (lokal aufgenommen, nach dem Start hochgeladen und als `voice.attach` angehängt), Beginn (Default jetzt), Team (Default eigenes Team) und weitere Monteure des Teams (eigener User immer dabei). Große Felder (≥ 48 px), `type="tel"`/`inputMode="tel"` für Nummern, Status/Fehler immer mit Text + Icon. |
| §19.3 Vorläufige Datensätze | `createEmergencyOrder` in **einer** Transaktion (`inTransaction`): vorläufiger Kunde (`status provisional`, `isProvisional true`, **ohne Kundennummer**) bzw. bestehender Kunde → Ansprechpartner (wiederverwendet bei gleichem Namen + Telefon) → vorläufiges Objekt (`provisional`) bzw. bestehendes Objekt des Kunden → Auftrag über L2 `createWorkOrder` (`isEmergency`, Auftragsart `notdienst`, Nummer aus Sequenz `emergency` N-…, Priorität `urgent`, Status `in_progress`, `plannedStart` = Beginn) → Team/Teamleiter/Zuweisungen → L4 `startSession` (WorkSession + Arbeitszeit-Segment). Danach Event `emergency.created` (`startedAt`). Anschließend normale Einsatzseite `/m/orders/[id]` (Zeiten, Material, Fotos, Tätigkeiten, Bericht, Unterschrift aus L4/L5). |
| Offline (ARCHITEKTUR §4.6) | Sync-Op `emergency.create` mit `clientIds { workOrder, session, customer?, site? }`: Zod-Schema in `src/lib/sync/ops.ts`, Registry-Eintrag in `services/sync/external-ops.ts`. Idempotent doppelt: gleiche `clientOpId` → `duplicate` (L4), neue `clientOpId` mit gleicher Session-Client-ID → Replay liefert denselben Auftrag. Modul `emergency` wird in der Op geprüft (Sync-Route ist `field`-gegated). `idMap` bildet alle Client-IDs auf Server-IDs ab. Der Wizard sendet über `submitOp` (L7 kann die Outbox dahinter tauschen; die Client-IDs bleiben über Wiederholungen stabil). |
| §19.4 Benachrichtigung | Nach Einreichen des Abschlussberichts (v1) eines Notdienstauftrags → `emergency.completed` mit `number`, `technician` (Ersteller), `customer`, `startedAt` (erste Session), `endedAt` (letztes Session-Ende), `status` (i. d. R. `in_review`), `occurrenceId`. L6 versendet die Pflichtmail im Format §19.4 und In-App ans Backoffice. |
| §19.3 / §39 Backoffice-Nachbearbeitung | `/work-orders/emergency-review` (Recht `emergency:review`, Modul-Gate `emergency` + `work_orders`): Tabs „Zu prüfen" (vorläufiger Kunde/Objekt oder Status `technically_completed`/`signature_pending`/`in_review`) und „Alle Notdienste"; Karten mit Nummer, Status, Kunde/Objekt (Kennzeichnung „Vorläufig"), Monteur, Beginn, Fortschrittsbalken **„x von 5 Prüfschritten erledigt"**. Detail `/work-orders/emergency-review/[id]` mit den Prüfschritten: **1 Kunde** – bestätigen (Kundennummer wird jetzt vergeben, dann L1 `confirmProvisionalCustomer`) · bestehendem Kunden zuordnen (Kandidaten über L1 `findDuplicateCustomers` oder Kundennummer; Objekte, Ansprechpartner, Aufträge (Version +1), Dokumente werden umgehängt, vorläufiger Datensatz → `merged` + `mergedIntoId`) · Dublette zusammenführen (L1 `mergeCustomers`, `customer:merge` + Bestätigung); **2 Objekt** – korrigieren/bestätigen (L1 `updateSite`) · bestehendem Objekt zuordnen (L2 `updateWorkOrder`, ungenutztes vorläufiges Objekt → Soft Delete über L1 `deleteSite`); **3 Auftrag ergänzen** – Titel, Beschreibung, Auftragsart, Abrechnungsart (L2 `updateWorkOrder`, Versionsprüfung); **4 Bericht** – Berichtsliste mit Link zur L5-Freigabe; **5 Abrechnung** – L2 `releaseForBilling` (freigegebener Abschlussbericht), zusätzlich gesperrt solange Kunde/Objekt vorläufig (`blocked`, `master_data_open`). |
| Dashboard | Kachel „neue Notdiensteinsätze" verlinkt für Nutzer mit `emergency:review` auf `/work-orders/emergency-review` (sonst weiter auf die L2-Liste). |
| §7.3 Dubletten | Kandidaten in der Prüfung, niemals automatische Zusammenführung; Merge nur mit Bestätigung. |
Jede Mutation: Permission (Guard in der Action **und** Service) · Scope (`workOrderScope`, bei der Erfassung Team-/Zuweisungsprüfung) · Zod · `writeAuditLog` (before/after; zusätzlich Entität `emergency` je Prüfschritt) · Events über `emitEvent`. Fachdaten ausschließlich über `ctx.db`, Mehrschritt-Schreibvorgänge über `inTransaction`.
**Such-Service** `searchCustomersForEmergency(ctx, q)`: nur `emergency:create`, ab 2 Zeichen, max. 10 Treffer, nur aktive/vorläufige Kunden, Rückgabe `{ id, customerNumber, name, city, provisional }`; jeder Zugriff wird auditiert (`action "export"`, Entität `emergency_customer_search`, Suchbegriff + Treffer-IDs). `listSitesForEmergency` analog (`{ id, name, address }`, Entität `emergency_site_lookup`). Mandantentrennung über `dbForTenant`/RLS.
**Sichtbarkeit:** Ein vorläufiger Kunde ist für Monteure ausschließlich über einen sichtbaren Auftrag erreichbar – der bestehende `workOrderScope` enthält `isEmergency && createdById = userId` (ARCHITEKTUR §2), zusätzlich sehen Mitglieder/Teamleiter des gewählten Teams und Zugewiesene den Auftrag.
## 2. Dateien
**Neu (Ownership L8)**
- `src/lib/emergency/schemas.ts` – Zod `emergencyCreatePayload` (Sync-Payload = Service-Input), `emergencyOrderPatchSchema`, `REVIEW_STEPS`
- `src/server/services/emergency/create.ts` – `createEmergencyOrder(ctx, input, deps?)`
- `src/server/services/emergency/lookup.ts` – `searchCustomersForEmergency`, `listSitesForEmergency`, `emergencyTeamOptions`
- `src/server/services/emergency/completion.ts` – `buildEmergencyCompletedData`, `onCompletionReportSubmitted`
- `src/server/services/emergency/sync-ops.ts` – `applySyncOp` für `emergency.create`
- `src/server/services/emergency/review.ts` – `listEmergencyReviews`, `getEmergencyReview`, `reviewProgress`, `confirmEmergencyCustomer`, `assignEmergencyToCustomer`, `mergeEmergencyCustomer`, `correctEmergencySite`, `assignEmergencySite`, `completeEmergencyOrderData`, `releaseEmergencyForBilling`
- `src/server/actions/emergency/capture.ts` (Suche/Objekte, `moduleGuard("emergency")` + `guard("emergency:create")`), `src/server/actions/emergency/review.ts` (7 Actions, je `guard("emergency:review", …)`)
- `src/app/(field)/m/emergency/page.tsx` (Platzhalter ersetzt), `src/components/emergency/emergency-wizard.tsx`
- `src/app/(app)/work-orders/emergency-review/{layout.tsx,page.tsx,[id]/page.tsx}`, `src/components/emergency/{review-form,review-progress}.tsx`
- `messages/de/emergency.json`, `messages/en/emergency.json`
- `scripts/test-notdienst-flow.ts`, `scripts/test-notdienst-review.ts`
**Fremd-Eingriffe (Einzeiler, im Auftrag erlaubt bzw. zwingend)**
- `src/server/services/sync/external-ops.ts`: Registry-Zeile `emergency.create` aktiviert
- `src/lib/sync/ops.ts`: `"emergency.create": emergencyCreatePayload` (+ Import)
- `src/app/(app)/dashboard/page.tsx` (L2): `href` der Kachel `emergency_new` → `/work-orders/emergency-review` (bei `emergency:review`)
- `src/lib/nav.ts`: Eintrag „Notdienst-Prüfung" (`module: "emergency"`, `emergency:review`, Icon `Siren`) + Label `emergencyReview` in `messages/{de,en}/nav.json`
- `src/components/audit-trail.tsx`: Entity-Labels `emergency_customer_search`, `emergency_site_lookup`
- **`src/server/services/reports/submit.ts` (L5) – nicht in der Einzeiler-Liste, aber für Liefergegenstand 3 zwingend:** Import + ein Aufruf `await onCompletionReportSubmitted(ctx, wo.id, occurrenceId)` direkt nach `advanceOrder` für Abschlussbericht v1. Der Hook ist ein No-op für normale Aufträge und wirft nie. Alternative ohne L5-Eingriff wäre nur ein Event-Abonnement im Fundament (`emitEvent` auf `report.submitted`) – bitte beim Merge bestätigen.
## 3. Tests
`npm run gate` **grün**: prisma generate, tsc (0 Fehler), lint (0 Fehler, 2 Warnungen in fremden Dateien), build inkl. Modul-Guard-Check (29 Action-Dateien), **44/44 Testskripte grün** (davon 2 neu).
| Skript | Prüfungen | Inhalt |
|---|---|---|
| `test-notdienst-flow.ts` | 69 | Erfassung mit vorläufigem Kunden (Nummer N-, `in_progress` + Historie, Auftragsart `notdienst`, Kunde/Objekt vorläufig ohne Kundennummer, Ansprechpartner, Default-Team/-Zuweisung, laufende WorkSession, Audit, `emergency.created` ans Backoffice, Akteur ausgenommen); **Transaktion**: Fehler nach dem Auftrags-Insert → kein Kunde/Objekt/Kontakt/Auftrag/Session, Nummernkreis ohne Lücke; **Sichtbarkeit**: vorläufiger Kunde für Ersteller sichtbar, anderer Monteur und Mandant B → `not_found`; **Such-Service**: fremde Kunden auffindbar, nur minimale Felder, Audit, Mandant B findet nichts, ohne `emergency:create` → `forbidden`, Objektabfrage minimal/auditiert/mandantengetrennt; bestehender Kunde + Objekt, Kontakt-Wiederverwendung, Objekt eines anderen Kunden/fremdes Team/fremder Kollege → `invalid`, Mandant B mit A-Kunden-ID → nichts angelegt, Backoffice → `forbidden`, Pflichtfelder; **Sync-Op**: applied mit idMap, gleiche `clientOpId` → `duplicate`, Replay mit neuer `clientOpId` → derselbe Auftrag ohne Doppelanlage, Offline-Flag, ungültige Payload → `rejected invalid`, Replay durch anderen Nutzer abgelehnt, Modul deaktiviert → `forbidden`; **Abschluss**: Session beenden → Abschlussbericht → Unterschrift „Kunde abwesend" → Absenden → Auftrag `in_review`, `emergency.completed` ans Backoffice, Daten Monteur/Kunde/Beginn ≤ Ende/Status, Pflichtmail protokolliert, normaler Auftrag ohne Notdienst-Event |
| `test-notdienst-review.ts` | 49 | **Rollen**: Monteur/Teamleiter → `forbidden` (Liste, Detail, bestätigen, zuordnen, Abrechnung, Merge); **Mandant B**: Liste leer, Detail/bestätigen/zuordnen/ergänzen → `not_found`, Daten unverändert; normaler Auftrag → `not_found`; Liste + Fortschritt 0/5 + Monteur; Dublettenkandidat über Telefon/Name; Beginn/Ende; **Zuordnen**: gleicher/fremder Kunde → `invalid`, Auftrag (Version +1), Objekt, Kontakt umgehängt, Quelle `merged`, Audit, erneut → `conflict`; **Objekt**: zuordnen + vorläufiges Objekt soft-gelöscht, fremdes Objekt → `invalid`, korrigieren + bestätigen; **Bestätigen**: K-Nummer vergeben, aktiv, zweites Mal → `conflict`; **Merge** ohne Bestätigung → `invalid`, Monteur → `forbidden`, mit Bestätigung umgehängt; **Auftrag ergänzen** inkl. Validierung und Versionskonflikt; **Abrechnung**: vorläufige Stammdaten → `blocked master_data_open`, ohne freigegebenen Bericht → `blocked`, nach Freigabe → `released_for_billing`, 5/5, aus „Zu prüfen" entfernt, unter „Alle" sichtbar, Prüfschritte auditiert |
Hinweis Lane-DB: `.env` des Worktrees (nicht eingecheckt) nutzt `craftvia_notdienst` für `DATABASE_URL` **und** `RLS_DATABASE_URL`.
## 4. Stubs / Abhängigkeiten
- **Keine Stubs.** Genutzt: L1 `findDuplicateCustomers`, `confirmProvisionalCustomer`, `mergeCustomers`, `updateSite`, `deleteSite`, `customerScope`; L2 `createWorkOrder`, `updateWorkOrder`, `releaseForBilling`, `ensureDefaultOrderTypes`, `workOrderScope`/`activeTeamIds`; L4 `startSession`, `submitOp`, `uploadFieldFile`, Sync-Registry; L5 Abschlussbericht/Unterschrift (unverändert) + Hook in `submit.ts`; L6 Empfänger/Pflichtmail für `emergency.*`.
- **L7 Offline:** Der Wizard ruft `submitOp({ opType: "emergency.create", … })`. Liefert die Outbox später ein Ergebnis ohne Server-ID, zeigt der Wizard „wird übertragen, sobald Verbindung besteht". Die Kundensuche braucht Verbindung; offline bleibt „Neuer Kunde". Die Sprachnotiz wird nur online nach dem Start hochgeladen.
## 5. Bekannte Lücken / Hinweise
1. **L5-Eingriff** (siehe §2): eine Zeile + Import in `services/reports/submit.ts`.
2. **Audit „Lesezugriff":** `AuditAction` kennt kein `read`; der Suchzugriff wird als `export` protokolliert (Fundament-Vorschlag: Aktion `read`/`access`).
3. `mergeCustomers` (L1) nutzt `ctx.db.$transaction([...])` direkt und kann daher nicht innerhalb von `inTransaction` laufen; der Merge-Schritt ruft ihn eigenständig auf (L1 ist in sich atomar, bei `RLS_ENFORCED=true` gilt der Fundament-Hinweis aus §4.8).
4. `writeAuditLog` schreibt über den Owner-Client außerhalb der Transaktion; eigene Audit-Einträge der Erfassung werden deshalb erst nach dem Commit geschrieben. `createWorkOrder`/`startSession` protokollieren innerhalb – bei einem Rollback bleiben deren Audit-Zeilen stehen (bekannt aus L2).
5. `assignEmergencySite` läuft in `inTransaction`; `updateWorkOrder` emittiert sein Event dabei im Transaktionskontext (pg meldet eine Deprecation-Warnung wegen paralleler Queries im Transaktions-Client, funktional ohne Auswirkung).
6. Globale Eindeutigkeit von `WorkSession.clientId` (Schema): ein Replay mit derselben Session-Client-ID in einem anderen Mandanten scheitert mit einem internen Fehler (nicht gespeichert, keine Datenwirkung).
7. Geschäftszeiten-Hinweis (optional) nicht umgesetzt.
8. `PII_REFERENCE_FIELDS` (Fundament): keine neuen Felder – genutzt werden bestehende (`Customer.createdById`, `WorkOrder.createdById`, `WorkSession.userId`).
## 6. Screens / Routen
| Route | Rolle | Inhalt |
|---|---|---|
| `/m/emergency` | Monteur, Teamleiter (`emergency:create`) | 3-Schritt-Erfassung → „Einsatz starten" → `/m/orders/[id]` |
| `/work-orders/emergency-review` (`?filter=all`) | Backoffice, Admin (`emergency:review`) | Liste mit Fortschritt |
| `/work-orders/emergency-review/[id]` | Backoffice, Admin | Prüfschritte 1–5 |
| `POST /api/v1/sync` Op `emergency.create` | Monteur, Teamleiter | Offline-/Online-Anlage |
## 7. Smoke
HTTP-Smoke gegen den Dev-Server `:3108` (Lane-DB, Seed-Mandanten `demo`/`demo2`). Session-Cookies wurden lokal ohne Passworteingabe erzeugt (`finalizeIdentityLogin` + Auth.js `encode`). **15/16 Prüfungen grün:**
- ohne Session: `/m/emergency` → 307 Login
- Monteur: `/m/emergency` rendert Schritt 1 (bestehender/neuer Kunde); Backoffice sieht dort den Hinweis „nicht freigeschaltet"
- `POST /api/v1/sync` `emergency.create` → `applied` mit idMap; gleiche `clientOpId` → `duplicate`; fremder Origin → 403; Backoffice (ohne `emergency:create`) → abgelehnt
- Monteur: `/m/orders/[id]` des neuen Notdienstes rendert (N-Nummer, Grund)
- Backoffice: `/work-orders/emergency-review` (Fortschritt), `?filter=all`, Detail mit allen Prüfschritten → 200
- Mandant `demo2`: Detail → 404, Liste ohne Notdienste von `demo`
- Monteur/Teamleiter: Prüfseiten → 307 (nicht zugänglich)
- Navigation „Notdienst-Prüfung" wird gerendert
- ✗ **`/dashboard` → 500 im Dev-Server (vorbestehender L2-Fehler, nicht durch L8):** `dashboard/page.tsx` ruft `buttonCls()` aus der Client-Datei `components/work-orders/action-form.tsx` („use client") in einer Server-Komponente auf (Zeilen 134/137, schon auf `d5c1221` vorhanden). Die neue Kachel-Verlinkung ist deshalb nur im Code geprüft, nicht im gerenderten Dashboard. Fix-Vorschlag an L2/Architekt: `buttonCls` in eine Datei ohne „use client" verschieben.
Nicht durchgeführt: visuelle Prüfung im Browser (Responsive 375/768/1024 px) und Aufrufe der Review-Server-Actions über die UI (fachlich durch `test-notdienst-review.ts` abgedeckt). Der Smoke hinterlässt zwei Smoke-Notdienste im Mandanten `demo` der Lane-DB.
+98
View File
@@ -0,0 +1,98 @@
# Lane L7 – Offline & PWA (`lane/offline`)
Stand: 2026-09-14 · Basis `d5c1221` (`feature/craftvia-mvp`)
## 1. Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §3.2 PWA | Service Worker `public/sw.js` (statisch, kein Build-Plugin), Manifest `start_url /m`, `scope /`, `display standalone`, Icons 192/512/maskable vorhanden, iOS-Meta (`appleWebApp`, apple-touch-icon) steht bereits im Root-Layout; Installationshinweis im Profil (`beforeinstallprompt`-Button, sonst iOS-/Browser-Anleitung) |
| §22 Offline-Hinweis, Sync-Status | Sync-Badge im Mobile-Header (Zahl offen, Warnsymbol bei Fehler/Konflikt/abgelaufener Anmeldung, Text + Icon); Offline-Hinweis in Offline-Ansicht und Sync-Seite; automatische Zwischenspeicherung des Notizentwurfs in IndexedDB |
| §23.2 Offline verfügbare Daten | Vorab-Download nach Login, alle 5 min und nach jedem erfolgreichen Sync: heute + 3 Tage + laufende Aufträge (Kunde, Objekt, Kontakte, Checkliste, Material, Pflichtfotos, Dokument-Metadaten, Objekt-Historie); Dokumente der Kategorien technische Zeichnung, Grundriss, Schaltplan, Montageanleitung, Sicherheitsunterlage ≤ 25 MB im Dokument-Cache (LRU 300 MB); `navigator.storage.persist()` + Speicheranzeige |
| §23.3 Offline erfassbar | Zeiten (Start/Losfahren/Pause/Weiter/Ende), Status „Annehmen“, Notizen, Checkliste, Material (L4-Formulare), Fotos und Sprachnotizen über die Upload-Warteschlange; Bericht/Unterschrift laufen ebenfalls über `submitOp` (Ops von L5), sobald L5 sie offline nutzt |
| §23.4 Synchronisation | Outbox je Op: lokale ID (`clientOpId`), Server-IDs (idMap), Zeitstempel, Benutzer/Mandant, `baseVersion`, Status queued/sending/applied/conflict/rejected, Versuche, letzter Fehler; Auslöser: online-Event, 30 s, Sichtbarkeitswechsel, „Jetzt synchronisieren“, Background Sync (Bonus) |
| §23.5 Konflikte | nichts überschrieben (Serverprüfung L4), Konflikt lokal gelistet mit Klartext „Auftrag wurde im Büro geändert – Ihre Statusänderung wurde nicht übernommen. Das Büro prüft den Vorgang.“, nicht verwerfbar (nur ausblendbar); unabhängige Ops laufen weiter |
| US-012 | alle Kriterien: Daten offline, lokale Speicherung, Foto-Warteschlange, sichtbarer Status, Übertragung bei Verbindung, keine stille Überschreibung |
| ARCHITEKTUR §4.6 | identischer Pfad online/offline: Outbox → `POST /api/v1/sync` (Batch ≤ 50) → `applyOperations`; Uploads vorher über `POST /api/v1/uploads` |
| `OFFLINE_MAX_DAYS` (Default 7) | älteres Bundle → Hinweis „veraltet“ (Sync-Seite, Offline-Ansicht); alte Ops werden trotzdem gesendet, Server speichert `SyncOperation.clientCreatedAt` (E2E geprüft) |
### Kernregeln der Outbox (`src/lib/offline/outbox-core.ts`, reine Funktionen)
- Reihenfolge je Auftrag strikt; eine noch ausstehende Op (Backoff, Upload fehlt) blockiert nur Folge-Ops **desselben** Auftrags.
- Endgültige Ergebnisse (applied/conflict/rejected) blockieren nichts.
- Blob-Referenz `documentId: "blob:<clientId>"` → Upload zuerst → Payload umgeschrieben; Upload endgültig abgelehnt → Op `rejected` („Datei wurde nicht angenommen“).
- idMap wird auf wartende Ops angewendet (eigenes `clientId`-Feld bleibt, Idempotenz).
- Verkettete `baseVersion`: Eine Statusänderung hinter einer eigenen, noch offenen Session-/Status-Op desselben Auftrags übernimmt die `entityVersion` aus deren Server-Ergebnis (sonst würde die eigene Kette immer kollidieren).
- Transiente Fehler (Netz, 5xx, `internal`) → exponentielles Backoff 2 s … 5 min mit Jitter; 401 → Pass stoppt, Hinweis „neu anmelden“, nichts geht verloren.
- „Erneut versuchen“ bei `rejected` erzeugt eine neue `clientOpId` (der Server hat das alte Ergebnis gespeichert).
- Optimistische Ansicht = Server-Snapshot + eigene offene Ops (Snapshot bleibt unverändert → abgelehnte Ops verschwinden automatisch aus der Ansicht).
### Service Worker (`public/sw.js`)
- `/_next/static/**` cache-first, versionierter Cache (`craftvia-static-<VERSION>`, alte beim Aktivieren gelöscht, max. 600 Einträge).
- Navigationen unter `/m/**` network-first; offline: `/m`, `/m/orders`, `/m/orders/<id>[/…]` → `/m/offline?from=…` (rendert aus IndexedDB), andere Seiten → zuletzt gecachte Seite; `/m/offline` + referenzierte Assets werden bei Installation und nach jedem Shell-Mount vorgeladen.
- Dokumente (`/api/v1/field/documents/<id>`, `/files/<id>`) cache-first nur, wenn vom Vorab-Download im Dokument-Cache abgelegt; LRU-Zeitstempel beim Zugriff.
- Nie behandelt/gecacht: Nicht-GET, `/api/v1/sync`, `/api/v1/uploads`, `/api/v1/field/bundle`, `/api/auth`, `/api/platform-auth`, `/login`, `/logout`, `/select-tenant`, `/platform`, fremde Origins.
- Update-Flow: neue SW-Version wartet → Hinweis „Neue Version verfügbar – neu laden“ → `SKIP_WAITING` → Reload. CSP: `worker-src 'self'` passt, keine Änderung an `next.config.ts` nötig.
- Im Dev-Server nur opt-in (`localStorage.setItem("craftvia.sw","1")`), sonst würden HMR-Chunks gecacht.
### Datentrennung
IndexedDB `craftvia-offline` (Stores `outbox`, `blobs`, `bundle`, `meta`), jeder Datensatz mit `tenantId`, `userId`, Index `ctxKey = tenantId:userId`; jede Leseoperation filtert danach. Logout (Profil) löscht die Daten des Kontexts sowie Seiten-/Dokument-Cache – bei nicht übertragenen Einträgen vorher Warnung mit „Erst synchronisieren“ / „Trotzdem abmelden“. Beim Start eines anderen Kontexts (Mandantenwechsel, anderer Nutzer) werden Seiten-/Dokument-Cache geleert und fremde Kontexte ohne offene Einträge gelöscht; Kontexte mit offenen Einträgen bleiben getrennt erhalten, bis dieser Nutzer sich wieder anmeldet.
## 2. Routen / Screens
| Route | Inhalt |
|---|---|
| `/m/sync` | Verbindung, letzte Synchronisation, letzter Vorab-Download, Veraltet-Hinweis, offene Ops/Uploads mit Größe und Upload-Fortschritt, Warteschlange (Versuche, nächster Versuch), Fehler & Konflikte im Klartext (erneut versuchen, verwerfen mit Bestätigung nur für `rejected`, Konflikthinweis ausblenden), Speicher (`storage.estimate`, persistiert ja/nein, Anzahl Offline-Aufträge), „Für offline speichern“, „Offline-Ansicht öffnen“, „Lokale Daten zurücksetzen“ (Bestätigung + Warnung bei offenen Einträgen) |
| `/m/offline` | Offline-Ansicht aus IndexedDB: Auftragsliste (Status, Zeitfenster, Adresse, „n Änderungen warten“, Konflikt-/Fehlerhinweis) und Auftragsdetail (Zeitaktionen Annehmen/Losfahren/Arbeit starten/Pause/Weiter/Zeiterfassung beenden, Hinweise, Objekt mit Zugang/Parken/Sicherheit/Technik, Kunde + Anrufen, Leistungsumfang, Checkliste, Foto aufnehmen, Pflichtfotos, Notiz mit Entwurf, Materialfortschritt, Dokumente mit „offline verfügbar“, Objekt-Historie) |
| Mobile-Header | Sync-Badge (Link auf `/m/sync`), Update-Hinweis |
| `/m/profile` | Installationshinweis, Abmelden mit Offline-Schutz |
Browser-Check 375×812 (Dev-Server :3107, `monteur@demo.example`): `/m/sync` und `/m/offline` gerendert, Badge „Synchron“, Sync und Bundle-Pull liefen automatisch; Touch-Ziele ≥ 48 px (Badge 44 px im Header).
## 3. Dateien
**Neu (Ownership L7)**
- `public/sw.js`
- `src/lib/offline/`: `types.ts`, `outbox-core.ts`, `bundle-core.ts`, `sync-engine.ts`, `memory-store.ts`, `db.ts` (IndexedDB), `outbox.ts` (Browser-Outbox, Sync-Loop, `submitOp`, `queueBlob`), `prefetch.ts` (Bundle-Pull, Dokument-Cache, Speicher), `read.ts`, `drafts.ts`, `doc-cache.ts`, `ids.ts`
- `src/components/offline/`: `offline-runtime.tsx` (Server-Wrapper: Session-Kontext + `OFFLINE_MAX_DAYS`), `offline-runtime-client.tsx` (SW-Registrierung/Update, Sync-Loop), `sync-badge.tsx`, `sync-panel.tsx`, `offline-view.tsx`, `logout-form.tsx`, `install-hint.tsx`, `hooks.ts` (`useOfflineState`, `useOfflineDraft`), `format.ts`
- `src/app/(field)/m/sync/{layout,page}.tsx` (Platzhalter ersetzt, Modul-Gate `field`), `src/app/(field)/m/offline/{layout,page}.tsx`
- `messages/de/offline.json`, `messages/en/offline.json`
- `scripts/test-offline-core.ts`, `scripts/test-offline-sync-e2e.ts`
**Eingriffe in L4-Dateien (vom Auftrag vorgesehen, minimal)**
- `src/lib/field/client-ops.ts`: `submitOp` delegiert an die Outbox (Signatur unverändert), `newClientId`/`deviceId` nach `lib/offline/ids.ts` verschoben und re-exportiert, zusätzlich `isQueued`, `queueBlob` exportiert
- `src/app/(field)/m/layout.tsx`: `<OfflineRuntime />` im Header (Import + 1 Zeile)
- `src/components/field/photo-capture.tsx`, `voice-recorder.tsx`: `uploadFieldFile` → `queueBlob` (Upload durch die Outbox vor `photo.attach`/`voice.attach`), kein `router.refresh()` bei lokal gespeicherten Einträgen
- `src/components/field/note-form.tsx`: Entwurf in IndexedDB (`useOfflineDraft`) statt localStorage
- `src/app/(field)/m/(core)/profile/page.tsx`: `<InstallHint />`, `<LogoutForm>` statt Formular
- `public/site.webmanifest`: `start_url` `/dashboard` → `/m`
Keine Schemaänderung, keine Migration, keine neuen npm-Abhängigkeiten, keine Fundament-Dateien geändert, keine Fremd-Einzeiler in `nav.ts`/`processors/index.ts`/Audit-Labels.
## 4. Tests
- `scripts/test-offline-core.ts` – **67 Prüfungen**, ohne Infrastruktur: Reihenfolge je Auftrag, Batch ≤ 50 (120 Ops → 3 Batches), Retry/Backoff (Netz, `internal`, 401, endgültige Fehler, „erneut versuchen“ mit neuer `clientOpId`), Konflikt stoppt keine unabhängigen Ops, Duplicate eines Konflikts, Blob-Upload vor Op inkl. Payload-Umschreibung/Freigabe/abgelehntem Upload/Upload-Backoff, idMap, verkettete `baseVersion`, Mandanten-/User-Trennung der lokalen Stores (Outbox, Blobs, Bundle, Entwürfe, `clearContext`, Sync-Pass sendet nur eigene Ops), optimistische Ansicht, Vorab-Auswahl heute + 3 Tage + laufend, Veraltet/`OFFLINE_MAX_DAYS`, Dokumentauswahl ≤ 25 MB, LRU, Service-Worker-Regeln (Konstanten synchron, nur GET, Ausschlüsse, Fallback, Update-Flow).
- `scripts/test-offline-sync-e2e.ts` – **38 Prüfungen** gegen die Lane-DB mit echten Services (`applyOperations`, `storeFieldUpload`, `getFieldBundle`): 20 Ops offline (10 Tage alt) → 1 Upload → **ein Batch → 20× applied**, `clientCreatedAt` protokolliert, Datensätze korrekt; **Wiederholung → 20× duplicate**, keine Doppelanlagen; Konflikt durch Büroänderung stoppt unabhängige Notiz nicht; **Mandantentrennung** (B: kein Auftrag von A im Bundle, Notiz/Statusänderung/Foto-Upload auf A → not_found/abgelehnt, A unverändert); **Rollen/Scope** (Monteur ohne Zuweisung: nicht im Bundle, Ops → not_found endgültig, keine Session angelegt).
Manueller Browser-Test (Chrome DevTools, Produktions-Build oder Dev mit `craftvia.sw=1`) – Ablauf: als Monteur `/m` öffnen (SW installiert, Bundle geladen) → DevTools › Network › Offline → Auftrag öffnen (Weiterleitung auf `/m/offline?from=/m/orders/<id>`) → „Arbeit starten“ (Status „In Arbeit“, Badge „1 offen“) → Foto aufnehmen („Wartet auf Übertragung“) → Notiz speichern → Online → Badge wird innerhalb weniger Sekunden zu „Synchron“, `/m/sync` „Alles übertragen“, Backoffice zeigt Session/Foto/Notiz. In dieser Lane automatisiert geprüft: Rendering beider Seiten, automatischer Sync + Pull, 105 Logikprüfungen; das DevTools-Offline-Umschalten selbst ist über das Browser-Tool nicht steuerbar und wurde nicht ausgeführt.
HTTP-Smoke (:3107, Seed-Nutzer): `/m/sync`, `/m/offline`, `/m/profile`, `/m` → 200 mit erwarteten Inhalten; `/sw.js` 200 `application/javascript`; `/site.webmanifest` 200 mit `start_url /m`; `/api/v1/field/bundle` 200 (Monteur, `admin2@` Mandant demo2); anonym `/m/sync` → 307 Login, anonym `POST /api/v1/sync` → 401.
## 5. Stubs & Abhängigkeiten
Keine Stubs. Genutzt: L4 `/api/v1/sync`, `/api/v1/uploads`, `/api/v1/field/bundle`, `/api/v1/field/documents/[id]`, `resolveActions`/`StatusBadge`/UI-Klassen aus `components/field`. L5 (Bericht/Unterschrift) kann `useOfflineDraft("report:<id>:<typ>")` aus `components/offline/hooks.ts` für den Berichtsentwurf nutzen – noch nicht eingebunden (L5-Dateien).
## 6. Bekannte Lücken / Hinweise an den Architekten
1. **`src/proxy.ts` (Fundament):** `/sw.js` liegt hinter dem Session-Gate. Registrierung/Update funktionieren nur angemeldet (bei abgelaufener Session schlägt der Update-Check still fehl, der alte SW bleibt). Empfehlung: `sw.js` in die Matcher-Ausnahme aufnehmen; zusätzlich in `next.config.ts` für `/sw.js` `Cache-Control: no-cache` setzen (heute greift Next-Default `max-age=0`, `updateViaCache: "none"` ist gesetzt).
2. **Abschließen offline** ist bewusst gesperrt (Hinweis): Pflichtangaben-Prüfung braucht Serverdaten. Arbeitsende (`session.end`) geht offline.
3. **Material in der Offline-Ansicht** nur als Fortschritt; Erfassung über die L4-Seite (Formulare laufen bereits über die Outbox, aber die Seite selbst ist ein Server Component und offline nur als gecachte Seite erreichbar, falls vorher besucht).
4. **Bundle ohne Notizen/Fotos/Sessions**: Die Offline-Ansicht zeigt nur eigene lokale Einträge; die eigene Session wird aus dem Auftragsstatus abgeleitet (bei Teamaufträgen mit mehreren Monteuren ungenau). Erweiterung von `getFieldBundle` (L4) um `mySession` empfohlen.
5. **Verkettete baseVersion**: Eine Büroänderung zwischen einer eigenen additiven Session-Op und der folgenden Statusänderung desselben Auftrags wird nicht als Konflikt erkannt, weil Session-Ops laut §4.6 konfliktfrei sind.
6. **Mandantenwechsel im Backoffice** (`tenant-switcher.tsx`, Fundament) löscht lokale Daten nicht direkt; das geschieht beim nächsten Öffnen der Mobile-Shell (Kontextwechsel → Caches geleert, fremde Kontexte ohne offene Einträge gelöscht).
7. **Background Sync** nur Chromium; Safari/iOS synchronisieren bei geöffneter App (online-Event, 30 s, Sichtbarkeit).
8. Veraltete Keys `field.sync.immediate/offlineHint/status` in `messages/*/field.json` (L4) werden nicht mehr genutzt.
9. Wurde ein Foto erfasst, aber nie angehängt (Formular verworfen), wird der lokale Blob nach 24 h gelöscht.
## 7. Gate
`npm run gate` grün: prisma generate, tsc, lint (0 Fehler, 2 bestehende Warnungen außerhalb L7), build inkl. Modul-Guard-Check (27 Action-Dateien), **44/44 Testskripte** (davon neu `test-offline-core.ts` 67 ✓, `test-offline-sync-e2e.ts` 38 ✓).
+146
View File
@@ -0,0 +1,146 @@
# Lane L13 – Planung (`lane/planung`)
Stand: 2026-09-15 · Basis `d3bc7f2` (`feature/craftvia-mvp`, L1–L11 integriert) · Spec §11, §21, §35/§36, §44 · Brandbook §12
Menüpunkt „Planung“ mit Plantafel (Standard: heute + nächste 4 Werktage) und Live-Lage (ohne GPS), Kolonnenkapazität, Konflikte, Verzugswarnungen, „früher fertig“ mit Vorzieh-Vorschlägen, Einsatz-Empfehlungen nach Luftlinie + Terminlage, Geocoding über OpenStreetMap Nominatim. **Alles sind Hinweise und Vorschläge – es wird nie automatisch umgeplant.**
## 1. Umfang / umgesetzte Nutzerentscheidungen
| Punkt | Umsetzung |
|---|---|
| **Teams = Kolonnen** | Ein Team fährt und arbeitet gemeinsam (2+ Personen). Kapazität = `Team.dailyCapacityMinutes` (Kolonnen-Arbeitstag, Default 480) an `Team.workingDays` (Bitmaske Mo=1 … So=64, Default Mo–Fr) – **nicht** Personenstunden; ohne aktive Mitglieder 0. Auftragsdauer ist Kolonnenzeit. Auslastung = Summe geplanter Kolonnendauern / Kolonnenkapazität. Popup „Teamkapazität“ auf der Plantafel (`team:manage`, Audit). |
| Dauer | `WorkOrder.plannedDurationMinutes` → Beginn/Ende am selben Tag (mit Uhrzeit) → `OrderType.defaultDurationMinutes` → 120 min. Mehrtägig: explizite Dauer gleichmäßig verteilt, sonst je Tag Auftragsart-Standard bzw. 480 min. Ende exakt um Mitternacht zählt zum Vortag. |
| Konflikte | `overbooked` (Minuten über Kapazität), `overlap` (zwei Aufträge derselben Kolonne mit Uhrzeit überlappen), `assignee_double_booked` (Person in zwei Kolonnen/Aufträgen gleichzeitig; in beiden Zeilen sichtbar), `outside_working_days`; **Hinweis** `crew_incomplete` (an Arbeitstagen < 2 aktive Mitglieder; `severity: hint`, gelb mit Info-Icon, zählt nicht als Konflikt). |
| **Menü „Planung“** | Top-Level direkt unter „Dashboard“ mit Unterpunkten „Plantafel“ (Standard, `/planning`) und „Live-Lage“ (`/planning/live`); sichtbar für `work_order:read_all` und Teamleiter (`report:approve_team`). |
| **Plantafel** `/planning` | Standard **„Heute + nächste 4 Werktage“** (5 Spalten, erste Spalte „Heute“ hervorgehoben), Kolonnen als Zeilen mit Name, Mitglieder-Kurzliste und Kolonnenkapazität; je Zelle Auftragskarten (Nummer, Kunde, Ort, Uhrzeit, Dauer, Statusgruppe mit Icon, Priorität, Notdienst), Auslastungsbalken („6 h / 8 h · 75 %“), Konflikt-/Hinweis-Liste, Verzugs- und Gefährdungs-Badges; heutige Spalte zusätzlich **Live-Status je Kolonne** (Unterwegs/In Arbeit/Pause/frei + aktive Personen, Verzug, „früher fertig“). Umschalter **Heute** (Stundenraster 6–20 Uhr, **Kolonnen als parallele Spalten**, Zeit vertikal, überlappende Aufträge nebeneinander) · **5 Tage** · **Woche** · **Nächste Woche**; vor/zurück/heute. Filter Team/Auftragsart/Priorität (wirken auf Karten, nicht auf die Auslastung). Seitenleiste „Ungeplante Aufträge“ mit „Vorschläge“. Panel „Früher fertig“ mit „Vorziehen“. Unter 1024 px Hinweis + einfache Liste. |
| Drag & Drop | `@dnd-kit/core` (Pointer- und Keyboard-Sensor am Griff): Ablegen auf Tag (Uhrzeit bleibt bzw. 08:00) oder Stunde (Heute-Ansicht) → Bestätigungs-Popover (Team, Datum, Beginn, „ohne Uhrzeit“, Dauer vorbelegt) → `POST /api/v1/planning/schedule`. **Tastatur-Alternative:** „Einplanen …“/„Verschieben …“ je Karte öffnet dasselbe Popover (Fokus hinein, Escape schließt, Fokus zurück). Ergebnis in `role="status"` inkl. Konfliktanzahl; Versionskonflikt → Meldung + Neuladen. |
| `scheduleWorkOrder` | `inTransaction`: `baseVersion` Pflicht → `conflict` „Auftrag wurde zwischenzeitlich geändert“; nur Status draft/review_required/planned/assigned/accepted; **L2 `assignWorkOrder`** (Teamwechsel/Erstzuweisung, Event `work_order.assigned`; Einzelzuweisungen bleiben nur als aktive Mitglieder des Zielteams) + **L2 `updateWorkOrder`** (Beginn/Ende) + Dauer (`writeWithVersion`); Audit „planning“ (before/after); Länge bleibt beim Verschieben ohne neues Ende erhalten; Antwort mit Konflikten des Zieltages. Rechte `work_order:assign` + `work_order:write`. |
| **Live-Lage ohne GPS** `/planning/live` | Status aus der aktiven WorkSession je Person (`en_route` → Unterwegs, `running` → In Arbeit, `paused` → Pause, keine → frei), „seit“ aus dem offenen Zeitabschnitt. **Ein Marker je Kolonne** am Objekt des laufenden Auftrags (Auftrag des aktivsten Mitglieds), Popup/Liste mit Mitgliedern und deren individuellem Status (inkl. abweichendem Auftrag); Monteure ohne Team einzeln. Farbe + inline-SVG-Icon + Text, roter Ring bei Verzug. Karte (Leaflet, OSM-Kacheln, Namensnennung) + Liste, mobil Tabs; Filter Team/Status; Polling 30 s + „Zuletzt aktualisiert“; „Heute ohne Beginn“; „Früher fertig“ mit Vorzieh-Links. Hinweis „Standort = Einsatzort des laufenden Auftrags, keine GPS-Ortung.“ |
| **Früher fertig** | `getFreedCapacity(ctx, { date })`: Aufträge des Tages mit Uhrzeit, Status technically_completed/signature_pending/in_review/released_for_billing/billed, Ende der letzten (Uhr-)Session < geplanter Beginn + Dauer (≥ 15 min) → freie Minuten je Kolonne (früher fertig gesamt, ab jetzt verfügbar), tatsächliche Dauer aus freigegebenen Zeiten. Vorschläge (nur heute): spätere Aufträge derselben Kolonne heute/nächste 5 Tage, die in die freie Zeit passen („Vorziehen“), und ungeplante Aufträge im Umkreis 15 km (Luftlinie). Badge „Team Nord · 2 h früher fertig“ in Live-Lage und Plantafel; „Vorziehen“ öffnet das Einplan-Popover vorbelegt. |
| **Verzugswarnungen** | `evaluateDelays`: erfasste Kolonnen-Arbeitszeit des laufenden Auftrags = **zeitliche Vereinigung** der `work`-Segmente aller Sessions des Auftrags (nicht über Personen summiert) vs. geplante Dauer: ≥ 80 % → „Verzug droht“, ≥ 100 % → „Dauer überschritten“ (Live-Lage Marker/Liste, Plantafel Karte, Text + Icon). **Folgeauftrag gefährdet:** nächster geplanter Auftrag derselben Kolonne heute; Restzeit (≥ 0) + Fahrzeit (Luftlinie × 1,3 / 50 km/h, mind. 10 min) → Beginn (mit Uhrzeit) überschritten oder Überziehung + Fahrzeit sprengt die Tageskapazität → Karte „gefährdet durch Verzug A-00041“. |
| **Meldungen** | Job `planning-watch` (BullMQ Job-Scheduler alle 5 min, alle Mandanten): `planning.overrun` (dedupe je Auftrag, erneut je weitere +50 %), `planning.followup_at_risk` (je Folgeauftrag/Tag), `planning.capacity_freed` (je Kolonne/Tag/Auftrag) → **nur In-App** an Backoffice (`work_order:read_all` + `report:approve`) + Teamleiter der Kolonne. Dedupe-Ledger: append-only Audit-Log (`entity = planning_alert`, Schlüssel je Mandant). Anzeige wird bei jedem Laden serverseitig berechnet (auch ohne Redis); Events entstehen nur im Job. |
| **Empfehlungen** | `recommendAssignments(ctx, { workOrderId, days = 10, radiusKm = 25 })`: Zielobjekt braucht Koordinaten (sonst Hinweis „Adresse nicht verortet“ + Geocoding-Job; `not_found` eigener Hinweis). Je Arbeitstag und Kolonne: nächster geplanter Auftrag (Luftlinie) + **freie Kolonnenkapazität**; ohne freie Kapazität ausgeschlossen, zu wenig → „knapp“. Score 0,6 Distanz + 0,2 Kapazität + Datum (Gewicht nach Priorität) − 0,25 knapp; Top 5 mit Text („3,0 km von A-00001 · Team Nord hat am Mo 28.09. noch 4 h frei“) und vorgeschlagenem Beginn. `findNearbyUnplanned` (5 km). UI in der Seitenleiste und im Auftragsdetail; „Übernehmen“ nur vorbelegt, **kein Auto-Speichern**. |
| **Geocoding** | `services/geo`: `GeocodingProvider`, `nominatim.ts` (strukturiert, Timeout 8 s, `Accept-Language: de`, `countrycodes`, User-Agent `Craftvia/<version> (+APP_BASE_URL)` bzw. `GEOCODING_USER_AGENT`, Limiter 1/s je Prozess), `none`. Job `geocode-site` (Concurrency 1 + BullMQ-Limiter 1/s, `failed` → Retry). Cache `Site.latitude/longitude` + `geocodeQuery` + `geocodeStatus` + `geocodedAt`; unveränderte Adresse nie erneut; manuelle Koordinaten nie überschrieben. Auslöser Objekt anlegen/Adressänderung/Import-Bestätigung – nur Queue, nie inline. `scripts/geocode-backfill.ts`. Ohne Netz/Provider: „ohne Ortsangabe“, nie Fehler. |
| Dashboard | Kacheln „Planung heute“ (Kolonnen im Einsatz / gesamt, Konflikte heute, ungeplante Aufträge) und „Konflikte diese Woche“ – additiv. |
## 2. Datenschutz – Live-Lage (verbindliche Entscheidung)
- **Keine Standortdaten des Geräts.** Angezeigt wird ausschließlich der **Einsatzort des laufenden Auftrags** (Objektadresse bzw. deren Koordinaten). `WorkSession.startLat/startLng`, `deviceInfo` und Foto-Koordinaten werden nicht selektiert und sind nie Teil einer Antwort (per Test über das serialisierte Ergebnis aller Sichten geprüft).
- Status und Verzug stammen aus der Zeiterfassung (aktive Uhr-Sessions, getrackte Arbeitssegmente). Kein Tracking, keine Historie der Live-Lage, keine zusätzlich gespeicherten Personendaten; das Dedupe-Ledger enthält nur Auftrags-/Team-IDs und Prozentwerte.
- Sichtbarkeit: Backoffice sieht alle Kolonnen des Mandanten, Teamleiter nur ihre geleiteten Kolonnen (Aufträge außerhalb des Scopes als „Einsatz außerhalb Ihres Bereichs“), Monteure keinen Zugriff. Verzugs-/Früher-fertig-Meldungen gehen nur an Backoffice und den Teamleiter der Kolonne.
- Empfehlung vor Produktivbetrieb (Spec §44 „Umgang mit Standortdaten“): Information der Beschäftigten / ggf. Betriebsvereinbarung, da sichtbar ist, wer gerade an welchem Einsatzort arbeitet und ob ein Einsatz länger dauert.
- Geocoding überträgt nur Objektadressen an den Geocoding-Dienst; Kartenkacheln lädt der Browser vom Kachelserver (IP-Adresse des Nutzers wird übertragen) → Datenschutzhinweis bzw. eigener Kacheldienst.
## 3. OpenStreetMap-Nutzungsrichtlinien & Produktionsempfehlung
- **Nominatim** (<https://operations.osmfoundation.org/policies/nominatim/>): nur serverseitig (Worker/Skript), max. 1 Anfrage/s (Limiter + BullMQ-Limiter + Concurrency 1), eindeutiger User-Agent mit Kontakt-URL, Ergebnisse am Objekt gecacht, kein Massen-Geocoding im Request, keine Autocomplete-Suche. Limiter gilt je Prozess – bei mehreren Worker-Replikas nur eine mit `GEOCODING_PROVIDER=nominatim`.
- **Kacheln** (<https://operations.osmfoundation.org/policies/tiles/>): Namensnennung sichtbar, Browser-Referrer, kein Vorabladen; der öffentliche Kachelserver ist nicht für hohe Last gedacht.
- **Produktion:** eigener bzw. vertraglich gebundener Geocoding- und Kacheldienst (selbst gehostetes Nominatim/Photon + Tile-Server oder EU-Anbieter mit AVV). Umschalten per `GEOCODING_URL`/`GeocodingProvider`, `MAP_TILE_URL` + `MAP_ATTRIBUTION`. **`MAP_TILE_URL` muss beim `next build` gesetzt sein** (Kachel-Host in CSP `img-src`).
- Env (in `.env.example`): `GEOCODING_PROVIDER=nominatim|none`, `GEOCODING_URL`, `GEOCODING_USER_AGENT`, `MAP_TILE_URL` (Default `https://tile.openstreetmap.org/{z}/{x}/{y}.png`), `MAP_ATTRIBUTION` (Default „© OpenStreetMap-Mitwirkende“).
## 4. Datenmodell – Migration `20260915120000_planung`
Nur Spalten (RLS unverändert, keine neue Tabelle, keine TENANT_MODELS-/PII-Änderung):
- `Team.dailyCapacityMinutes Int @default(480)` (Kolonnen-Arbeitstag), `Team.workingDays Int @default(31)`.
- `OrderType.defaultDurationMinutes Int?` + Backfill der Standardarten (Montage 480, Reparatur 180, Wartung 120, Störung 120, Notdienst 120, Besichtigung 60, Abnahme 60, Nacharbeit 120); neue Mandanten über `DEFAULT_ORDER_TYPES`.
- `WorkOrder.plannedDurationMinutes Int?`.
- `Site.geocodedAt DateTime?`, `Site.geocodeStatus String?` (`ok|not_found|failed|skipped`), `Site.geocodeQuery String?`.
**Zeiterfassung (L12):** keine Schemaänderung durch L13. `services/planning/time-tracking.ts` nutzt `TimeEntry.source = tracked` (Live/Verzug), `TimeEntry.approvalStatus = approved` (tatsächliche Dauer abgeschlossener Aufträge) und `WorkSession.manual = false` – die Filter schalten sich automatisch ein, sobald der generierte Prisma-Client die L12-Felder kennt (vorher wirkungslos, damit die Lane gegen beide Schemata kompiliert). WorkSessions/TimeEntries werden nur gelesen.
## 5. Dateien
**Neu (Lane-Pfade):**
- `src/lib/geo/distance.ts`, `src/lib/planning/{days,capacity,text}.ts`
- `src/server/services/geo/{config,provider,nominatim,rate-limit,normalize,geocode-site,dispatch}.ts`
- `src/server/services/planning/{access,data,board,schedule,recommend,live,watch,summary,team-settings,time-tracking}.ts`
- `src/server/jobs/processors/{geocode-site,planning-watch}.ts`
- `src/server/actions/work_orders/planning.ts` (Kolonnenkapazität, `moduleGuard("work_orders")` + `guard("team:manage")`)
- `src/app/(app)/planning/{layout,page}.tsx`, `src/app/(app)/planning/live/page.tsx`
- `src/app/api/v1/planning/{board,schedule,recommendations,live}/route.ts`
- `src/components/planning/{planning-board,schedule-dialog,recommendations-panel,live-situation,live-map,live-tone,capacity-form,suggestions-section,conflicts-tile}.tsx|ts`
- `messages/{de,en}/planning.json`
- `prisma/migrations/20260915120000_planung/migration.sql`
- `scripts/test-planung-{core,recommend,live,geocode,watch}.ts`, `scripts/geocode-backfill.ts`
**Eingriffe außerhalb planning/geo (minimal, additiv):**
| Datei | Änderung |
|---|---|
| `prisma/schema.prisma` | 7 Spalten (s. §4) |
| `src/lib/work-orders/defaults.ts` | `defaultDurationMinutes` je Standard-Auftragsart |
| `src/server/services/sites/sites.ts` | je 1 Zeile nach Anlage/Adressänderung `requestSiteGeocoding` (+ 2 Importe) |
| `src/server/services/imports/confirm.ts` | 1 Zeile nach Bestätigung mit neuem Objekt (+ 1 Import) |
| `src/server/jobs/queues.ts` | Queue-Namen `geocode-site`, `planning-watch` + Job-Scheduler „alle 5 min“ in `scheduleRecurringJobs` |
| `src/server/jobs/processors/index.ts` | 2 Registrierungen |
| `scripts/craftvia-worker.ts` | Concurrency 1 + Limiter 1/s für `geocode-site` |
| `src/lib/events.ts` | Event-Typen `planning.capacity_freed`, `planning.overrun`, `planning.followup_at_risk` |
| `src/server/services/notifications/recipients.ts` | Regel für die 3 Events (Backoffice + Teamleiter, `mailToUsers: false`) + 3 Facts |
| `src/server/services/notifications/handle-event.ts` | Textvariablen `team`, `minutes`, `percent`, `blocker` |
| `messages/{de,en}/notifications.json` | Typ-Labels + Texte der 3 Events, Audit-Objektart `planning_alert` |
| `next.config.ts` | CSP `img-src` um Kachel-Host(s) aus `MAP_TILE_URL` (Default `https://tile.openstreetmap.org https://*.tile.openstreetmap.org`) |
| `src/lib/nav.ts`, `messages/{de,en}/nav.json` | „Planung“ + Unterpunkte „Plantafel“/„Live-Lage“; `NavItem.sub`/`exact` (optional) |
| `src/components/nav-link.tsx` | optionales `exact` (Unterpunkt nicht auf Unterpfaden aktiv) |
| `src/app/(app)/layout.tsx` | Einrückung für `sub`, eindeutiger `key`, `exact` durchgereicht (Hauptnavigation) |
| `src/app/(app)/dashboard/page.tsx` | Kacheln „Planung heute“ + „Konflikte diese Woche“ (1 Import + 1 Zeile) |
| `src/app/(app)/work-orders/[id]/page.tsx` | Abschnitt „Einsatz-Vorschläge“ auf der Übersicht (2 Importe + 1 Zeile) |
| `src/lib/api/openapi.ts`, `docs/craftvia/API.md` | 4 Planungs-Endpunkte + Tag „Planung“ |
| `scripts/smoke-auth.ts` | Planungsseiten/-API je Rolle |
| `.env.example`, `docs/craftvia/ABNAHME.md` | Env-Block; §3 Kalender/Live-Lage/Verzug/Empfehlungen, §4 Standortdaten |
| `package.json`, `package-lock.json` | neue Abhängigkeiten (s. §7) |
Nicht angefasst: `services/field/**`, `components/field/**`, `app/(field)/m/**`, `TimeEntry`/`WorkSession`-Schema, Zeit-Freigaben, `time.*`-Events (L12), `src/server/rbac.ts` (keine neuen Rechte).
## 6. Tests
| Skript | ✓ | Inhalt |
|---|---|---|
| `test-planung-core` | 92 | Haversine/Formatierung; Tage (Bitmaske, Zeitumstellung, heute + nächste Werktage, Werktage vor/zurück); Dauerregeln inkl. mehrtägig/Mitternacht; **Kolonnenkapazität** (aktive Mitglieder gültig ab/bis/inaktiv/doppelt, Kapazität nicht × Personen, 0 ohne Mitglieder, Wochenende); Auslastung; Konflikte rein + DB (overbooked 10 h bei 225 %, overlap, anschließend ≠ overlap, assignee_double_booked in beiden Zeilen, outside_working_days); **crew_incomplete** als Hinweis (nicht im Konfliktzähler, färbt keine Karte); Vereinigung von Arbeitssegmenten, Fahrzeitschätzung (50 km → 78 min, min. 10), Meldestufen 100/150/200 %; Mitgliedschaft „gültig bis“; Kolonnenkapazität setzen (Wirkung, Audit, Teamleiter forbidden, Mandant B not_found, invalid); Filter; Rollen (Teamleiter nur eigene Kolonne lesend, Monteur forbidden) und **Mandantentrennung**; Zeitraum-Validierung; Dashboard-Zähler; `scheduleWorkOrder`: Zuweisung + Termin + Dauer, Version, Konflikte im Ergebnis, **Event** `work_order.assigned`, **Audit**, Länge beim Verschieben, **Versionskonflikt** mit Klartext, **atomar** (Zuweisung, Statushistorie, Audit zurückgerollt), Rechte/Scope/Mandant B, ungültiges Datum, laufender Auftrag |
| `test-planung-recommend` | 27 | nächste Kolonne mit freier Kolonnenzeit (3 km, 4 h frei, Text, Beginn nach letztem Auftrag), Kolonne ohne Kapazität trotz größerer Nähe ausgeschlossen, außerhalb Radius ausgeschlossen, **nächster Kandidat mit Kapazität gewinnt**, „knapp“, Top 5, englischer Text, Radius-Hinweis; **ohne Koordinaten** → Hinweis, `not_found`, ohne Objekt; nahe ungeplante Aufträge; Rollen; **Mandantentrennung** (identische Koordinaten in B nie in A-Vorschlägen, B → not_found) |
| `test-planung-live` | 26 | Status je Person aus Session, „seit“, beendete Session zählt nicht, Standort = Objekt, ohne Ortsangabe, heute noch geplant, „Heute ohne Beginn“; **ein Eintrag je Kolonne** mit individuellen Mitgliederstatus und abweichendem Auftrag, Kolonne Süd Pause, Monteure gehören zur Kolonne; **keine Geräte-Koordinaten/Geräteinfo** im Ergebnis; Filter; Teamleiter nur eigene Kolonne; Monteur forbidden; Mandant B |
| `test-planung-watch` | 41 | Job/Queue registriert, L12-Adapter je Schema; **Kolonnenzeit vereinigt** (100 statt 140 min), 50 % ok, **83 % „Verzug droht“**, Folgeauftrag rechtzeitig (10:31 < 10:45) bzw. **gefährdet** (10:51 > 10:45, Fahrzeit 31 min für 20 km); Plantafel-Karten (Verzug, Dauer überschritten, gefährdet), Live-Lage Kolonne mit Verzug; **Meldungen**: keine bei 83 %, `planning.overrun` bei 108 %, **Dedupe**, erneut bei 150 %, nicht vor 200 %, `followup_at_risk` einmal je Tag, Ledger-Einträge; **Empfänger** Backoffice + Teamleiter der Kolonne, nicht Monteure/andere Teamleiter, **nur In-App**, Texte; Gefährdung durch **Tageskapazität** (Folgeauftrag ohne Uhrzeit); **früher fertig**: 2 h früher, 90 min ab jetzt, Vorziehen (passend ja, zu lang nein), Umkreis (1 km ja, fern nein), vorbelegter Beginn, **keine automatische Änderung**, Live-Lage-Anzeige, `capacity_freed` einmal + Dedupe + Text; Rollen (Monteur forbidden, anderer Teamleiter sieht nichts); **Mandantentrennung** (B: keine Auswertung/Meldungen/Ledger, Dedupe-Schlüssel in B blockiert A nicht) |
| `test-planung-geocode` | 39 | Normalisierung/Änderungserkennung; Nominatim-URL, User-Agent-Default/Override, Provider none, Kachel-Defaults; Provider mit injiziertem `fetch` (Treffer, Header, Limiter, not_found, HTTP-Fehler); **Drosselung** mit Fake-Uhr; `geocodeSite` mit Fake-Provider (ok + Audit, **Cache**, Neuverortung, **not_found**, failed → Retry, manuelle Koordinaten, none, unvollständig, Mandant B); Processor; Hook nie inline; **Nominatim nie aufgerufen** |
**Gesamt: 225 Prüfungen in 5 neuen Skripten.** Gate/RLS/Smoke: §8.
## 7. Neue Abhängigkeiten (begründet)
| Paket | Lizenz | Größe | Begründung |
|---|---|---|---|
| `leaflet` 1.9.4 | BSD-2-Clause | ~42 KB min+gz JS, 4 KB CSS | Kartenanzeige der Live-Lage (Nutzervorgabe); nur auf `/planning/live` per `next/dynamic` (`ssr: false`); Styles lokal, Marker inline SVG, keine CDN-Assets. |
| `@dnd-kit/core` 6.3.1 | MIT | ~15 KB min+gz | Drag & Drop der Plantafel (Nutzervorgabe, war nicht vorhanden); Pointer- und Keyboard-Sensor, Screenreader-Ansagen; nur auf `/planning`. |
| `@types/leaflet` 1.9.22 (dev) | MIT | – | Typen für tsc. |
## 8. Gate, RLS, Smoke
- **`npm run gate` grün** (Vordergrund): prisma generate, tsc, lint (0 Fehler; 3 bestehende Warnungen in `scripts/test-betrieb-api.ts`, `src/app/(app)/layout.tsx` `CraftviaLogo`, `src/server/services/field/mime.ts`), build inkl. Modul-Guard-Check (33 Action-Dateien), **70/70 Testskripte** (davon 5 neu, 225 Prüfungen).
- **`RLS_ENFORCED=true npm run test` grün: 70/70** (Lane-DB `craftvia_planung`, `RLS_DATABASE_URL` auf dieselbe DB).
- **Smoke** `BASE=http://localhost:3114 npx tsx scripts/smoke-auth.ts` gegen den Produktions-Build (`next start -p 3114`): **OK — 111 Prüfungen** (alle Rollen). Neu: Backoffice `/planning` (Standard 5 Tage: `data-planning-view="days"`, `data-day-count="5"`, „Heute“, Team Nord + Team Süd, Seitenleiste), `?view=today` (Kolonnen als Spalten, „Ganztägig“), `?view=week` (7 Tage), Prefill `?schedule=`, `/planning/live` (Datenschutzhinweis, OSM-Namensnennung, keine Start-Koordinaten), `GET /api/v1/planning/{board,live,recommendations}` (live ohne `startLat/startLng/deviceInfo`), Dashboard „Planung heute“ + „Konflikte diese Woche“; Teamleiter `/planning` + `/planning/live` nur Team Nord, „Nur Lesezugriff“; Monteur `/planning*` → 404; Mandant demo2 ohne Demo-Teams/-Monteure, Empfehlungen für Demo-Auftrag → 404. Renderzeiten im Produktions-Build 30–170 ms.
- Hinweis Dev-Server: `next dev` im Worktree blieb beim zweiten Start beim Kompilieren von `/dashboard` hängen (Turbopack wählt wegen mehrerer Lockfiles das Hauptrepo inkl. aller Worktrees als Workspace-Root); der erste Dev-Lauf renderte alle Planungsseiten fehlerfrei, der finale Smoke lief daher gegen `next start`.
- **Beispieldaten für die Plantafel:** `npx tsx scripts/planning-demo.ts` plant relativ zu **heute** eine Woche für beide Kolonnen (hohe Auslastung, Überbuchung, Überschneidung, Mehrtagesauftrag, ungeplante Aufträge). Die Demo-Daten aus `demo-seed.ts` liegen relativ zum Seed-Tag und wandern sonst aus dem Standardzeitraum; `--reset` entfernt die Beispiele wieder.
- **Nicht durchgeführt:** visuelle Prüfung im Browser (Drag & Drop, Karte, Tagesansicht mit parallelen Kolonnen, 1024/768/375 px) – erfordert eine angemeldete Browsersitzung (Passwort-/Token-Eingabe durch den Agenten). Bitte manuell mit `backoffice@demo.example` prüfen. Demo-Objekte haben ohne Geocoding keine Koordinaten: `npx tsx scripts/geocode-backfill.ts --tenant=demo` (≈ 1 s je Objekt) oder Koordinaten am Objekt eintragen.
## 9. Bekannte Lücken / offene Punkte
1. **L12-Abgleich beim Merge:** Feldnamen `TimeEntry.source`/`approvalStatus`, `WorkSession.manual` sind gegen die Vorgabe implementiert (Adapter aktiviert sich automatisch); tatsächliche Enum-Werte (`tracked`, `approved`) und „eine Uhr je Nutzer“ beim Merge prüfen. Die Live-Lage liest `WorkSession.status` (`en_route|running|paused|ended`) – ändert L12 die Semantik, `SESSION_STATUS` in `services/planning/live.ts` nachziehen.
2. **Dedupe-Ledger im Audit-Log** (append-only, je Mandant): bei parallel laufenden Worker-Replikas sind vereinzelte Doppelmeldungen möglich (kein Unique-Index). Meldungen erscheinen im Audit-Viewer als „Planungshinweis“.
3. **Früher fertig/Verzug** nur für Aufträge mit Uhrzeit; „Folgeauftrag gefährdet“ betrachtet nur den nächsten Auftrag derselben Kolonne am selben Tag. Die Kolonnen-Marker zeigen den Auftrag des aktivsten Mitglieds; arbeitet eine Kolonne getrennt an zwei Orten, stehen abweichende Aufträge nur im Popup/in der Liste.
4. **Events in Transaktionen** (bekannt aus L10b): `scheduleWorkOrder` ruft `assignWorkOrder`/`updateWorkOrder` innerhalb `inTransaction`; In-App-Benachrichtigungen rollen mit zurück, eine eingereihte E-Mail kann trotzdem versendet werden.
5. **Auftragsart-Dauer** nicht in `/settings/order-types` editierbar (L2-Seite); Kolonnenkapazität nur im Plantafel-Popup, nicht im Teams-Formular (L1).
6. **Nominatim-Limiter je Prozess** (s. §3); Notdienst-Objekte (L8) und Altbestände werden nicht automatisch verortet → Backfill bzw. Adressänderung. Manuelle Koordinaten werden nie ersetzt (zum Neuverorten leeren).
7. **Mehrtägige Aufträge:** Kapazitätsanteil heuristisch; keine Überschneidungsprüfung für mehrtägige/ganztägige Aufträge. Heute-Ansicht: Aufträge vor 6 bzw. nach 20 Uhr am Rand. „5 Tage“ blendet Wochenendtage aus (Aufträge am Wochenende in der Wochenansicht).
8. **Ungeplant** umfasst zusätzlich zugewiesene/angenommene Aufträge ohne Termin. Seitenleiste max. 200, Plantafel max. 3 000 Aufträge je Zeitraum.
9. **Empfehlungen** nur Luftlinie (keine Routen, Spec §35) und nur Kolonnen mit geplantem Auftrag in der Nähe.
10. CSP-Erweiterung wird beim Build ausgewertet (`MAP_TILE_URL` vor `next build`). Dashboard-Kachel „Planung heute“ berechnet Plantafel + Live-Lage je Aufruf (für MVP-Größen unkritisch).
## 10. Routen
| Route | Inhalt |
|---|---|
| `/planning` | Plantafel: Standard 5 Werktage ab heute; `?view=today` (Kolonnen als Spalten), `?view=week`, `&date=YYYY-MM-DD`; Filter `teamId`/`orderTypeId`/`priority`; `?capacity=<teamId>` Popup; `?schedule=<id>&scheduleTeam=&day=&time=` vorbelegtes Einplan-Popover |
| `/planning/live` | Live-Lage (Kolonnen-Marker, Liste, früher fertig, Filter, Polling) |
| `GET /api/v1/planning/board` | Plantafel-Daten |
| `POST /api/v1/planning/schedule` | Einplanen |
| `GET /api/v1/planning/recommendations?workOrderId=` | Empfehlungen + nahe ungeplante Aufträge |
| `GET /api/v1/planning/live` | Live-Lage inkl. Kolonnen und „früher fertig“ |
| Job `planning-watch` | alle 5 min: Verzug/Überschreitung, gefährdete Folgeaufträge, früher fertig → Meldungen |
+103
View File
@@ -0,0 +1,103 @@
# Lane L10a – Qualität & Abnahmetests (`lane/qualitaet`)
Stand: 2026-09-14 · Basis `a7d4b02` (`feature/craftvia-mvp`, L1–L9 integriert) · Spec §27, §34, §37–§40, §42, §43
## 1. Umfang / erfüllte Spec-Punkte
| Liefergegenstand | Umsetzung |
|---|---|
| 1 Demo-Seed | `prisma/seed.ts` ruft `scripts/lib/demo-seed.ts` (nur `SEED_DEMO=true` oder außerhalb Production, `SEED_DEMO=false` schaltet ab). Alles über die echten Fachservices (Statushistorie, Audit, Berichts-Snapshots konsistent), idempotent (Kundennummern `K-D…`, externe Nummern `DEMO-…`, feste Client-IDs beim Notdienst). Mandant `demo`: Team Nord (Tina Teamleiter, Max Monteur, Nora Nordmann), Team Süd (Sven Südfeld, Paul Petersen), 7 Kunden + 1 vorläufiger (Notdienst), 12 Objekte mit Zugangshinweisen (+ vorläufiges Notdienst-Objekt), Auftragsarten + 2 Checklisten-Vorlagen, 20 Aufträge über alle Statusgruppen (Neu, geplant/kommend, heute, unterwegs, in Arbeit/pausiert/Material fehlt, überfällig, mehrtägig mit Tagesbericht, Unterschrift ausstehend, zur Prüfung, technisch abgeschlossen, zur Abrechnung, abgerechnet, Notdienst), Materialvorgaben, Zeiten/Notizen/Material/Fotos, 2 freigegebene Abschlussberichte mit Unterschrift in der Historie von „Wohnanlage Elbblick – Haus A“, 1 Import in Prüfung (Musterdokument + Fake-Extraktion). Mandant `demo2`: 1 Kunde, 1 Objekt, 1 Auftrag. Neue Seed-Nutzer: `teamleiter2@`, `monteur2@`, `monteur3@demo.example`. Alle Personen/Adressen/Telefonnummern erfunden. |
| 2 E2E-Prozesstests §43.3 | 7 Skripte `scripts/test-e2e-*.ts` (Service-Ebene, eigene zz-Mandanten, Fixture `scripts/lib/e2e-fixture.ts`) – siehe §4 |
| 3 Sicherheitstests §43.4 | 4 Skripte `scripts/test-security-*.ts` inkl. HTTP-Test gegen den echten Server; RLS-Lauf dokumentiert (§5) |
| 4 Authentifizierter Durchstich | `scripts/smoke-auth.ts` auf alle Kernseiten je Rolle (Admin, Backoffice, Teamleiter, Monteur Nord/Süd, zweiter Mandant) erweitert: IDs aus den Demo-Daten, Status + erwarteter Text + keine Fehlerseite + keine Fremddaten; `PERF=1` misst die Render-Zeit |
| 5 Performance §34.1 | `scripts/seed-load.ts` (5 000 Aufträge, idempotent, `--reset`), Messung dev + Production-Build, Query-Pläne geprüft – keine Index-Migration nötig (§6) |
| Sicherheitsbefund behoben | Migration `20260914200000_qualitaet_audit_append_only`: `REVOKE UPDATE, DELETE, TRUNCATE ON audit_logs FROM craftvia_app` (§3) |
## 2. Dateien
- `prisma/seed.ts` (Demo-Nutzer + Aufruf Demo-Daten, expliziter Prozess-Exit wegen offener Queue-Handles)
- `scripts/lib/demo-seed.ts`, `scripts/lib/e2e-fixture.ts` (neu)
- `scripts/seed-load.ts` (neu), `scripts/smoke-auth.ts` (erweitert)
- `scripts/test-e2e-{regular-order,multiday-order,emergency,offline-sync,guards,import-duplicates,tenant-isolation}.ts` (neu)
- `scripts/test-security-{roles,uploads,auth,http}.ts` (neu)
- `prisma/migrations/20260914200000_qualitaet_audit_append_only/migration.sql` (neu)
- `docs/craftvia/lanes/qualitaet.md`
**Keine Änderungen an Fachcode, Fundament, `src/server/api/**`, Docker/CI.** Keine Fremd-Einzeiler, keine Stubs, keine neuen npm-Abhängigkeiten, keine Schemaänderung.
## 3. Migration (begründet)
`20260914200000_qualitaet_audit_append_only`: Die App schreibt Audit-Einträge nur per INSERT (`writeAuditLog`, Owner-Client) und liest sie im Viewer; kein Service/keine Action/keine Route ändert oder löscht sie (statisch geprüft in `test-security-auth.ts`). Bisher hatte die RLS-App-Rolle `craftvia_app` über `enable_tenant_rls` volle DML-Rechte auf `audit_logs` – mit `RLS_ENFORCED=true` hätte ein kompromittierter Fachpfad eigene Audit-Einträge manipulieren können (§43.4 „Audit-Log-Manipulation“). Backup/Restore und DSGVO-Löschung laufen über die Owner-Rolle und sind nicht betroffen. **Hinweis:** Ein späteres erneutes `SELECT enable_tenant_rls('audit_logs')` würde die Rechte wieder vergeben – dann die REVOKE-Zeile wiederholen. Keine TENANT_MODELS-/Topologie-Änderung.
## 4. Tests
`npm run gate` **grün**: prisma generate, tsc, lint (0 Fehler, 2 bestehende Warnungen außerhalb L10a), build inkl. Modul-Guard-Check, **60/60 Testskripte** (davon 11 neu). Prüfungen der neuen Skripte (✓-Zeilen im Gate-Lauf):
| Skript | ✓ | Inhalt |
|---|---|---|
| `test-e2e-regular-order` | 57 | §38 komplett: Import (Fake-Provider) → Dubletten-/Objektkandidat → Bestätigung → Zuweisung (+ Benachrichtigung) → Bundle (Zugangshinweis, internes Dokument verborgen) → Annehmen/Losfahren/Arbeit/Pause → Material Soll/Ist + Zusatz (ohne Grund abgelehnt) → Fotos (Pflichtfoto) → Checkliste → Blocker bei laufender Zeit → Zeiten berechnet → Abschlussbericht (Vorbelegung, Pflichtangabe) → Unterschrift → Teamleiter-/Backoffice-Freigabe (Bericht danach unveränderlich) → PDF (skip ohne Chromium; lokal erzeugt, `%PDF`, Checksumme) → Abrechnung → abgerechnet → vollständige Statushistorie + Audit → Objekt-Historie (Backoffice, Teamkollege). Mandant B und Monteur ohne Zuweisung an jedem Schritt abgewiesen |
| `test-e2e-multiday-order` | 22 | 2 Einsatztage, 2 Monteure, Tagesbericht je Tag nur mit Daten seines Tages (Zeiten 480/420 min, Fotos, Notizen, Material), zweiter Tagesbericht desselben Tages → conflict, daily_report_created → in_progress am Folgetag, Abschlussbericht über beide Tage (900 min), Freigaben, Abrechnung, Berichtsliste, Historie (3 freigegebene Berichte) |
| `test-e2e-emergency` | 31 | §39: Monteur legt Notdienst mit vorläufigem Kunden an (N-Nummer, laufende Session, Event), Dokumentation, Abschluss „Kunde abwesend“ (Grund Pflicht) → Zur Prüfung, `emergency.completed` + `signature_missing` + Pflichtmail; Rollen (Monteur/Teamleiter forbidden) + Mandant B; Backoffice: Dublettenkandidat, Abrechnung blockiert (Stammdaten/Bericht), Kunde zuordnen (vorläufiger → merged, Version +1), Objekt bestätigen, Auftrag ergänzen, Freigabe 5/5, abgerechnet, Historie |
| `test-e2e-offline-sync` | 40 | 12 Ops (2 Tage alt) in einem Batch → applied inkl. idMap und Gerätezeitstempeln, Wiederholung → duplicate ohne Doppelanlage, fremde clientOpId → rejected, Pflichtfoto fehlt → rejected blocked, Konflikt (Büroänderung) → conflict ohne Überschreiben + unabhängige Notiz applied + `sync.failed` an Monteur/Backoffice + Konfliktliste, Übernehmen (als Gerätenutzer) / Verwerfen, ungültige und nicht registrierte Ops, Scope und Mandant B |
| `test-e2e-guards` | 31 | Pflichtfoto + Checklistenpunkt „mit Foto“ blockieren Abschluss, Bericht und Sync-Op; Blocker verschwinden erst mit passenden Fotos; Nicht-Bild/fremdes Foto abgewiesen. Unterschrift: ohne → signature_pending (kein Weg zur Prüfung/Abrechnung), Nachreichen → in_review, erfasste Unterschrift unveränderlich, Verweigerung/Abwesenheit nur mit Grund (Snapshot, Hinweis ans Backoffice), Bild fehlt/fremd → invalid, „später“, „nicht erforderlich“ nur ohne Pflicht |
| `test-e2e-import-duplicates` | 24 | Kandidaten über Kundennummer, Firmenname, Adresse (Str./Straße), Telefon (anderes Format), E-Mail (Groß-/Kleinschreibung); nie automatische Zuordnung/Zusammenführung; Entscheidung „neu“ protokolliert, vergebene Nummer → conflict; manuelle Anlage „Mögliche Dublette“; Zusammenführen nur mit Bestätigung + Recht; Mandant B / Monteur |
| `test-e2e-tenant-isolation` | 159 | **Systematisch:** alle 35 Modelle aus `TENANT_MODELS` (Test scheitert bei neuem Modell ohne Fixture) – aus Mandant B findMany/findFirst/findUnique/count/update/updateMany/delete/deleteMany wirkungslos, Zeile unverändert, create mit fremder tenantId landet in B; **Postgres-RLS** je Tabelle als `craftvia_app` (Kontext B sieht/ändert/löscht nichts, Kontext A sieht, ohne Kontext 0 Zeilen); **67 Fachservice-Pfade** (Kunden, Objekte, Teams, Aufträge, Einsatz, Dokumente, Berichte, Import, Notdienst, Sync, Benachrichtigungen, Audit, Lotse) → not_found bzw. invalid bei Referenzen; 15 Listen/Suche/Bundle/Dashboard ohne A-Daten; A danach unverändert |
| `test-security-roles` | 113 | Rollen-Matrix aus `rbac.ts`: 26 Permission-Proben × 4 Rollen, Erwartung unabhängig aus `ROLE_DEFS` (fehlt → forbidden bzw. not_found bei Sichtbarkeitsrechten, vorhanden → nie forbidden); Abdeckungsprüfung aller Permissions (begründete Ausnahmen: Nutzer-/Rollenverwaltung → `test-tenant-users-authz.ts`, `document:read` → HTTP-Test); Sichtbarkeit read_team/Teamleiter-Dokumente |
| `test-security-uploads` | 41 | EXE/ELF/ZIP/HTML/SVG mit harmloser Endung → type_mismatch/unsupported_type, nichts gespeichert; Polyglot nur mit erkanntem Bildtyp; Übergröße je Art (15/25 MB), leer, Import; Dateinamen (Traversal, Backslash, CR/LF, reservierte Zeichen, Länge) → normalisiert, Key im Mandantenpräfix; Einsatz-Uploads; Sichtbarkeits-Eskalation; Malware-Befund und nicht erreichbarer ClamAV → fail closed; Audit nur für gespeicherte Dateien |
| `test-security-auth` | 31 | Login-Sperre nach 5 Fehlversuchen (auch richtiges Passwort abgewiesen, auditiert, Entsperren), kein Konto-Orakel, letzter Admin nicht aussperrbar; Rate-Limit Reset je IP/Konto; Session-Kill-Switch; Audit-Log: statischer Scan (keine Mutation in app/actions/services/api/jobs/lib), keine /api-Audit-Route, Viewer rein lesend, DB-Rechte `craftvia_app` (SELECT/INSERT ja, UPDATE/DELETE nein), Tenant-Guard, als `craftvia_app` UPDATE/DELETE → permission denied |
| `test-security-http` | 108 | Gegen `next start` (Produktions-Build aus dem Gate, freier Port) oder `SECURITY_BASE`: ohne Session 14 /api/v1-Routen → 401, Datei-Routen/Seiten → Login ohne Bytes; **manipulierte IDs: alle /api/v1-Routen mit ID (work-orders inkl. assign/transition/materials/documents/daily-/completion-report, reports approve/pdf/files, customers, sites history, imports + confirm, field documents, uploads) sowie `/files/<id>` und `/imports/<id>/file` → 404** ohne Daten im Body, Sync → rejected not_found, Listen/Bundle ohne A-Daten, Monteur ohne Zuweisung → 404, unsinnige IDs → 404 statt 500; Rollenaktionen → 403 + Audit „denied“; Uploads über `/documents/upload`, `/api/v1/uploads`, `/api/v1/work-orders/import` (EXE, HTML, SVG, Polyglot mit Download `image/jpeg` + `attachment` + `nosniff` + CSP, Traversal, CR/LF ohne Header-Injection, > 25 MB, Server bleibt erreichbar); CSRF (fremder Origin → 403); Sessions (manipuliert, fremder Schlüssel, abgelaufen, Kill-Switch, deaktiviert); PATCH/PUT/DELETE auf Audit-Pfade ohne Wirkung; Login-Sperre über `/api/auth/callback/credentials` (Positivkontrolle); Sicherheits-Header |
Statuscodes: Die HTTP-Tests akzeptieren für `invalid` 400 **oder** 422 und prüfen sonst 401/403/404 exakt – damit gültig vor und nach der Vereinheitlichung durch L10b. Die Offline-Prüfung „nicht registrierte Op“ ermittelt den Op-Typ dynamisch aus `EXTERNAL_OPS` (L10b registriert `report.submit`/`report.save_draft`). Schreibende Requests senden den eigenen Origin (L10b-Same-Origin-Prüfung). Je Nutzer weit unter 300 Requests/min (L10b-Rate-Limit).
## 5. RLS-Lauf (`RLS_ENFORCED=true`, `RLS_DATABASE_URL` auf die Lane-DB)
- **Alle neuen E2E- und Sicherheitsskripte: 11/11 grün** (inkl. Migration append-only).
- **Gesamte Testsuite: 58/60 grün.** Die zwei Abweichungen liegen in Fundament-Tests und sind keine Isolationslücken:
- `test-garage-storage.ts`: Abbruch „RLS_ENFORCED=true, aber RLS_DATABASE_URL fehlt“ – das Skript lädt die Umgebung offenbar erst nach dem Import von `db.ts` (fail-secure greift). Testumgebungsthema.
- `test-tenant-isolation.ts` (F-02): „findUnique über Compound-Key (fremd) → Throw“ erwartet den Owner-Pfad; mit scharfer RLS ist die fremde Zeile unsichtbar und das Ergebnis `null` – ebenfalls fail-closed, kein Datenabfluss. Test sollte beide Ausprägungen akzeptieren.
## 6. Performance (§34.1) – 5 020 Aufträge im Mandanten `demo`
`scripts/seed-load.ts` (3,1 s für 5 000 Aufträge inkl. Zuweisungen, Materialvorgaben, Historie, Berichte) → `PERF=1 scripts/smoke-auth.ts` (zweiter, warmer Request):
| Seite | Dev-Server | Production-Build (`next start`) |
|---|---|---|
| `/dashboard` (Backoffice) | 233 ms | 30 ms |
| `/work-orders` (Karten / Tabelle / überfällig / Gruppe / Suche / Seite 3) | 252–319 ms | 33–40 ms |
| `/reports` | 467 ms | 53 ms |
| `/search?q=` | 206 ms | 30 ms |
| `/m` / `/m/orders` (Teamleiter, Monteur mit ~2 500 Aufträgen im Team) | 724 / 1 536 ms | 63–67 / 104–111 ms |
Alle Seiten deutlich < 2 s. Query-Pläne (EXPLAIN ANALYZE) der Liste mit Monteur-Scope, Dashboard-Zählungen (überfällig, Berichte zur Prüfung), Paginierung und Berichtsliste: < 2 ms, Index-Scans auf den bestehenden Indizes (`work_orders(tenant_id, planned_start|status)` u. a.). Kein N+1: Liste und Dashboard laden mit festen Abfragen (Dashboard 11 parallele Counts + 2). **Keine Index-Migration nötig.** Last-Daten danach mit `--reset` wieder entfernt.
## 7. Authentifizierter Durchstich (Dev-Server :3110, Demo-Seed)
`BASE=http://localhost:3110 npx tsx scripts/smoke-auth.ts` → **OK, 94/94 Prüfungen**:
- **Admin:** Dashboard, Einstellungen (Nutzer, Audit, E-Mail, Lotse + Protokoll, Auftragsarten, Checklisten, Nummernkreise), Konto, Teams.
- **Backoffice:** Startseite → `/dashboard`, Aufträge (Karten, Tabelle, überfällig, Statusgruppe, Suche, Seite 3), Auftragsdetail mit allen 9 Tabs, Konflikte, Notdienst-Prüfung (Liste + Detail), Importe + **Prüfmaske**, Kunden (+ vorläufig, Detail), Objekte (+ Detail, **Historie** mit freigegebenem Auftrag), Berichte (+ freigegeben, zur Prüfung), Dokumente, Benachrichtigungen, Suche, Teams.
- **Teamleiter:** `/` → `/m`, Heute, Aufträge, mehrtägiger Auftrag, Auftrag zur Prüfung, Berichte; Auftrag von Team Süd → 404.
- **Monteur (Nord):** `/` → `/m`, Heute, Aufträge (Tab laufend), Auftragsdetail (Zugangshinweis), Fotos, Material, Notizen, Checkliste, Zeiten, Tagesbericht, Abschlussbericht, **Unterschrift**, Notdienst-Auftrag, Notdienst-Erfassung, Sync, Offline, Profil, eigenes Foto; fremder Auftrag/Kunde → 404, `/dashboard` → `/m`.
- **Monteur (Süd):** Heute, Unterschrift ausstehend; Auftrag von Team Nord → 404.
- **Zweiter Mandant (admin2@):** Dashboard/Aufträge/Kunden/Suche ohne Demo-Daten; Auftrag, Kunde, Objekt, Bericht, Notdienst, Import, Datei aus `demo` → 404.
Gefundene Fehler im Durchstich: **keine** (kein Fachcode geändert). Visuelle Prüfung im Browser (375/768/1024 px) nicht durchgeführt: der Browser-Login würde eine Passworteingabe bzw. das Einschleusen eines Session-Tokens durch den Agenten erfordern; die mobilen Seiten wurden in L4/L7 visuell geprüft.
## 8. Stubs / Abhängigkeiten
Keine Stubs. Genutzt werden ausschließlich die integrierten Services von L1–L9. Kompatibilität mit L10b siehe §4 (Statuscodes, Same-Origin, Rate-Limit, registrierte Sync-Ops).
## 9. Befunde, bekannte Lücken, Hinweise an den Architekten
1. **Login ohne IP-basiertes Limit** (nur Sperre je Identität nach 5 Fehlversuchen; Reset/Einladung haben IP+Konto-Limits). Password-Spraying über viele Konten wird nicht gebremst → Kandidat für das L10b-Rate-Limit (auf `/api/auth/callback/credentials`).
2. **Polyglot-Dateien** (gültige Signatur + angehängter Fremdinhalt) passieren den Magic-Byte-Scanner. Mitigiert durch Speicherung mit erkanntem MIME-Typ, Auslieferung als `attachment` + `nosniff` + CSP (HTTP-Test); echter Inhaltsscan nur mit `CLAMAV_HOST`.
3. **Doppelte Audit-Einträge bei Einsatz-Uploads:** `storeFieldUpload` (L4) schreibt nach `storeFile` einen zweiten `document/create`-Eintrag. Harmlos, aber redundant.
4. **`confirmImport` validiert das Formular vor der Mandanten-/Existenzprüfung** → fremde ID mit ungültigem Formular ergibt `invalid` statt `not_found` (kein Datenabfluss; mit gültigem Formular 404).
5. **Mobile Listen ohne Paginierung** (`/m/orders` max. 200, „Heute“ max. 100 Aufträge je Abruf; §34.1 „Auftragslisten müssen paginiert werden“). Performance unkritisch; bei sehr großen Teams fallen Einträge aus der Liste.
6. **RLS-Fundament-Tests** (§5): `test-garage-storage.ts` (Env-Ladereihenfolge) und `test-tenant-isolation.ts` (Compound-Key erwartet Throw statt `null`) für `RLS_ENFORCED=true` anpassen.
7. **Demo-Termine sind relativ zum Seed-Zeitpunkt** („heute“, „überfällig“); der Seed ist idempotent und verschiebt bestehende Demo-Aufträge nicht. Für frische Demo-Daten Mandant neu aufsetzen.
8. **Seed in Production:** `prisma/seed.ts` lädt `scripts/lib/demo-seed.ts` nur bei aktivierten Demo-Daten dynamisch; ein Production-Image ohne `scripts/` braucht `SEED_DEMO` ungesetzt (Default in Production: aus).
9. Der HTTP-Sicherheitstest startet im Gate den Build per `next start` („does not work with output: standalone“ ist nur eine Warnung). Ohne Build und ohne `SECURITY_BASE` wird er übersprungen (Exit 0).
## 10. Screens / Routen
Keine neuen Routen. Geprüft (Smoke/HTTP): siehe §7 und §4 (`test-security-http`).
+98
View File
@@ -0,0 +1,98 @@
# Lane L1 – Stammdaten
Branch `lane/stammdaten` (Basis `bf44567` auf `feature/craftvia-mvp`). Spec: §7 Kunden, §8 Objekte,
§11 Teams, §24 Dokumente (Objekt-/Kunden-Dokumente), US-003, US-005, US-011.
Keine Schemaänderung, keine neue Migration, keine neuen npm-Abhängigkeiten.
## 1. Umfang / erfüllte Spec-Punkte
| Punkt | Umsetzung |
|---|---|
| §7.1 Kundendaten | alle Felder, Status aktiv/inaktiv/vorläufig/zusammengeführt, Anlage/Änderung als Popup, Soft Delete (blockiert bei offenen Aufträgen) |
| Kundennummer | `nextNumber(…, "customer")` (K-00001), manuell überschreibbar, eindeutig je Mandant; der Nummernkreis überspringt manuell vergebene Nummern |
| §7.2 Ansprechpartner | mehrere je Kunde, Funktion, Telefon/Mobil/E-Mail, bevorzugter Kontaktweg, Bemerkungen, Soft Delete |
| §7.3 / US-003 Dubletten | `src/lib/customers/duplicates.ts` (Normalisierung: Kleinschreibung, Umlaute/ß, Rechtsformen, Straße/Str., Telefon nur Ziffern, +49→0; Scoring 0..1 als probabilistisches ODER über Kundennummer 1.0 · E-Mail 0.6 · Name exakt 0.6 / ähnlich 0.45 · Telefon 0.5 · Adresse 0.4/0.25; Schwelle 0.4) + `services/customers/duplicates.ts#findDuplicateCustomers(ctx, candidate, { excludeId?, limit? })` → `{ customerId, score, reasons, customerNumber, displayName, city, status }[]` (Obermenge des Vertrags). Manuelle Anlage zeigt „Mögliche Dublette“ mit Link auf bestehende Kunden und „Trotzdem neu anlegen“ |
| Zusammenführen | `mergeCustomers(ctx, { sourceId, targetId, confirm: true })` nur mit `customer:merge` + Bestätigungs-Checkbox; hängt Kontakte, Objekte, Aufträge (Version +1 für Offline-Konflikte), Dokumente um; Quelle → `merged` + `mergedIntoId`; Audit für Quelle und Ziel; nie automatisch |
| Vorläufige Kunden | `confirmProvisionalCustomer(ctx, id)` (provisional → active), Filter „Vorläufig“, Hinweisbanner; für L8 nutzbar |
| §8.1 Objekte | alle Felder inkl. Zugangs-/Park-/Sicherheits-/technische Hinweise (hervorgehoben, Sicherheit mit Warnkante + Icon), Kontakt des Kunden oder Freitext, Koordinaten optional, Kartenlink OpenStreetMap (Koordinaten oder Adresse, kein Embed) |
| §8.2 / §24 Dokumente | Tab „Dokumente“ an Kunde und Objekt: Upload (Kategorie, Sichtbarkeit, Titel), neue Version, Versionsliste, Metadaten bearbeiten, Soft Delete; `/documents` Backoffice-Übersicht mit Filter Kategorie/Kunde/Objekt/Auftragsnummer, „nur aktuelle Versionen“ |
| §8.3 / US-011 Historie | `getSiteHistory(ctx, siteId, { onlyApproved, page, pageSize })`: neueste zuerst, Datum (Einsatzbeginn/Termin), Auftrag, Auftragsart, Team, Status (Brandbook-Statusgruppe), durchgeführte Arbeiten (ActivityNote work_done), Material aggregiert (ohne not_used), Fotoanzahl, Links auf freigegebene Berichte, Unterschrift ja/nein, offene Folgearbeiten (followUpWork + follow_up-Notizen) mit Warnkante/Icon/Text |
| §11.1 Teams | Liste + Popup: Teamleiter, Mitglieder mit gültig ab/bis, Telefon, Fahrzeug, Einsatzgebiet, interne Hinweise, Status; nur aktive Mandanten-Nutzer; Soft Delete (blockiert bei offenen Aufträgen) |
| §4.3 Dokumenten-Service | `storeFile` (Allowlist PDF/JPEG/PNG/WebP/HEIC/Audio, Magic Bytes vs. deklarierter Typ, Limits Bild 15 MB / PDF 25 MB / Audio 20 MB, Dateiname normalisiert, SHA-256, `storage.put`, Versionierung über `lineageId`), `FileScanner` (Magic Bytes + ClamAV-INSTREAM, wenn `CLAMAV_HOST` gesetzt), `getDownloadUrl` → `/files/<documentId>` |
| Download-Route | `src/app/(app)/files/[documentId]/route.ts` ersetzt `files/[...key]`: Session + DB-autoritatives `document:read` → Dokument im Mandanten → `allowedDocumentVisibility` → Auftrag in `workOrderScope` bzw. (ohne Auftrag) Objekt in `siteScope` / Kunde in `customerScope`; sonst 404; `attachment` + `nosniff` + `no-store` |
| §29 API | `GET/POST /api/v1/customers`, `GET/PATCH /api/v1/customers/[id]`, `GET/POST /api/v1/sites`, `GET /api/v1/sites/[id]/history`; Paginierung `?page&pageSize` → `{ data, pagination }`; Fehler `{ error: { code, message, details? } }` |
## 2. Querschnitt, den andere Lanes nutzen
- `src/server/api/context.ts`
- `requireApiContext(moduleKey | null, ...permissions) → ServiceCtx` – Session-Cookie wie Guard; Mitgliedschaft, Identity-Status, Kill-Switch, Passwortzwang und **effektive Rechte aus der DB**; Fehler als `ApiError` (401/403); `moduleKey = null` für modulübergreifende Endpunkte (Downloads).
- `assertSameOrigin(req)` – CSRF-Schutz (Origin/Sec-Fetch-Site) für schreibende Route Handler.
- `requirePageContext(moduleKey)` – Lesekontext für Seiten (Modul-Gate + JWT-Rechte, wie in AGENTS.md für Lesepfade vorgesehen).
- `src/server/api/respond.ts` – `withApi`, `toErrorResponse` (ServiceError/ZodError/ForbiddenError/ModuleDisabledError/Isolation → HTTP), `json`, `paginated`, `parsePagination`, `readJson`.
- `src/server/api/action-state.ts` – einheitlicher Rückgabetyp für Formular-Actions (Fehlercodes statt Servermeldungen, CWE-209).
- `src/server/services/documents/{store,access,scanner}.ts` – Dokumenten-Service (siehe oben), `listDocuments`, `documentReadWhere`, `authorizeDocumentAccess`, `openDocumentContent`.
- `src/components/sites/site-history.tsx` – Server-Komponente, von L4 read-only einbindbar (`linkOrders={false}`).
- Upload aus dem Browser: `POST /documents/upload` (multipart; 303-Redirect mit `?docOk`/`?docError` oder JSON bei `Accept: application/json`).
## 3. Dateien
- Services: `src/server/services/customers/{customers,contacts,duplicates,merge,schemas,format}.ts`, `src/server/services/sites/{sites,history,map-link}.ts`, `src/server/services/teams/teams.ts`, `src/server/services/documents/{store,access,scanner}.ts`
- Lib: `src/lib/customers/duplicates.ts`
- API-Hilfen: `src/server/api/{context,respond,action-state}.ts`
- Actions: `src/server/actions/customers/{customers,contacts}.ts`, `src/server/actions/sites/sites.ts`, `src/server/actions/teams/teams.ts`, `src/server/actions/documents/documents.ts` (alle `moduleGuard` + `await guard(...)`)
- Routen: `src/app/(app)/customers/{page,[id]/page}.tsx`, `src/app/(app)/sites/{page,[id]/page}.tsx`, `src/app/(app)/teams/page.tsx`, `src/app/(app)/documents/{page.tsx,upload/route.ts}`, `src/app/(app)/files/[documentId]/route.ts` (alt `files/[...key]` entfernt), `src/app/api/v1/customers/{route,[id]/route}.ts`, `src/app/api/v1/sites/{route,[id]/history/route}.ts`
- Komponenten: `src/components/customers/{form-ui,action-form,status,customer-form,contact-form,merge-form}.tsx`, `src/components/sites/{site-form,site-history}.tsx`, `src/components/teams/team-form.tsx`, `src/components/documents/{document-panel,document-table,document-upload-form,document-edit-form}.tsx`
- Texte: `messages/{de,en}/{customers,sites,teams,documents}.json`
- Fremd-Einzeiler: `src/components/audit-trail.tsx` (Entity-Label `contact: "Ansprechpartner"`)
- Tests: `scripts/test-stammdaten-{duplicates,customers,sites,teams,documents}.ts`, Fixtures `scripts/lib-stammdaten-fixtures.ts`
## 4. Tests
`npm run gate` grün: prisma generate, tsc, lint (0 Fehler; 2 Warnungen im L6-Platzhalter `handle-event.ts`), build inkl. Modul-Guard-Check, **27/27 Testskripte** (davon 5 neu).
| Skript | Inhalt |
|---|---|
| `test-stammdaten-duplicates` | Normalisierung, Scoring/Schwelle, DB-Suche inkl. formatierter Telefonnummern, `excludeId`, zusammengeführte ausgeschlossen, **Mandant B findet nichts von A**, **Monteur ohne Auftrag findet nichts** |
| `test-stammdaten-customers` | Nummernkreis/manuelle Nummer/Eindeutigkeit je Mandant, Dublettenhinweis + Bestätigung, Validierung, Audit before/after, Ansprechpartner, vorläufig → aktiv, **Mandantentrennung** (lesen/ändern/Kontakt/bestätigen/löschen/zusammenführen → not_found), **Monteur** ohne Zuweisung not_found / mit Teamauftrag sichtbar / keine Schreibrechte, Merge (Rechte, Bestätigung, keine Mandantenkreuzung, Umhängen, Versionserhöhung, Audit, doppelt → conflict), Soft Delete |
| `test-stammdaten-sites` | Objekt-CRUD, Kontakt/Kunde-Validierung, Koordinaten, Kartenlink, Historie (Reihenfolge, Arbeiten, interne Notizen ausgeblendet, Material aggregiert, Fotos, Berichte, Unterschrift, Folgearbeiten, Paginierung), **Monteur sieht nur freigegebene Einsätze**, ohne sichtbaren Auftrag not_found, **Mandantentrennung**, Soft Delete |
| `test-stammdaten-teams` | Anlage, Mitglieder gültig ab/bis, fremder/inaktiver Nutzer abgelehnt, doppelte Mitglieder, Namenskonflikt, Wirkung auf `activeTeamIds`, Monteur liest aber verwaltet nicht, **Mandantentrennung**, Soft Delete |
| `test-stammdaten-documents` | Scanner (Magic Bytes, Mismatch, Allowlist, Alias), Dateinamen, **falscher Magic Byte → abgelehnt**, leer/zu groß/Sichtbarkeit, SHA-256, Versionierung, **backoffice_only für Monteur verweigert**, team_lead nur Teamleiter, Auftrags- und Objekt-Scope, **fremder Mandant verweigert**, Listenfilter, Byte-Roundtrip Garage, Metadaten/Soft Delete |
**Smoke gegen den Dev-Server** (Port 3101, Seed-Nutzer `backoffice@demo.example`, `monteur@demo.example`, `admin2@demo.example`; Session-Cookie lokal über `finalizeIdentityLogin` + Auth.js-`encode` ausgestellt, ohne Passworteingabe): **alle Prüfungen grün**.
- API: Anlage 201 mit K-Nummer, Dublette 409 `possible_duplicates`, Validierung 422 `invalid`, Cross-Site-Origin 403, Liste/Suche mit `pagination.total`, Detail, PATCH, Objekt anlegen, Historie `meta.onlyApproved`; Monteur ohne Zuweisung 404/403; Mandant demo2 → 404.
- Dateien: Upload → `?docOk=1`, falscher Magic Byte → `?docError=type_mismatch`, externes `returnTo` → kein Open Redirect, JSON-Upload, Download 200 byte-identisch mit `attachment` + `nosniff`, `backoffice_only` als Monteur 404, fremder Mandant 404, ohne Session Redirect.
- Server-Rendering (200 + erwarteter Text): `/customers` (+ `?status=provisional`, `?new=1`), `/customers/[id]` mit allen Tabs sowie `?edit=1`/`?merge=1`, `/sites` (+ `?new=1&customerId=`), `/sites/[id]` (Stammdaten, Kartenlink, Dokumente inkl. Upload-Rückmeldungen, Historie, `?edit=1`), `/teams` (+ `?new=1`, Monteur „Nur Lesezugriff.“), `/documents` (+ Kategoriefilter); Monteur `/customers/[id]` → 404, demo2 `/sites/[id]` → 404.
- Visuell im Browser: Kundenliste, Anlage-Popup, Ansprechpartner über das Client-Formular angelegt (Server Action → Liste aktualisiert); Viewport 800 px und 375 px.
## 5. Stubs / Abhängigkeiten zu anderen Lanes
- Keine Stubs nötig. Genutzt werden die Verträge aus dem Architektur-Commit (`visibility.ts`, `numbering.ts`, `status.ts`, `storage/adapter.ts`).
- Links auf `/work-orders/[id]` (L2) und `/reports/[id]` (L5) zeigen bis zum Merge ins Leere (404).
- Vertrag für L3: Architektur nennt `findDuplicateCustomers(db, candidate)`, umgesetzt ist (wie im Lane-Auftrag) `findDuplicateCustomers(ctx, candidate, opts?)`; Rückgabe ist eine Obermenge von `{ customerId, score, reasons }`.
- L4/L5/L8 legen Fotos, Sprachnotizen, Unterschriften, Berichts-PDFs über `storeFile` ab: mit `workOrderId` genügt `field:execute`, `report:write`, `emergency:create` oder `document:write` + Auftrag im Scope; unverknüpfte Originale (Import) brauchen `document:write` oder `import:write`.
## 6. Bekannte Lücken / offene Punkte
1. **Fundament – Proxy:** `/api/v1/**` ohne Session-Cookie wird von `src/proxy.ts` auf `/login` umgeleitet (307) statt `401` JSON. Vorschlag: `/api/v1` im Proxy durchlassen (die Handler prüfen selbst über `requireApiContext`).
2. **Fundament – Upload-Größe:** Mit aktivem Proxy puffert Next.js Request-Bodies nur bis `proxyClientMaxBodySize` (Default 10 MB). PDFs bis 25 MB brauchen `experimental.proxyClientMaxBodySize: "26mb"` in `next.config.ts` (nicht in dieser Lane geändert). Größere Uploads werden bis dahin mit `too_large` abgewiesen.
3. **Fundament – DSGVO:** Personenreferenzen der Fachmodelle (u. a. `Customer.createdById`, `Document.uploadedById`, `Team.leaderUserId`, `TeamMember.userId`) sind noch nicht in `src/server/dsgvo/pii-fields.ts` eingetragen (AGENTS.md Regel 6).
4. **Historie für Monteure (Entscheidung zur Prüfung):** Das Objekt muss über einen sichtbaren Auftrag erreichbar sein (`siteScope`). Dann sieht der Monteur **alle freigegebenen** Einsätze am Objekt, auch die anderer Teams (US-005 „freigegebene frühere Berichte“). Nicht freigegebene fremde Einsätze bleiben verborgen. Soll strikt nur `workOrderScope` gelten, ist das eine Zeile in `services/sites/history.ts`.
5. Downloads werden nicht modulgegated (`moduleKey = null`), weil Fotos/Berichte modulübergreifend sind; Rechte, Sichtbarkeit und Scope gelten weiterhin. Downloads werden nicht auditiert.
6. Soft Delete eines Dokuments löscht das Objekt im Speicher nicht (Aufbewahrung). Die Tests hinterlassen kleine Testobjekte im Garage-Bucket (Adapter hat kein `delete`).
7. Objekt-Formular: Die Kontaktauswahl steht nur bei bekanntem Kunden bereit (Bearbeiten bzw. Anlage aus dem Kundendetail). Bei freier Anlage folgt die Auswahl nach dem Speichern.
8. Dublettensuche: Namens-/PLZ-/E-Mail-Vorfilter per SQL (max. 200 Kandidaten), Telefonvergleich über die Telefonspalten im Speicher (max. 10 000 Zeilen) – für MVP-Größen ausreichend; bei großen Mandanten normalisierte Suchspalten ergänzen.
9. OpenAPI-Dokumentation der Endpunkte folgt mit L10.
10. **Fundament – Backoffice-Layout mobil:** Bei 375 px belegt die feste Sidebar (`w-60` in `src/app/(app)/layout.tsx`) den Großteil der Breite. Die L1-Seiten umbrechen Formulare/Filter und scrollen Tabellen horizontal, eine einklappbare Sidebar fehlt aber (Mobile-Zielgruppe nutzt `/m`, L4).
## 7. Screens / Routen
| Route | Inhalt |
|---|---|
| `/customers` | Suche, Statusfilter inkl. „Vorläufig“, 25 je Seite, „Neuer Kunde“ (`?new=1`, Dublettenhinweis) |
| `/customers/[id]` | Tabs Stammdaten · Ansprechpartner (`?contact=new|<id>`) · Objekte · Aufträge (Lesesicht → `/work-orders/[id]`) · Dokumente; Aktionen Bearbeiten (`?edit=1`), Kunden bestätigen, Zusammenführen (`?merge=1`), Löschen (`?delete=1`) |
| `/sites` | Suche, Statusfilter, 25 je Seite, „Neues Objekt“ (`?new=1&customerId=`) |
| `/sites/[id]` | Tabs Stammdaten (Hinweise hervorgehoben, Kartenlink) · Dokumente · Historie (Backoffice: alle / nur freigegebene) |
| `/teams` | Liste mit Mitgliedern (aktuell/beendet/ab Datum), Popup Anlage/Bearbeitung/Löschen; Monteur: Nur-Lese-Ansicht |
| `/documents` | Übersicht mit Filtern, Upload, Bearbeiten, neue Version, Entfernen |
| `/files/[documentId]` | autorisierter Download |
| `/documents/upload` | Upload-Endpunkt (POST, multipart) |
+108
View File
@@ -0,0 +1,108 @@
# Lane L15 – Testphase & Onboarding (`lane/testphase`)
Stand: 2026-09-16 · Basis `6b8cdf5` (`feature/craftvia-mvp`, L1–L14 integriert) · Betriebsdoku: [../TESTPHASE.md](../TESTPHASE.md)
## 1. Umfang / erfüllte Punkte
| Punkt | Umsetzung |
|---|---|
| **1 Datenmodell** | Migration `20260916100000_testphase`: `Tenant.plan` (`FULL`/`TRIAL`), `trialSource`, `trialStartedAt`, `trialEndsAt` (exklusives Ende = Beginn des Folgetags in Europe/Berlin), `convertedAt`, `readOnlySince`, `deletionDueAt`, `trialDeletedAt`, Versandmarker `trialReminder7/3/1At`, `trialExpiredNoticeAt`, `trialDeletionNoticeAt` (Plattform-Daten, keine RLS). `TrialSignup` (Plattform-Tabelle ohne `tenant_id`): Firmendaten, E-Mail, Argon2id-Hash (+ Pepper, wird nach Bestätigung/Ablauf geleert), Enddatum, Beispieldaten/Module, Token-**SHA-256**, Ablauf 24 h, IP-**HMAC**, Status. `TenantExport` (Mandanten-Tabelle, `enable_tenant_rls`, beide `TENANT_MODELS`, `requestedById` in `pii-fields.ts`). `TenantSettings.onboarding` (Checkliste). |
| **2 Öffentlicher Wizard** `/testen` | 5 Schritte (Betrieb · Admin-Konto · Testzeitraum · Einrichtung · Zusammenfassung), Fortschrittsanzeige, alle Werte in einem State (Zurück ohne Datenverlust), Validierung je Schritt im Browser **und** per Server-Action (`checkTrialStepAction`, gleiche Regeln aus `lib/trial/signup.ts`), finale Prüfung serverseitig. Branche: Auswahlliste + Freitext, Betriebsgröße optional, Passwort-Policy wie Identity, Enddatum per Datumsfeld (Vorbelegung heute + 14, erlaubt morgen … heute + `TRIAL_MAX_DAYS`, Anzeige „Testphase bis TT.MM.JJJJ (X Tage)“), Beispieldaten ja/nein, Module vorausgewählt (abwählbar), Pflicht-Checkboxen mit Platzhalter-Seiten `/testen/nutzungsbedingungen`, `/testen/datenschutz`. Double-Opt-in-Mail → `/testen/bestaetigen` zeigt die Anmeldung, **Einrichten erst per POST** (Mail-Scanner verbrauchen nichts) → `provisionTrialTenant` (tenant-admin, TRIAL, Ende des Tages Berlin, Löschung +30 Tage) → **direkt angemeldet** über den bestehenden `login-ticket`-Provider → `/dashboard?welcome=1` mit **„Erste Schritte“** (Firmendaten, Team, Monteur einladen, erster Auftrag, mobile App; automatisch erkannt oder abhakbar, ausblendbar). Missbrauchsschutz: Rate-Limit je IP und je E-Mail (neue Scopes `trialSignup`, `trialConfirm`, `trialStepCheck`), Honeypot, identische Antwort bei vorhandener E-Mail (+ Hinweis-Mail, gleicher Hash-Aufwand gegen Timing), Slug-Kollisionen (Reservierung per `create`, `-2 … -20`, Zufallssuffix), ältere Links derselben Adresse entwertet. |
| **3 Plattform-Admin** | `/admin/trial` „Testmandant anlegen“ (Firma, Branche, Admin, Enddatum bis 1 Jahr, Beispieldaten) → Einladung über `issueToken("invitation")` + `sendUserInvitationMail` (bestehende Person: nur verknüpft). Mandantenliste: Spalte „Version“ mit Badges „Test bis …“ / „abgelaufen – nur lesen“ / „Löschung am …“ / „gelöscht“ / „Vollversion“, Filter Alle/Test/Voll. Mandantendetail: Karte „Testphase“ mit Enddatum ändern/verlängern (auch nach Ablauf), umwandeln, sofort beenden, Löschung vormerken/abbrechen – jeweils Popup mit Bestätigungs-Checkbox, nur Voll-Admins, Audit (scope platform, am Mandanten) mit before/after. |
| **4 Nur-Lesen nach Ablauf** | `services/trial/state.ts#assertTenantWritable` → `ServiceError("blocked", "trial_expired", { readOnly, message, deletionDueAt })`, zeitgenau über `trialEndsAt`. Zentral in `moduleGuard` (alle Modul-Actions inkl. Lotse), `requireApiContext` für jede nicht lesende `/api/v1`-Anfrage (Methode aus `withApi`, AsyncLocalStorage → 422; Sync, Uploads eingeschlossen), explizit in `/documents/upload`, `POST /api/v1/work-orders/[id]/documents` (ohne `withApi`), Einstellungen, Nutzer-/Rollenverwaltung, Lotse-Einstellungen, Onboarding. Worker überspringt `import-extraction`/`transcription` abgelaufener Mandanten. Offen bleiben Login, Konto, Lesen, `/files`, PDFs, Export. Lesepfade, die `moduleGuard` nutzen (mobile Seitenkontexte, Import-Datei-Download), verwenden `moduleGuard(key, { read: true })`; der Guard-Check verbietet diesen Modus in Actions. Banner in Backoffice und mobiler App: „Testphase endet in X Tagen“ (ab 7 Tagen) bzw. „Testphase abgelaufen – nur Lesezugriff. Daten werden am … gelöscht.“ + Kontakt (`TRIAL_CONTACT_EMAIL`) + Export-Link (Admins). |
| **5 Export** | `/settings/export` (tenant:manage, auch im Nur-Lesen-Zustand): Worker-Job `tenant-export` baut ZIP mit `csv/` (Semikolon, BOM, Formel-Injektion entschärft) + `json/` für Kunden, Ansprechpartner, Objekte, Teams, Nutzer (ohne Secrets), Aufträge, Checklisten, Material (Vorgabe/Verbrauch), Einsätze, Zeiten, Notizen, Berichte, Unterschriften, Fotos, Dokumente, Meilensteine, Abrechnung + `dateien/` (alle gespeicherten Dateien inkl. Berichts-PDFs, Limit 500 MB) + `LIESMICH.txt`; Ablage über `storage.put` unter `<tenantId>/uploads/`, Download `/settings/export/<id>` (Session + DB-autoritatives `tenant:manage`, 7 Tage, Audit). Der DSGVO-/Backup-Export ist ein Betreiber-Werkzeug (alle Tabellen, JSON, Plattform-Portal) – wiederverwendet wurde sein ZIP-Writer (`backup/zip.ts`). |
| **6 Lebenszyklus-Job** | Queue `trial-lifecycle`, Job-Scheduler `trial-lifecycle-daily` (24 h) in `scheduleRecurringJobs`. Erinnerungen 7/3/1 Tage (verpasster Lauf → nur die nächstliegende, ältere als erledigt markiert), Ablaufmail, Löschhinweis 7 Tage vorher, Löschung an `deletionDueAt` über `dsgvo/deletion.ts#offboardTenant` (Topologie aller `TENANT_MODELS`, Identities ohne Rest-Mitgliedschaft, Löschnachweis) + Objektspeicher-Präfix `<tenantId>/` + Plattform-Audit. Jede Mail wird per bedingtem Update beansprucht (genau einmal, auch parallel). Doppelprüfung + Claim unmittelbar vor dem Löschen (TRIAL, nicht umgewandelt, abgelaufen, fällig → `SUSPENDED`, danach `ARCHIVED`). Anmeldungen werden 7 Tage nach Link-Ablauf gelöscht. |
| **7 i18n/Doku** | Namespace `trial` (de/en), `nav.dataExport`; Mail-Vorlagen `trial_confirm`, `trial_existing_account`, `trial_reminder`, `trial_expired`, `trial_deletion_notice` (de/en, `TRIAL_TEMPLATE_KEYS`). `docs/craftvia/TESTPHASE.md` (Lebenszyklus, env, Jobs, Sperre, Datenschutz), Abschnitte in `DEPLOY.md` (7.4) und `API.md`, env-Beispiele. |
### Entscheidungen
- **Beispieldaten:** `scripts/lib/demo-seed.ts` setzt sieben Nutzer mit festen Rollen sowie Foto-/PDF-/Import-Pipeline voraus. Ein Testmandant startet mit genau einem Admin → `services/trial/sample-data.ts` ist eine gekürzte Variante nach demselben Prinzip (ausschließlich echte Fachservices: 3 Kunden, 3 Objekte, „Beispielteam“, 4 Aufträge in Entwurf/Geplant/Zugewiesen/Prüfung; Kennung `BEISPIEL-`, zählt nicht für die Checkliste).
- **Provisionierung:** `provisionTenant` upsertet per Slug. Damit zwei gleichnamige Anmeldungen nie im selben Mandanten landen, reserviert `provisionTrialTenant` den Slug per `create` (inkl. Testphasen-Feldern) und ruft erst dann `provisionTenant` auf.
- **Sync im Nur-Lesen-Zustand:** Der Batch wird mit 422 abgewiesen, bevor eine Operation angewendet wird; die Outbox behandelt Nicht-OK beim Batch als vorübergehend und behält die Operationen (kein Datenverlust nach Verlängerung/Umwandlung). `services/sync/apply.ts` blieb unverändert.
- **Plattform-Wizard** als eine Seite mit vier nummerierten Abschnitten (Betreiber-Werkzeug, Einladung statt Passwort).
## 2. Dateien
**Neu**
- Migration `prisma/migrations/20260916100000_testphase/`
- `src/lib/trial/{dates,signup}.ts`
- `src/server/services/trial/{config,state,signup,abuse,provision,sample-data,admin,lifecycle,export,onboarding,mail,jobs,storage-purge}.ts`
- `src/server/actions/trial-{signup,platform,tenant}.ts`
- `src/server/jobs/processors/{trial-lifecycle,tenant-export}.ts`
- `src/app/testen/{layout,page}.tsx`, `src/app/testen/{bestaetigen,nutzungsbedingungen,datenschutz}/page.tsx`
- `src/app/(app)/settings/export/page.tsx`, `src/app/(app)/settings/export/[id]/route.ts`, `src/app/(platform)/admin/trial/page.tsx`
- `src/components/trial/{signup-wizard,confirm-form,legal-placeholder,trial-banner,getting-started,trial-badge,trial-admin-card,platform-forms}.tsx`
- `messages/{de,en}/trial.json`, `docs/craftvia/TESTPHASE.md`, dieser Bericht
- Tests/Smoke: `scripts/test-testphase-{signup,readonly,lifecycle}.ts`, `scripts/lib/testphase-fixture.ts`, `scripts/smoke-testphase.ts`
**Fundament-/Fremdeingriffe (laut Auftrag erlaubt, jeweils minimal)**
| Datei | Eingriff | Grund |
|---|---|---|
| `prisma/schema.prisma` | Tenant-Felder, Enum `TenantPlan`, `TenantSettings.onboarding`, Modelle `TrialSignup`, `TenantExport` | Punkt 1 |
| `src/server/provision.ts` | optional `admin.passwordHash`, `admin.mustChangePassword`, `modules`; Rückgabe um `adminUserId`, `identityId`, `identityCreated` ergänzt; Identity per `findUnique`+`create` statt `upsert` (Verhalten für Bestandsaufrufer gleich) | Hash aus der Anmeldung übernehmen, Modulauswahl, Einladung nur für neue Identity |
| `src/server/action-guard.ts` | `assertTenantWritable` nach Modul-Check; Option `{ read: true }` | zentrale Sperre; Lesepfade |
| `src/server/api/respond.ts` | `withApi` legt die Methode in AsyncLocalStorage ab (`currentApiMethod`, `isMutatingApiRequest`) | Sperre nur für nicht lesende Anfragen |
| `src/server/api/context.ts` | `assertApiWriteAllowed(tenantId)` in `requireApiContext` | zentrale API-Sperre |
| `src/app/(app)/documents/upload/route.ts`, `src/app/api/v1/work-orders/[id]/documents/route.ts` | je 1 Zeile `assertTenantWritable` | Upload-Routen ohne `withApi` |
| `src/server/actions/{tenant-settings,tenant-users,lotse-settings}.ts` | je 1 Zeile Sperre (+ Klartext in `tenant-users#actionError`) | Einstellungen/Nutzerverwaltung gesperrt |
| `src/server/services/field/page-context.ts`, `src/app/(field)/m/emergency/page.tsx`, `src/app/(app)/imports/[id]/file/route.ts` | `moduleGuard(key, { read: true })` | Lesepfade ohne Sperre (Smoke-Befund: sonst 500 auf `/m`) |
| `src/proxy.ts` | `/testen` in `PUBLIC_PATHS` | öffentliche Anmeldung |
| `src/server/rate-limit.ts` | Scopes `trialSignup`, `trialConfirm`, `trialStepCheck` | Missbrauchsschutz |
| `src/server/mail/templates.ts` | 5 Vorlagen de/en, `TRIAL_TEMPLATE_KEYS` | Mails |
| `src/server/jobs/{queues,processors/index}.ts`, `scripts/craftvia-worker.ts` | 2 Queues + Scheduler, 2 Registry-Zeilen, Sperrprüfung vor jedem Job | Punkt 5/6 |
| `src/app/(app)/layout.tsx`, `src/app/(field)/m/layout.tsx` | `<TrialBanner>` | Banner |
| `src/app/(app)/dashboard/page.tsx` (L2) | 1 Zeile `<GettingStarted>` | Checkliste im Dashboard |
| `src/app/(platform)/admin/page.tsx`, `admin/[id]/page.tsx` | Badge-Spalte, Filter, Button; Karte `TrialAdminCard` | Punkt 3 |
| `src/lib/nav.ts`, `messages/{de,en}/nav.json` | Eintrag „Datenexport“ | Einzeiler |
| `src/server/db.ts`, `src/server/backup/topology.ts`, `src/server/dsgvo/pii-fields.ts` | `TenantExport` | Pflicht bei neuer Tenant-Tabelle |
| `scripts/check-module-guards.ts` | 3 Einträge, Kategorie `PUBLIC` (jede Action braucht Rate-Limit), Verbot des Lese-Modus in Actions | Top-Level-Actions |
| `scripts/test-e2e-tenant-isolation.ts` (L10) | 1 Fixture-Zeile `TenantExport` | Test verlangt Abdeckung aller Tenant-Modelle |
| `.env.example`, `.env.prod.example`, `.env.coolify.example`, `docs/craftvia/{DEPLOY,API}.md` | Testphase-Abschnitte | Betrieb |
**Neue env-Variablen:** `TRIAL_MAX_DAYS` (Default 30), `TRIAL_CONTACT_EMAIL` (optional). Keine neuen npm-Abhängigkeiten.
## 3. Tests
| Skript | Prüfungen | Inhalt |
|---|---|---|
| `test-testphase-signup.ts` | 76 | Grenzen (morgen … heute + 30, Vorbelegung), Pflichtfelder, Passwort-Policy, Modul-/Checkbox-Pflicht, Normalisierung, `TRIAL_MAX_DAYS`, Ende des Tages Berlin (Winter/Sommer/Umstellung); ungültig/Honeypot ohne Seiteneffekt; Double-Opt-in (nur Token-Hash, Argon2id, IP-HMAC, 24 h, kein Mandant vor Bestätigung, ältere Links entwertet, Ablauf → expired + Hash geleert, Einmalverwendung); Provisionierung (TRIAL, Enddatum, Löschtermin, Slug, tenant-admin, Login-Passwort = Wizard-Passwort, Modulauswahl, Beispieldaten über Fachservices, Audit); Enumeration (identische Antwort, Hinweis-Mail ohne Link, keine offene Anmeldung); Rate-Limit je IP/E-Mail inkl. Retry-After, jede öffentliche Action limitiert, Proxy; ohne Beispieldaten; Slug-Kollision `-2`; Mandant B liest/ändert keine Kunden von A |
| `test-testphase-readonly.ts` | 59 | Checkliste (automatisch erkannt, Beispieldaten zählen nicht, manuell, Monteur forbidden/unsichtbar, Vollversion ohne Checkliste); Ablauf → `blocked trial_expired` mit Klartext; `withApi`+Sperre: POST/DELETE 422, GET 200, Mandant B 200; Sync-Batch 422 ohne angewendete Operation; statisch: moduleGuard, requireApiContext, **alle 24 mutierenden /api/v1-Routen**, Upload-Routen, Einstellungen, Nutzerverwaltung, Lotse, Worker, Lese-Modus nur in 3 Lesepfaden und nie in Actions; Jobs (Import/Transkription übersprungen, PDF/Export/Mandant B nicht); Lesen erlaubt; Export im Nur-Lesen-Zustand (ZIP, CSV mit BOM, JSON, Datei byte-identisch, keine Daten von B, keine Hashes, Formel-Injektion, paralleler Export conflict, Mandant B not_found, Monteur forbidden, Download-Ablauf, Audit); Verlängern hebt die Sperre auf; Mandant B unverändert |
| `test-testphase-lifecycle.ts` | 67 | Erinnerungen 7/3/1 genau einmal, verpasster Lauf; Ablaufmail einmal + readOnlySince; Löschhinweis einmal; Löschung nur fällig (Tabellen leer, Identity, Speicherobjekt, Nachweis, Plattform-Audit, keine Wiederholung); Vollversion B und nicht fälliger Test B2 unberührt; Umwandeln/Verlängern/Abbrechen verhindern Löschung; Vormerken ≥ 7 Tage, folgt neuem Ende; Audit before/after je Aktion; Plattform-Rechte (Mandanten-Admin, Read-only-Admin → forbidden, Action ohne Plattform-Session abgewiesen, Datumsgrenzen, Vollversion invalid, Mandanten-Actions ohne Testphasen-Änderung); Wizard (freies Enddatum, Einladung, Passwortzwang, Token, Beispieldaten, bestehende Person ohne neue Einladung, Audit); Vorlagen de/en; Processor + Scheduler |
**Gate (`npm run gate`) grün:** prisma generate, tsc, lint (0 Fehler; 3 vorbestehende Warnungen in fremden Dateien), build inkl. Guard-Check (39 Action-Dateien), **79/79 Testskripte**. Lane-DB `craftvia_testphase`, `RLS_DATABASE_URL` auf dieselbe DB. Ein Lauf mit `RLS_ENFORCED=true` wurde nicht durchgeführt.
**HTTP-Smoke** (`next build && next start -p 3115`, Session-Cookies ohne Passworteingabe): `scripts/smoke-testphase.ts` **32/32** – anonym `/testen` (Schritt 1 von 5), Bestätigung mit ungültigem Token, Rechtstexte, Redirects; abgelaufener Admin: Banner + Löschdatum + Export-Link, Lesen, Exportseite, `GET /api/v1/customers` 200, **422 `trial_expired`** für `POST /api/v1/customers`, `POST /api/v1/work-orders/[id]/documents`, `POST /documents/upload`; abgelaufener Monteur: `/m`, `/m/orders`, `/m/emergency` mit Banner, Auftrag ohne Zuweisung 404, Bundle 200, **422** für `/api/v1/sync` und `/api/v1/uploads`, keine Exportseite; laufender Test: „Testphase endet in 3 Tagen.“, „Erste Schritte“, Willkommen; Plattform: Liste mit Badges/Filtern, Wizard, Detail mit Karte und Popups. Zusätzlich `scripts/smoke-auth.ts` **137/137** (keine Regression). Der erste Smoke-Lauf fand den 500 auf `/m` (Lese-Kontext über `moduleGuard`) → behoben in `273a453`.
**Visuell:** nicht geprüft – die Navigation des Browser-Panels auf localhost wurde abgelehnt. Bitte `/testen` (Wizard, 375/768/1024 px), Banner und Plattform-Karte manuell ansehen.
## 4. Stubs / Abhängigkeiten
Keine Stubs. Genutzt: `provisionTenant`, `issueToken`/`sendUserInvitationMail`, `login-ticket`/`signIn`, `offboardTenant` + Topologie, `storage`/`readStoredBytes`, `buildZip`, `enqueueMail`, `enqueueJob`/Scheduler, Fachservices L1/L2 (Kunden, Objekte, Teams, Aufträge, Zuweisung).
## 5. Bekannte Lücken / offene Punkte
1. **Offline-Fotos nach Ablauf:** `/api/v1/uploads` antwortet 422; die Outbox (L7) wertet 422 beim Upload als endgültig ungültig und verwirft das Blob auf dem Gerät. Sync-Operationen bleiben erhalten. Vorschlag L7: `blocked`/`trial_expired` als vorübergehend behandeln.
2. **Fehlertexte in fremden Formularen:** Actions anderer Lanes zeigen bei der Sperre ihre eigene Fehlerdarstellung (Code `blocked`/`trial_expired`, teils generisch); der Banner erklärt den Zustand. Eine einheitliche Klartext-Zuordnung je Lane steht aus.
3. **Rate-Limits je App-Instanz** (In-Memory wie SEC2/L10b).
4. **Rechtstexte** sind Platzhalter (`messages/*/trial.json` → `legal.*`).
5. **Fallback nach Bestätigung:** Scheitert die direkte Anmeldung (z. B. künftige MFA-Pflicht), leitet die Action auf `/login?trial=ready`; die Login-Seite (Fundament) zeigt dazu keinen eigenen Hinweis.
6. **Plattform-Einladung an bestehende Personen:** wird nur verknüpft (wie `createTenantUser`), ohne Hinweis-Mail.
7. **Export** im Speicher gebaut (Dateien bis 500 MB, kein ZIP64/Streaming); alte Export-Dateien werden nicht automatisch aus dem Speicher entfernt (nur mit dem Mandanten).
8. **Worker-Sperre** nur für `import-extraction` und `transcription`; Benachrichtigungs-Mails aus Ereignissen vor dem Ablauf laufen weiter.
9. **Slug** eines gelöschten Testmandanten bleibt belegt (Zeile `ARCHIVED` mit Löschnachweis); neue Anmeldungen erhalten ein Suffix.
10. **Checkliste:** „Mobile App öffnen“ nur manuell abhakbar; Checkliste nur für Mandanten, die als Testphase gestartet sind.
11. **RLS-Modus** (`RLS_ENFORCED=true`) für die neuen Tests nicht gelaufen.
## 6. Screens / Routen
| Route | Zugriff | Inhalt |
|---|---|---|
| `/testen` | öffentlich | Wizard „Kostenlos testen“ |
| `/testen/bestaetigen?token=` | öffentlich | Anmeldung prüfen, „Jetzt einrichten“ (POST) → Dashboard |
| `/testen/nutzungsbedingungen`, `/testen/datenschutz` | öffentlich | Platzhalter-Rechtstexte |
| `/dashboard` | Backoffice | Karte „Erste Schritte“ (Admins, Testphasen-Mandanten) |
| alle `(app)`- und `/m`-Seiten | angemeldet | Testphasen-Banner |
| `/settings/export`, `/settings/export/<id>` | `tenant:manage` | Datenexport anfordern/herunterladen |
| `/admin` | Plattform | Spalte „Version“, Filter Alle/Test/Voll, Button „Testmandant anlegen“ |
| `/admin/trial` | Plattform (Voll-Admin für Aktion) | Wizard „Testmandant anlegen“ |
| `/admin/<id>?trial=extend|convert|end|schedule|cancel` | Plattform-Voll-Admin | Karte „Testphase“ + Bestätigungs-Popups |
+94
View File
@@ -0,0 +1,94 @@
# Lane L12 – Zeiterfassung (`lane/zeiterfassung`)
Stand: 2026-09-15 · Basis `d3bc7f2` (`feature/craftvia-mvp`, L1–L11 integriert)
Ziel: Zeiterfassung der Monteure deutlich einfacher – laufende Uhr überall, „Für heute beenden“, Auto-Wechsel zwischen Aufträgen, Zeit nachtragen + Tagesübersicht. **Entscheidung des Nutzers:** Monteure tragen eigene Zeiten nach / schlagen Korrekturen vor – mit Freigabe durch Teamleiter (eigene Teams) oder Backoffice; bis zur Freigabe zählen sie nicht für Bericht und Abrechnung.
## 1. Umfang
| Punkt | Umsetzung |
|---|---|
| Datenmodell | Migration `20260915120000_zeiterfassung_freigabe` (nur additive Spalten, keine neue Tabelle → keine RLS-/TENANT_MODELS-Änderung): `TimeEntry.source` (`tracked`/`manual`), `approvalStatus` (`approved`/`pending`/`rejected`, Default approved → Bestand unverändert), `approvedById` (entscheidende Person, auch bei Ablehnung), `approvedAt`, `rejectionReason`, `pendingChange` (Json: vorgeschlagene Korrektur `{startedAt, endedAt, type, reason, requestedAt, requestedById}`), `note`; Index `(tenant_id, approval_status)`. `WorkSession.manual` (Boolean). Begründung eines Nachtrags liegt in `correctionReason`. PII: `TimeEntry.approvedById` in `pii-fields.ts`. |
| Manuelle Session (Begründung) | Nachträge hängen an einer „manuellen“ Session je User + Auftrag + lokalem Tag (`manual = true`, Status `ended`, Start/Ende = min/max der Einträge). So bleiben alle bestehenden Lesepfade (Auftrag → Sessions → Einträge: Bericht, Zeiten-Tab, Bundle, Lotse, Abschluss-Blocker) ohne Sonderfälle korrekt; `ended` löst keine Blocker/aktive-Session-Logik aus. Eine eigene Tabelle hätte jeden Lesepfad verdoppelt. |
| Rechte | `field:record_own_time` → technician, team-lead; `time:approve` → team-lead, backoffice, tenant-admin (Katalog). `scripts/sync-role-permissions.ts` zieht bestehende Mandanten nach (lokal ausgeführt: +12 Zuordnungen). |
| Services `services/field/time-entries.ts` | `addManualTimeEntry` (Scope, Status nachtragbar = Feldstatus + `in_review`, ≤ 7 Tage zurück ab Tagesbeginn Mandanten-TZ, nicht Zukunft, Ende > Beginn, ≤ 16 h, keine Überlappung mit eigenen nicht abgelehnten Einträgen → `invalid`/`overlap`; eigener Eintrag → pending + Event; `forUserId` mit `field:correct_time` nur für aktive Mitglieder geführter Teams bzw. Backoffice → approved; idempotent über `clientId`), `proposeTimeCorrection` (nur eigener, abgeschlossener Eintrag ≤ 7 Tage; approved bleibt gültig, `pendingChange`; bei noch offenem Nachtrag werden die offenen Werte direkt geändert), `approveTimeEntry`/`approveTimeEntries`/`rejectTimeEntry` (`time:approve`, Team-Scope, nie eigene Einträge, Überlappungsprüfung bei Freigabe, Korrektur → Werte übernommen + `corrected`), `listPendingTimeEntries`, `countPendingTimeEntries`, `pendingTimeOfOrder`, `getMyTimeOverview`, `listRecordableOrders`, `listRecordableUsers`. Alle Mehrschritt-Writes in `inTransaction`, Audit before/after, Events nach Commit. |
| Team-Scope | Backoffice (`work_order:read_all`) → alle; Teamleiter → Einträge auf Aufträgen seiner geführten aktiven Teams, Aufträge mit `teamLeadUserId = er` oder von aktiven Mitgliedern seiner Teams. Außerhalb → `not_found`. |
| Sessions `services/field/sessions.ts` | Alle Session-Mutationen jetzt in `inTransaction`. **Auto-Wechsel:** `startSession`/`resumeSession` werfen `conflict other_session_running {workOrderId, number, title}`, wenn der Nutzer auf einem anderen Auftrag eine laufende/Anfahrts-Session hat; mit `switchFromOther: true` wird diese in derselben Transaktion pausiert (Pause-Segment, Auftrag `paused`, falls sonst niemand arbeitet). `stopForToday` (Session beendet, Auftrag bleibt offen → `paused` über `transitionWorkOrder`, kein Abschluss). `switchSegment` (`work`/`return_travel`/`material_procurement`). `getMyActiveSession` (laufende vor pausierter, Segmentstart, aufsummierte Dauer ohne Pausen). `correctTimeEntry`: eigene Einträge → `forbidden own_entry`, räumt offene Vorschläge ab. |
| Zählen nur freigegeben | `reports/build-content.ts` (Summen nur approved, neues optionales Feld `time.pendingMinutes` im Snapshot), Backoffice Zeiten-Tab (`workMinutes` approved, `pendingMinutes`), mobile Auftragsdetail/Zeiten, Lotse-Vollständigkeit, Meine Zeiten. **Abrechnungsfreigabe** (`completion.ts#transitionBlockers`) → `blocked` mit `missing_field: pending_time_entries`, solange Nachträge oder Korrekturvorschläge offen sind. Bericht einreichen warnt nur (Hinweis auf `/m/orders/[id]/report`). |
| Sync/Offline | Ops `session.stop_day`, `session.segment`, `time.add_manual`, `time.propose_correction` (Envelope + Zod in `lib/sync/ops.ts`, Dispatch in `sync/apply.ts`), `session.start`/`session.resume` mit `switchFromOther?`. Outbox ohne Sonderlogik; `VERSION_CHANGING_OPS` um stop_day/segment ergänzt (verkettete baseVersion). Optimistische Ansicht (`bundle-core.ts`): stop_day → Uhr aus, Auftrag pausiert; add_manual → `local.manualTimes` mit pending. Abgelehnte Ops im Klartext auf `/m/sync` (`offline.problem.timeOverlap`, `timeWindow`, `otherSession`). Konfliktmeldung des Auto-Wechsels trägt die Auftragsdaten (`other_session_running:{…}`). |
| Events/Benachrichtigungen | `time.approval_requested` → Teamleiter des Auftrags-Teams bzw. der Teams des Monteurs (mit `time:approve`, ohne Antragsteller), sonst Backoffice (`read_all` + `time:approve`); In-App + E-Mail (über Einstellungen abbestellbar). `time.approved`/`time.rejected` → Monteur, nur In-App, Ablehnungsgrund im Text. Links: Backoffice `/work-orders/time-approvals`, Teamleiter `/m/approvals`, Monteur `/m/time`. |
## 2. Screens / Routen
| Route | Inhalt |
|---|---|
| Mobile-Shell (alle `/m`-Seiten) | **Laufende-Uhr-Leiste** über der Bottom-Nav: Status (Text + Punkt), „A-00042 · Titel · 1:23 h“ (Sekunden-Tick), Tipp → Auftrag, Pause/Weiter, „Für heute beenden“ (zweistufig), Link „Meine Zeiten“; offline aus lokalem Zustand (ohne Dauer). Badge-Zähler offener Freigaben am „Profil“. |
| `/m`, `/m/orders` | je Karte direkter Button Arbeit starten / Pause / Weiter; laufende Karte mit breiter Statuskante + Text „Zeit läuft“/„Pausiert“. |
| Auto-Wechsel | Bottom-Sheet „A-00041 läuft noch. Pausieren und A-00042 starten?“ [Wechseln] [Abbrechen] – aus Karte, Auftragsdetail und Uhr-Leiste. |
| `/m/orders/[id]` | laufend: primär „Pause“, sekundär „Für heute beenden“ (mit Hinweis), Textlink „Auftrag abschließen …“ (bestehender Flow mit Bestätigung/Blockern); kleinere Segment-Aktionen „Rückfahrt starten“, „Material holen“, „Zurück zur Arbeit“. |
| `/m/orders/[id]/time` | Badges manuell/zur Freigabe/abgelehnt/Korrektur beantragt, freigegebene + offene Summe, eigene Einträge „Korrektur vorschlagen“, fremde (Teamleiter) Direktkorrektur, „Zeit nachtragen“ für den Auftrag. |
| `/m/time` | **Meine Zeiten:** Tages-Chips (Heute, Gestern, weitere 6 Tage), Summen freigegeben/offen, Einträge je Auftrag mit Art, von–bis, Dauer, Badges, „Korrektur vorschlagen“, nicht übertragene lokale Nachträge. |
| `/m/time/new` | **Zeit nachtragen:** Auftrag (letzte 7 Tage/laufend), optional „Für“ (Teammitglied, nur `field:correct_time`), Art-Chips, Datum, Von–Bis oder Dauer (15/30/60 min), Begründung Pflicht mit Vorschlägen, Notiz, Hinweis „Wird nach Freigabe durch Teamleiter oder Büro gezählt“. |
| `/m/approvals` | Teamleiter (`time:approve`): offene Nachträge/Korrekturen (alt → neu), große Buttons Freigeben / Ablehnen mit Grund. |
| `/work-orders/time-approvals` | Backoffice: Tabelle Mitarbeiter, Auftrag, Datum, von–bis (alt → neu), Dauer, Art, Begründung; Einzel- und Sammelfreigabe, Ablehnen mit Grund. Navigation „Zeiten zur Freigabe“, Dashboard-Kachel. |
| `/work-orders/[id]?tab=times` | Summe freigegeben + offen, Badges, Freigabe/Ablehnung inline. |
## 3. Dateien
**Neu**
- `prisma/migrations/20260915120000_zeiterfassung_freigabe/migration.sql`
- `src/server/services/field/time-entries.ts`, `src/lib/field/time-rules.ts`
- `src/server/actions/work_orders/time-approvals.ts`
- `src/components/field/`: `running-clock-bar.tsx`, `switch-session-sheet.tsx`, `card-time-button.tsx`, `time-entry-badges.tsx`, `correction-proposal-form.tsx`, `manual-time-form.tsx`, `local-pending-times.tsx`, `approval-actions.tsx`, `pending-time-notice.tsx`
- `src/components/work-orders/time-approval-forms.tsx`
- `src/app/(field)/m/(core)/time/page.tsx`, `time/new/page.tsx`, `approvals/page.tsx`, `src/app/(app)/work-orders/time-approvals/page.tsx`
- `scripts/test-zeiterfassung-service.ts`, `scripts/test-zeiterfassung-sync.ts`
**Geändert (Field/Sync, L4/L7-Pfade)**
- `src/server/services/field/{sessions,time-correction,queries}.ts`, `src/server/actions/field/time.ts`
- `src/components/field/{primary-action,order-card,bottom-nav}.tsx`, `src/app/(field)/m/layout.tsx`, `(core)/page.tsx`, `(core)/orders/page.tsx`, `(core)/orders/[id]/{page,time/page,report/page}.tsx`, `(core)/profile/page.tsx`
- `src/lib/sync/{envelope,ops}.ts`, `src/server/services/sync/apply.ts`
- `src/lib/offline/{outbox-core,bundle-core}.ts`, `src/components/offline/offline-view.tsx` (Aktion „Für heute beenden“)
**Geändert außerhalb field/sync**
- `prisma/schema.prisma`; `src/server/rbac.ts` (2 Rechte + Rollen); `src/server/dsgvo/pii-fields.ts` (1 Zeile)
- `src/lib/events.ts` (3 Events, entityType `time_entry`); `src/server/services/notifications/{recipients,handle-event}.ts` (Empfänger, Link, Grund im Text)
- `src/server/services/reports/build-content.ts`, `src/lib/reports/content.ts` (optional `pendingMinutes`)
- `src/server/services/work-orders/{completion,detail,dashboard}.ts`, `src/components/work-orders/detail-tabs.tsx` (Zeiten-Tab), `src/app/(app)/dashboard/page.tsx` (Kachel), `src/lib/nav.ts` (1 Eintrag)
- `src/server/services/lotse/completeness.ts` (nur freigegebene Arbeitszeit)
- `messages/{de,en}/{field,offline,notifications,nav,dashboard,workOrders}.json`
- `scripts/test-security-roles.ts` (Matrix-Proben für die neuen Rechte), `scripts/smoke-auth.ts`
- `docs/craftvia/ABNAHME.md` §4 „Arbeitszeitkorrektur“
Keine neuen npm-Abhängigkeiten, keine neuen Tabellen, keine Stubs.
## 4. Tests
- `scripts/test-zeiterfassung-service.ts` – **78 Prüfungen**: Validierung (Überlappung → `invalid overlap`, > 7 Tage, Zukunft, > 16 h, Ende vor Beginn, ohne Grund, ohne Zuweisung → not_found, Backoffice ohne Recht, Monteur für Kollegen), manuelle Session, Events (Teamleiter statt Monteur/Backoffice; eigener Teamleiter-Nachtrag → Backoffice) + Audit, pending zählt nicht (Tagesbericht, Zeiten-Tab, Meine Zeiten), Freigabe-Rollen (Monteur nie, Teamleiter nicht eigene, fremder Teamleiter not_found, Backoffice alle, Sammelfreigabe, Direktkorrektur eigener Einträge forbidden), Listen/Badge/Dashboard, Freigabe → zählt + `time.approved`, Ablehnung mit Grund + `time.rejected` (Text, Link) + abgelehnter Zeitraum frei, Korrekturvorschlag (alter Wert bis Freigabe, danach neu + corrected; abgelehnter Vorschlag lässt Eintrag gültig), Direktanlage für Teammitglied (approved) bzw. Nicht-Mitglied forbidden, Abrechnungsfreigabe blocked `pending_time_entries` und nach Freigabe möglich, „Für heute beenden“ (Session ended, Auftrag paused, kein Blocker), Auto-Wechsel (conflict mit Nummer, nichts angelegt, fehlgeschlagener Wechsel lässt andere Session laufen, mit Flag pausiert + genau eine laufende Uhr, Weiter ebenfalls geprüft), Segmente + `getMyActiveSession`, Mandantentrennung (B kann weder freigeben, ablehnen, sehen, nachtragen noch vorschlagen).
- `scripts/test-zeiterfassung-sync.ts` – **29 Prüfungen**: Payload-Schemas, 4 neue Ops in einem Batch applied (idMap, entityVersion), gleiche clientOpIds → duplicate ohne Doppelanlage, Event über Sync, Überlappung → rejected mit Klartext-Schlüssel (`timeOverlap`/`timeWindow`), Korrekturvorschlag über Sync, Auto-Wechsel-Konflikt mit Auftragsnummer (`otherSession`) und Wechsel mit Flag, verkettete baseVersion, optimistische Ansicht (stop_day, add_manual pending), Mandant B / Monteur ohne Zuweisung → not_found.
- Bestehend angepasst: `test-security-roles.ts` (Proben `field:record_own_time`, `time:approve`).
- Gate/RLS: siehe §7.
**HTTP-Smoke** (Dev-Server :3113, `scripts/smoke-auth.ts`, Demo-Seed, Rechte per `sync-role-permissions` nachgezogen): **105/105 grün** – neu: Backoffice `/work-orders/time-approvals`, Dashboard-Kachel; Teamleiter `/m/time`, `/m/approvals`, Profil-Link; Monteur Uhr-Leiste auf `/m` („Laufende Zeiterfassung“, „Für heute beenden“), `/m/time` (heute/gestern), `/m/time/new`, `/m/approvals` ohne Recht → Hinweis, Zeiten-Seite des Auftrags. Visuelle Browserprüfung nicht durchgeführt (Login würde Passworteingabe/Token-Einschleusen durch den Agenten erfordern).
## 5. Bekannte Lücken / Hinweise
1. **Demo-/Bestandsdaten mit zwei laufenden Sessions** eines Nutzers (vor L12 möglich, z. B. Seed: `monteur@` auf A-00009 und N-00001) bleiben bestehen; die Uhr-Leiste zeigt die neueste, der nächste Start/Weiter erzwingt den Wechsel.
2. **Backoffice hat kein `field:correct_time`** (Rollenfilter schließt `field:*` aus): Direktanlage für Mitarbeiter mobil nur für Teamleiter/Mandantenadmin; Backoffice gibt frei/lehnt ab. Falls gewünscht, Recht im Katalog ergänzen.
3. **Offline-Uhr** kennt keine Dauer (Bundle ohne Einträge) – offline nur Auftrag + Status. Auto-Wechsel-Dialog erscheint offline erst beim Sync (Op wird dann auf `/m/sync` im Klartext abgelehnt); der optimistische Wechsel pausiert die andere Auftragsansicht lokal nicht.
4. **Zeitzone**: Formular nutzt die Gerätezeit, Server prüft das 7-Tage-Fenster in der Mandanten-Zeitzone (`TenantSettings.timezone`).
5. **Transaktionen + Events**: `transitionWorkOrder` emittiert Events innerhalb der Session-Transaktion (bestehendes Verhalten, ABNAHME §5); pg meldet dabei eine Deprecation-Warnung für parallele Queries auf dem Transaktions-Client (harmlos).
6. **Nachträge auf abgerechneten/stornierten Aufträgen** nicht möglich (`work_order_status`); nach Abrechnungsfreigabe erfasste Korrekturen gibt es damit nicht.
7. Freigabefrist/Eskalation und Lohn-Export nicht umgesetzt (ABNAHME §4 „zu klären“).
## 6. Migration
`20260915120000_zeiterfassung_freigabe` – rein additiv (2 Enums, 7 Spalten `time_entries`, 1 Spalte `work_sessions`, 1 Index). Bestehende Zeilen: `tracked` + `approved` → unverändertes Verhalten. Deploy: `prisma migrate deploy` + `scripts/sync-role-permissions.ts` (Nutzer neu anmelden).
## 7. Gate
`npm run gate` **grün**: prisma generate, tsc, lint (0 Fehler, 3 bestehende Warnungen außerhalb L12), build inkl. Modul-Guard-Check (33 Action-Dateien), **67/67 Testskripte** (davon neu `test-zeiterfassung-service` 78 ✓, `test-zeiterfassung-sync` 29 ✓).
Im ersten Gate-Lauf schlugen `test-notdienst-flow`/`test-notdienst-review` fehl: Die Notdienst-Erfassung startet eine Session, während der Monteur noch auf einem anderen Auftrag arbeitet → neue Regel „eine laufende Uhr“ lieferte `other_session_running`. Behoben in `services/emergency/create.ts` (`switchFromOther: true` – ein Notdiensteinsatz pausiert die laufende Uhr automatisch).
**RLS-Lauf** `RLS_ENFORCED=true npm run test` (RLS_DATABASE_URL auf die Lane-DB `craftvia_zeiterfassung`): **67/67 Testskripte grün** – alle Session-/Freigabe-Writes laufen über `inTransaction` und sind damit auch unter scharfer RLS atomar.
@@ -0,0 +1,61 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [6 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
endobj
5 0 obj
<< /Length 2221 >>
stream
BT /F2 16 Tf 56 790 Td (Kranich Haustechnik GmbH) Tj ET
BT /F1 8 Tf 56 777 Td (Deichweg 4 \267 21029 Hamburg \267 Tel. 040 0000 1000 \267 info@kranich-haustechnik.example.org) Tj ET
BT /F1 10 Tf 56 737 Td (Musterbau GmbH) Tj ET
BT /F1 10 Tf 56 722 Td (z. Hd. Frau Jana K\366hler) Tj ET
BT /F1 10 Tf 56 707 Td (Hafenstra\337e 12) Tj ET
BT /F1 10 Tf 56 692 Td (20457 Hamburg) Tj ET
BT /F2 14 Tf 56 656 Td (Auftragsbest\344tigung) Tj ET
BT /F1 10 Tf 56 636 Td (Auftragsnummer: AB-2026-0815 Datum: 02.09.2026) Tj ET
BT /F1 10 Tf 56 621 Td (Ihr Angebot: ANG-2026-0342 Kundennummer: K-10042) Tj ET
BT /F1 10 Tf 56 606 Td (Bauvorhaben: Neubau B\374rogeb\344ude Speicherhof, Am Kaiserkai 30, 20457 Hamburg) Tj ET
BT /F1 10 Tf 56 591 Td (Ansprechpartnerin vor Ort: Jana K\366hler, Tel. 040 0000 2233, j.koehler@musterbau.example.org) Tj ET
BT /F2 10 Tf 56 576 Td (Ausf\374hrungszeitraum: 12.10.2026 bis 16.10.2026) Tj ET
BT /F1 10 Tf 56 552 Td (Sehr geehrte Frau K\366hler,) Tj ET
BT /F1 10 Tf 56 537 Td (vielen Dank f\374r Ihren Auftrag. Wir best\344tigen die Ausf\374hrung folgender Leistungen:) Tj ET
BT /F2 10 Tf 56 513 Td (Pos. Bezeichnung Art.-Nr. Menge Einheit) Tj ET
BT /F1 10 Tf 56 498 Td (1 Montage W\344rmepumpe Luft/Wasser 12 kW 1 Stk) Tj ET
BT /F1 10 Tf 56 483 Td (2 W\344rmepumpe Aerotherm 12 kW WP-AT-12 1 Stk) Tj ET
BT /F1 10 Tf 56 468 Td (3 Pufferspeicher 500 l PS-500 1 Stk) Tj ET
BT /F1 10 Tf 56 453 Td (4 Kupferrohr 22 mm CU-22 24 m) Tj ET
BT /F1 10 Tf 56 438 Td (5 Inbetriebnahme und Einweisung 4 Std) Tj ET
BT /F2 10 Tf 56 414 Td (Gesamtbetrag netto: 18.450,00 \200 zzgl. 19 % MwSt.: 3.505,50 \200 Gesamt: 21.955,50 \200) Tj ET
BT /F1 10 Tf 56 390 Td (Hinweise: Zufahrt \374ber Tor 2, Anmeldung beim Bauleiter. Kran ist bauseits zu stellen.) Tj ET
BT /F1 10 Tf 56 375 Td (Referenz: Ihre Bestellung BE-7781 vom 28.08.2026) Tj ET
BT /F1 10 Tf 56 345 Td (Mit freundlichen Gr\374\337en) Tj ET
BT /F1 10 Tf 56 330 Td (Kranich Haustechnik GmbH) Tj ET
endstream
endobj
6 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents 5 0 R >>
endobj
xref
0 7
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000121 00000 n
0000000218 00000 n
0000000320 00000 n
0000002593 00000 n
trailer
<< /Size 7 /Root 1 0 R >>
startxref
2729
%%EOF
@@ -0,0 +1,54 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [6 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
endobj
5 0 obj
<< /Length 1425 >>
stream
BT /F2 16 Tf 56 790 Td (Kranich Haustechnik GmbH) Tj ET
BT /F1 8 Tf 56 777 Td (Deichweg 4 \267 21029 Hamburg \267 Tel. 040 0000 1000 \267 info@kranich-haustechnik.example.org) Tj ET
BT /F1 10 Tf 56 737 Td (Elbblick Wohnen eG) Tj ET
BT /F1 10 Tf 56 722 Td (Hausverwaltung) Tj ET
BT /F1 10 Tf 56 707 Td (Neum\374hlen 7) Tj ET
BT /F1 10 Tf 56 692 Td (22763 Hamburg) Tj ET
BT /F2 14 Tf 56 656 Td (Auftrag Wartung Heizungsanlagen) Tj ET
BT /F1 10 Tf 56 636 Td (Auftrag Nr. W-26-117 \267 Kunden-Nr. K-20017 \267 Hamburg, den 01.09.2026) Tj ET
BT /F1 10 Tf 56 621 Td (Objekt: Wohnanlage Elbhang, \326velg\366nne 45, 22605 Hamburg) Tj ET
BT /F1 10 Tf 56 606 Td (Ansprechpartner: Herr Timo Brandt, Hausmeister, Tel. +49 40 0000 4545) Tj ET
BT /F1 10 Tf 56 586 Td (Termin: KW 41 nach Absprache mit dem Hausmeister) Tj ET
BT /F2 10 Tf 56 564 Td (Leistungsumfang:) Tj ET
BT /F1 10 Tf 56 549 Td (J\344hrliche Wartung von 2 Gas-Brennwertkesseln inkl. Abgasmessung und Protokoll.) Tj ET
BT /F1 10 Tf 56 534 Td (1 Wartung Gas-Brennwertkessel 2 Stk) Tj ET
BT /F1 10 Tf 56 519 Td (2 Wartungsset Dichtungen 2 Satz) Tj ET
BT /F1 10 Tf 56 504 Td (3 Abgasmessung mit Protokoll 2 Stk) Tj ET
BT /F1 10 Tf 56 482 Td (Besondere Hinweise: Heizungsraum im Keller, Schl\374ssel beim Hausmeister.) Tj ET
BT /F1 10 Tf 56 467 Td (R\374ckfragen bitte an verwaltung@elbblick.example.org) Tj ET
endstream
endobj
6 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents 5 0 R >>
endobj
xref
0 7
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000121 00000 n
0000000218 00000 n
0000000320 00000 n
0000001797 00000 n
trailer
<< /Size 7 /Root 1 0 R >>
startxref
1933
%%EOF
@@ -0,0 +1,51 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [6 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
endobj
5 0 obj
<< /Length 1061 >>
stream
BT /F2 16 Tf 56 790 Td (Kranich Haustechnik GmbH) Tj ET
BT /F1 8 Tf 56 777 Td (Deichweg 4 \267 21029 Hamburg \267 Tel. 040 0000 1000 \267 info@kranich-haustechnik.example.org) Tj ET
BT /F1 10 Tf 56 737 Td (Frau) Tj ET
BT /F1 10 Tf 56 722 Td (Petra Sommer) Tj ET
BT /F1 10 Tf 56 707 Td (Lindenallee 9a) Tj ET
BT /F1 10 Tf 56 692 Td (2148 Hamburg) Tj ET
BT /F2 14 Tf 56 656 Td (Auftragsbest\344tigung Reparatur) Tj ET
BT /F1 10 Tf 56 636 Td (Auftrag: AB-2026-0901 Datum: 03.09.2026) Tj ET
BT /F1 10 Tf 56 621 Td (Einsatzort: wie oben) Tj ET
BT /F1 10 Tf 56 606 Td (Telefon: 0170 0000 987 E-Mail: petra.sommer\(at\)example.org) Tj ET
BT /F1 10 Tf 56 591 Td (Geplanter Termin: 18.09.2026, Ende 17.09.2026) Tj ET
BT /F1 10 Tf 56 569 Td (Leistung: Austausch defekter Thermostatkopf im Bad, Pr\374fung Heizk\366rperventile.) Tj ET
BT /F1 10 Tf 56 554 Td (1 Thermostatkopf Standard TK-100 1 Stk) Tj ET
BT /F1 10 Tf 56 539 Td (2 Arbeitszeit Monteur 1,5 Std) Tj ET
BT /F2 10 Tf 56 517 Td (Gesamt brutto: 189,40 \200) Tj ET
endstream
endobj
6 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents 5 0 R >>
endobj
xref
0 7
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000121 00000 n
0000000218 00000 n
0000000320 00000 n
0000001433 00000 n
trailer
<< /Size 7 /Root 1 0 R >>
startxref
1569
%%EOF
-323
View File
@@ -1,323 +0,0 @@
# GAP-Report Runde 2 (strenger Re-Sweep): Richtlinien & Verfahrensanweisungen — ISMS-Vorlagenpaket v2
| Angabe | Wert |
|--------|------|
| Prüfgegenstand | `seed/isms-vorlagenpaket-v2/` (Source of Truth; **nicht** der Spiegel unter `.next/standalone/…`) |
| Standard | VDA ISA 2027 (Information Security) / ISO 27001:2022 / NIS2-Kontext |
| Umfang | 15 Richtlinien (L00, R01–R14), 13 Verfahrensanweisungen (VA-01–VA-13), Baseline, Nachweisregister, `mapping.json` (316 Anforderungen) |
| Prüfraster (streng) | **WAS · WIE · WO eindeutig dokumentiert · WER · NACHWEIS** — je Anforderung. „erfüllt" nur, wenn ALLE fünf Dimensionen konkret beantwortet sind **und** operative Prozesse auf eine VA bzw. zentral gepflegte Werte auf Register/Baseline verweisen. Sobald eine Dimension fehlt/vage/doppeldeutig ist → mindestens „teilweise". |
| Runde | **2 (Korrektur der zu milden Runde 1)** |
| Erstellt | 2026-07-22 |
| Status | Review-Report (read-only) — es wurde **nichts** am Vorlagenpaket geändert; nur diese Report-Datei wurde neu erzeugt. |
> Hinweis: Reiner Prüfbericht. Keine Datei des Vorlagenpakets, kein Code, keine DB, kein Seed wurde geändert; kein Commit, kein Push. Alle Textvorschläge sind einpflegefertig, aber **nicht** eingepflegt.
---
## 1. Management-Summary
Das Vorlagenpaket hat weiterhin einen **überdurchschnittlichen Reifegrad** (zentrale Baseline mit BL-IDs, zentrales Nachweisregister, 316 REQ-Anker ↔ 316 Mapping-IDs ohne Waisen, `-elev`-Blöcke für alle HOCH/SEHR-HOCH-Controls). Runde 1 hat diesen Reifegrad jedoch **zu wohlwollend** in Verdikte übersetzt: Sie hat Anforderungen als „erfüllt" gewertet, deren Umsetzungstext den strengen Rubric (eindeutiger Nachweisort, Inline-VA-Verweis, verwaltetes Register) nicht besteht. Runde 2 legt den skeptischen Maßstab konsequent an.
**Was gegenüber Runde 1 strenger/korrigiert wurde:**
- **8 Controls von „erfüllt" auf „teilweise" herabgestuft** (Details in §2): **1.2.3, 2.1.2, 3.1.1, 5.2.6, 5.2.8, 5.3.4-KI, 6.1.3, 7.1.2**. Die neue Control-Verteilung ist **14 erfüllt / 30 teilweise / 2 GAP** (Runde 1: 22 / 22 / 2).
- **Doppeldeutige Verortung** („im {{TOOL_TICKET}} **bzw.** ISMS-Tool") wird konsequent als **fehlender eindeutiger Nachweisort (G2)** gewertet — betrifft u. a. 1.2.3 und 1.6.1.
- **„Liste/Freigabeliste im ISMS-Tool"** ohne Register-ID, Pflichtattribute und Review-Turnus wird als **fehlendes verwaltetes Register (G4/G8)** gewertet — auch dort, wo Runde 1 „erfüllt" vergeben hatte (1.3.3, 1.3.4, 5.3.4, 5.3.4-KI, 6.1.3, 5.2.8, 5.2.7, 5.1.2).
- **Inline-VA-Verweis** wird control-genau geprüft: Von **23 Controls mit zuständiger VA** verweisen nur **8** im Umsetzungstext auf ihre VA; **15** nennen das Verfahren nur generisch bzw. im Anhang → G3.
**Die drei in Runde 1 bestätigten Misses — in Runde 2 sauber aufgearbeitet:**
1. **ISA 1.2.3 / R01 §3.3 (Informationssicherheit in Projekten):** In Runde 1 fälschlich „erfüllt". Tatsächlich deckt **keine VA** 1.2.3 ab (kein `FULFILLS 1.2.3` in den VA-Headern, kein Eintrag in `mapping.json → verfahren`). Der Umsetzungstext verortet doppeldeutig („die Einstufung wird im {{TOOL_TICKET}} **bzw. ISMS-Tool** dokumentiert"), und weder der genannte „dokumentierte Kriterienkatalog" noch ein **Projektregister/Projektverzeichnis** sind als verwaltetes Artefakt referenziert. → **Neu bewertet: teilweise**; Befund **G4** (neue VA „Informationssicherheit in Projekten") **+ G2/G8** (eindeutiger Nachweisort, Kriterienkatalog- und Projektregister). Textvorschläge in §8 (A-N1).
2. **Rendering-/Konsistenzbug `{{TOOL_TICKET}}` u. ä.:** Der Default von `{{TOOL_TICKET}}` ist **„das Ticketsystem"**; das Muster „im {{TOOL_TICKET}}" rendert daher zu **„im das Ticketsystem"** (Doppelartikel). Analog `{{TOOL_NAME}}` = „das ISMS-Tool" und `{{TOOL_IAM}}` = „das zentrale Verzeichnis …". Systemischer **G6**-Befund mit **11 konkreten Fundstellen** (§6), inkl. eines zusätzlichen Kasus-Fehlers bei `in {{TOOL_IAM}}` (R08:153).
3. **Software-Whitelist als verwaltetes Register mit Lieferantenbezug UND Asset-Kopplung:** Empfehlung **REG-SW-WHITELIST** mit Spalten u. a. Software, Version/Patch-Stand, **Quelle/Lieferant/Dienstleister**, Freigabestatus, Verantwortlich, Review — **doppelt cross-verlinkt** an **R13/VA-10 (Lieferantensteuerung)** (zugelassene Software hat einen steuerbaren Lieferanten) und an **R02/VA-08 (Asset & Klassifizierung)** (zugelassene Software ist als Asset geführt). Analog **REG-EXT-SERVICES** für externe/Cloud-/KI-Dienste (§5, §8 A-N3).
**Reifegrad-Einschätzung Runde 2:** Inhaltlich bleibt die Abdeckung auf Ebene der 312 konsolidierten ISA-Zeilen hoch; die Herabstufungen betreffen überwiegend **Nachweisführung und Verortung** (Inline-Verweis, verwaltetes Register, eindeutiger Ort), nicht fehlenden Sachinhalt. Zwei echte Inhalts-GAPs bleiben (**2.1.1**, **3.1.3**). Mit der Roadmap in §7 erreicht das Paket ein durchgängig audittaugliches Niveau.
---
## 2. Bewertungs-Übersicht
### 2.1 Verteilung Control-Ebene (46 Controls)
| Verdikt | Runde 1 | **Runde 2** | Controls (Runde 2) |
|---------|:------:|:-----------:|--------------------|
| **erfüllt** | 22 | **14** | 1.1.1; 1.2.1; 1.2.2; 3.1.4; 4.1.2; 5.1.1; 5.2.2; 5.2.3; 5.2.4; 5.2.5; 5.2.9; 5.3.3; 6.1.1; 7.1.1 |
| **teilweise** | 22 | **30** | 1.2.3; 1.3.1; 1.3.2; 1.3.3; 1.3.4; 1.4.1; 1.5.1; 1.5.2; 1.6.1; 1.6.2; 1.6.3; 2.1.2; 2.1.3; 2.1.4; 3.1.1; 4.1.1; 4.1.3; 4.2.1; 5.1.2; 5.2.1; 5.2.6; 5.2.7; 5.2.8; 5.3.1; 5.3.2; 5.3.4; 5.3.4-KI; 6.1.2; 6.1.3; 7.1.2 |
| **GAP** | 2 | **2** | 2.1.1; 3.1.3 |
> Die 46 Controls = 45 Controls in `mapping.json` **+** der „Phantom"-Control **3.1.3** (IMPL-Stub in R07 ohne `REQ`-Anker und ohne `mapping.json`-Eintrag; siehe GAP G-B2).
### 2.2 Gegenüber Runde 1 KORRIGIERTE Einstufungen (alle: erfüllt → teilweise/GAP)
| Control | Richtlinie | Runde 1 | **Runde 2** | Grund der Verschärfung | GAP-Typ |
|---------|-----------|---------|-------------|------------------------|---------|
| **1.2.3** | R01 | erfüllt | **teilweise** | Keine VA deckt 1.2.3 ab; Verortung doppeldeutig („{{TOOL_TICKET}} bzw. ISMS-Tool"); Kriterienkatalog & Projektregister nicht als verwaltete Artefakte referenziert | G2, G4, G8, G6 |
| **2.1.2** | R05 | erfüllt | **teilweise** | „Ein Verfahren zum Umgang mit Verstößen ist beschrieben" — Verfahren nicht verortet/verlinkt (kein VA-/Dok-Verweis) | G1, G2 |
| **3.1.1** | R07 | erfüllt | **teilweise** | Zutrittsvergabe/-entzug (S1) und Besuchermanagement (S2) nur als „berücksichtigt" pauschaliert; kein Verfahren/VA verortet | G1, G3 |
| **5.2.6** | R10 | erfüllt | **teilweise** | VA-06 erfüllt laut `FULFILLS` 5.2.6-M1, wird im IMPL 5.2.6 aber **nicht** inline referenziert | G3 |
| **5.2.8** | R04 | erfüllt | **teilweise** | Kritische IT-Dienste nur „identifiziert"; kein verwaltetes Register (RTO/RPO nur im `-elev`-Block, keine BIA-Register-ID) | G8 |
| **5.3.4-KI** | R12 | erfüllt | **teilweise** | „Freigabeliste im ISMS-Tool" ist informelles Register ohne Register-ID/Attribute/Turnus (REG-EXT-SERVICES) | G8, G4 |
| **6.1.3** | R13 | erfüllt | **teilweise** | VA-10 erfüllt laut `FULFILLS` 6.1.3-M1, IMPL 6.1.3 verweist nicht inline; „Liste der IT-Dienste" (elev) informell | G3, G8 |
| **7.1.2** | R14 | erfüllt | **teilweise** | Kein referenzierter Prozess für Betroffenenrechte/Löschfristen-Review (nur BL-DEL-01 statisch); keine Datenschutz-Pflege-VA | G4 |
### 2.3 Anforderungsebene (316 Einzelanforderungen) — Schwerpunkte
- **Inline-VA-Verweis:** 23 Controls haben eine zuständige VA; **8** verweisen inline (5.1.1, 5.2.4, 5.2.5*, 5.2.8, 5.2.9, 5.3.4, 5.3.4-KI, 6.1.1), **15** nicht (G3). *5.2.5 verweist auf VA-06, nicht auf das ebenfalls zuständige VA-04.
- **Register/Baseline-Bezug fehlt (G8/G4)** bei allen „Liste im Tool"-Formulierungen: 1.2.3 (Kriterienkatalog/Projektregister), 1.3.3 & 5.3.4/5.3.4-KI (externe/Cloud/KI-Dienste), 1.3.4 (Software-Whitelist), 1.5.1 (Auditplan), 2.1.1 (sensible Rollen), 5.1.2 & 5.2.7 (Netz/Netzdienste), 5.2.8 & 6.1.3 (kritische IT-Dienste).
- **Leere Anforderung / Mapping-Bruch:** 3.1.3 (leerer `REQ`-Block, IMPL-Stub ohne Mapping); ISA **3.1.2** fehlt in R07 und `mapping.json` vollständig.
- **Strukturhinweis:** Control **1.1.1** (L00) hat **keinen gebündelten IMPL-Block**; `impl_anchor == req_anchor` — die „Umsetzung" ist die Leitlinien-Prosa selbst. Inhaltlich vertretbar (Leitlinie), aber vom übrigen `IMPL <control>`-Muster abweichend (siehe auch F-Tooling).
---
## 3. Vollständige Verlinkungs-/Coverage-Matrix — ALLE 46 Controls
Spalten: **Inline im Umsetzungstext verlinkt?** = verweist der `IMPL <control>`-Block per `{{LINK:VA-xx}}`/„siehe … Verfahren" auf die zuständige VA (ja) oder steht die VA nur generisch/im Anhang (nein); „n.a." = keine VA zuständig.
| # | Control | Anf.-Stufe(n) | Zuständige VA (FULFILLS) | Inline verlinkt? | Register/Baseline-Bezug | Status | Bemerkung |
|--:|---------|---------------|--------------------------|:---------------:|--------------------------|--------|-----------|
| 1 | 1.1.1 | M×5 / S×4 | n.a. | n.a. | Nachweisregister; ISMS-Tool | erfüllt | Leitlinie; kein IMPL-Block (`impl_anchor=req_anchor`) |
| 2 | 1.2.1 | M×6 | n.a. | n.a. | ISMS-Tool; Managementbewertung | erfüllt | Governance vollständig verortet |
| 3 | 1.2.2 | M×4 / S×2 / H×1 | n.a. | n.a. | Rollenmatrix; ISMS-Tool | erfüllt | Funktionstrennung im `-elev` |
| 4 | 1.2.3 | M×1 / S×3 / H×1 | **keine** | **n.a. (VA fehlt)** | **kein Kriterienkatalog-/Projektregister; keine BL** | **teilweise** | **KORRIGIERT** v. erfüllt; G2/G4/G8/G6 (Miss #1) |
| 5 | 1.3.1 | M×2 / S×1 | VA-08 | **nein** | Asset-Inventar (ISMS-Tool) | teilweise | G3 |
| 6 | 1.3.2 | M×3 / S×1 | VA-08 | **nein** | Klassifizierungsschema | teilweise | G3 |
| 7 | 1.3.3 | M×2 / S×4 | n.a. | n.a. | „Freigabeliste im ISMS-Tool" (informell) | teilweise | G8/G4 → REG-EXT-SERVICES |
| 8 | 1.3.4 | M×2 / S×5 / V×1 | n.a. | n.a. | „Whitelist im ISMS-Tool" (informell) | teilweise | G8/G4 → REG-SW-WHITELIST (Miss #3) |
| 9 | 1.4.1 | M×4 / S×4 | VA-09 | **nein** | Risikoregister (ISMS-Tool) | teilweise | G3 |
| 10 | 1.5.1 | M×5 / S×1 | n.a. | n.a. | „Auditplan" (informell); keine BL-Frequenz | teilweise | G4/G2/G8 → VA-15, REG-AUDIT-PLAN |
| 11 | 1.5.2 | M×2 / S×1 | n.a. | n.a. | unabhängige Prüfung; kein Turnus/BL | teilweise | G2/G8 |
| 12 | 1.6.1 | M×3 / S×6 / V×1 | VA-01 | **nein** | Meldeweg; „{{TOOL_TICKET}} **bzw.** ISMS-Tool" | teilweise | G3/G2/G6 (doppeldeutig) |
| 13 | 1.6.2 | M×3 / S×3 / H×5 / V×1 | VA-01 | **nein** | {{TOOL_TICKET}} | teilweise | G3/G6 |
| 14 | 1.6.3 | M×3 / S×6 / H×5 / V×1 | VA-02 | **nein** | Krisenplan; keine BL-Übungsfrequenz | teilweise | G3/G8 |
| 15 | 2.1.1 | M×3 / S×2 | **keine** | **n.a. (VA fehlt)** | **kein Register sensibler Rollen** | **GAP** | G1/G2/G4/G5 → VA-14, REG-SENS-ROLES |
| 16 | 2.1.2 | M×2 / S×3 | n.a. | n.a. | Personalakte; Verstoß-„Verfahren" unverortet | **teilweise** | **KORRIGIERT** v. erfüllt; G1/G2 |
| 17 | 2.1.3 | M×1 / S×6 | VA-12 | **nein** | BL-HR-01; {{TOOL_NAME}} | teilweise | G3/G6 |
| 18 | 2.1.4 | M×1 / S×2 / H×1 | n.a. | n.a. | „eine Regelung" — nicht verortet | teilweise | G2/G1 |
| 19 | 3.1.1 | M×3 / S×5 / H×1 | n.a. | n.a. | BL-PHY-01/02; Besucher/Zutritt generisch | **teilweise** | **KORRIGIERT** v. erfüllt; G1/G3 → VA-17 |
| 20 | 3.1.3 | — (leer) | n.a. | n.a. | IMPL-Stub ohne REQ/Mapping | **GAP** | G6/G1; ISA 3.1.2 fehlt zudem ganz |
| 21 | 3.1.4 | M×1 / S×1 / H×1 | n.a. | n.a. | TECH_MDM; BL-EP-02 | erfüllt | Baseline-verankert |
| 22 | 4.1.1 | M×1 / S×1 / H×1 | VA-03 | **nein** | BL-IAM-07; TOOL_IAM | teilweise | G3 |
| 23 | 4.1.2 | M×2 / S×3 / H×1 / V×1 | n.a. | n.a. | BL-IAM-01/02; TECH_MFA | erfüllt | Baseline-verankert |
| 24 | 4.1.3 | M×7 / S×10 | VA-03 | **nein** | TOOL_IAM | teilweise | G3 |
| 25 | 4.2.1 | M×2 / S×5 / H×1 / V×2 | VA-03 | **nein** | BL-IAM-05; RECERT_FREQ | teilweise | G3 |
| 26 | 5.1.1 | M×1 / S×1 / H×1 | VA-07 | **ja** | BL-CRY-02/05 | erfüllt | Vorbildliche Referenzkette |
| 27 | 5.1.2 | M×3 / S×3 / H×1 / V×1 | VA-07 | **nein** | BL-CRY-01/04; Netzdienste informell | teilweise | G3/G8 → REG-NET |
| 28 | 5.2.1 | M×1 / S×4 / H×1 | VA-04 | **nein** | BL-OPS-09; {{TOOL_TICKET}} | teilweise | G3/G6 |
| 29 | 5.2.2 | M×2 / S×1 | n.a. | n.a. | Trennung Dev/Test/Prod | erfüllt | Konkret |
| 30 | 5.2.3 | M×2 / S×8 | n.a. | n.a. | BL-OPS-03; TECH_MALWARE | erfüllt | Baseline-verankert |
| 31 | 5.2.4 | M×5 / S×3 / H×2 / V×1 | VA-13 | **ja** | BL-OPS-04; LOG_RETENTION | erfüllt | Referenzkette vollständig |
| 32 | 5.2.5 | M×3 / S×3 | VA-04, VA-06 | **teils** | BL-OPS-01/02; PATCH_SLA_CRIT | erfüllt | VA-06 inline; VA-04 nicht inline |
| 33 | 5.2.6 | M×5 / S×3 / H×1 / V×1 | VA-06 | **nein** | BL-OPS-07/08; PENTEST_FREQ | **teilweise** | **KORRIGIERT** v. erfüllt; G3 |
| 34 | 5.2.7 | M×2 / S×2 / H×1 | n.a. | n.a. | BL-NET-01/02; „Netzplan" informell | teilweise | G8/G2 → REG-NET |
| 35 | 5.2.8 | M×2 / S×3 / H×7 / V×3 | VA-02 | **ja** | kein REG-CRIT-SERVICES; RTO/RPO nur elev | **teilweise** | **KORRIGIERT** v. erfüllt; G8 |
| 36 | 5.2.9 | M×2 / S×1 / H×2 / V×3 | VA-05 | **ja** | BL-OPS-05; BACKUP_SCHEME/RETENTION | erfüllt | Referenzkette vollständig |
| 37 | 5.3.1 | M×4 / S×5 / V×1 | n.a. | n.a. | {{TOOL_TICKET}}; keine Secure-Dev-VA | teilweise | G4/G1/G6 → VA-16 |
| 38 | 5.3.2 | M×1 / S×3 / H×1 | n.a. | n.a. | „über ein Verfahren" (generisch) | teilweise | G1/G4 → VA-16 |
| 39 | 5.3.3 | S×1 | n.a. | n.a. | BL-DEL-01; Löschprotokoll | erfüllt | Konkret |
| 40 | 5.3.4 | M×1 / S×1 | VA-11 | **ja** | „Freigabeliste im ISMS-Tool" (informell) | teilweise | G8 → REG-EXT-SERVICES |
| 41 | 5.3.4-KI | M×3 / S×1 | VA-11 | **ja** | „Freigabeliste im ISMS-Tool" (informell) | **teilweise** | **KORRIGIERT** v. erfüllt; G8/G4 |
| 42 | 6.1.1 | M×3 / S×2 / H×3 / V×2 | VA-10 | **ja** | BL-SUP-01; Lieferantenverzeichnis | erfüllt | Referenzkette vollständig |
| 43 | 6.1.2 | M×4 / S×5 | VA-10 | **nein** | NDAs „im ISMS-Tool" (informell) | teilweise | G3 |
| 44 | 6.1.3 | M×5 / S×2 / H×5 | VA-10 | **nein** | „Liste der IT-Dienste" (elev, informell) | **teilweise** | **KORRIGIERT** v. erfüllt; G3/G8 |
| 45 | 7.1.1 | M×2 / S×1 | n.a. | n.a. | Compliance-/Rechtsregister (ISMS-Tool) | erfüllt | Register vorhanden |
| 46 | 7.1.2 | M×3 | n.a. | n.a. | VVT (ISMS-Tool); BL-DEL-01 | **teilweise** | **KORRIGIERT** v. erfüllt; G4 (Betroffenenrechte/Löschfristen-Prozess) → VA-18 |
---
## 4. GAP-Tabelle
Legende Schwere: **hoch** = unmittelbar auditrelevant / Inhaltslücke · **mittel** = schwächt Nachweisführung / Konsistenz · **niedrig** = Feinschliff.
GAP-Typen: G1 zu generisch · G2 kein/eindeutiger Nachweisort fehlt · G3 fehlende Inline-Verlinkung · G4 fehlende VA/Register · G5 Verantwortlicher unklar · G6 Widerspruch/Redundanz/veraltet/Rendering · G7 Schutzbedarf nicht adressiert · G8 Baseline-/Register-Referenz fehlt.
| # | Richtlinie | Control | Anf. (M/S/H/V) | Umsetzung heute (Kurz) | GAP-Typ | Schwere | Empfehlung | Konkreter Textvorschlag | Ziel-Referenz |
|---|-----------|---------|----------------|------------------------|---------|---------|------------|--------------------------|---------------|
| **N1** | R01 | **1.2.3** | M/S/H | „Klassifizierung anhand Kriterienkatalog; Einstufung im {{TOOL_TICKET}} **bzw. ISMS-Tool**; Maßnahmen als Aufgaben" — keine VA, doppeldeutiger Ort, Kriterienkatalog/Projektregister nicht referenziert | **G4, G2, G8, G6** | **hoch** | Neue VA „Informationssicherheit in Projekten"; Kriterienkatalog + Projektregister als verwaltete Artefakte; eindeutigen Ort setzen; Rendering fixen | siehe A-N1 | Neue **VA-19**, **REG-PROJECTS**, Kriterienkatalog (BL-PROJ-01) |
| G-B1 | R05 | 2.1.1 | M×3 / S×2 | „Sensible Tätigkeiten sind bestimmt; Eignung im rechtlich zulässigen Rahmen geprüft" — ohne Register, ohne Einstellungs-/Verifizierungs-VA, WER unklar | G1, G2, G4, G5 | **hoch** | Register sensibler Tätigkeitsbereiche + VA-14 (Eignungs-/Verifizierungsprozess) | siehe A-G-B1 | Neue **VA-14**, **REG-SENS-ROLES** |
| G-B2 | R07 | 3.1.3 | — (leer) | **Anforderungsblock leer**; IMPL-Stub ohne `REQ`/Mapping; **ISA 3.1.2 fehlt ganz** | G6, G1 | **hoch** | Scope 3.1.2/3.1.3 klären; REQ-Anker + Mapping ergänzen **oder** Stub entfernen | siehe A-G-B2 | `mapping.json`, R07 |
| N2 | R05 | 2.1.2 | M/S | „Ein Verfahren zum Umgang mit Verstößen ist beschrieben" — Verfahren nicht verortet/verlinkt | G1, G2 | mittel | Verstoß-/Disziplinarverfahren benennen & verorten (Dok/VA-14) | „… ein dokumentiertes Verfahren zum Umgang mit Verstößen **(siehe {{LINK:VA-14}})** ist etabliert; Nachweis in der Personalakte." | {{LINK:VA-14}} / Personalakte |
| N3 | R07 | 3.1.1 | M/S/H | Besuchermanagement, Zutrittsvergabe/-entzug (S1/S2) nur als „berücksichtigt" pauschaliert | G1, G3 | mittel | Zutritts-/Besuchermanagement-Verfahren verorten | „… Zutrittsrechte werden über {{TOOL_TICKET}} vergeben/entzogen **(Ablauf siehe {{LINK:VA-17}})**; Besuchermanagement (Registrierung/Begleitung) ist geregelt (BL-PHY-…)." | Neue **VA-17** |
| N4 | R10 | 5.2.6 | M/S/H/V | Härtung/technische Prüfungen; VA-06 erfüllt 5.2.6-M1, aber **nicht inline** referenziert | G3 | mittel | VA-06 im IMPL 5.2.6 inline referenzieren | „… risikoorientiert geprüft **(siehe {{LINK:VA-06}})**; Ergebnisse werden gespeichert, der Leitung berichtet …" | {{LINK:VA-06}} |
| N5 | R04 | 5.2.8 | M/S/H/V | Kritische IT-Dienste „identifiziert"; RTO/RPO nur im `-elev`-Block; kein Register | G8 | mittel | Register kritischer IT-Dienste inkl. BIA/RTO/RPO referenzieren | „Kritische IT-Dienste sind mit Geschäftsauswirkung im **Register kritischer IT-Dienste ({{LINK:REG-CRIT-SERVICES}})** (inkl. RTO/RPO, Wiederanlaufreihenfolge) erfasst … (siehe {{LINK:VA-02}})." | **REG-CRIT-SERVICES** |
| N6 | R12 | 5.3.4-KI | M/S | „Freigabeliste im ISMS-Tool" ohne Register-ID/Attribute/Turnus | G8, G4 | mittel | KI-/externe Dienste als verwaltetes Register mit Lieferant+Asset-Kopplung | siehe A-N6 | **REG-EXT-SERVICES**, {{LINK:VA-11}} |
| N7 | R13 | 6.1.3 | M/S/H | VA-10 erfüllt 6.1.3-M1, IMPL nicht inline; „Liste IT-Dienste/Dienstleister" (elev) informell | G3, G8 | mittel | VA-10 inline; Dienste-/Dienstleister-Register formalisieren | „… Verantwortlichkeiten … definiert **(siehe {{LINK:VA-10}})**; betroffene IT-Dienste und Dienstleister im **{{LINK:REG-EXT-SERVICES}}** geführt." | {{LINK:VA-10}}, **REG-EXT-SERVICES** |
| N8 | R14 | 7.1.2 | M | VVT im Tool; kein referenzierter Prozess für Betroffenenrechte/Löschfristen-Review | G4 | mittel | Datenschutz-/Compliance-Pflege-VA referenzieren | siehe A-N8 | Neue **VA-18** |
| N9 | R09 | 5.1.2 | M/S/H/V | VA-07 erfüllt 5.1.2-M1/S1, IMPL 5.1.2 verweist nicht inline; Netzdienste nur „identifiziert" | G3, G8 | mittel | VA-07 inline; Netzdienste-Register | „Genutzte Netzdienste sind im **{{LINK:REG-NET}}** identifiziert/dokumentiert; Krypto-/Schlüsselverwaltung **(siehe {{LINK:VA-07}})**." | {{LINK:VA-07}}, **REG-NET** |
| F3 | R02 | 1.3.1, 1.3.2 | M/S | IMPL ohne Inline-Verweis auf VA-08 | G3 | mittel | VA-08 im Umsetzungstext inline referenzieren | „… als Katalog gepflegt **(siehe {{LINK:VA-08}})**; Zu-/Abgänge über {{TOOL_TICKET}}." | {{LINK:VA-08}} |
| F4 | R03 | 1.4.1 | M/S | „Das dokumentierte Risikomanagement-Verfahren …" ohne Inline-Link auf VA-09 | G3 | mittel | VA-09 inline referenzieren | „Das dokumentierte Risikomanagement-Verfahren **(siehe {{LINK:VA-09}})** …" | {{LINK:VA-09}} |
| F5 | R04 | 1.6.1, 1.6.2 | M/S/H/V | „nach einem definierten Incident-Verfahren im {{TOOL_TICKET}}" ohne Inline-Link auf VA-01 | G3 | mittel | VA-01 inline referenzieren | „… nach einem definierten Incident-Verfahren **(siehe {{LINK:VA-01}})** … behandelt und dokumentiert." | {{LINK:VA-01}} |
| F6 | R04 | 1.6.3 | M/S/H/V | Krisenmanagement generisch; VA-02 nur im Anhang | G3, G8 | mittel | VA-02 inline; Übungsfrequenz in Baseline | „Ein Krisenmanagement … ist etabliert **(Auslösung/Wiederanlauf siehe {{LINK:VA-02}})** (Übungen BL-IR-01)." | {{LINK:VA-02}}, **BL-IR-01** |
| F7 | R05 | 2.1.3 | M/S | Awareness stark (BL-HR-01); VA-12 nur im Anhang | G3 | mittel | VA-12 inline referenzieren | „… mindestens {{REVIEW_CYCLE}} … geschult **(Ablauf siehe {{LINK:VA-12}})**." | {{LINK:VA-12}} |
| F8 | R08 | 4.1.1, 4.1.3, 4.2.1 | M/S/H/V | JML/Rezertifizierung stark, aber VA-03 nur im Anhang | G3 | mittel | VA-03 inline referenzieren | „Benutzerkonten werden über einen definierten Lebenszyklus (Joiner/Mover/Leaver) **(siehe {{LINK:VA-03}})** … verwaltet." | {{LINK:VA-03}} |
| F9 | R10 | 5.2.1 | M/S/H | „formales Change-Verfahren … im {{TOOL_TICKET}} (BL-OPS-09)" ohne Inline-Link auf VA-04 | G3 | mittel | VA-04 inline referenzieren | „Änderungen durchlaufen ein formales Change-Verfahren **(siehe {{LINK:VA-04}})** …" | {{LINK:VA-04}} |
| F10 | R02 | 1.3.4 | M/S/V | „Liste zugelassener Software (Whitelist) … im ISMS-Tool" ohne Register-ID, Lieferant/Freigabestatus | G8, G4 | mittel | REG-SW-WHITELIST mit Lieferant- UND Asset-Kopplung | siehe A-N3 (Miss #3) | **REG-SW-WHITELIST** |
| F11 | R02 / R12 | 1.3.3 / 5.3.4 | M/S | „Freigabeliste im ISMS-Tool" für externe/Cloud-/KI-Dienste — generisch | G8, G4 | mittel | Gemeinsames REG-EXT-SERVICES mit Schutzbedarf/Exit/Lieferant | siehe A-N6 | **REG-EXT-SERVICES**, {{LINK:VA-11}} |
| F12 | R03 | 1.5.1, 1.5.2 | M/S | Audits „nach einem Auditplan"; kein Audit-Verfahren, keine Verortung/Frequenz | G2, G4, G8 | mittel | Audit-Verfahren (VA-15) + Audit-Programm-Register + Baseline-Frequenz | siehe A-F12 | Neue **VA-15**, **REG-AUDIT-PLAN**, **BL-GOV-01** |
| F13 | R11 | 5.3.1, 5.3.2 | M/S/H/V | Security-by-Design/Abnahme, aber keine VA; 5.3.2 „über ein Verfahren umgesetzt" | G4, G1 | mittel | VA-16 (Sichere Beschaffung/Entwicklung & Abnahme) inline | siehe A-F13 | Neue **VA-16** |
| F14 | R09 / R10 | 5.1.2 / 5.2.7 | M/S/H | „Netzplan/Segmentierungskonzept wird gepflegt", „Netzdienste identifiziert" — ohne Register-ID | G2, G8 | niedrig | Verwaltetes Netz-/Netzdienste-Register mit Turnus | „… ein aktueller Netzplan/Segmentierungskonzept wird im **{{LINK:REG-NET}}** gepflegt (Review {{REVIEW_CYCLE}})." | **REG-NET** |
| F16 | R06 | 2.1.4 | M/S/H | „Mobiles Arbeiten ist in einer Regelung festgelegt" — welche, wo? | G2, G1 | niedrig | Regelung konkret benennen/verorten | „Mobiles Arbeiten ist in der **Regelung mobiles Arbeiten (im ISMS-Tool ({{TOOL_NAME}}) hinterlegt)** festgelegt …" | {{TOOL_NAME}} |
| F17 | — (Tooling) | — | — | `mapping.json` ohne `implementation`-Feld (0/316); Integrationsleitfaden §5/§8 setzt es voraus (Assessment-Export) | G6 | mittel | Feld `implementation` ergänzen **oder** Leitfaden korrigieren (`impl_anchor`-Auflösung dokumentieren) | siehe A-F17 | `mapping.json` / `00_Integrationsleitfaden_Wizard.md` |
| F18 | Alle (elev) | div. | H/V | `-elev`-Blöcke mischen HOCH- und SEHR-HOCH-Sätze unter **einem** `FLAG_ELEVATED_PROTECTION`; bei nur HOCH erscheint „Bei sehr hohem Schutzbedarf …" | G6, G7 | niedrig | SEHR-HOCH-Sätze in `{{#if FLAG_VERY_HIGH_PROTECTION}}` auslagern | siehe A-F18 | R04/R08/R10 u. a. `-elev`-Blöcke |
| F19 | R13 | 6.1.2 | M/S | NDA-Prozess stark; VA-10 deckt 6.1.2-M1/S1, IMPL nicht inline | G3 | niedrig | VA-10 auch bei NDA inline referenzieren | „… gültige NDAs auf Basis geprüfter Standardvorlagen **(Ablauf siehe {{LINK:VA-10}})** …" | {{LINK:VA-10}} |
| F21 | VA-10 | 6.1.x | — | RACI-Schritttexte in der Tabelle abgeschnitten („… (Schutzb", „…(Selbstauskunft/Nac") | G6 | niedrig | Spaltentext vervollständigen | RACI-Schrittbezeichnungen ausschreiben | VA-10 |
| **R-1..R-11** | R01/R04/R05/R08/R10/R11 | div. | — | **Rendering-Doppelartikel** „im {{TOOL_TICKET/TOOL_NAME/TOOL_IAM}}" → „im **das** …" | G6 | mittel | Präposition anpassen (siehe §6) | „… dokumentiert **im Ticketsystem ({{TOOL_TICKET}})** …" bzw. „… **im {{TOOL_TICKET}}**" → „… **in {{TOOL_TICKET}}**"-Muster entschärfen | §6 |
---
## 5. Empfohlene neue VAs / Register
### 5.1 Neue Verfahrensanweisungen
| ID | Titel | Zweck | Betroffene Controls | Priorität |
|----|-------|-------|---------------------|-----------|
| **VA-19** *(NEU, Miss #1)* | **Informationssicherheit in Projekten** | Projektklassifizierung nach dokumentiertem Kriterienkatalog; Risikobewertung in früher Phase & bei Änderungen; Maßnahmenableitung/-verfolgung; Pflege des Projektregisters; ISB-Einbindung bei erhöhtem Schutzbedarf | 1.2.3 (M1, S1–S3, H1) | **hoch** |
| **VA-14** | Personalsicherheit – Eignungsprüfung & sensible Tätigkeiten | Definition sensibler Bereiche; Einstellungs-/Verifizierungsprozess (Identität, Referenzen, Führungszeugnis im rechtlich zulässigen Rahmen); Umgang mit Verstößen gegen IS-/Vertraulichkeitspflichten | 2.1.1 (M1–M3, S1–S2), 2.1.2 | **hoch** |
| **VA-15** | Interne Audits & Complianceprüfungen | Auditprogramm, -planung, Durchführung, Berichterstattung, Maßnahmenverfolgung; unabhängige Überprüfung | 1.5.1 (M1–M5, S1), 1.5.2 (M1–M2, S1) | mittel |
| **VA-16** | Sichere Beschaffung, Entwicklung & Abnahme | Sicherheitsanforderungen in Design/Beschaffung/Änderung; Abnahmetests; (bei Eigenentwicklung) Secure-Coding/SAST/Dependency-Scan; Testdaten-Handling | 5.3.1 (M1–M4, S1–S5, V1), 5.3.2 | mittel |
| **VA-17** *(optional)* | Zutritts- & Besuchermanagement (physisch) | Vergabe/Entzug Zutrittsrechte, Besucherregistrierung/-begleitung, Umgang mit Betriebsmitteln | 3.1.1 (S1–S5), 3.1.3 | niedrig |
| **VA-18** *(optional)* | Datenschutz- & Compliance-Pflege | Rechtsregister-Review, Löschfristen/Löschkonzept, Betroffenenrechte, VVT-Pflege | 7.1.1, 7.1.2 | niedrig |
### 5.2 Neue / zu formalisierende Register
| Register-ID | Inhalt / Pflichtattribute | Cross-Link | Ersetzt heutige Formulierung in | Priorität |
|-------------|---------------------------|-----------|----------------------------------|-----------|
| **REG-SW-WHITELIST** *(Miss #3)* | Software, Version/Patch-Stand, **Quelle/Lieferant/Dienstleister**, Freigabestatus, Freigeber/Verantwortlich, Review-Datum | **R13/VA-10** (Lieferant steuerbar) **+ R02/VA-08** (als Asset geführt) | R02 1.3.4 („Liste zugelassener Software") | mittel |
| **REG-EXT-SERVICES** *(Miss #3 analog)* | Externe/Cloud/KI-Dienste: Schutzbedarf, Datenlokation (EU), Verschlüsselung, Exit-Strategie, **Quelle/Lieferant**, Freigabestatus, Freigeber | **R13/VA-10 + R02/VA-08** | R02 1.3.3; R12 5.3.4 / 5.3.4-KI; R13 6.1.3 | mittel |
| **REG-SENS-ROLES** | Sensible Tätigkeitsbereiche/Rollen, geforderte Eignungsnachweise, Prüftiefe | R05/VA-14 | R05 2.1.1 | **hoch** |
| **REG-PROJECTS** *(NEU, Miss #1)* | Projekte, IS-Klassifizierung, Risikobewertung, abgeleitete Maßnahmen/Status, ISB-Einbindung | R01/VA-19 | R01 1.2.3 | **hoch** |
| **REG-CRIT-SERVICES** | Kritische IT-Dienste, BIA-Einstufung, RTO/RPO, Abhängigkeiten, Wiederanlaufreihenfolge | R04/VA-02 | R04 5.2.8; R13 6.1.3 | mittel |
| **REG-NET** | Netzplan/Segmentierung, Netzdienste, Zonen, Review-Turnus | R09/R10, VA-07 | R09 5.1.2; R10 5.2.7 | niedrig |
| **REG-AUDIT-PLAN** | Auditprogramm: Zeitplan, Umfang, geprüfte Controls, Prüfer, Ergebnisse | R03/VA-15 | R03 1.5.1 (S1) | mittel |
> Mehrere „Register" existieren heute implizit als Datensätze im ISMS-Tool. Empfehlung: als **benannte, verwaltete Register mit Register-ID** führen und über `{{LINK:REG-…}}` konsistent referenzieren (analog zur Baseline-/VA-Mechanik). Dazu Link-Auflösungstabelle (Integrationsleitfaden §6) und ggf. `variables.schema.json` um die neuen Ziele erweitern.
### 5.3 Ergänzung Baseline
| BL-ID | Parameter | Vorschlag |
|-------|-----------|-----------|
| **BL-GOV-01** | Audit-/Prüfzyklus | Interne Prüfung {{REVIEW_CYCLE}}; unabhängige Prüfung/Assessment mind. alle 3 Jahre bzw. nach grundlegenden Änderungen |
| **BL-IR-01** | Krisen-/Notfallübungen | Tabletop {{REVIEW_CYCLE}} (HOCH); Vollübung mit Entscheidungsträgern (SEHR HOCH) |
| **BL-PROJ-01** | Projekt-Klassifizierungskriterien | Dokumentierter Kriterienkatalog zur IS-Klassifizierung von Projekten (Auslöser/Schwellen für ISB-Einbindung) |
---
## 6. Konsistenz-/Rendering-Befunde (G6)
**Ursache:** Mehrere Variablen-Defaults beginnen bereits mit einem Artikel: `{{TOOL_TICKET}}`=„das Ticketsystem", `{{TOOL_NAME}}`=„das ISMS-Tool", `{{TOOL_IAM}}`=„das zentrale Verzeichnis (Entra ID / Active Directory)", `{{TECH_MDM}}`=„das eingesetzte MDM", `{{TECH_MFA}}`=„die eingesetzte MFA-Lösung" usw. Steht davor eine kontrahierte Präposition („im", „in"), entsteht beim Rendern ein **Doppelartikel** („im **das** Ticketsystem") bzw. ein Kasus-Fehler.
**Konkrete Fundstellen (Datei : Zeile — Control):**
| # | Fundstelle | Muster (rendert zu) | Control |
|---|-----------|----------------------|---------|
| R-1 | R01_ISMS-Organisation-und-Rollen.md:110 | „im {{TOOL_TICKET}}" → „im **das** Ticketsystem" (**2×** in der Zeile) | 1.2.3 |
| R-2 | R04_Incident-…:69 | „im {{TOOL_TICKET}} **bzw. ISMS-Tool**" (Doppelartikel **+** doppeldeutiger Ort G2) | 1.6.1 |
| R-3 | R04_Incident-…:126 | „im {{TOOL_TICKET}}" → „im **das** Ticketsystem" | 1.6.2 |
| R-4 | R05_Personalsicherheit-…:111 | „im {{TOOL_NAME}}" → „im **das** ISMS-Tool" | 2.1.3 |
| R-5 | R08_Identitaets-…:45 | „im {{TOOL_TICKET}}" → „im **das** Ticketsystem" | 4.1.1 |
| R-6 | R08_Identitaets-…:153 | „**in** {{TOOL_IAM}}" → „in **das** zentrale Verzeichnis …" (Doppelartikel **+** Kasus: müsste „im zentralen Verzeichnis") | 4.1.3 |
| R-7 | R08_Identitaets-…:199 | „im {{TOOL_TICKET}}" → „im **das** Ticketsystem" | 4.2.1 |
| R-8 | R10_Betriebssicherheit.md:57 | „im {{TOOL_TICKET}}" → „im **das** Ticketsystem" | 5.2.1 |
| R-9 | R10_Betriebssicherheit.md:203 | „im {{TOOL_TICKET}}" → „im **das** Ticketsystem" | 5.2.5 |
| R-10 | R11_Sichere-Systembeschaffung-…:67 | „im {{TOOL_TICKET}}" → „im **das** Ticketsystem" | 5.3.1 |
| R-11 | R01_…:110 (2. Vorkommen) | „als Aufgaben im {{TOOL_TICKET}} nachgehalten" → „im **das** …" | 1.2.3 |
> **Zusätzlich doppeldeutig verortet (G2, „… bzw. ISMS-Tool"):** R01:110 („im {{TOOL_TICKET}} **bzw. ISMS-Tool**") und R04:69 („Formular im {{TOOL_TICKET}} **bzw. ISMS-Tool**"). Diese Stellen brechen den Grundsatz „ein Nachweisort je Sachverhalt" und sind Auslöser der Herabstufung von 1.2.3 (und Mitgrund bei 1.6.1).
>
> **Grammatikalisch unkritisch** (kein Fix nötig) sind Vorkommen ohne kontrahierte Präposition, z. B. „über {{TOOL_IAM}}", „und {{TOOL_TICKET}}", „{{TOOL_TICKET}}-Aufträge" — hier passt der eingebettete Artikel bzw. es entsteht keine Doppelung.
**Empfohlener Fix (zwei Varianten):**
- **Variante A (Text):** Präpositionsmuster „im {{VAR}}" → „**in {{VAR}}**" bzw. Artikel explizit ausschreiben: „**im Ticketsystem ({{TOOL_TICKET}})**". Bei R08:153 zusätzlich Kasus/Präposition „**im** {{TOOL_IAM}}" verwenden.
- **Variante B (Daten):** Variablen-Defaults ohne führenden Artikel definieren (`{{TOOL_TICKET}}`=„Ticketsystem") und Artikel im Fließtext setzen. **Achtung:** wirkt global auf alle Fundstellen; nur konsistent umsetzen.
---
## 7. Priorisierte Roadmap
**Priorität 1 — Inhalts-GAPs & Fehleinstufungen mit hoher Auditrelevanz:**
1. **N1 / VA-19 / REG-PROJECTS** — R01 1.2.3 Informationssicherheit in Projekten: VA anlegen, Kriterienkatalog (BL-PROJ-01) + Projektregister formalisieren, eindeutigen Ort setzen, Rendering fixen. *(Miss #1)*
2. **G-B1 / VA-14 / REG-SENS-ROLES** — R05 2.1.1 Personalsicherheit konkretisieren.
3. **G-B2** — R07 3.1.3 leeren Anforderungsblock beheben; ISA-Scope 3.1.2/3.1.3 klären; Mapping ergänzen oder Stub entfernen.
**Priorität 2 — Nachweiskette & Konsistenz (geringer Aufwand, hoher Nutzen):**
4. **F3–F9, F19, N4, N7, N9** — Inline-Verlinkung der Verfahren in den 15 Umsetzungstexten vereinheitlichen (VA-01/02/03/04/06/07/08/09/10/12).
5. **§6 / R-1..R-11** — Rendering-Doppelartikel `{{TOOL_TICKET}}` u. ä. beheben; doppeldeutige Orte („bzw. ISMS-Tool") auflösen. *(Miss #2)*
6. **F17** — `mapping.json` ↔ Integrationsleitfaden abgleichen (`implementation`-Feld ergänzen oder Leitfaden korrigieren).
**Priorität 3 — Register formalisieren (Reifegrad):**
7. **F10/F11/N5/N6/N7 / REG-SW-WHITELIST, REG-EXT-SERVICES, REG-CRIT-SERVICES, REG-NET** — verwaltete Register mit Pflichtattributen + `{{LINK:REG-…}}`; **REG-SW-WHITELIST/REG-EXT-SERVICES mit Lieferant- UND Asset-Kopplung**. *(Miss #3)*
8. **F12 / VA-15 / REG-AUDIT-PLAN / BL-GOV-01** und **F13 / VA-16** — Audit- und Secure-Development-Verfahren.
**Priorität 4 — Feinschliff:**
9. **N2, N3, N8, F14, F16, F18, F21** — Verstoß-Verfahren verorten; VA-17/VA-18 (optional); Netz-/mobile-Regelung verorten; SEHR-HOCH-`{{#if}}`-Trennung; VA-10 RACI-Text.
---
## 8. Anhang: Einfügefertige Textvorschläge
Alle Vorschläge im bestehenden Stil (Variablen `{{…}}`, Baseline-/VA-/Register-Verweise), **nicht** eingepflegt.
### A-N1 — R01 §3.3 (ISA 1.2.3), IMPL 1.2.3 (Ersatztext, Miss #1)
> Projekte werden zu Beginn anhand des **dokumentierten Kriterienkatalogs (BL-PROJ-01)** hinsichtlich Informationssicherheitsbedarf klassifiziert; Einstufung, Risikobewertung und abgeleitete Maßnahmen werden im **Projektregister ({{LINK:REG-PROJECTS}})** geführt. In einer frühen Projektphase und bei Änderungen erfolgt eine Risikobewertung nach dem **Verfahren Informationssicherheit in Projekten ({{LINK:VA-19}})**; Maßnahmen werden als Aufgaben **in {{TOOL_TICKET}}** nachgehalten und vor Projektabschluss geprüft. Verantwortlich ist die Projektleitung; bei erhöhtem Schutzbedarf wird {{ROLE_ISB}} eingebunden.
*Ergänzung Abschnitt 8 „Verwandte Dokumente" von R01:* `- Zugehörige Verfahren: {{LINK:VA-19}}; Register: {{LINK:REG-PROJECTS}}`
*Hinweis:* Damit entfällt die doppeldeutige Verortung („{{TOOL_TICKET}} bzw. ISMS-Tool"), der Kriterienkatalog wird als Baseline-Artefakt (BL-PROJ-01) geführt, und die fehlende VA/das fehlende Register werden geschlossen.
### A-G-B1 — R05 3.1 (ISA 2.1.1), IMPL 2.1.1 (Ersatztext)
> Sensible Arbeitsbereiche und Tätigkeiten sind im **Register sensibler Tätigkeiten ({{LINK:REG-SENS-ROLES}})** bestimmt und mit der geforderten Prüftiefe hinterlegt; Anforderungen an Positionen sind in Stellenbeschreibungen dokumentiert und werden erfüllt. Identitätsverifizierung sowie die persönliche und – bei sensiblen Rollen – erweiterte Eignungsprüfung (Gespräch, Referenzen, Führungszeugnis im rechtlich zulässigen Rahmen) erfolgen nach dem **Eignungs- und Verifizierungsverfahren ({{LINK:VA-14}})**; Verantwortlich: {{ROLE_HR_LEAD}}; Nachweis in der Personalakte.
### A-G-B2 — R07 3.2 (ISA 3.1.3) Anforderungsblock (heute leer)
**(a) Falls 3.1.3 im ISA-Scope ist** — Anforderungstext + Anker ergänzen und `mapping.json`-Einträge nachziehen:
> `<!-- REQ 3.1.3-M1 -->`
> - **[MUSS]** Der Umgang mit unterstützenden Betriebsmitteln (z. B. Verkabelung, Strom-/Klimaversorgung, Serverräume) ist bestimmt; Schutz gegen Ausfall, Wartung und Überwachung sind geregelt.
**(b) Falls 3.1.3 nicht im Scope ist** — Abschnitt 3.2 samt IMPL-Stub entfernen, damit kein Umsetzungstext ohne zugehörige Anforderung/Mapping verbleibt.
In beiden Fällen: Klären, ob **ISA 3.1.2** bewusst ausgelassen ist (fehlt in R07 und `mapping.json`) und dokumentieren.
### A-N3 — R02 3.4 (ISA 1.3.4), IMPL 1.3.4 (REG-SW-WHITELIST, Miss #3)
> Software (inkl. Spezial-/Wartungssoftware) wird vor Einsatz freigegeben. Zugelassene Software wird im **Register Software-Whitelist ({{LINK:REG-SW-WHITELIST}})** mit Version/Patch-Stand, **Quelle/Lieferant**, Freigabestatus und Freigeber geführt; jeder Eintrag ist mit dem verantwortlichen **Lieferanten ({{LINK:VA-10}} / {{LINK:R13}})** und – als verwaltetes Asset – mit dem **Asset-Inventar ({{LINK:VA-08}} / {{LINK:R02}})** verknüpft. Beschaffung/Freigabe läuft über {{TOOL_TICKET}}; Repositorys sind gegen Manipulation geschützt; Verantwortlich: {{ROLE_IT_LEAD}}; Review {{REVIEW_CYCLE}}.
### A-N6 — R12 3.1 (ISA 5.3.4 / 5.3.4-KI), IMPL (REG-EXT-SERVICES)
> … Cloud-/KI-Dienste werden vor Nutzung bewertet (Schutzbedarf, Datenlokation/EU, Verschlüsselung, Exit) und von {{ROLE_ISB}} freigegeben; Freigaben werden im **Register externe IT-/Cloud-/KI-Dienste ({{LINK:REG-EXT-SERVICES}})** mit Schutzbedarf, Datenlokation, **Quelle/Lieferant**, Freigabestatus und Freigeber geführt und mit **Lieferantensteuerung ({{LINK:VA-10}})** sowie **Asset-Inventar ({{LINK:VA-08}})** verknüpft (Ablauf siehe {{LINK:VA-11}}); die ausschließliche Nutzung freigegebener Dienste wird {{REVIEW_CYCLE}} geprüft.
### A-N8 — R14 (ISA 7.1.2), IMPL 7.1.2 (Datenschutz-Pflege-VA)
> … das Verzeichnis der Verarbeitungstätigkeiten wird im ISMS-Tool ({{TOOL_NAME}}) geführt, TOM und Löschkonzepte (BL-DEL-01) sind geregelt; Rechtsregister-Review, Löschfristen und Betroffenenrechte werden nach dem **Datenschutz-/Compliance-Pflegeverfahren ({{LINK:VA-18}})** bearbeitet; Verantwortlich: {{ROLE_DPO}}.
### A-F12 — R03 3.2 (ISA 1.5.1), IMPL 1.5.1 (VA-/Register-Verweis)
> Die Einhaltung von Richtlinien, Verfahren und technischen Anforderungen wird organisationsweit nach dem **Audit-Programm ({{LINK:REG-AUDIT-PLAN}})** und dem **Audit-/Complianceprüfungs-Verfahren ({{LINK:VA-15}})** durch interne Audits und Kontrollen regelmäßig überprüft (Turnus BL-GOV-01); Ergebnisse werden aufgezeichnet und aufbewahrt, Abweichungen als Maßnahmen im {{TOOL_NAME}} nachverfolgt; Verantwortlich: {{ROLE_ISB}}.
### A-F13 — R11 3.1 (ISA 5.3.1), IMPL 5.3.1 (VA-Verweis)
> Informationssicherheitsanforderungen sind fester Bestandteil von Design, Beschaffung, Erweiterung und Änderung von IT-Diensten (Security by Design); Anforderungsspezifikation, Prüfung und Abnahmetests unter Sicherheitsaspekten erfolgen nach dem **Verfahren Sichere Beschaffung/Entwicklung & Abnahme ({{LINK:VA-16}})**; Produktivsetzung erst nach Prüfung **in {{TOOL_TICKET}}**. Produktivdaten in Tests werden vermieden/anonymisiert, Testsysteme angemessen geschützt.{{#if FLAG_DEV_INHOUSE}} Für die Eigenentwicklung gelten Secure-Coding-Vorgaben mit Code-Reviews und automatisierten Sicherheitstests (SAST/Dependency-Scan) gemäß {{LINK:VA-16}}.{{/if}}
### A-F17 — `mapping.json` / Integrationsleitfaden
**Variante 1 (Daten an Leitfaden angleichen):** je Anforderung ein Feld `implementation` mit dem gebündelten Umsetzungstext (bzw. eindeutiger Kennung des Control-IMPL-Blocks) aufnehmen, damit der Assessment-Export (§8, „Implementation description") direkt aus `mapping.json` speisbar ist.
**Variante 2 (Leitfaden an Daten angleichen, geringerer Aufwand):** In §5/§8 dokumentieren, dass `implementation` **nicht** in `mapping.json` liegt, sondern zur Laufzeit über `impl_anchor` (control-gebündelter Anker `IMPL <control>`) aus der jeweiligen `.md` aufgelöst wird. **Zusätzlich klären:** Für Control **1.1.1** ist `impl_anchor == req_anchor` (kein `IMPL 1.1.1`-Block; Leitlinien-Prosa) — diese Sonderauflösung im Leitfaden explizit ausweisen.
### A-F18 — Trennung SEHR-HOCH in `-elev`-Blöcken (Muster)
Statt eines gemeinsamen `{{#if FLAG_ELEVATED_PROTECTION}}`-Blocks, der HOCH- und SEHR-HOCH-Sätze mischt:
> `{{#if FLAG_HIGH_PROTECTION}}` … HOCH-Umsetzungssätze … `{{/if}}`
> `{{#if FLAG_VERY_HIGH_PROTECTION}}` … „Bei sehr hohem Schutzbedarf …" … `{{/if}}`
So erscheint der SEHR-HOCH-Umsetzungstext nur, wenn auch die zugehörigen `[SEHR HOCH]`-Anforderungen im Set sind (betrifft u. a. IMPL 1.2.2-elev, 1.6.2-elev, 1.6.3-elev, 4.1.2-elev, 4.2.1-elev, 5.1.2-elev, 5.2.4-elev, 5.2.6-elev, 5.2.9-elev, 6.1.1-elev).
---
## 9. Positiv-Befunde (zur Absicherung, kein Handlungsbedarf)
- **Baseline-Disziplin:** konkrete Werte durchgängig in `Technische-Sicherheits-Baseline.md` zentralisiert (31 BL-IDs); Umsetzungstexte referenzieren korrekt (z. B. R08 4.1.2 → BL-IAM-01/02, R10 5.2.9 → BL-OPS-05).
- **Vorbildliche Referenzketten:** 5.1.1, 5.2.4, 5.2.8, 5.2.9, 6.1.1 verbinden IMPL ↔ VA ↔ Baseline eindeutig — Zielbild für die übrigen Controls.
- **Mapping-Integrität:** 316 REQ-Anker ↔ 316 Mapping-IDs, keine Waisen (verifiziert); VA-`FULFILLS`-Header stimmen mit `mapping.json → verfahren` überein.
- **Schutzbedarf:** `-elev`-Abdeckung für alle Controls mit HOCH/SEHR-HOCH-Anforderungen vorhanden (Trennungs-Feinschliff siehe F18).
- **KI-/GenAI-Ergänzung (R12 3.2)** inhaltlich stark und aktuell (Datenklassen je Dienst, kein Training auf Eingaben, Human-in-the-Loop, EU AI Act) — die Herabstufung von 5.3.4-KI betrifft ausschließlich die Register-Formalisierung, nicht den Sachinhalt.
@@ -1,168 +0,0 @@
# Contracts-Checkliste — Kickoff Dev A × Dev B (15 Min.)
> Zweck: Die Naht zwischen **Paket Dev A** (Wizard-Shell/Scoping/AL) und **Paket Dev B**
> (Engines/Fragebogen/Richtlinien) **einmal gemeinsam bestätigen**, bevor beide parallel starten.
> Grundlage: der identische *Contracts-Anhang* §1–§6 in beiden Entwicklerpaketen.
> Vorgehen: Punkt für Punkt durchgehen, abhaken, offene Entscheidungen unten eintragen, beide signieren.
> Danach laufen A und B konfliktarm parallel — einzige echte Nahtstellen sind
> `prisma/schema.prisma`, `scripts/check-module-guards.ts` und die Step-Registry.
**Basis-Branch:** `dev` · **Feature-Branches:** `dev/a*-…` (A) bzw. `dev/b*-…` (B) · **PR-Ziel:** `dev`
Kein Push durch die Agenten; häufig auf `dev` rebasen; kleine PRs je Story.
---
## §1 Task-Objekt — **Owner: Dev B** (A konsumiert)
Dev B besitzt das Modell + `createTask`-Action; Dev A / Wizard-Gates rufen sie auf.
- [ ] `type`: `document_create | evidence_provide | technical | organizational | validation | policy_approval`
- [ ] Felder: `owner`, `dueDate`, `priority: 'hoch'|'mittel'|'niedrig'`, `status`
- [ ] `resources` (JSON): `{ tool, budget, personnel, time }`
- [ ] `origin` (Herkunft/Trigger) vorhanden
- [ ] `links` (polymorph): `{ control?, risk?, document?, asset? }`
- [ ] Bestehender `policy_approval`-Flow bleibt unverändert lauffähig (Regression grün)
- [ ] **Signatur der `createTask`-Action steht fix**, bevor A ihre Gates verdrahtet
→ Signatur hier notieren: `_____________________________________________`
## §2 Validierungsstatus — **Owner: Dev A** (B konsumiert)
- [ ] `enum ObjectReviewStatus { offen, in_bearbeitung, zur_validierung, validiert, zurueckgewiesen }`
- [ ] Zusatzfelder: `reviewComment`, `reviewerId`
- [ ] Regel bestätigt: **„Nur `validiert` zählt als bestätigt."**
- [ ] RBAC-Recht `validate_objects` + Rolle `external_validator` (klonbar) angelegt
- [ ] Feld-Konvention (an welchen Objekten der Status hängt) für Wizard-Gates fix
## §3 Flags in `variables.schema.json` — **Owner: Dev B** (Namen fix, A entwickelt dagegen)
- [ ] Namen unverändert übernommen (keine Umbenennung ohne beidseitige Abstimmung):
`FLAG_INCLUDE_SHOULD, FLAG_HIGH_PROTECTION, FLAG_VERY_HIGH_PROTECTION,
FLAG_ELEVATED_PROTECTION, FLAG_PERSONAL_DATA, FLAG_PROTOTYPE_PROTECTION,
FLAG_CLOUD_USED, FLAG_AI_USED, FLAG_OT_USED, FLAG_DEV_INHOUSE, FLAG_MOBILE_WORK,
FLAG_MOBILE_DEVICES, FLAG_CRYPTO_PKI, FLAG_EXTERNAL_IT, FLAG_CUSTOMER_SYSTEMS,
FLAG_ISB_EXTERNAL, FLAG_ISB_INTERNAL`
- [ ] `FLAG_ELEVATED_PROTECTION` bleibt **abgeleitet** (`applyProtection`) — nie manuell setzen
- [ ] Nach Schema-Änderung: `python3 seed/isms-vorlagenpaket-v2/_verify.py` → **OK**
## §4 Prüfziele — beidseitig fix
- [ ] Werte: `informationssicherheit | prototypenschutz | datenschutz`
- [ ] Wirkung bestätigt: Kapitel `8.x` nur bei Prototyp, `9.x` nur bei Datenschutz
## §5 Step-Registry — **Owner: Dev A** (B konsumiert)
- [ ] Signatur fix: `registerStep({ key, title, order, guard, component })`
- [ ] Schritt-Keys/Reihenfolge:
`1 scoping · 2 context · 3 roles · 4 policies · 5 assets · 6 risks · 7 controls · 8 gap · 9 readiness`
- [ ] Klar: **B liefert die Komponenten für `context` (Schritt 2) und `policies` (Schritt 4)**;
A stellt die Registry-Schnittstelle stabil bereit, bevor B einklinkt
## §6 Migrations-Protokoll — beidseitig verbindlich
- [ ] Vor jedem Migrations-Erzeugen zuerst auf `dev` rebasen
- [ ] **Migrationen nie gleichzeitig ohne Absprache** erstellen
- [ ] Je Branch **genau eine Migration pro Story**
- [ ] Prisma-7-Flow: `migrate diff --from-config-datasource … --to-schema … --script`
→ RLS-DO-Block manuell anhängen → `migrate deploy` → `generate`
- [ ] Neue tenant-gebundene Modelle → `TENANT_MODELS` (`src/server/db.ts`) **und** RLS-Policy
- [ ] Neue Server-Action-Datei → in `scripts/check-module-guards.ts` eintragen (sonst Build-Fail)
---
## Erwartete Migrations-Reihenfolge (gemeinsam festlegen)
Beide erzeugen Migrationen an `schema.prisma` — Reihenfolge vorab abstimmen, um Konflikte zu vermeiden.
Vorschlag (an tatsächlicher Story-Reihenfolge ausrichten):
| # | Story | Owner | Migration (Arbeitsname) |
|---|-------|-------|-------------------------|
| 1 | A1-1 | A | `onboarding_progress` |
| 2 | F1 | B | `tasks_wizard_fields` |
| 3 | F2 | A | (Enum `ObjectReviewStatus` + `external_validator`) |
| 4 | A2-2 | A | `wizard_scope` |
| 5 | B3 | B | `wizard_facts` |
- [x] Reihenfolge oben bestätigt / angepasst — A1-1(A) → F1(B) → F2(A) → A2-2(A) → B3(B)
- [x] Wer erzeugt die **erste** Migration? → **Dev A** (`onboarding_progress`, A1-1) — von Dev B bestätigt; B rebast danach zuerst
## Offene Fachpunkte (aus C0 — vor bzw. begleitend zu klären)
- [ ] Neue `FLAG_*` in `variables.schema.json` ergänzt (F4, Dev B)
- [ ] Vorlagen `P01`/`D01` + `VA-20` angelegt (B4-3, Dev B)
- [ ] Baseline `BL-*` normalisiert
- [ ] ISB-Freigabe der neuen/geänderten Texte (fachlich, kein Code)
---
## Sign-off
- Dev A bestätigt §1–§6 + Migrations-Reihenfolge: **Claude (Dev A)** (Datum: 2026-07-28)
- Dev B bestätigt §1–§6 + Migrations-Reihenfolge: **Claude (Dev B)** (Datum: 2026-07-28)
> Änderungen an §1–§6 nach dem Kickoff nur **gemeinsam** und mit Update dieser Datei
> **und** von `docs/STAND-dev-branch.md`.
---
## Dev A — Kickoff-Vorbereitung (Positionen & Vorschläge, warten auf B-Bestätigung)
> Nicht-bindend bis zum gemeinsamen Sign-off. „A-Vorschlag" = braucht B's Ja; „A bestätigt" =
> liegt in A's Hoheit und ist von A's Seite geklärt.
**§1 Task-Objekt (Owner B):** A bestätigt die Feldliteralwerte (`type`-Werte, `priority`, `resources`,
`origin`, `links`) wie im Anhang. **A braucht von B eine fixe `createTask`-Signatur, bevor A Gates/Trigger
verdrahtet.** A-Vorschlag als Startpunkt:
`createTask(input: { type, title, origin, owner?, dueDate?, priority?, resources?, links? }): Promise<Task>`
(Server-Action in B's Hoheit; A ruft nur auf). → **B bestätigt/ändert Signatur:** ✅ **bestätigt (unverändert)** —
`createTask(input: { type, title, origin, owner?, dueDate?, priority?, resources?, links? }): Promise<Task>`.
Mapping/Defaults (B-Hoheit, F1): `owner` → DB-Spalte `assigneeId` (eine verantwortliche Person; optional, da B1-Vorschläge unassigned starten);
`priority` default `'mittel'`; `resources = { tool?, budget?, personnel?, time? }` (alle optional); `links = { control?, risk?, document?, asset? }` (IDs/Refs).
`status` setzt die Action **serverseitig** (Default `OPEN`; auto-generierte B1-Vorschläge starten im Vorschlags-Status zur Bestätigung) — **kein** Input-Feld.
Bestehende `entityType/entityId/entityRef` bleiben für den `policy_approval`-Flow erhalten (F1 fügt nur Felder hinzu); `links.document` bildet den Dokumentbezug ab.
**§2 Validierungsstatus (Owner A) — A bestätigt:**
- `enum ObjectReviewStatus { offen, in_bearbeitung, zur_validierung, validiert, zurueckgewiesen }` + `reviewComment`, `reviewerId`.
- Regel „Nur `validiert` zählt als bestätigt" gilt für alle Wizard-Gates.
- RBAC-Recht `validate_objects` + klonbare Rolle `external_validator`.
- **Feld-Konvention (Gate-Quelle):** In A1-1 trägt der Schritt-Datensatz `OnboardingProgress` den Review-Status;
das „Weiter"-Gate liest den Status des Vorgänger-Schritts. Sobald Domänenobjekte je Schritt existieren
(spätere Stories), wandert die Statusquelle auf das jeweilige Primärobjekt (Generalisierung = A3).
→ **B nimmt das für seine Schritte (2 context, 4 policies) so an:** ✅ **ja** — B liest für `context` (2) und `policies` (4)
den Review-Status des Vorgänger-Schritts aus `OnboardingProgress` (Quelle A1-1); „nur `validiert` zählt". B schreibt in diesen Schritten
nur Fakten/Flags bzw. Richtlinien-Objekte, **nicht** den Gate-Status selbst. Hinweis: Schritt 4 nutzt weiterhin den bestehenden
`policy_approval`-Workflow für die inhaltliche Freigabe; das Wizard-„Weiter"-Gate hängt aber am `OnboardingProgress`-Status, nicht am Task.
**§3 Flags (Owner B):** A übernimmt die Namen aus §3 **unverändert**, benennt nichts um, behandelt
`FLAG_ELEVATED_PROTECTION` als abgeleitet (nie manuell). A entwickelt `scope-filter.ts` gegen diese Namen,
**bevor** F4 landet. → **B bestätigt Namensliste final:** ✅ **final** — die 17 Namen bleiben unverändert. Ist-Stand geprüft:
**14 bereits** in `variables.schema.json` vorhanden; F4 ergänzt **genau die 3 fehlenden** `FLAG_PROTOTYPE_PROTECTION`, `FLAG_ISB_EXTERNAL`,
`FLAG_ISB_INTERNAL` (+ fehlende `ROLE_*/TECH_*` aus C2, **ohne** FLAG-Namen anzufassen). `FLAG_ELEVATED_PROTECTION` bleibt abgeleitet. Nach F4: `_verify.py` → OK.
**§4 Prüfziele:** A bestätigt `informationssicherheit | prototypenschutz | datenschutz`; Kap. `8.x` nur bei
Prototyp, `9.x` nur bei Datenschutz (C1-Methodik).
**§5 Step-Registry (Owner A) — A bestätigt & stellt bereit:**
- Signatur `registerStep({ key, title, order, guard, component })`, Keys/Reihenfolge
`1 scoping · 2 context · 3 roles · 4 policies · 5 assets · 6 risks · 7 controls · 8 gap · 9 readiness`.
- A liefert die stabile Registry-Schnittstelle in A1-1, **bevor** B `context` (2) und `policies` (4) einklinkt.
→ **B bestätigt Schnittstelle als ausreichend:** ✅ **ausreichend** — `registerStep({ key, title, order, guard, component })` + Keys 1–9
genügen, um `context` (2) und `policies` (4) einzuklinken. Einzige Bitte an A: `guard` muss das **Ausblenden** eines Schrittes erlauben
(z. B. `policies` nur bei aktivem Richtlinien-Modul) und die `component`-Client/Server-Boundary sauber halten. Keine weiteren Felder von B benötigt.
**§6 Migrations-Reihenfolge — A-Vorschlag:**
| # | Story | Owner | Migration | Anmerkung |
|---|-------|-------|-----------|-----------|
| 1 | A1-1 | A | `onboarding_progress` | **A erzeugt die erste Migration** |
| 2 | F1 | B | `tasks_wizard_fields` | B rebast danach zuerst auf `dev` |
| 3 | F2 | A | `object_review_status` (Enum + `external_validator`) | |
| 4 | A2-2 | A | `wizard_scope` | |
| 5 | B3 | B | `wizard_facts` | |
→ **Wer erzeugt die erste Migration? A-Vorschlag: Dev A (A1-1).** B bestätigt: ✅ **ja** — Dev A erzeugt `onboarding_progress` zuerst;
B rebast danach zuerst auf `dev` und erzeugt dann `tasks_wizard_fields` (F1).
→ Reihenfolge oben bestätigt/angepasst: ✅ **bestätigt (unverändert)** — A1-1(A) → F1(B) → F2(A) → A2-2(A) → B3(B); je Branch genau eine Migration, nie gleichzeitig.
**Nicht-blockierend für A1-1** (betrifft spätere A-Stories bzw. B/Berater): neue `FLAG_*` in
`variables.schema.json` (F4), `seed/scoping/c1-scope.json` erzeugt A aus der C1-Tabelle (445 Zeilen),
P01/D01/VA-20 + Baseline-Normalisierung (B4/Berater), ISB-Freigabe (fachlich).
@@ -1,82 +0,0 @@
# Umsetzungspaket **Dev A** — Wizard-Fundament, Scoping & AL2/AL3 zentral
> Claude-Code-Prompt für Entwickler A. Parallel zu **Paket Dev B**. Basis-Branch **`dev`**. Fachcontent: `Fachcontent_Wizard_C1-C9/C1_Scoping-AL-Pruefziel.md`. Repo-Doku: `docs/HANDOVER-DEV.md`, `STAND-dev-branch.md`.
## Auftrag (Kurz)
Baue das **Wizard-Grundgerüst**, den **Scoping-Schritt** und verlege den **AL2/AL3-Schalter zentral ins Admin-/Superadmin-Portal**. Du lieferst die Klammer, in die Dev B seine Schritt-Inhalte einklinkt.
## Branches (unter `dev` abzweigen, PR-Ziel `dev`)
- `dev/a1-wizard-shell`
- `dev/a2-scoping-admin-al`
Häufig auf `dev` rebasen. Kleine PRs je Story.
## Datei-Hoheit (nur DU fasst diese an)
`src/app/(app)/onboarding/**` · `src/server/actions/onboarding.ts` · `src/lib/scope-filter.ts` · `src/app/(platform)/admin/**` (AL-Einstellung) · `src/lib/modules.ts` (nur Eintrag `onboarding`) · Scope-/Progress-Modelle in `prisma/schema.prisma`.
**Koordiniert (mit Dev B abstimmen, siehe Contracts-Anhang):** `prisma/schema.prisma` (Migrations-Reihenfolge), `scripts/check-module-guards.ts` (deine neuen Actions eintragen), Validierungsstatus-Enum.
---
## Story A1 — Wizard-Shell & Navigation (`dev/a1-wizard-shell`)
**A1-1 Step-Registry & State-Machine (L)**
- Neues Modul **`onboarding`** in `src/lib/modules.ts` (Default aktiv für Bestands-Tenants).
- Bereich `src/app/(app)/onboarding/**` mit einer **Step-Registry**: Schritte registrieren sich mit `{ key, title, order, guard, component }`. Dev B klinkt „context" (Schritt 2) und „richtlinien" (Schritt 4) ein — definiere die Registry-Schnittstelle stabil (Contracts-Anhang §5).
- **State-Machine**: `offen → in_bearbeitung → zur_validierung → validiert` je Schritt; **Gate**: „Weiter" ist gesperrt, solange das Vorgänger-Objekt nicht `validiert` ist (nutzt Validierungsstatus, Contracts §2).
- **Persistenter Fortschritt** je Mandant: Modell `OnboardingProgress` (tenant-gebunden, `TENANT_MODELS`+RLS), Migration `onboarding_progress`.
- Server-Action-Datei `src/server/actions/onboarding.ts` über `moduleGuard("onboarding")`; in `scripts/check-module-guards.ts` eintragen.
- **AK:** Fortschritt resumierbar; Gate blockiert korrekt; Schritte via Registry einklinkbar; `build`/Guard-Check grün.
**A1-2 Dashboard-Kachel (S)**
- Kachel „Onboarding-Fortschritt" in `dashboard/page.tsx` analog der bestehenden Freigabe-Kachel (Anteil erledigter Schritte, nächster offener Schritt).
---
## Story F2 (dein Anteil der Foundation) — Validierungsstatus-Enum (`dev/a1-wizard-shell`)
- Lege das **wiederverwendbare Enum** `ObjectReviewStatus` + Feld-Konvention an (Contracts §2), das Wizard-Gates und später A3 nutzen. RBAC-Recht `validate_objects`; Rolle **`external_validator`** ergänzen.
- **AK:** Enum + Recht vorhanden; Wizard-Gate liest den Status; Rolle im RBAC klonbar.
- **Hinweis:** Die vollständige Generalisierung über alle Objekttypen ist Story A3 (späterer Branch) — hier nur Enum + Wizard-Nutzung.
---
## Story A2 — Scoping + AL2/AL3 zentral im Adminportal (`dev/a2-scoping-admin-al`)
**A2-1 AL2/AL3 zentral (M)** — *explizite Anforderung*
- Der Assessment-Level (AL2/AL3) wird **ausschließlich** im **Admin-/Superadmin-Portal je Mandant** gesetzt → **einzige Quelle der Wahrheit**. UI in `src/app/(platform)/admin/**`, Action in `src/server/actions/admin.ts`/`platform.ts`.
- Mandanten-`/settings` und der Scoping-Schritt zeigen AL nur **read-only** an (kein Änderungsrecht mehr im Tenant).
- AL treibt die bestehenden Flags `FLAG_HIGH_PROTECTION`/`FLAG_VERY_HIGH_PROTECTION` und den vorhandenen Coverage-Filter (Coverage zeigt bei AL2 keine „sehr hoch"-Controls — Verhalten beibehalten, Quelle umziehen).
- **AK:** AL ist nur im Adminportal änderbar; Änderung propagiert in Coverage + Zusatzanforderungen; Tenant kann AL nicht mehr selbst setzen.
**A2-2 Scope-Objekt + Filter-Engine (M)**
- Scoping (Schritt 1) erfasst: **Prüfziele** (Informationssicherheit / Prototypenschutz / Datenschutz), Standorte, Geltungsbereich, Ausschlüsse → Modell `WizardScope` (tenant-gebunden, RLS).
- **`src/lib/scope-filter.ts`**: gegeben (AL, gesetzte `FLAG_*`, aktive Prüfziele) → liefert die **aktiven Anforderungen**. Datengrundlage: **C1-Tabelle** (412 Zeilen: `Control · Anforderungs-ID · Typ · AL2 · AL3 · Prüfziel · Scope-Bedingung(Flag)`), als Seed/JSON `seed/scoping/c1-scope.json` einlesen (ID = `mapping.json`-`id`).
- Filterlogik exakt nach C1-Methodik: MUSS/SOLL = AL2+AL3 (SOLL über `FLAG_INCLUDE_SHOULD`); HOCH nur bei `FLAG_HIGH_PROTECTION`; SEHR HOCH nur bei `FLAG_VERY_HIGH_PROTECTION`; Kapitel 8.x nur bei Prüfziel Prototyp, 9.x nur bei Datenschutz.
- **AK / Tests:** Unit-Tests gegen repräsentative C1-Zeilen (z. B. `1.2.2-H1` erscheint nur bei `FLAG_HIGH_PROTECTION`; `1.1.1-S1` nur bei `FLAG_INCLUDE_SHOULD`; `8.*` nur bei Prototyp). Geänderter Scope blendet nachgelagerte Schritte korrekt.
**Fachcontent:** `C1_Scoping-AL-Pruefziel.md`. **Abhängigkeit:** A1 (Shell), Contracts §2 (Status), §3 (Flags-Namen von Dev B/F4 — bis dahin gegen die im Contracts-Anhang fixierten Namen entwickeln).
---
## Definition of Done (jede Story)
`npx tsc --noEmit` → `npm run lint` → `npm run build` (Guard-Check) grün · neue Action in `check-module-guards.ts` · neue tenant-Modelle in `TENANT_MODELS`+RLS+Migration (RLS-DO-Block) · Browser-Verifikation · Demo-Seed lauffähig.
## Reihenfolge
A1-1 → F2 → A1-2 → A2-1 → A2-2. Danach (Folge-Paket): A3 (Validierung generalisieren), A5/A6/A7/A8.
---
## Contracts-Anhang (identisch in Paket A und B — an der Naht abstimmen)
**§1 Task-Objekt (Dev B besitzt das Modell, du konsumierst es):**
`Task { type: 'document_create'|'evidence_provide'|'technical'|'organizational'|'validation'|'policy_approval', owner, dueDate, priority: 'hoch'|'mittel'|'niedrig', status, resources: {tool,budget,personnel,time}, origin, links: {control?,risk?,document?,asset?} }`. Gates/Trigger erzeugen Aufgaben über die Action von Dev B.
**§2 Validierungsstatus (du besitzt das Enum):**
`enum ObjectReviewStatus { offen, in_bearbeitung, zur_validierung, validiert, zurueckgewiesen }` + `reviewComment`, `reviewerId`. Rolle `external_validator`. „Nur `validiert` zählt als bestätigt."
**§3 Flags (Dev B pflegt `variables.schema.json`, Namen sind fix):**
`FLAG_INCLUDE_SHOULD, FLAG_HIGH_PROTECTION, FLAG_VERY_HIGH_PROTECTION, FLAG_ELEVATED_PROTECTION, FLAG_PERSONAL_DATA, FLAG_PROTOTYPE_PROTECTION, FLAG_CLOUD_USED, FLAG_AI_USED, FLAG_OT_USED, FLAG_DEV_INHOUSE, FLAG_MOBILE_WORK, FLAG_MOBILE_DEVICES, FLAG_CRYPTO_PKI, FLAG_EXTERNAL_IT, FLAG_CUSTOMER_SYSTEMS, FLAG_ISB_EXTERNAL, FLAG_ISB_INTERNAL`.
**§4 Prüfziele:** `informationssicherheit | prototypenschutz | datenschutz`.
**§5 Step-Registry (du definierst, B konsumiert):** `registerStep({ key, title, order, guard, component })`; Schritt-Keys: `1 scoping · 2 context · 3 roles · 4 policies · 5 assets · 6 risks · 7 controls · 8 gap · 9 readiness`.
**§6 Migrations-Protokoll:** Vor dem Erzeugen einer Migration auf `dev` rebasen; Migrationen nie gleichzeitig ohne Absprache erstellen; je Branch genau eine Migration pro Story.
@@ -1,100 +0,0 @@
# Umsetzungspaket **Dev B** — Aufgaben, Regel-Engine, Fragebogen & Richtlinien-Import/Upload
> Claude-Code-Prompt für Entwickler B. Parallel zu **Paket Dev A**. Basis-Branch **`dev`**. Fachcontent: `C2_Fragenkatalog-Wirkung.md`, `C3_Vorlagen-Annotation.md`, `C0_README` (offene Punkte). Repo-Doku: `docs/HANDOVER-DEV.md`, `STAND-dev-branch.md`.
## Auftrag (Kurz)
Baue die **Regel-Engine**, den **Fragebogen**, die **Aufgaben-Erzeugung** und die **Richtlinien: Import bei Modul-Aktivierung + manueller Upload**. Du lieferst die Inhalte/Engines, die sich in die Wizard-Shell von Dev A einklinken.
## Branches (unter `dev` abzweigen, PR-Ziel `dev`)
- `dev/b1-tasks-erweiterung`
- `dev/b2-regel-engine`
- `dev/b3-fragebogen`
- `dev/b4-richtlinien-import-upload`
Häufig auf `dev` rebasen. Kleine PRs je Story.
## Datei-Hoheit (nur DU fasst diese an)
`src/server/actions/tasks.ts` (Task-Modell/Logik) · `src/lib/rules/**` · `seed/isms-vorlagenpaket-v2/variables.schema.json` · Fragebogen unter `src/app/(app)/onboarding/steps/context/**` und `.../policies/**` (in Dev-A-Registry eingeklinkt) · `prisma/import-policies.ts` · `src/app/(app)/policies/**` (Upload/Import-Einstieg) · `seed/isms-vorlagenpaket-v2/` (P01/D01/VA-20).
**Koordiniert (mit Dev A abstimmen):** `prisma/schema.prisma` (Migrations-Reihenfolge), `scripts/check-module-guards.ts`, Step-Registry-Schnittstelle (§5).
---
## Story F1 — Aufgaben-Objekt-Schema erweitern (`dev/b1-tasks-erweiterung`)
- Erweitere das bestehende `Task`/`TaskComment` (heute Typ `policy_approval`) um die Felder aus Contracts §1 (`type`, `owner`, `dueDate`, `priority`, `status`, `resources` JSON, `origin`, polymorphe `links`). Bestehender Freigabe-Flow bleibt lauffähig.
- Migration `tasks_wizard_fields`; `TENANT_MODELS`+RLS bleiben.
- **AK:** neue Felder vorhanden/typisiert; `policy_approval`-Flow unverändert grün.
## Story B1 — Auto-Generierung mit Bestätigung (`dev/b1-tasks-erweiterung`)
- Aus Triggern (Schritt 3/4/6/7/8 + Zurückweisung) **Aufgabenvorschlag** erzeugen (Vorschlag → Bearbeiter bestätigt/verwirft). Trigger-Set exakt aus **C2 §8** (z. B. „ISB nicht benannt" → `organizational`/Control 1.2.2; „externe IT-Dienstleister = ja" → `evidence_provide`/Control 6.1.1; „kein Restore-Test" → `technical`/BL-OPS-06/Control 5.2.9).
- Anzeige/Filter im Modul „Aufgaben" (`src/app/(app)/tasks/`), Ressourcenfelder editierbar.
- **AK:** Trigger erzeugt korrekt verknüpften Vorschlag; Bestätigung übernimmt; Verwerfen dokumentiert.
---
## Story F4 — Variablen/Flags erweitern (`dev/b2-regel-engine`)
- In `seed/isms-vorlagenpaket-v2/variables.schema.json` ergänzen: `FLAG_PROTOTYPE_PROTECTION`, `FLAG_ISB_EXTERNAL`, `FLAG_ISB_INTERNAL` + fehlende `ROLE_*`/`TECH_*` aus C2. Namen = Contracts §3 (fix, da Dev A darauf entwickelt).
- **AK:** `python3 seed/isms-vorlagenpaket-v2/_verify.py` → **OK**.
## Story F3 + B2 — Regel-/Mapping-Engine (`dev/b2-regel-engine`)
- **DSL** (Contracts §7) als Typen in `src/lib/rules/dsl.ts`; **Engine** in `src/lib/rules/engine.ts`: `Bedingung(Antwort|Scope|Flag) → Wirkung(Klausel ein/aus · Control relevant · Risiko/Asset-Bezug · Aufgabe)`.
- Koppelt an vorhandene `{{#if FLAG}}`-Render-Flags und die Lieferanten-Anforderungs-Engine (Muster).
- **Regeldaten** aus **C2 §5** (Q-FEAT-01…10) als Regelobjekte hinterlegen (z. B. `FLAG_CLOUD_USED` → R12-Klauseln + Controls 5.3.2/5.3.4/6.1.3 + Risiken R-CLOUD-*).
- **AK / Tests:** Unit-Tests, die für jede Q-FEAT-Antwort die erwarteten Wirkungen prüfen; Antwortänderung propagiert deterministisch.
## Story B3 — Fragebogen/Fakten (Schritt 2) (`dev/b3-fragebogen`)
- Dynamischer, **bedingter** Fragebogen mit Abschnitten **A–F aus C2** (Antworttypen + Anzeige-Bedingungen); Antworten als **wiederverwendbare Faktenobjekte** (Migration `wizard_facts`, RLS).
- Abschnitt **E** (Baseline) als **vorbelegte Defaults** aus `Technische-Sicherheits-Baseline.md` — nur bestätigen/anpassen, keine Ersteingabe.
- Zentrale Variablen bleiben serverseitig gesperrt (nur `/settings`, siehe `src/lib/policy-variables.ts`) — Fragebogen schreibt Fakten/Flags, nicht die gesperrten Variablen.
- Einklinken über Dev-A-Step-Registry (§5), Schritt-Key `context`.
- **AK:** Fragen A–F vorhanden; bedingte Sichtbarkeit über Flags; Antwort propagiert in abhängige Objekte (via B2); Baseline-Defaults vorbelegt.
---
## Story B4 — Richtlinien: Import bei Aktivierung + manueller Upload (`dev/b4-richtlinien-import-upload`)
**B4-1 Vorlagen-Import bei Modul-Aktivierung (M)** — *explizite Anforderung*
- Aktiviert der Superadmin das **Richtlinien-Modul** für einen Mandanten, wird das Vorlagenpaket **mandantenweit nicht-destruktiv importiert**: kapsle die bestehende Logik `prisma/import-policies.ts` (Diff/Upsert/`archivedAt`, `{dryRun}`) als **serverseitig aufrufbare Action/Job**. Zusätzlich Button „Vorlagen importieren/aktualisieren" in Admin **und** unter `/policies`.
- **AK:** Modul-Aktivierung triggert Import; idempotent; Status/Freigabe/Overrides/Variablenwerte bleiben erhalten; Änderungsreport sichtbar.
**B4-2 Manueller Upload eigener Richtlinien im Menü (M)** — *explizite Anforderung; Storage später*
- Unter `/policies` Einstieg „Eigene Richtlinie hochladen" mit **Pflicht-Control-Zuordnung** (Multi-Select aus Control-Katalog, optional Anforderungs-IDs) gemäß **C3 §4**.
- Datei-Persistenz über ein **gestubbtes Storage-Adapter-Interface** `src/server/storage/adapter.ts` (echtes Backend = Folge-Epic S1). Upload legt Metadaten + Block-Modell + Control-Mapping an; die Zuordnung fließt wie ein `<!-- REQ -->`-Anker in die Nachweislage (Schritt 7).
- **AK:** „Eigene Richtlinie hochladen" vorhanden mit Control-Zuordnung; Storage-Adapter gekapselt (später ohne UI-Änderung verdrahtbar); hochgeladenes Dokument erscheint in der Coverage/Nachweislage.
**B4-3 Proto/DS-Vorlagen P01/D01 + VA-20 + mapping.json (M)**
- Neue Vorlagen `P01_Prototypenschutz.md` (+ Verfahren `VA-20_Prototypen-Zutritt-und-Transport`) und `D01_Datenschutz.md` nach **C3 §2-Konvention** anlegen; `mapping.json`-Einträge im gleichen Schema für 8.x/9.x (IDs aus C1/C6-Dekomposition); Prototyp-Klauseln in `{{#if FLAG_PROTOTYPE_PROTECTION}}`.
- `_verify.py` um die neuen Verzeichnisse/Anker erweitern.
- **AK:** `python3 _verify.py` → **OK**; neue Anforderungen erscheinen im Scope, wenn Prüfziel Prototyp/Datenschutz aktiv (Dev-A-Filter).
**Fachcontent:** `C3` (Annotationskonvention + P01/D01), `C2` (Flag-Wirkung), `C0` (offene Punkte 1–3).
---
## Definition of Done (jede Story)
`npx tsc --noEmit` → `npm run lint` → `npm run build` (Guard-Check) grün · neue Action in `check-module-guards.ts` · neue tenant-Modelle in `TENANT_MODELS`+RLS+Migration (RLS-DO-Block) · bei Seed-Änderung `_verify.py` → **OK** · Browser-Verifikation.
## Reihenfolge
F1 → B1 → F4 → (F3+B2) → B3 → B4-1 → B4-2 → B4-3. Danach (Folge-Paket): A6-Risikokatalog-Anbindung (C4), B5 Umsetzungshinweise (C6), B6 Versionierung, B7 Export (C9).
---
## Contracts-Anhang (identisch in Paket A und B — an der Naht abstimmen)
**§1 Task-Objekt (du besitzt das Modell):**
`Task { type: 'document_create'|'evidence_provide'|'technical'|'organizational'|'validation'|'policy_approval', owner, dueDate, priority: 'hoch'|'mittel'|'niedrig', status, resources: {tool,budget,personnel,time}, origin, links: {control?,risk?,document?,asset?} }`. Dev A/Wizard-Gates rufen deine `createTask`-Action zum Erzeugen von Aufgaben.
**§2 Validierungsstatus (Dev A besitzt das Enum, du konsumierst):**
`enum ObjectReviewStatus { offen, in_bearbeitung, zur_validierung, validiert, zurueckgewiesen }` + `reviewComment`, `reviewerId`. Rolle `external_validator`.
**§3 Flags (du pflegst `variables.schema.json`, Namen sind fix):**
`FLAG_INCLUDE_SHOULD, FLAG_HIGH_PROTECTION, FLAG_VERY_HIGH_PROTECTION, FLAG_ELEVATED_PROTECTION, FLAG_PERSONAL_DATA, FLAG_PROTOTYPE_PROTECTION, FLAG_CLOUD_USED, FLAG_AI_USED, FLAG_OT_USED, FLAG_DEV_INHOUSE, FLAG_MOBILE_WORK, FLAG_MOBILE_DEVICES, FLAG_CRYPTO_PKI, FLAG_EXTERNAL_IT, FLAG_CUSTOMER_SYSTEMS, FLAG_ISB_EXTERNAL, FLAG_ISB_INTERNAL`.
**§4 Prüfziele:** `informationssicherheit | prototypenschutz | datenschutz`.
**§5 Step-Registry (Dev A definiert, du konsumierst):** `registerStep({ key, title, order, guard, component })`; du lieferst Komponenten für `context` (Schritt 2) und `policies` (Schritt 4).
**§6 Migrations-Protokoll:** Vor dem Erzeugen einer Migration auf `dev` rebasen; Migrationen nie gleichzeitig ohne Absprache erstellen; je Branch genau eine Migration pro Story.
---
## Gemeinsamer Kickoff (15 Min., beide Entwickler, vor Story-Start)
Contracts §1–§6 gemeinsam bestätigen (Feldnamen, Enum-Werte, Flag-Namen, Registry-Signatur, Migrations-Reihenfolge). Danach arbeiten beide Pakete konfliktarm parallel; einzige echte Nahtstellen sind `schema.prisma`, `check-module-guards.ts` und die Step-Registry — alle drei sind im Contracts-Anhang fixiert.
@@ -1,68 +0,0 @@
# Anweisung an den Berater — Fachcontent-Zulieferung (parallel zur Entwicklung)
Der Onboarding-Wizard hat seinen größten Engpass **nicht im Code, sondern im Fachcontent**. Die folgenden Arbeitspakete **C1–C9** laufen **parallel** zur Entwicklung und schalten jeweils eine oder mehrere Entwickler-Epics scharf. Bitte im angegebenen **Format** liefern und die **„muss vorliegen bis"**-Reihenfolge einhalten — sonst werden C2/C3/C4/C5/C6 zum kritischen Pfad.
> Bezug: Entwickler-Epics siehe `Wizard-Entwickler-Backlog.md`. Bestehendes Vorlagenpaket: `seed/isms-vorlagenpaket-v2/` (34 Dokumente, `mapping.json`, `_verify.py`).
---
## Übersicht (was schaltet was frei)
| WP | Inhalt | Schaltet frei | Format | Muss vorliegen bis |
|---|---|---|---|---|
| **C1** | AL2/AL3-Kennzeichnung + Prüfziel-Zuordnung je Control | A2 (Scoping/Filter) | Tabelle/CSV je Control | vor M2 |
| **C2** | Fragenkatalog + Antwort→Wirkung-Mapping | B2 Regel-Engine, B3 Fragebogen | strukturierte Tabelle | vor M1/M2 |
| **C3** | Regelfähige Vorlagen-Auszeichnung | B4 Richtlinien | Annotation im Vorlagenpaket | vor M2 |
| **C4** | VDA-/Standard-Risiko-Katalog + Standardmaßnahmen | A6 Risiko | Tabelle/CSV | vor M4 |
| **C5** | Reifegrad-Logik je Control | A7 Control-Assessment | Regeltabelle | vor M5 |
| **C6** | Umsetzungshinweise je Teilanforderung | B5 + A7 | strukturierter Text je Anforderung | fortlaufend, Kern vor M3 |
| **C7** | ISMS-Soll-Rollenmodell + Funktionstrennung | A4 Rollen | Regelliste + Vorlagen | vor M3 |
| **C8** | Priorisierungslogik Gap + Quick-Wins | A8 Gap | Regeltext | vor M6 |
| **C9** | Auswertungs-/Interpretationstexte + Export-Layout | B7 Readiness | Text + Layout-Skizze | vor M6 |
---
## Arbeitspakete im Detail
### C1 — Scoping-Grundlage (→ A2)
Je VDA-ISA-Control angeben: Zutreffen bei **AL2 / AL3** (SEHR HOCH), Zuordnung zu **Prüfzielen** (Info-Sicherheit / Prototypenschutz / Datenschutz), typische **Ausschluss-/Scope-Regeln**.
**Format:** eine Zeile je Control/Teilanforderung mit Spalten `Control-ID · Typ (MUSS/SOLL/HOCH/SEHR HOCH) · AL2? · AL3? · Prüfziel · Scope-Bedingung`. (Baut auf den vorhandenen Flags/Typen im `mapping.json` auf.)
### C2 — Fragenkatalog + Antwort→Wirkung-Mapping (→ B2, B3)
Der geführte Fragebogen (Schritt 2) **und** die Regel-Engine hängen hieran. Je Frage: Text, Antworttyp, **Bedingung** (wann anzeigen), und die **Wirkung** jeder Antwort: welche **Variable/Platzhalter** gefüllt wird, welche **Klausel** ein-/ausgeblendet wird, welche **Controls/Assets/Risiken** betroffen sind, ob eine **Aufgabe** entsteht.
**Format:** Tabelle `Frage-ID · Frage · Antwortoptionen · Anzeige-Bedingung · Wirkung(Variable / Klausel / Control / Risiko / Aufgabe)`. **Kritischer Pfad — bitte zuerst.**
### C3 — Regelfähige Vorlagen-Auszeichnung (→ B4)
Die 34 Vorlagen so **annotieren**, dass klar ist: welche **Klausel/Baustein** bei welcher **Antwort/Flag** erscheint bzw. entfällt, und welche **Controls** ein Dokument belegt.
**Format:** Auszeichnung direkt im Vorlagenpaket-Stil (bestehende `{{#if FLAG}}`-Mechanik + `mapping.json`-Control-Verknüpfung); danach `python3 _verify.py` → **OK**.
### C4 — Risiko-Katalog (→ A6)
Kuratierter **Standard- und VDA-geforderter Risiko-Katalog**: je Risiko Beschreibung, betroffene **Assets/Controls**, empfohlene **Standardmaßnahmen**, Default-Bewertungshinweis.
**Format:** Tabelle `Risiko-ID · Titel · Beschreibung · Controls · Asset-Typen · Standardmaßnahme(n) · Default-Einschätzung`.
### C5 — Reifegrad-Logik (→ A7)
Regeln, wie aus vorhandenen **Belegen** (Dokument/Risiko/Asset + Validierungsstatus) ein **Reifegrad-Vorschlag** je Control entsteht, und was der **Zielreifegrad** ist. Vorschlag ist regelbasiert, **Bestätigung durch Bearbeiter Pflicht**.
**Format:** Regeltabelle `Control · Bedingung(Belege) · Reifegrad-Vorschlag · Zielreifegrad · offene-Punkt-Kriterium`.
### C6 — Umsetzungshinweise (→ B5, A7)
Je **Teilanforderung** ein Hinweis mit: **organisatorischer** Umsetzungsoption, **technischer** Option, typischen **Nachweisen**, passender **Vorlage** (Verweis), **Ressourcenindikation** (Tool/Personal/Budget/Zeit). Filterbar nach AL2/AL3.
**Format:** strukturierter Block je Anforderungs-ID (Felder org/tech/Nachweise/Vorlage/Ressourcen). **Fortlaufend, Kern-Set vor M3.**
### C7 — ISMS-Soll-Rollenmodell + Funktionstrennung (→ A4)
Soll-Rollen (GF, ISB, DSB, IT-Verantwortung), **Funktionstrennungs-Regeln** (welche Kombination unzulässig), und die **Bestellungs-/Ernennungs-Vorlagen** (ISB-Bestellung etc.) mit Rollen-Platzhaltern.
**Format:** Regelliste + Vorlagen im Paket-Stil (Platzhalter).
### C8 — Priorisierungslogik Gap (→ A8)
Wie offene Punkte priorisiert werden (Muss-/AL3-kritisch = hoch), Dedup-Kriterien, Definition **Quick-Wins**.
**Format:** kurzer Regeltext + Beispiel-Priorisierung.
### C9 — Auswertung & Export (→ B7)
Interpretationstexte fürs Reifegrad-Dashboard, empfohlene nächste Schritte vor dem Assessment, und das **Layout des VDA-ISA-Katalog-Exports** (Reihenfolge, Felder, „bestätigt/unbestätigt").
**Format:** Textbausteine + Layout-Skizze.
---
## Prozess-Hinweise
- Zulieferung **iterativ** je Kapitel/Control-Gruppe möglich (nicht „alles auf einmal") — die Entwicklung kann teilbefüllt starten.
- **ISB-Freigabe** der neuen/angepassten Texte (VA/Richtlinien) ist ein eigener fachlicher Schritt (steht bereits offen auf `dev`).
- Alle Vorlagen-/Katalog-Änderungen nach dem Einspielen mit **`python3 seed/isms-vorlagenpaket-v2/_verify.py` → OK** gegenprüfen.
@@ -1,120 +0,0 @@
# Onboarding-Wizard — Machbarkeitsanalyse (Product Owner)
Bewertung des Berater-Fahrplans (`Onboarding_Wizard_Fahrplan_Detail.md`) gegen den dokumentierten Funktionsstand (`HANDOVER-PM.md`, Stand 2026-07-20). Ziel: Umsetzbarkeit, Wiederverwendung vorhandener Funktionen, neue Funktionen, Komplexitäten — als Grundlage für die anschließende Aufgaben-/Epic-Definition.
**Status-Legende:** ✅ Vorhanden (direkt nutzbar) · 🟡 Teilweise (erweitern) · 🟥 Neu (bauen)
**Aufwand:** S ≤ 1 Tag · M 2–4 Tage · L > 1 Woche (Entwicklung; Fachcontent separat)
---
## 1. Gesamturteil
**Der Wizard ist umsetzbar — und sitzt auf einem für dieses Vorhaben ungewöhnlich starken Fundament.** Rund zwei Drittel der fachlichen Bausteine existieren bereits produktiv (Assets/BIA, Risikoanalyse, Richtlinien-/Template-Engine mit Control-Mapping, importierter VDA-ISA-Katalog mit 316 Anforderungen/45 Controls, AL2/AL3-Schalter, Vier-Augen-Freigabe, Lieferanten-Reifegrad-/Gate-Logik, Coverage-Matrix, Audit-Log, Mandantenfähigkeit).
Der Wizard ist damit **weniger „neues Modul" als vielmehr eine geführte Orchestrierung vorhandener Module** plus einige neue Quer­schnitts-Engines. Der eigentliche Aufwand liegt — wie der Fahrplan selbst richtig betont — **nicht in der Software, sondern im Fachcontent** (regelfähige Vorlagen, VDA-Risiko-Katalog, Umsetzungshinweise, Reifegrad-Logik). Das ist der kritische Pfad und zugleich die Stelle, an der die GEFIM-Projekt-DNA (~100 Projekte) zum Tragen kommt.
**Drei Dinge müssen früh und sauber gebaut werden, sonst werden sie später teuer nachgezogen:** (1) ein generisches Aufgaben-Modul, (2) der generische Validierungs-Workflow, (3) der Regel-/Mapping-Layer. Sie sind die Klammer um alle Schritte.
---
## 2. Querschnittsmechaniken (Kap. 2 des Fahrplans)
| Mechanik | Status | Vorhanden (wiederverwendbar) | Neu zu bauen | Aufwand | Risiko |
|---|:--:|---|---|:--:|---|
| **2.1 Validierungs-Workflow** | 🟡 | Vier-Augen-Freigabe Richtlinien (Entwurf→In Freigabe→Freigegeben), Lieferanten-ISB-Reifegrad-Freigabe, RBAC, Audit-Log | **Generisches** Status-/Review-Modell über alle Objekttypen (Modul/Richtlinie/Risiko/Control-Bewertung), Rolle „externer Berater" als Validierer, Kommentare, „unbestätigt zählt nicht" in der Auswertung | M | Querschnitt — früh bauen, nicht anflanschen |
| **2.2 Umsetzungshinweise** | 🟡→🟥 | `implementation`-Feld je Anforderung im Katalog, Anwender-Handbuch (kuratiert, Deep-Links) | Strukturiertes Hinweis-Modell (org/tech/Nachweise/Vorlagen-Verweis/**Ressourcenindikation**), Scope-/Antwort-Filter, Inline-Panel | Backend S · FE S · **Content L** | Content-Flaschenhals (SME) |
| **2.3 Maßnahmen → Aufgaben-Modul** | 🟡 | Maßnahmen-Kanban (risikobezogen), berechnetes Restrisiko | **Generisches** Aufgaben-Objekt (Typ/Verantwortlich/Fälligkeit/Priorität/**Ressourcen**/Herkunft/Verknüpfung Control·Risiko·Dok·Asset), Auto-Vorschlag aus Triggern + Bestätigung | M–L | **Keystone** — alle Schritte hängen daran |
| **2.4 Audit-Trail & Versionierung** | 🟡 | Audit-Log vorhanden | Echte Versionierung (Richtlinien-Diff ist offen), Versionierung von Katalog/Templates/Risiko-Katalog + Instanz-Referenz + Update-Propagation | M–L | Verzahnt mit offener „Versionierung+Diff" & destruktivem Re-Import |
---
## 3. Fachliche Bausteine / Datenobjekte (Kap. 3)
| Baustein | Status | Vorhanden | Neu / Lücke | Aufwand |
|---|:--:|---|---|:--:|
| Control-Katalog VDA ISA | ✅ | Import 316 Anforderungen / 45 Controls, je Anforderung adressierbar, Typen MUSS/SOLL/HOHER/SEHR HOHER | Ggf. explizite AL2/AL3-Kennzeichnung je Anforderung sichtbar machen (Flags vorhanden) | S |
| Template-Bibliothek | ✅ | 28 Dokumente, Platzhalter/Variablen, optionale Klauseln via `{{#if}}`, Control-Verknüpfung, Freigabe | Regel-Tagging über Feature-Flags hinaus (Scope/Antwort-getrieben) | S–M |
| Upload eigener Dokumente | 🟡 | (geplant) Word-Upload als Block-Modell + Anhang, versioniert | Control-Zuordnung des Uploads in die Nachweislage; Upload selbst ist noch offen | M |
| Fakten-/Fragemodell | 🟡 | Variablen-Pflegestelle, Stammdaten (Settings) → ISMS-Variablen | **Dynamischer, bedingter Fragebogen** als wiederverwendbare Faktenobjekte | M |
| Risiko-Katalog | 🟡 | Bedrohungs-/Schwachstellen-Kataloge, Risikoregister, Control-Verknüpfung | Kuratierter **Standard-/VDA-Risiko-Katalog** (vordefiniert, erweiterbar) | Backend M · **Content L** |
| Regel-/Mapping-Layer | 🟡 | Handlebars-Flags, Variablen-Mapping, Lieferanten-Anforderungs-Engine (Schutzbedarf→Stufen) | **Generalisierung:** Antwort → betroffene Controls/Assets/Risiken (nicht nur Dokument-Klauseln) | L |
| Reifegrad-/Gap-Engine | 🟡 | Coverage-Matrix (Control↔Dokument), Lieferanten-Reifegrad-Freigabe, Gate→Risiko | Control-**Scoring aus Belegen** + Markierung offener Teilanforderungen | Backend M–L · SME M |
---
## 4. Die neun Schritte (Kap. 4)
| Schritt | Status | Trägt auf vorhandenem auf | Neu zu bauen | Aufwand |
|---|:--:|---|---|:--:|
| **1 Scoping** | 🟡 | AL2/AL3-Schalter (global + Override), TISAX-Level in Settings | Scope-Objekt (Prüfziele/Standorte/Geltungsbereich/Ausschlüsse) + **Filterung** der Control-/Template-/Risiko-Sets | FE S · Backend M |
| **2 Kontextaufnahme** | 🟡 | Stammdaten→Variablen | Geführter, **bedingter Fragebogen** + wiederverwendbare Faktenobjekte + Propagation | M |
| **3 Rollen & Verantwortlichkeiten** | 🟡 | RBAC/Rollen je Mandant, RACI-Matrix (Lieferanten) | ISMS-Rollenmodell (GF/ISB/DSB/IT), **Funktionstrennungs-Prüfung**, Rollen-Platzhalter in Dokumenten (ISB-Bestellung) | Backend M · FE S |
| **4 Richtlinien & VA** | ✅🟡 | Template-Tailoring (a) voll: variablenbasiert, Klausel-Ein/Ausblendung, Freigabe | (b) Upload+Control-Mapping (Upload offen); (c) „später erstellen" → Aufgabe (hängt an 2.3) | (a) ✅ · (b) M · (c) S |
| **5 Asset-Inventar** | ✅ | Assets & BIA: C/I/A, Schutzbedarf, Eigentümer, Vererbung, Abhängigkeiten, Risiko-Verknüpfung | „unvollständig/kein Eigentümer" → Aufgabe (Trigger) | S |
| **6 Risikomanagement** | ✅🟡 | 5×5-Heatmap, Register, Behandlung, Maßnahmen, Control-Verknüpfung | VDA-Risiko-Katalog-Auswahl; Maßnahme→Aufgabe (2.3); Methodik konfigurierbar (siehe Offen #2) | M |
| **7 Control-Zuordnung & Reifegrad** | 🟡→🟥 | Coverage-Matrix, Beleg-Verknüpfung, Lieferanten-Reifegrad-Muster | **Control-Assessment-Oberfläche** (SoA ist bisher Platzhalter) + Reifegrad-Selbsteinschätzung + Gap-Markierung je Teilanforderung | Backend L · FE L · SME L |
| **8 Gap- & Maßnahmenableitung** | 🟥 | Ergebnisse aus 6/7 | Aggregation/Dedup/Priorisierung → konsolidierte Gap-Liste (hängt an 2.3) | M |
| **9 Assessment-Readiness** | 🟥 | validierte Objekte, Coverage-Daten | Reifegrad-Dashboard + vorausgefüllte VDA-ISA-Katalogsicht + **Export** (koppelt an offenen DOCX/PDF-Export) | Backend M · FE L |
| **Wizard-Shell (implizit)** | 🟥 | — | Geführter Multi-Step-Flow: Zustand, Fortschritt, Gates, Wiederaufnahme, „bestätigt/unbestätigt"-Propagation | M–L |
---
## 5. Was wirklich neu gebaut werden muss (verdichtete Liste)
1. **Wizard-Shell / State-Machine** (Flow, Fortschritt, Gates, Resume). 🟥 M–L
2. **Generisches Aufgaben-Modul** (2.3) — Keystone. 🟡→ M–L
3. **Generischer Validierungs-Workflow** (2.1) inkl. externer-Berater-Rolle. 🟡→ M
4. **Regel-/Mapping-Layer generalisiert** (Antwort→Controls/Assets/Risiken). 🟡→ L
5. **Scope-Objekt + Filter-Engine** (Schritt 1). 🟥 M
6. **Dynamischer Fragebogen** (Schritt 2). 🟥 M
7. **ISMS-Rollenmodell + Funktionstrennung** (Schritt 3). 🟥 M
8. **Control-Assessment / Reifegrad-Gap-Engine** (Schritt 7, SoA-Surface). 🟥 L
9. **Gap-Konsolidierung** (Schritt 8). 🟥 M
10. **Assessment-Readiness-Dashboard + Export** (Schritt 9, koppelt an DOCX/PDF-Export). 🟥 L
11. **Umsetzungshinweis-Content-Modell + Inline-Panel** (2.2). 🟡→ Backend/FE S, Content L
12. **Versionierung/Diff + Update-Propagation** (2.4; löst zugleich offenen destruktiven Re-Import). 🟡→ M–L
**Direkt wiederverwendbar (kaum/kein Neubau):** Assets/BIA · Risikoanalyse (Kern) · Richtlinien-Template-Engine · Control-Katalog VDA-ISA · AL2/AL3-Schalter · Vier-Augen-Freigabe (als Muster) · Coverage-Matrix · Lieferanten-Anforderungs-/Reifegrad-Engine (als Muster) · Audit-Log · Multi-Tenant/RBAC · Stammdaten→Variablen.
---
## 6. Größte Komplexitäten & Risiken (priorisiert)
1. **Fachcontent ist der kritische Pfad** (bestätigt der Fahrplan selbst): regelfähige Vorlagen, VDA-Risiko-Katalog, **Umsetzungshinweise je Teilanforderung**, Reifegrad-Logik. Muss **parallel und früh** von der Beratung erstellt werden — sonst blockiert er M3/M4/M5/M7.
2. **Regel-/Mapping-Engine (Generalisierung).** Heute: Klausel-Flags + Variablen + Lieferanten-Engine. Neu: eine Antwort steuert Dokument-Klauseln **und** Control-Relevanz **und** Risiko-/Asset-Bezug. Architektur-Kernrisiko — Regel-Syntax und Test­barkeit früh festzurren.
3. **Control-Assessment/Reifegrad (Schritt 7).** Das „SoA & Controls"-Modul ist bisher nur Platzhalter; hier entsteht die eigentliche Assessment-Oberfläche. Größter einzelner FE/Backend-Block.
4. **Versionierung & Update-Propagation.** Solange Re-Import destruktiv ist und Richtlinien keine echte Versionierung haben, sind laufende Kundeninstanzen bei Katalog-/Template-Updates gefährdet. Muss vor „Katalog lebt beim Kunden" gelöst sein.
5. **Keystones Aufgaben-Modul & Validierungs-Workflow** früh, sonst teurer Umbau (Schritte 3–9 hängen daran).
6. **Export/North-Star** (vorausgefüllter VDA-ISA-Katalog) koppelt an den offenen DOCX/PDF-Export.
---
## 7. Antworten auf die offenen Entscheidungspunkte des Fahrplans (Kap. 7), PO-Sicht
1. **Upload-Gap-Check:** Start mit **manueller Control-Zuordnung** (deckt sich mit dem bereits geplanten Word-Upload); automatischer Inhalt↔Anforderung-Abgleich später als KI-Ausbaustufe (koppelt an geplanten „KI-Wizard").
2. **Risiko-Methodik:** **mandantenspezifisch konfigurierbar**, aber zuvor die zwei bestehenden Matrizen (5×5 Risikoanalyse / 4×4 FB-80-04 im Richtlinienmodul) **auf eine zentrale Skala vereinheitlichen** (steht bereits als offener Punkt).
3. **Versionierung/Propagation:** **nicht-destruktives Update mit Diff** und expliziter Übernahme je Instanz (löst zugleich den destruktiven Re-Import). Voraussetzung für „Katalog beim Kunden".
4. **Validierungs-Granularität:** **pro Objekt** (einzelne Richtlinie/Risiko/Control-Bewertung) **plus** Modul-Gate — die Freigabe-Logik ist heute schon objektbezogen (Richtlinien) und wird generalisiert.
5. **Aufgaben-Modul:** existiert **nur teilweise** (Maßnahmen-Kanban, risikobezogen) → **muss generalisiert werden**; die Task-Erzeugung ist mitzuplanen (Keystone).
6. **Reifegrad-Vorschlag:** **regelbasiert vorgeschlagen, mit Pflicht zur Bestätigung** durch den Bearbeiter (entspricht dem vorhandenen Lieferanten-Reifegrad-Freigabe-Muster).
---
## 8. Empfohlene Reihenfolge (für die Aufgaben-Definition)
Angelehnt an M1–M7 des Fahrplans, sortiert nach Abhängigkeit und Wiederverwendung:
1. **Querschnitt-Keystones zuerst:** Aufgaben-Modul (2.3), Validierungs-Workflow (2.1), Versionierung/Diff (2.4), Wizard-Shell. *(sonst später teurer Umbau)*
2. **Regel-/Scope-Fundament:** Scope-Objekt + Filter (Schritt 1), Regel-/Mapping-Layer, Fragebogen (Schritt 2).
3. **Wiederverwendung einklinken:** Richtlinien (Schritt 4, größtenteils vorhanden), Assets (Schritt 5, vorhanden), Risiko (Schritt 6, Kern vorhanden + VDA-Katalog).
4. **Neuer Kern:** Control-Assessment/Reifegrad (Schritt 7), Gap-Konsolidierung (Schritt 8).
5. **Output:** Assessment-Readiness-Dashboard + Export (Schritt 9).
6. **Durchgängig parallel:** Fachcontent (SME/Beratung) + Umsetzungshinweise (2.2).
---
## 9. Nächster Schritt
Auf dieser Basis die **Entwickler-Aufgaben als Epics/Stories** definieren — vorgeschlagene Epic-Schnitte:
**E1** Aufgaben-Modul · **E2** Validierungs-Workflow · **E3** Wizard-Shell & Navigation · **E4** Scope & Regel-/Mapping-Layer · **E5** Fragebogen/Fakten · **E6** ISMS-Rollen & Funktionstrennung · **E7** Richtlinien-Anbindung (Upload/Control-Mapping) · **E8** Risiko-Katalog & -Anbindung · **E9** Control-Assessment/Reifegrad-Gap · **E10** Gap-Konsolidierung · **E11** Assessment-Readiness/Export · **E12** Versionierung/Propagation · **E13** Umsetzungshinweise (Content+Panel) · **C1** Fachcontent (querlaufend).
> Offen für die Feinspezifikation: Regel-Syntax, Aufgaben-Objekt-Schema, Reifegrad-Scoring-Formel, Export-Format des VDA-ISA-Katalogs. Diese vier zuerst festzurren — sie determinieren den Rest.

Some files were not shown because too many files have changed in this diff Show More