Files
craftvia/docs/craftvia/ARCHITEKTUR.md
T
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

151 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
**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`).
## 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.