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

98 lines
16 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.
# 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 |