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

16 KiB
Raw Blame History

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