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