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

17 KiB
Raw Blame History

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:

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/.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.