From cadaedc6ccc7e4decea9186340fab7e6f15f1653 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 14 Sep 2026 18:19:19 +0200 Subject: [PATCH] =?UTF-8?q?L10b=20Betrieb=20&=20Aufr=C3=A4umen:=20Deploy?= =?UTF-8?q?=20=E2=80=93=20craftvia-worker,=20CI-Testjob,=20DEPLOY.md,=20Ce?= =?UTF-8?q?rtvia-Doku=20archiviert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .env.coolify.example | 54 ++- .env.example | 39 ++ .env.prod.example | 133 +++++-- .gitea/workflows/ci.yml | 66 +++- .github/workflows/ci.yml | 66 +++- Dockerfile | 6 +- README.md | 7 +- deploy/garage.toml | 2 +- docker-compose.coolify.prebuilt.yml | 108 +++++- docker-compose.coolify.yml | 109 +++++- docs/{ => _certvia-archiv}/DEPLOY-COOLIFY.md | 0 .../DEPLOY-PROD-CONTABO.md | 0 .../DEPLOY-PROD-PREBUILT.md | 0 .../DEVOPS-INTEGRATION-RUNBOOK.md | 0 .../FEINDESIGN-identity-mandanten.md | 0 docs/{ => _certvia-archiv}/HANDOVER-DEVOPS.md | 0 .../KONZEPT-backup-restore.md | 0 .../KONZEPT-backup-target.md | 0 .../KONZEPT-garage-migration.md | 0 .../{ => _certvia-archiv}/KONZEPT-haertung.md | 0 .../KONZEPT-identity-mandanten.md | 0 docs/{ => _certvia-archiv}/KONZEPT-ui-i18n.md | 0 docs/_certvia-archiv/README.md | 41 +++ docs/{ => _certvia-archiv}/SEC1-MAIL.md | 0 .../SEC2-AUTH-SELFSERVICE.md | 0 .../{ => _certvia-archiv}/SECRETS-REGISTER.md | 0 .../UEBERGABE-identity-mandanten.md | 0 ...Aufgabenpaket-Sicherheit-Administration.md | 0 .../sicherheit/README.md | 0 .../sicherheit/SEC1-Mail-Fundament-Detail.md | 0 .../SEC2-Auth-SelfService-Detail.md | 0 .../Sicherheit-und-Administration-Konzept.md | 0 docs/craftvia/DEPLOY.md | 335 ++++++++++++++++++ scripts/bootstrap-admin.ts | 2 +- scripts/build-and-push-images.sh | 4 +- scripts/garage-provision.ts | 2 +- scripts/test-auth-selfservice.ts | 2 +- scripts/test-garage-storage.ts | 2 +- 38 files changed, 902 insertions(+), 76 deletions(-) rename docs/{ => _certvia-archiv}/DEPLOY-COOLIFY.md (100%) rename docs/{ => _certvia-archiv}/DEPLOY-PROD-CONTABO.md (100%) rename docs/{ => _certvia-archiv}/DEPLOY-PROD-PREBUILT.md (100%) rename docs/{ => _certvia-archiv}/DEVOPS-INTEGRATION-RUNBOOK.md (100%) rename docs/{ => _certvia-archiv}/FEINDESIGN-identity-mandanten.md (100%) rename docs/{ => _certvia-archiv}/HANDOVER-DEVOPS.md (100%) rename docs/{ => _certvia-archiv}/KONZEPT-backup-restore.md (100%) rename docs/{ => _certvia-archiv}/KONZEPT-backup-target.md (100%) rename docs/{ => _certvia-archiv}/KONZEPT-garage-migration.md (100%) rename docs/{ => _certvia-archiv}/KONZEPT-haertung.md (100%) rename docs/{ => _certvia-archiv}/KONZEPT-identity-mandanten.md (100%) rename docs/{ => _certvia-archiv}/KONZEPT-ui-i18n.md (100%) create mode 100644 docs/_certvia-archiv/README.md rename docs/{ => _certvia-archiv}/SEC1-MAIL.md (100%) rename docs/{ => _certvia-archiv}/SEC2-AUTH-SELFSERVICE.md (100%) rename docs/{ => _certvia-archiv}/SECRETS-REGISTER.md (100%) rename docs/{ => _certvia-archiv}/UEBERGABE-identity-mandanten.md (100%) rename docs/{ => _certvia-archiv}/sicherheit/Aufgabenpaket-Sicherheit-Administration.md (100%) rename docs/{ => _certvia-archiv}/sicherheit/README.md (100%) rename docs/{ => _certvia-archiv}/sicherheit/SEC1-Mail-Fundament-Detail.md (100%) rename docs/{ => _certvia-archiv}/sicherheit/SEC2-Auth-SelfService-Detail.md (100%) rename docs/{ => _certvia-archiv}/sicherheit/Sicherheit-und-Administration-Konzept.md (100%) create mode 100644 docs/craftvia/DEPLOY.md diff --git a/.env.coolify.example b/.env.coolify.example index 0e401c1..48c5ea7 100644 --- a/.env.coolify.example +++ b/.env.coolify.example @@ -1,7 +1,8 @@ # Referenz für die Coolify-Environment-Variablen (Testserver, intern). -# ECHTE Secrets NUR in Coolify eintragen –€” diese Datei enthält nur Platzhalter. +# ECHTE Secrets NUR in Coolify eintragen – diese Datei enthält nur Platzhalter. # In Coolify: Ressource -> Environment Variables (Bulk-Paste möglich). # Hostnamen sind die Compose-Service-Namen (postgres/redis/garage), NICHT localhost. +# Betriebsdoku: docs/craftvia/DEPLOY.md # --- Datenbank (Service "postgres") --- POSTGRES_USER=craftvia @@ -9,6 +10,12 @@ POSTGRES_PASSWORD=CHANGE_ME_db_password POSTGRES_DB=craftvia DATABASE_URL=postgresql://craftvia:CHANGE_ME_db_password@postgres:5432/craftvia?schema=public +# --- Row Level Security (F-04) --- +# Auf dem Testserver zunächst false (Owner-Betrieb). Zum Scharfschalten craftvia_app mit +# LOGIN + Passwort versehen (ALTER ROLE craftvia_app WITH LOGIN PASSWORD '';), dann: +RLS_ENFORCED=false +# RLS_DATABASE_URL=postgresql://craftvia_app:CHANGE_ME_app_password@postgres:5432/craftvia?schema=public + # --- Redis (Service "redis") --- # F-18: Redis läuft mit requirepass. NUR REDIS_PASSWORD setzen — REDIS_URL wird in der # docker-compose.coolify.yml daraus abgeleitet (redis://:${REDIS_PASSWORD}@redis:6379) @@ -44,29 +51,60 @@ GARAGE_ADMIN_TOKEN=CHANGE_ME_openssl_rand_hex_32 # `backups` auf /app/.backups (app + backup-worker) — Pfad hier NICHT aendern, ausser # der Mount wird angepasst. BACKUP_LOCAL_DIR=/app/.backups +# BACKUP_ENC_KEY= (leer = AUTH_SECRET) -# --- Auth (NextAuth) --- +# --- Auth (Auth.js v5) --- # AUTH_SECRET: openssl rand -base64 32 # AUTH_URL: exakt die Coolify-Domain des app-Service (http:// für intern) AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32 # PASSWORD_PEPPER (Härtung §1): openssl rand -hex 32 — frisch je Umgebung, NICHT rotierbar, nie ins Artefakt. PASSWORD_PEPPER=CHANGE_ME_openssl_rand_hex_32 +# MFA_ENC_KEY= (leer = aus AUTH_SECRET abgeleitet; nach dem Setzen nicht mehr ändern) AUTH_URL=http://REPLACE-WITH-COOLIFY-SSLIP-DOMAIN # Hinter Reverse-Proxy (Coolify/Traefik) für Auth.js v5 zwingend, sonst UntrustedHost: AUTH_TRUST_HOST=true # --- Demo-Seed (NUR Testserver!) --- -# true => migrate-Job legt nach der Migration den Demo-Mandanten + Nutzer an +# true => migrate-Job legt nach der Migration die Demo-Mandanten + Nutzer an # (admin@demo.example / Demo1234!). In Produktion NICHT setzen / auf false lassen. RUN_DEMO_SEED=true -# --- KI-Provider (optional, aktuell ungenutzt) --- -AI_PROVIDER=anthropic -AI_API_KEY= - -# --- E-Mail (optional, im Test ungenutzt) --- +# --- E-Mail (optional; ohne SMTP bleiben Mails "pending") --- SMTP_HOST= SMTP_PORT=1025 +SMTP_SECURE= SMTP_USER= SMTP_PASSWORD= SMTP_FROM=craftvia@example.com +MAIL_FROM_NAME=Craftvia +MAIL_REPLY_TO= +# Basis für absolute Links in Mails/PDFs; leer = AUTH_URL. +APP_BASE_URL= + +# --- KI: Auftragsimport-Extraktion & Lotse (Anthropic, optional) --- +# Ohne ANTHROPIC_API_KEY: manuelle Erfassung, kein Lotse-Entwurf. +AI_EXTRACTION_PROVIDER=anthropic +ANTHROPIC_API_KEY= +ANTHROPIC_MODEL= + +# --- KI: Transkription (Whisper-kompatibel, optional) --- +TRANSCRIPTION_PROVIDER=openai-compatible +TRANSCRIPTION_API_URL=https://api.openai.com/v1/audio/transcriptions +TRANSCRIPTION_API_KEY= +TRANSCRIPTION_MODEL=whisper-1 + +# --- KI: Kostenbremse & Aufbewahrung KI-Protokoll --- +# Tokens je Mandant je Kalendermonat (ein+aus), 0 = unbegrenzt. +AI_MONTHLY_TOKEN_LIMIT=0 +# Ein-/Ausgaben im KI-Protokoll (AiGeneration) nach N Tagen leeren/pseudonymisieren. +AI_GENERATION_RETENTION_DAYS=180 + +# --- Craftvia: API-Rate-Limits (je Nutzer/Minute) --- +API_RATE_LIMIT_PER_MINUTE=300 +# /api/v1/sync, /api/v1/uploads, /api/v1/field/** +API_FIELD_RATE_LIMIT_PER_MINUTE=1200 + +# --- Craftvia: Offline/PWA & Malware-Scan --- +OFFLINE_MAX_DAYS=7 +CLAMAV_HOST= +CLAMAV_PORT=3310 diff --git a/.env.example b/.env.example index 56ffbf9..a81f2c4 100644 --- a/.env.example +++ b/.env.example @@ -101,5 +101,44 @@ TRANSCRIPTION_API_KEY= TRANSCRIPTION_MODEL=whisper-1 # --- Craftvia: optionaler Malware-Scan für Uploads (ClamAV clamd) --- +# Leer = nur Allowlist/Magic-Byte-Prüfung; gesetzt = zusätzlich clamd INSTREAM. CLAMAV_HOST= CLAMAV_PORT=3310 + +# --- Craftvia: Berichts-PDF (Worker, playwright-core + Chromium) --- +# Leer = Playwright-Chromium bzw. lokal installiertes Google Chrome (Entwicklerrechner). +# Im Docker-Worker-Image fest /usr/bin/chromium. +PDF_CHROMIUM_PATH= + +# --- Craftvia: Offline/PWA --- +# Ab wie vielen Tagen ein lokal gespeichertes Auftragsbundle als veraltet gilt (1–365). +OFFLINE_MAX_DAYS=7 + +# --- Craftvia: Rate Limits der REST-API (je Nutzer, Anfragen pro Minute) --- +# Allgemein für /api/v1/**. +API_RATE_LIMIT_PER_MINUTE=300 +# Einsatz-/Sync-Endpunkte (/api/v1/sync, /api/v1/uploads, /api/v1/field/**) – höher, weil +# die PWA nach Offline-Phasen Outbox und Fotos in Schüben nachsendet. +API_FIELD_RATE_LIMIT_PER_MINUTE=1200 + +# --- Craftvia: KI-Protokoll & Kostenbremse --- +# Nach N Tagen leert/pseudonymisiert ein Worker-Job Ein-/Ausgaben im KI-Protokoll +# (AiGeneration); Metadaten (Art, Modell, Tokens, Zeitpunkt) bleiben erhalten. +AI_GENERATION_RETENTION_DAYS=180 +# Tokens (ein + aus) je Mandant je Kalendermonat; darüber lehnen Lotse und +# Import-Extraktion ab. 0 = unbegrenzt. +AI_MONTHLY_TOKEN_LIMIT=0 + +# --- Optionale Fundament-Variablen (Default leer) --- +# MFA_ENC_KEY: Schlüssel für TOTP-Secrets at-rest (leer = aus AUTH_SECRET abgeleitet). +# ⚠ Nach dem Setzen nicht mehr ändern. +# MFA_ENC_KEY= +# BACKUP_ENC_KEY: Verschlüsselung der Backup-Artefakte (leer = AUTH_SECRET). +# BACKUP_ENC_KEY= +# WebAuthn/Passkeys: Origin und RP-ID (leer = aus AUTH_URL abgeleitet). +# WEBAUTHN_ORIGIN=http://localhost:3000 +# WEBAUTHN_RP_ID=localhost +# Demo-Seed-Passwort (Default Demo1234!). +# SEED_PASSWORD= +# RLS-Test hart statt Skip, wenn craftvia_app kein LOGIN hat (CI). +# RLS_TEST_REQUIRED=true diff --git a/.env.prod.example b/.env.prod.example index 7c60439..4d8b616 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -1,6 +1,7 @@ -# Referenz für die PRODUKTIV-Env-Variablen (Contabo-VPS + Coolify). -# ECHTE Secrets NUR in Coolify eintragen –€” diese Datei enthält nur Platzhalter. -# Unterschiede zum Testserver: HTTPS-AUTH_URL, KEIN Demo-Seed, stattdessen Bootstrap-Admin. +# Referenz für die PRODUKTIV-Env-Variablen (Coolify, docker-compose.coolify[.prebuilt].yml). +# ECHTE Secrets NUR in Coolify eintragen – diese Datei enthält nur Platzhalter. +# Unterschiede zum Testserver: HTTPS-AUTH_URL, KEIN Demo-Seed, stattdessen Bootstrap-Admin, +# RLS scharf. Betriebsdoku: docs/craftvia/DEPLOY.md # --- Datenbank (Service "postgres") --- POSTGRES_USER=craftvia @@ -9,68 +10,124 @@ POSTGRES_DB=craftvia DATABASE_URL=postgresql://craftvia:CHANGE_ME_starkes_db_passwort@postgres:5432/craftvia?schema=public # --- Row Level Security scharfschalten (F-04) --- -# RLS_ENFORCED=true –†’ die App verbindet sich als eingeschränkte Rolle craftvia_app -# (NOBYPASSRLS) und setzt app.tenant_id pro Transaktion; FORCE ROW LEVEL SECURITY -# macht die Policies dann scharf. Ist der Kontext nicht gesetzt, sieht craftvia_app -# NULL Zeilen –€” daher NUR mit korrekt gesetztem RLS_DATABASE_URL einschalten. +# RLS_ENFORCED=true – app und craftvia-worker verbinden sich als eingeschränkte Rolle +# craftvia_app (NOBYPASSRLS) und setzen app.tenant_id pro Transaktion; FORCE ROW LEVEL +# SECURITY macht die Policies dann scharf. Ist der Kontext nicht gesetzt, sieht craftvia_app +# NULL Zeilen – daher NUR mit korrekt gesetztem RLS_DATABASE_URL einschalten (sonst +# bricht die App beim Start bewusst ab). # WICHTIG: Die Owner-/Migrate-Rolle in DATABASE_URL MUSS BYPASSRLS/Superuser sein -# (Migrationen, Seed und der mandantenübergreifende Login-Lookup laufen darüber), -# sonst sähe der Login keine Nutzer. craftvia_app in Prod EINMALIG mit LOGIN + starkem -# Passwort versehen: ALTER ROLE craftvia_app WITH LOGIN PASSWORD ''; +# (Migrationen, Seed, Mail-/Backup-Worker und der mandantenübergreifende Login-Lookup +# laufen darüber). craftvia_app wird von der Baseline-Migration NOLOGIN angelegt und in +# Prod EINMALIG mit LOGIN + starkem Passwort versehen: +# ALTER ROLE craftvia_app WITH LOGIN PASSWORD ''; RLS_ENFORCED=true RLS_DATABASE_URL=postgresql://craftvia_app:CHANGE_ME_starkes_craftvia_app_passwort@postgres:5432/craftvia?schema=public -# --- Redis --- -# F-18: Redis läuft mit requirepass. REDIS_PASSWORD setzen (stark!) und identisch -# in die REDIS_URL einsetzen (redis://:@redis:6379). +# --- Redis (Service "redis") --- +# F-18: Redis läuft mit requirepass. NUR REDIS_PASSWORD setzen – REDIS_URL wird in den +# Coolify-Compose-Dateien daraus abgeleitet (redis://:${REDIS_PASSWORD}@redis:6379). REDIS_PASSWORD=CHANGE_ME_starkes_redis_passwort -REDIS_URL=redis://:CHANGE_ME_starkes_redis_passwort@redis:6379 -# --- Objektspeicher (MinIO) –€” S3_* muss zu MINIO_ROOT_* passen --- -S3_ENDPOINT=http://minio:9000 -S3_ACCESS_KEY=CHANGE_ME_GK_plus_24_hex -S3_SECRET_KEY=CHANGE_ME_starkes_minio_passwort +# --- Objektspeicher (Service "garage", S3-kompatibel) --- +# Bucket/Key legt der Init-Job "garage-provision" an (Admin-API). Format erzwungen: +# S3_ACCESS_KEY = "GK" + 24 Hex -> echo "GK$(openssl rand -hex 12)" +# S3_SECRET_KEY = 64 Hex -> openssl rand -hex 32 +S3_ENDPOINT=http://garage:3900 +S3_ACCESS_KEY=GK000000000000000000000000 +S3_SECRET_KEY=CHANGE_ME_openssl_rand_hex_32 S3_BUCKET=craftvia-documents -MINIO_ROOT_USER=craftvia -MINIO_ROOT_PASSWORD=CHANGE_ME_starkes_minio_passwort +S3_REGION=us-east-1 +# Garage-Daemon-Secrets (LITERAL in Coolify setzen, nicht via ${...}). +GARAGE_RPC_SECRET=CHANGE_ME_openssl_rand_hex_32 +GARAGE_ADMIN_TOKEN=CHANGE_ME_openssl_rand_hex_32 +# GARAGE_ZONE=dc1 +# GARAGE_CAPACITY_BYTES=100000000000 # --- Backup-Zielspeicher (optional) --- # Ziel der Backup-/DSGVO-Artefakte ist im Betreiber-Portal (/admin/backup) waehlbar # (Lokal/S3) und wird verschluesselt in der DB gehalten. Praezedenz: DB-Config → -# Env (S3_*/BACKUP_LOCAL_DIR) → lokaler Default. Sobald im Portal gespeichert, hat -# die DB-Config Vorrang. Fuer „Lokal" auf ein gemountetes, persistentes Volume zeigen. +# Env (S3_*/BACKUP_LOCAL_DIR) → lokaler Default. Fuer „Lokal" mountet die Compose-Datei +# das persistente Volume `backups` auf /app/.backups (app + backup-worker). BACKUP_LOCAL_DIR=/app/.backups +# Optionaler eigener Backup-Bucket (nur wenn der Backup-Store auf S3 laeuft). +# BACKUP_S3_BUCKET= +# Verschluesselung der Backup-Artefakte (AES-256-GCM); leer = AUTH_SECRET. Je Umgebung +# eigener Wert, alte Keys bis Retention-Ende aufbewahren. +BACKUP_ENC_KEY=CHANGE_ME_openssl_rand_hex_32 -# --- Auth (NextAuth) –€” Produktiv über HTTPS --- +# --- Auth (Auth.js v5) – Produktiv über HTTPS --- # AUTH_SECRET: openssl rand -base64 32 (frisch, NICHT der Testwert) AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32 # PASSWORD_PEPPER (Härtung §1): openssl rand -hex 32 — frisch je Umgebung, NICHT rotierbar, nie ins Artefakt. PASSWORD_PEPPER=CHANGE_ME_openssl_rand_hex_32 -AUTH_URL=https://app.craftvia.de -# AUTH_TRUST_HOST ist im Compose fest auf true (hinter dem Coolify-Proxy) –€” nicht nötig. +# MFA_ENC_KEY: TOTP-Secrets at-rest (leer = aus AUTH_SECRET). ⚠ Nach dem Setzen nicht mehr ändern. +MFA_ENC_KEY=CHANGE_ME_openssl_rand_hex_32 +AUTH_URL=https://app.craftvia.example +# AUTH_TRUST_HOST ist im Compose fest auf true (hinter dem Coolify-Proxy) – nicht nötig. +# Passkeys/WebAuthn: leer = aus AUTH_URL abgeleitet. +# WEBAUTHN_ORIGIN=https://app.craftvia.example +# WEBAUTHN_RP_ID=app.craftvia.example -# --- KI-Provider (optional) --- -AI_PROVIDER=anthropic -AI_API_KEY= - -# --- E-Mail (produktives SMTP-Relay, sobald Einladungs-/Mailflow aktiv) --- +# --- E-Mail (produktives SMTP-Relay; SPF/DKIM/DMARC der Absenderdomain vorher einrichten) --- SMTP_HOST= SMTP_PORT=587 +# true = implizites TLS (465), false = STARTTLS (587); leer = aus Port abgeleitet. +SMTP_SECURE= SMTP_USER= SMTP_PASSWORD= -SMTP_FROM=noreply@craftvia.de +SMTP_FROM=no-reply@craftvia.example +MAIL_FROM_NAME=Craftvia +MAIL_REPLY_TO= +# Basis für absolute Links in Mails/PDFs; leer = AUTH_URL. +APP_BASE_URL=https://app.craftvia.example + +# --- KI: Auftragsimport-Extraktion & Lotse (Anthropic) --- +# Ohne ANTHROPIC_API_KEY: graceful degradation (manuelle Erfassung, kein Lotse-Entwurf). +AI_EXTRACTION_PROVIDER=anthropic +ANTHROPIC_API_KEY= +# Leer = Default-Modell aus src/server/ai/client.ts +ANTHROPIC_MODEL= + +# --- KI: Transkription von Sprachnotizen (Whisper-kompatible API) --- +# Ohne TRANSCRIPTION_API_KEY bleibt die Transkription deaktiviert (Status "disabled"). +TRANSCRIPTION_PROVIDER=openai-compatible +TRANSCRIPTION_API_URL=https://api.openai.com/v1/audio/transcriptions +TRANSCRIPTION_API_KEY= +TRANSCRIPTION_MODEL=whisper-1 + +# --- KI: Kostenbremse & Aufbewahrung --- +# Tokens (ein + aus) je Mandant je Kalendermonat; darüber lehnen Lotse und +# Import-Extraktion ab. 0 = unbegrenzt. +AI_MONTHLY_TOKEN_LIMIT=0 +# Nach N Tagen leert/pseudonymisiert ein Worker-Job Ein-/Ausgaben im KI-Protokoll +# (AiGeneration). Frist mit dem DSB abstimmen. +AI_GENERATION_RETENTION_DAYS=180 + +# --- Craftvia: API-Rate-Limits (je Nutzer, Anfragen pro Minute) --- +API_RATE_LIMIT_PER_MINUTE=300 +# /api/v1/sync, /api/v1/uploads, /api/v1/field/** +API_FIELD_RATE_LIMIT_PER_MINUTE=1200 + +# --- Craftvia: Offline/PWA --- +OFFLINE_MAX_DAYS=7 + +# --- Craftvia: optionaler Malware-Scan (ClamAV clamd) --- +CLAMAV_HOST= +CLAMAV_PORT=3310 + +# PDF_CHROMIUM_PATH ist im Worker-Image/Compose fest /usr/bin/chromium – nicht setzen. # --- Demo-Seed: in PROD AUS lassen! --- RUN_DEMO_SEED=false -# --- Erst-Superadmin-Bootstrap (statt Demo-Seed) --- -# Beim ersten Deploy true setzen -> migrate-Job legt Admin + Mandant an (idempotent). -# Danach kann true bleiben (tut nichts, wenn der Admin existiert) oder auf false. +# --- Erst-Admin-Bootstrap (statt Demo-Seed) --- +# Beim ersten Deploy true setzen -> migrate-Job legt Plattform-Admin + ersten Mandanten an +# (idempotent). Danach auf false setzen oder stehen lassen (No-op, wenn vorhanden). BOOTSTRAP_ADMIN=true -BOOTSTRAP_ADMIN_EMAIL=admin@craftvia.de +BOOTSTRAP_ADMIN_EMAIL=admin@craftvia.example BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_initiales_admin_passwort BOOTSTRAP_ADMIN_NAME=Craftvia Admin -BOOTSTRAP_TENANT_NAME=Craftvia -BOOTSTRAP_TENANT_SLUG=craftvia -BOOTSTRAP_TENANT_SHORT=Craftvia +BOOTSTRAP_TENANT_NAME=Musterbetrieb GmbH +BOOTSTRAP_TENANT_SLUG=musterbetrieb +BOOTSTRAP_TENANT_SHORT=Musterbetrieb BOOTSTRAP_TENANT_SECTOR= diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 252e96d..011a3d6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -13,8 +13,50 @@ on: branches: ["main", "dev", "dev-*"] jobs: - build-and-check: + # Vollständiges Qualitäts-Gate (= `npm run gate`) gegen echte Infrastruktur: + # Postgres 16 mit pgvector + Redis als Service-Container. Garage/S3 wird bewusst + # weggelassen: der Storage-Adapter fällt ohne S3_* auf den Stub zurück, S3-abhängige + # Prüfungen (test-garage-storage, Byte-Abruf in test-einsatz-sync/test-berichte-pdf) + # überspringen sich. Ohne SMTP_HOST überspringt test-mail den echten Versand, ohne + # ANTHROPIC_API_KEY/TRANSCRIPTION_API_KEY die Live-KI-Tests. test-berichte-pdf + # überspringt sich, wenn im Runner-Image kein Chromium/Chrome startbar ist. + gate: runs-on: ubuntu-latest + timeout-minutes: 60 + services: + postgres: + image: pgvector/pgvector:0.8.0-pg16 + env: + POSTGRES_USER: craftvia + POSTGRES_PASSWORD: craftvia + POSTGRES_DB: craftvia + options: >- + --health-cmd "pg_isready -U craftvia" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + redis: + image: redis:7.4.2-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + # Nur CI-Dummywerte (keine echten Secrets). Gitea act_runner führt den Job in einem + # Container im selben Netz wie die Services aus → Hostnamen = Service-Namen + # (postgres/redis), NICHT localhost. Läuft der Runner im Host-Modus + # (Label ubuntu-latest:host), Hosts auf localhost umstellen und ports: ergänzen. + env: + DATABASE_URL: "postgresql://craftvia:craftvia@postgres:5432/craftvia?schema=public" + # RLS-Test (scripts/test-rls-enforcement.ts): RLS_ENFORCED bleibt aus, der Test schaltet + # selbst scharf. RLS_TEST_REQUIRED=true macht aus dem Skip einen harten Fehler. + RLS_DATABASE_URL: "postgresql://craftvia_app:craftvia_app_ci@postgres:5432/craftvia?schema=public" + RLS_TEST_REQUIRED: "true" + REDIS_URL: "redis://redis:6379" + AUTH_SECRET: "ci-dummy-auth-secret-0000000000000000" + AUTH_URL: "http://localhost:3000" + APP_BASE_URL: "http://localhost:3000" + PASSWORD_PEPPER: "0000000000000000000000000000000000000000000000000000000000000abc" steps: - name: Checkout uses: actions/checkout@v4 @@ -34,11 +76,20 @@ jobs: run: npm ci --include=optional --no-audit --no-fund - name: Prisma Client generieren - # Platzhalter-URL nur fürs Laden von prisma.config.ts — keine echte DB-Verbindung. - env: - DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public" run: npx prisma generate + - name: Migrationen anwenden + run: npx prisma migrate deploy + + - name: Demo-Seed + run: npx prisma db seed + + # Die Baseline-Migration legt craftvia_app NOLOGIN an; das Passwort ist ein Betriebs- + # Secret und wird nie migriert. Für den RLS-Test hier ein CI-Dummy-Passwort setzen + # (über die Prisma-Config-Datasource, damit kein psql-Client im Runner nötig ist). + - name: RLS-Rolle craftvia_app mit LOGIN versehen + run: echo "ALTER ROLE craftvia_app WITH LOGIN PASSWORD 'craftvia_app_ci';" | npx prisma db execute --stdin + - name: Typprüfung (tsc --noEmit) run: npx tsc --noEmit @@ -46,10 +97,11 @@ jobs: run: npm run lint - name: Build - env: - DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public" run: npm run build + - name: Tests (scripts/test-*.ts) + run: npm run test + audit: runs-on: ubuntu-latest steps: @@ -103,7 +155,7 @@ jobs: # Lockfile die @swc/helpers-Inkonsistenz trägt (siehe Dockerfile / Folgeänderung # der Dependency-Lane), bricht der Schritt mit ESBOMPROBLEMS ab — daher # continue-on-error. Alternative ohne npm-Baum-Validierung: Syft gegen das - # gebaute Image (siehe docs/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM). + # gebaute Image (siehe docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM). - name: SBOM erzeugen (CycloneDX) continue-on-error: true run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cyclonedx.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 644ec8f..11a983a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,51 @@ on: branches: ["main", "dev", "dev-*"] jobs: - build-and-check: + # Vollständiges Qualitäts-Gate (= `npm run gate`) gegen echte Infrastruktur: + # Postgres 16 mit pgvector + Redis als Service-Container. Garage/S3 wird bewusst + # weggelassen: der Storage-Adapter fällt ohne S3_* auf den Stub zurück, S3-abhängige + # Prüfungen (test-garage-storage, Byte-Abruf in test-einsatz-sync/test-berichte-pdf) + # überspringen sich. Ohne SMTP_HOST überspringt test-mail den echten Versand, ohne + # ANTHROPIC_API_KEY/TRANSCRIPTION_API_KEY die Live-KI-Tests. test-berichte-pdf nutzt + # Google Chrome des Runners (channel "chrome") oder überspringt sich ohne Browser. + gate: runs-on: ubuntu-latest + timeout-minutes: 60 + services: + postgres: + image: pgvector/pgvector:0.8.0-pg16 + env: + POSTGRES_USER: craftvia + POSTGRES_PASSWORD: craftvia + POSTGRES_DB: craftvia + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U craftvia" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + redis: + image: redis:7.4.2-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + # Nur CI-Dummywerte (keine echten Secrets). GitHub-Hosted-Runner: Service-Ports auf localhost. + env: + DATABASE_URL: "postgresql://craftvia:craftvia@localhost:5432/craftvia?schema=public" + # RLS-Test (scripts/test-rls-enforcement.ts): RLS_ENFORCED bleibt aus, der Test schaltet + # selbst scharf. RLS_TEST_REQUIRED=true macht aus dem Skip einen harten Fehler. + RLS_DATABASE_URL: "postgresql://craftvia_app:craftvia_app_ci@localhost:5432/craftvia?schema=public" + RLS_TEST_REQUIRED: "true" + REDIS_URL: "redis://localhost:6379" + AUTH_SECRET: "ci-dummy-auth-secret-0000000000000000" + AUTH_URL: "http://localhost:3000" + APP_BASE_URL: "http://localhost:3000" + PASSWORD_PEPPER: "0000000000000000000000000000000000000000000000000000000000000abc" steps: - name: Checkout uses: actions/checkout@v4 @@ -34,10 +77,20 @@ jobs: run: npm ci --include=optional --no-audit --no-fund - name: Prisma Client generieren - env: - DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public" run: npx prisma generate + - name: Migrationen anwenden + run: npx prisma migrate deploy + + - name: Demo-Seed + run: npx prisma db seed + + # Die Baseline-Migration legt craftvia_app NOLOGIN an; das Passwort ist ein Betriebs- + # Secret und wird nie migriert. Für den RLS-Test hier ein CI-Dummy-Passwort setzen + # (über die Prisma-Config-Datasource, damit kein psql-Client nötig ist). + - name: RLS-Rolle craftvia_app mit LOGIN versehen + run: echo "ALTER ROLE craftvia_app WITH LOGIN PASSWORD 'craftvia_app_ci';" | npx prisma db execute --stdin + - name: Typprüfung (tsc --noEmit) run: npx tsc --noEmit @@ -45,10 +98,11 @@ jobs: run: npm run lint - name: Build - env: - DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public" run: npm run build + - name: Tests (scripts/test-*.ts) + run: npm run test + audit: runs-on: ubuntu-latest steps: @@ -100,7 +154,7 @@ jobs: # Lockfile die @swc/helpers-Inkonsistenz trägt (siehe Dockerfile / Folgeänderung # der Dependency-Lane), bricht der Schritt mit ESBOMPROBLEMS ab — daher # continue-on-error. Alternative ohne npm-Baum-Validierung: Syft gegen das - # gebaute Image (siehe docs/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM). + # gebaute Image (siehe docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md, Abschnitt SBOM). - name: SBOM erzeugen (CycloneDX) continue-on-error: true run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cyclonedx.json diff --git a/Dockerfile b/Dockerfile index 1c50886..1f87b0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -123,7 +123,11 @@ COPY src ./src COPY messages ./messages ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public" RUN npx prisma generate +# Chromium legt beim Start ein Profil-/Crashpad-Verzeichnis unter $HOME an. /app gehört root +# → als User "app" bricht der Start mit "Failed to create headless user data directory" ab. +# Daher eigenes, beschreibbares Home-Verzeichnis für den non-root-User. RUN groupadd --system --gid 1001 app \ - && useradd --system --uid 1001 --gid app --home-dir /app app + && useradd --system --uid 1001 --gid app --home-dir /home/app --create-home app +ENV HOME=/home/app USER app CMD ["npx", "tsx", "scripts/craftvia-worker.ts"] diff --git a/README.md b/README.md index 1135299..62384ea 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ Login: `admin@demo.example` / `Demo1234!` (weitere Demo-Nutzer siehe AGENTS.md). ## Betrieb -Container-Build über das Multi-Stage-`Dockerfile` (Targets `runner`, `migrate`, `garage`), +Container-Build über das Multi-Stage-`Dockerfile` (Targets `runner`, `migrate`, `garage`, `worker`), Deployment mit `docker-compose.coolify.yml` bzw. `docker-compose.coolify.prebuilt.yml`. -Hinweise: `docs/DEPLOY-COOLIFY.md`, `docs/SECRETS-REGISTER.md` (aus dem Fundament übernommen, -Namen teils noch Certvia). +Hinweise: [`docs/craftvia/DEPLOY.md`](docs/craftvia/DEPLOY.md) (Betrieb, Secrets, Worker, RLS, Backup), +[`docs/craftvia/API.md`](docs/craftvia/API.md) (`/api/v1`). Übernommene Certvia-Dokumente liegen in +`docs/_certvia-archiv/`. diff --git a/deploy/garage.toml b/deploy/garage.toml index e82e47d..6db8e9f 100644 --- a/deploy/garage.toml +++ b/deploy/garage.toml @@ -1,5 +1,5 @@ # Garage-Objektspeicher — Basiskonfiguration (Single-Node pro Environment). -# Gehört zur MinIO→Garage-Migration, siehe docs/KONZEPT-garage-migration.md (§5). +# Gehört zur MinIO→Garage-Migration, siehe docs/_certvia-archiv/KONZEPT-garage-migration.md (§5). # # WICHTIG — KEINE Secrets in dieser Datei (sie ist im Repo eingecheckt): # rpc_secret ← wird zur Laufzeit aus GARAGE_RPC_SECRET gelesen diff --git a/docker-compose.coolify.prebuilt.yml b/docker-compose.coolify.prebuilt.yml index b402054..c2f7a71 100644 --- a/docker-compose.coolify.prebuilt.yml +++ b/docker-compose.coolify.prebuilt.yml @@ -8,7 +8,9 @@ # - Konfiguration über Coolify-Env-Variablen statt env_file: .env # - Service "migrate": Init-Job (prisma migrate deploy + Rollen-Rechte-Sync), läuft einmalig VOR app # - kein mailhog (Dev); Service "worker" = SEC1 Mail-Worker (BullMQ/Redis-Queue) -# In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen. +# - Service "craftvia-worker" = Craftvia-Job-Worker (Image craftvia-worker, Stage "worker") +# In Coolify als "Docker Compose Location" -> docker-compose.coolify.prebuilt.yml setzen. +# Betriebsdoku: docs/craftvia/DEPLOY.md # # Härtung (F-11/F-18): # - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt. @@ -97,13 +99,36 @@ services: # wählt bzw. als Env-Fallback. Muss auf das gemountete `backups`-Volume zeigen, # sonst sind Sicherungen beim Redeploy flüchtig. DB-Config hat Vorrang vor dieser Var. BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups} - AI_PROVIDER: ${AI_PROVIDER} - AI_API_KEY: ${AI_API_KEY} SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT} + SMTP_SECURE: ${SMTP_SECURE:-} SMTP_USER: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} SMTP_FROM: ${SMTP_FROM} + MAIL_FROM_NAME: ${MAIL_FROM_NAME:-} + MAIL_REPLY_TO: ${MAIL_REPLY_TO:-} + # Basis für absolute Links (Mails, PDFs); leer = Fallback AUTH_URL. + APP_BASE_URL: ${APP_BASE_URL:-} + # Craftvia-KI (ARCHITEKTUR §4.5). Ohne ANTHROPIC_API_KEY bzw. TRANSCRIPTION_API_KEY + # graceful degradation (Status "disabled", manuelle Eingabe). + AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-} + TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible} + TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-} + TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-} + TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-} + # Tokenbudget je Mandant je Kalendermonat (ein+aus); 0 = unbegrenzt. + AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0} + # Optionaler Malware-Scan der Uploads (clamd INSTREAM); leer = nur Typ-/Magic-Byte-Prüfung. + CLAMAV_HOST: ${CLAMAV_HOST:-} + CLAMAV_PORT: ${CLAMAV_PORT:-3310} + # PWA: ab wie vielen Tagen ein Offline-Bundle als veraltet gilt (1–365, Default 7). + OFFLINE_MAX_DAYS: ${OFFLINE_MAX_DAYS:-7} + # Rate Limits je Nutzer/Minute: /api/v1/** allgemein bzw. Einsatz-/Sync-Endpunkte + # (/api/v1/sync, /api/v1/uploads, /api/v1/field/**). + API_RATE_LIMIT_PER_MINUTE: ${API_RATE_LIMIT_PER_MINUTE:-300} + API_FIELD_RATE_LIMIT_PER_MINUTE: ${API_FIELD_RATE_LIMIT_PER_MINUTE:-1200} # Persistenter lokaler Backup-Zielspeicher (überlebt Redeploys). volumes: - backups:/app/.backups @@ -238,6 +263,81 @@ services: condition: service_completed_successfully restart: unless-stopped + # Craftvia-Job-Worker (ARCHITEKTUR §4.4, scripts/craftvia-worker.ts): je BullMQ-Queue ein + # Worker für import-extraction, transcription, report-pdf, image-derivatives. + # OHNE diesen Dienst bleiben Import-Extraktion, Transkription, Berichts-PDFs und + # Bild-Derivate in der Queue liegen (die App reiht bei gesetztem REDIS_URL nur ein). + # Image craftvia-worker = Dockerfile-Stage "worker" (tsx + src + Prisma-Client + + # Debian-Chromium + Schriften) — HTML→PDF läuft NUR hier, nie in app. + # Processors greifen über dbForTenant zu → bei RLS_ENFORCED=true wie app über + # RLS_DATABASE_URL (Rolle craftvia_app). Egress (default-Netz) für Anthropic-/ + # Transkriptions-API und SMTP. Kein Port, kein Traefik. + craftvia-worker: + image: ${REGISTRY:-registry.example.com/craftvia}/craftvia-worker:${IMAGE_TAG:-main} + command: ["npx", "tsx", "scripts/craftvia-worker.ts"] + environment: + DATABASE_URL: ${DATABASE_URL} + RLS_ENFORCED: ${RLS_ENFORCED:-false} + RLS_DATABASE_URL: ${RLS_DATABASE_URL} + REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 + AUTH_SECRET: ${AUTH_SECRET} + MFA_ENC_KEY: ${MFA_ENC_KEY:-} + AUTH_URL: ${AUTH_URL} + APP_BASE_URL: ${APP_BASE_URL:-} + # Objektspeicher (Garage): Import-PDFs/Sprachnotizen lesen, PDFs/Derivate schreiben. + S3_ENDPOINT: ${S3_ENDPOINT} + S3_ACCESS_KEY: ${S3_ACCESS_KEY} + S3_SECRET_KEY: ${S3_SECRET_KEY} + S3_BUCKET: ${S3_BUCKET} + S3_REGION: ${S3_REGION:-us-east-1} + # KI-Provider (siehe app). Ohne Key: Jobs enden mit Status "disabled". + AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-} + TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible} + TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-} + TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-} + TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-} + AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0} + # Aufbewahrung KI-Protokoll (AiGeneration): Ein-/Ausgaben älter als N Tage leeren. + AI_GENERATION_RETENTION_DAYS: ${AI_GENERATION_RETENTION_DAYS:-180} + # Chromium aus dem Debian-Paket (im Image bereits gesetzt, hier explizit). + PDF_CHROMIUM_PATH: /usr/bin/chromium + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + SMTP_SECURE: ${SMTP_SECURE:-} + SMTP_USER: ${SMTP_USER} + SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_FROM: ${SMTP_FROM} + MAIL_FROM_NAME: ${MAIL_FROM_NAME:-} + MAIL_REPLY_TO: ${MAIL_REPLY_TO:-} + # Chromium nutzt /dev/shm für Renderer-Speicher; render.ts setzt zusätzlich + # --disable-dev-shm-usage, 1 GB schützt dennoch vor Abstürzen bei Fotoberichten. + shm_size: "1gb" + networks: + - backend + - default + security_opt: + - "no-new-privileges:true" + cap_drop: + - ALL + deploy: + resources: + limits: + cpus: "1.0" + memory: 1536M + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + garage: + condition: service_healthy + garage-provision: + condition: service_completed_successfully + migrate: + condition: service_completed_successfully + restart: unless-stopped postgres: image: pgvector/pgvector:0.8.0-pg16 @@ -297,7 +397,7 @@ services: restart: unless-stopped # Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service - # (MinIO Community EOL/Maintenance-Mode). Konzept: docs/KONZEPT-garage-migration.md. + # (MinIO Community EOL/Maintenance-Mode). Konzept: docs/_certvia-archiv/KONZEPT-garage-migration.md. # Buckets/Keys werden NICHT über die S3-API angelegt, sondern vom Init-Job # "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) — # rein clusterintern, wie minio zuvor. Version gepinnt (kein latest). diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index 819984e..364c8f0 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -4,7 +4,10 @@ # - Konfiguration über Coolify-Env-Variablen statt env_file: .env # - Service "migrate": Init-Job (prisma migrate deploy + Rollen-Rechte-Sync), läuft einmalig VOR app # - kein mailhog (Dev); Service "worker" = SEC1 Mail-Worker (BullMQ/Redis-Queue) +# - Service "craftvia-worker" = Craftvia-Job-Worker (Import-Extraktion, Transkription, +# Berichts-PDF mit Chromium, Bild-Derivate) aus der Dockerfile-Stage "worker" # In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen. +# Betriebsdoku: docs/craftvia/DEPLOY.md # # Härtung (F-11/F-18): # - F-11: migrate nutzt die schlanke "migrate"-Stage (kein Next-Build), Images gepinnt. @@ -97,13 +100,36 @@ services: # wählt bzw. als Env-Fallback. Muss auf das gemountete `backups`-Volume zeigen, # sonst sind Sicherungen beim Redeploy flüchtig. DB-Config hat Vorrang vor dieser Var. BACKUP_LOCAL_DIR: ${BACKUP_LOCAL_DIR:-/app/.backups} - AI_PROVIDER: ${AI_PROVIDER} - AI_API_KEY: ${AI_API_KEY} SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT} + SMTP_SECURE: ${SMTP_SECURE:-} SMTP_USER: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} SMTP_FROM: ${SMTP_FROM} + MAIL_FROM_NAME: ${MAIL_FROM_NAME:-} + MAIL_REPLY_TO: ${MAIL_REPLY_TO:-} + # Basis für absolute Links (Mails, PDFs); leer = Fallback AUTH_URL. + APP_BASE_URL: ${APP_BASE_URL:-} + # Craftvia-KI (ARCHITEKTUR §4.5). Ohne ANTHROPIC_API_KEY bzw. TRANSCRIPTION_API_KEY + # graceful degradation (Status "disabled", manuelle Eingabe). + AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-} + TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible} + TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-} + TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-} + TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-} + # Tokenbudget je Mandant je Kalendermonat (ein+aus); 0 = unbegrenzt. + AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0} + # Optionaler Malware-Scan der Uploads (clamd INSTREAM); leer = nur Typ-/Magic-Byte-Prüfung. + CLAMAV_HOST: ${CLAMAV_HOST:-} + CLAMAV_PORT: ${CLAMAV_PORT:-3310} + # PWA: ab wie vielen Tagen ein Offline-Bundle als veraltet gilt (1–365, Default 7). + OFFLINE_MAX_DAYS: ${OFFLINE_MAX_DAYS:-7} + # Rate Limits je Nutzer/Minute: /api/v1/** allgemein bzw. Einsatz-/Sync-Endpunkte + # (/api/v1/sync, /api/v1/uploads, /api/v1/field/**). + API_RATE_LIMIT_PER_MINUTE: ${API_RATE_LIMIT_PER_MINUTE:-300} + API_FIELD_RATE_LIMIT_PER_MINUTE: ${API_FIELD_RATE_LIMIT_PER_MINUTE:-1200} # Persistenter lokaler Backup-Zielspeicher (überlebt Redeploys). volumes: - backups:/app/.backups @@ -242,6 +268,83 @@ services: condition: service_completed_successfully restart: unless-stopped + # Craftvia-Job-Worker (ARCHITEKTUR §4.4, scripts/craftvia-worker.ts): je BullMQ-Queue ein + # Worker für import-extraction, transcription, report-pdf, image-derivatives. + # OHNE diesen Dienst bleiben Import-Extraktion, Transkription, Berichts-PDFs und + # Bild-Derivate in der Queue liegen (die App reiht bei gesetztem REDIS_URL nur ein). + # Eigene Dockerfile-Stage "worker": tsx + src + Prisma-Client + Debian-Chromium und + # Schriften (fonts-dejavu-core, fonts-liberation) — HTML→PDF läuft NUR hier, nie in app. + # Processors greifen über dbForTenant zu → bei RLS_ENFORCED=true wie app über + # RLS_DATABASE_URL (Rolle craftvia_app). Egress (default-Netz) für Anthropic-/ + # Transkriptions-API und SMTP. Kein Port, kein Traefik. + craftvia-worker: + build: + context: . + target: worker + command: ["npx", "tsx", "scripts/craftvia-worker.ts"] + environment: + DATABASE_URL: ${DATABASE_URL} + RLS_ENFORCED: ${RLS_ENFORCED:-false} + RLS_DATABASE_URL: ${RLS_DATABASE_URL} + REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 + AUTH_SECRET: ${AUTH_SECRET} + MFA_ENC_KEY: ${MFA_ENC_KEY:-} + AUTH_URL: ${AUTH_URL} + APP_BASE_URL: ${APP_BASE_URL:-} + # Objektspeicher (Garage): Import-PDFs/Sprachnotizen lesen, PDFs/Derivate schreiben. + S3_ENDPOINT: ${S3_ENDPOINT} + S3_ACCESS_KEY: ${S3_ACCESS_KEY} + S3_SECRET_KEY: ${S3_SECRET_KEY} + S3_BUCKET: ${S3_BUCKET} + S3_REGION: ${S3_REGION:-us-east-1} + # KI-Provider (siehe app). Ohne Key: Jobs enden mit Status "disabled". + AI_EXTRACTION_PROVIDER: ${AI_EXTRACTION_PROVIDER:-anthropic} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-} + TRANSCRIPTION_PROVIDER: ${TRANSCRIPTION_PROVIDER:-openai-compatible} + TRANSCRIPTION_API_URL: ${TRANSCRIPTION_API_URL:-} + TRANSCRIPTION_API_KEY: ${TRANSCRIPTION_API_KEY:-} + TRANSCRIPTION_MODEL: ${TRANSCRIPTION_MODEL:-} + AI_MONTHLY_TOKEN_LIMIT: ${AI_MONTHLY_TOKEN_LIMIT:-0} + # Aufbewahrung KI-Protokoll (AiGeneration): Ein-/Ausgaben älter als N Tage leeren. + AI_GENERATION_RETENTION_DAYS: ${AI_GENERATION_RETENTION_DAYS:-180} + # Chromium aus dem Debian-Paket (im Image bereits gesetzt, hier explizit). + PDF_CHROMIUM_PATH: /usr/bin/chromium + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + SMTP_SECURE: ${SMTP_SECURE:-} + SMTP_USER: ${SMTP_USER} + SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_FROM: ${SMTP_FROM} + MAIL_FROM_NAME: ${MAIL_FROM_NAME:-} + MAIL_REPLY_TO: ${MAIL_REPLY_TO:-} + # Chromium nutzt /dev/shm für Renderer-Speicher; render.ts setzt zusätzlich + # --disable-dev-shm-usage, 1 GB schützt dennoch vor Abstürzen bei Fotoberichten. + shm_size: "1gb" + networks: + - backend + - default + security_opt: + - "no-new-privileges:true" + cap_drop: + - ALL + deploy: + resources: + limits: + cpus: "1.0" + memory: 1536M + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + garage: + condition: service_healthy + garage-provision: + condition: service_completed_successfully + migrate: + condition: service_completed_successfully + restart: unless-stopped postgres: image: pgvector/pgvector:0.8.0-pg16 @@ -301,7 +404,7 @@ services: restart: unless-stopped # Objektspeicher: Garage (S3-kompatibel) — ersetzt den früheren minio-Service - # (MinIO Community EOL/Maintenance-Mode). Konzept: docs/KONZEPT-garage-migration.md. + # (MinIO Community EOL/Maintenance-Mode). Konzept: docs/_certvia-archiv/KONZEPT-garage-migration.md. # Buckets/Keys werden NICHT über die S3-API angelegt, sondern vom Init-Job # "garage-provision" (Admin-API). Nichts nach außen (kein Traefik/ports:) — # rein clusterintern, wie minio zuvor. Version gepinnt (kein latest). diff --git a/docs/DEPLOY-COOLIFY.md b/docs/_certvia-archiv/DEPLOY-COOLIFY.md similarity index 100% rename from docs/DEPLOY-COOLIFY.md rename to docs/_certvia-archiv/DEPLOY-COOLIFY.md diff --git a/docs/DEPLOY-PROD-CONTABO.md b/docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md similarity index 100% rename from docs/DEPLOY-PROD-CONTABO.md rename to docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md diff --git a/docs/DEPLOY-PROD-PREBUILT.md b/docs/_certvia-archiv/DEPLOY-PROD-PREBUILT.md similarity index 100% rename from docs/DEPLOY-PROD-PREBUILT.md rename to docs/_certvia-archiv/DEPLOY-PROD-PREBUILT.md diff --git a/docs/DEVOPS-INTEGRATION-RUNBOOK.md b/docs/_certvia-archiv/DEVOPS-INTEGRATION-RUNBOOK.md similarity index 100% rename from docs/DEVOPS-INTEGRATION-RUNBOOK.md rename to docs/_certvia-archiv/DEVOPS-INTEGRATION-RUNBOOK.md diff --git a/docs/FEINDESIGN-identity-mandanten.md b/docs/_certvia-archiv/FEINDESIGN-identity-mandanten.md similarity index 100% rename from docs/FEINDESIGN-identity-mandanten.md rename to docs/_certvia-archiv/FEINDESIGN-identity-mandanten.md diff --git a/docs/HANDOVER-DEVOPS.md b/docs/_certvia-archiv/HANDOVER-DEVOPS.md similarity index 100% rename from docs/HANDOVER-DEVOPS.md rename to docs/_certvia-archiv/HANDOVER-DEVOPS.md diff --git a/docs/KONZEPT-backup-restore.md b/docs/_certvia-archiv/KONZEPT-backup-restore.md similarity index 100% rename from docs/KONZEPT-backup-restore.md rename to docs/_certvia-archiv/KONZEPT-backup-restore.md diff --git a/docs/KONZEPT-backup-target.md b/docs/_certvia-archiv/KONZEPT-backup-target.md similarity index 100% rename from docs/KONZEPT-backup-target.md rename to docs/_certvia-archiv/KONZEPT-backup-target.md diff --git a/docs/KONZEPT-garage-migration.md b/docs/_certvia-archiv/KONZEPT-garage-migration.md similarity index 100% rename from docs/KONZEPT-garage-migration.md rename to docs/_certvia-archiv/KONZEPT-garage-migration.md diff --git a/docs/KONZEPT-haertung.md b/docs/_certvia-archiv/KONZEPT-haertung.md similarity index 100% rename from docs/KONZEPT-haertung.md rename to docs/_certvia-archiv/KONZEPT-haertung.md diff --git a/docs/KONZEPT-identity-mandanten.md b/docs/_certvia-archiv/KONZEPT-identity-mandanten.md similarity index 100% rename from docs/KONZEPT-identity-mandanten.md rename to docs/_certvia-archiv/KONZEPT-identity-mandanten.md diff --git a/docs/KONZEPT-ui-i18n.md b/docs/_certvia-archiv/KONZEPT-ui-i18n.md similarity index 100% rename from docs/KONZEPT-ui-i18n.md rename to docs/_certvia-archiv/KONZEPT-ui-i18n.md diff --git a/docs/_certvia-archiv/README.md b/docs/_certvia-archiv/README.md new file mode 100644 index 0000000..0abccfb --- /dev/null +++ b/docs/_certvia-archiv/README.md @@ -0,0 +1,41 @@ +# Certvia-Archiv + +Craftvia ist aus dem Fundament des ISMS-Produkts **Certvia** hervorgegangen (Auth.js mit +Identity/Mitgliedschaften, RLS, Mail-/Backup-Worker, Garage, Härtung). Dieser Ordner hält +die dabei übernommenen Dokumente **unverändert** vor. Sie sind **nicht maßgeblich** für Craftvia. + +## Warum archiviert + +- Produkt-, Domain- und Personenbezug auf Certvia/ISMS (`app.certvia.de`, Gitea-/Coolify-Hosts, + Rollen ISB/DSB, Vorfall-Mail-Eingang, Risiko-Backfill), veraltete Namen (`isms_app`, + `isms-documents`, MinIO). +- Konzepte und Umsetzungs-Prompts sind umgesetzt. Der Ist-Stand steht im Code und in der + Craftvia-Doku. +- Der betriebsrelevante Inhalt (Coolify-Deploy, Prebuilt-Images, RLS-Aktivierung, Secrets, + Backup/Restore, Garage) ist in **[docs/craftvia/DEPLOY.md](../craftvia/DEPLOY.md)** + zusammengeführt und auf Craftvia umgeschrieben. + +Maßgeblich sind [AGENTS.md](../../AGENTS.md), [docs/craftvia/SPEC-CRAFTVIA.md](../craftvia/SPEC-CRAFTVIA.md), +[docs/craftvia/ARCHITEKTUR.md](../craftvia/ARCHITEKTUR.md) und [docs/craftvia/DEPLOY.md](../craftvia/DEPLOY.md). + +## Inhalt + +| Datei | Thema | Noch als Hintergrund nützlich für | +|---|---|---| +| `DEPLOY-COOLIFY.md` | Testserver via Coolify (Certvia) | – (ersetzt durch DEPLOY.md) | +| `DEPLOY-PROD-CONTABO.md` | Prod-VPS, LUKS, pgBackRest/age/restic, PITR, Vorfall-Mail-Eingang | Host-Encryption- und PITR-Details | +| `DEPLOY-PROD-PREBUILT.md` | Prebuilt-Images über die Registry | – (ersetzt durch DEPLOY.md) | +| `HANDOVER-DEVOPS.md` | frühe DevOps-Übergabe (Stand Juli 2026) | – | +| `DEVOPS-INTEGRATION-RUNBOOK.md` | Branch-Integration im Certvia-Team | – | +| `SECRETS-REGISTER.md` | Secrets-Register (Certvia) | Rotationsregeln (in DEPLOY.md übernommen) | +| `KONZEPT-backup-restore.md` | Backup-/Restore-/DSGVO-Engine | Designbegründung von `src/server/backup/**` | +| `KONZEPT-backup-target.md` | konfigurierbarer Backup-Zielspeicher | Designbegründung `/admin/backup` | +| `KONZEPT-garage-migration.md` | MinIO → Garage | Designbegründung Garage/`garage-provision` | +| `KONZEPT-haertung.md` | Pepper, Host-Encryption, Secrets | Designbegründung `PASSWORD_PEPPER` | +| `KONZEPT-identity-mandanten.md`, `FEINDESIGN-identity-mandanten.md`, `UEBERGABE-identity-mandanten.md` | zentrale Identity + Mandanten-Mitgliedschaften | Designbegründung Two-Step-Login/Mandantenwechsel | +| `KONZEPT-ui-i18n.md` | Betreiber-Konsole-UX, i18n | – | +| `SEC1-MAIL.md`, `SEC2-AUTH-SELFSERVICE.md` | Mail-Fundament, Passwort-Self-Service | Hintergrund zu `src/server/mail/**`, `scripts/test-mail.ts`, `scripts/test-auth-selfservice.ts` | +| `sicherheit/` | PO-Konzept und Claude-Code-Prompts SEC1–SEC6 (Certvia) | – | + +Die Querverweise **innerhalb** dieser Dokumente (`docs/…`) zeigen noch auf die alten Pfade. +Sie werden bewusst nicht nachgezogen. diff --git a/docs/SEC1-MAIL.md b/docs/_certvia-archiv/SEC1-MAIL.md similarity index 100% rename from docs/SEC1-MAIL.md rename to docs/_certvia-archiv/SEC1-MAIL.md diff --git a/docs/SEC2-AUTH-SELFSERVICE.md b/docs/_certvia-archiv/SEC2-AUTH-SELFSERVICE.md similarity index 100% rename from docs/SEC2-AUTH-SELFSERVICE.md rename to docs/_certvia-archiv/SEC2-AUTH-SELFSERVICE.md diff --git a/docs/SECRETS-REGISTER.md b/docs/_certvia-archiv/SECRETS-REGISTER.md similarity index 100% rename from docs/SECRETS-REGISTER.md rename to docs/_certvia-archiv/SECRETS-REGISTER.md diff --git a/docs/UEBERGABE-identity-mandanten.md b/docs/_certvia-archiv/UEBERGABE-identity-mandanten.md similarity index 100% rename from docs/UEBERGABE-identity-mandanten.md rename to docs/_certvia-archiv/UEBERGABE-identity-mandanten.md diff --git a/docs/sicherheit/Aufgabenpaket-Sicherheit-Administration.md b/docs/_certvia-archiv/sicherheit/Aufgabenpaket-Sicherheit-Administration.md similarity index 100% rename from docs/sicherheit/Aufgabenpaket-Sicherheit-Administration.md rename to docs/_certvia-archiv/sicherheit/Aufgabenpaket-Sicherheit-Administration.md diff --git a/docs/sicherheit/README.md b/docs/_certvia-archiv/sicherheit/README.md similarity index 100% rename from docs/sicherheit/README.md rename to docs/_certvia-archiv/sicherheit/README.md diff --git a/docs/sicherheit/SEC1-Mail-Fundament-Detail.md b/docs/_certvia-archiv/sicherheit/SEC1-Mail-Fundament-Detail.md similarity index 100% rename from docs/sicherheit/SEC1-Mail-Fundament-Detail.md rename to docs/_certvia-archiv/sicherheit/SEC1-Mail-Fundament-Detail.md diff --git a/docs/sicherheit/SEC2-Auth-SelfService-Detail.md b/docs/_certvia-archiv/sicherheit/SEC2-Auth-SelfService-Detail.md similarity index 100% rename from docs/sicherheit/SEC2-Auth-SelfService-Detail.md rename to docs/_certvia-archiv/sicherheit/SEC2-Auth-SelfService-Detail.md diff --git a/docs/sicherheit/Sicherheit-und-Administration-Konzept.md b/docs/_certvia-archiv/sicherheit/Sicherheit-und-Administration-Konzept.md similarity index 100% rename from docs/sicherheit/Sicherheit-und-Administration-Konzept.md rename to docs/_certvia-archiv/sicherheit/Sicherheit-und-Administration-Konzept.md diff --git a/docs/craftvia/DEPLOY.md b/docs/craftvia/DEPLOY.md new file mode 100644 index 0000000..90fa1d3 --- /dev/null +++ b/docs/craftvia/DEPLOY.md @@ -0,0 +1,335 @@ +# Craftvia – Betrieb & Deployment + +> Maßgeblich für Test- und Produktivbetrieb. Abgeleitet aus den Fundament-Runbooks (archiviert +> unter `docs/_certvia-archiv/`) und auf Craftvia umgeschrieben. Domains in diesem Dokument sind +> Platzhalter (`app.craftvia.example`). +> Deploy-Dateien: `docker-compose.coolify.yml` (Build auf dem Host) bzw. +> `docker-compose.coolify.prebuilt.yml` (fertige Images aus der Registry). +> Env-Referenzen: `.env.coolify.example` (Testserver), `.env.prod.example` (Produktion), +> `.env.example` (lokal). + +## 1. Architekturüberblick + +``` + Internet ──► Coolify-Proxy (Traefik, TLS) ──► app:3000 + │ + ┌─────────────── Netz "backend" (internal: true, kein Egress) ─┼──────────────────────────┐ + │ postgres (pgvector/pg16, RLS) redis (requirepass) garage (S3 :3900, Admin :3903) │ + │ ▲ ▲ ▲ ▲ ▲ ▲ ▲ │ + │ migrate app craftvia-worker worker backup-worker garage-provision │ + └──────────────────────────────────────────────────────────────────────────────────────────┘ + app, craftvia-worker, worker, backup-worker zusätzlich im Netz "default" (Egress/Proxy) +``` + +| Dienst | Dockerfile-Target / Image | Aufgabe | Lebensdauer | +|---|---|---|---| +| `migrate` | `migrate` / `craftvia-migrate` | `prisma migrate deploy` → Rollen-Rechte-Sync (`scripts/sync-role-permissions.ts`) → optional Demo-Seed (`RUN_DEMO_SEED`) bzw. Erst-Admin (`BOOTSTRAP_ADMIN`) | Init-Job, `restart: "no"` | +| `app` | `runner` / `craftvia-app` | Next.js standalone (Backoffice, PWA `/m`, REST `/api/v1/**`, Betreiber-Portal `/admin`) | dauerhaft, Healthcheck auf `/` | +| `craftvia-worker` | `worker` / `craftvia-worker` | BullMQ-Queues `import-extraction`, `transcription`, `report-pdf`, `image-derivatives` (`scripts/craftvia-worker.ts`); enthält Chromium + Schriften | dauerhaft | +| `worker` | `migrate` / `craftvia-migrate` | Mail-Worker (`scripts/mail-worker.ts`): Zustellung mit Retry/DLQ, täglicher Erinnerungslauf | dauerhaft | +| `backup-worker` | `migrate` / `craftvia-migrate` | Queue `backup-ops` (`scripts/backup-worker.ts`): Mandanten-Export/-Restore, DSGVO-Export, seriell | dauerhaft | +| `postgres` | `pgvector/pgvector:0.8.0-pg16` | Primärdatenbank, RLS-Policies `tenant_isolation` | Volume `pgdata` | +| `redis` | `redis:7.4.2-alpine` | Queues (BullMQ) | Volume `redisdata` | +| `garage` | `garage` / `craftvia-garage` | Objektspeicher (Dokumente, Fotos, PDFs, Backups), Config eingebacken aus `deploy/garage.toml` | Volumes `garage_meta`, `garage_data` | +| `garage-provision` | `migrate` / `craftvia-migrate` | Layout, Bucket, Access-Key, Rechte über die Admin-API (idempotent) | Init-Job | + +Startreihenfolge: `postgres` (healthy) → `migrate`; `garage` (healthy) → `garage-provision`; danach +`app`, `craftvia-worker`, `worker`, `backup-worker`. + +**Ohne laufende Worker** reiht die App bei gesetztem `REDIS_URL` Jobs nur ein: Import-Extraktion, +Transkription, Berichts-PDFs und Bild-Derivate bleiben dann liegen (`craftvia-worker`), Mails bleiben +`pending` (`worker`), Restore/Export bleiben `queued` (`backup-worker`). Ohne Redis laufen die +Craftvia-Processors inline in der App. Das ist nur für Dev/Demo gedacht: der PDF-Processor ist absichtlich nicht im +App-Bundle, Freigaben bleiben gültig, „PDF erzeugen" auf `/reports/[id]` stößt den Job erneut an. + +**Härtung (alle Dienste):** `no-new-privileges`, `cap_drop: ALL` (gezielte `cap_add` nur für die +Entrypoints von postgres/redis), CPU-/RAM-Limits, gepinnte Image-Tags, non-root-User `app` (UID +1001) in allen Node-Images, Redis mit Passwort. + +## 2. Domains, Proxy, TLS + +- In Coolify beim Service **`app`** die Domain setzen, z. B. `https://app.craftvia.example:3000` + (Port 3000 = Container-Port, Schema `https`). Coolify/Traefik stellt das Let's-Encrypt-Zertifikat aus. + Kein Host-Port wird exponiert. `garage`, `postgres`, `redis` und die Worker erhalten **keine** + Domain. +- `AUTH_URL` = exakt die öffentliche App-URL (`https://app.craftvia.example`), `AUTH_TRUST_HOST=true` + (im Compose Default). `APP_BASE_URL` für absolute Links in Mails/PDFs (leer = `AUTH_URL`). +- Passkeys: `WEBAUTHN_ORIGIN`/`WEBAUTHN_RP_ID` nur setzen, wenn sie von `AUTH_URL` abweichen. +- Uploads bis 25 MB laufen über den Proxy (`experimental.proxyClientMaxBodySize = 26mb`). Vorgelagerte + Proxies dürfen kein niedrigeres Body-Limit haben. +- DNS: A/AAAA-Record `app.craftvia.example` → Server-IP; Firewall nur 80/443 + SSH. + +## 3. Ersteinrichtung (Coolify) + +1. **Ressource:** Git-Repo (Deploy-Key, nur lesend), Build Pack **Docker Compose**, Compose Location + `docker-compose.coolify.yml` (oder `…prebuilt.yml`, siehe §9). +2. **Environment-Variablen** aus `.env.prod.example` bzw. `.env.coolify.example` eintragen (§4). Secrets + **literal** setzen, nicht über `${…}` referenzieren (Coolify-Interpolation). +3. **Persistent Storage prüfen:** `pgdata`, `garage_meta`, `garage_data`, `redisdata`, `backups`. +4. **Erster Deploy Produktion:** `RUN_DEMO_SEED=false`, `BOOTSTRAP_ADMIN=true` + `BOOTSTRAP_ADMIN_*` + + `BOOTSTRAP_TENANT_*`. Der `migrate`-Job legt über `scripts/bootstrap-admin.ts` den ersten Mandanten mit Mandanten-Admin + (`/login`) **und** einen Plattform-Admin (`/platform/login`, MFA-Einrichtung beim ersten Login) + an. Das Skript ist idempotent und überschreibt kein Passwort. Log im `migrate`-Container prüfen, danach Passwort ändern und + `BOOTSTRAP_ADMIN=false`. +5. **Testserver:** `RUN_DEMO_SEED=true` legt die Demo-Mandanten an (Logins siehe `AGENTS.md`). Nie in Produktion. +6. **RLS scharfschalten** (§6), **Smoke** (§10). + +## 4. Konfiguration & Secrets + +### 4.1 Variablen + +| Variable | Dienste | Pflicht | Bedeutung | +|---|---|---|---| +| `POSTGRES_USER/PASSWORD/DB`, `DATABASE_URL` | postgres, alle Node-Dienste | ja | Owner-Verbindung (Superuser/BYPASSRLS), Host `postgres` | +| `RLS_ENFORCED`, `RLS_DATABASE_URL` | app, craftvia-worker | Prod ja | scharfe RLS über Rolle `craftvia_app` (§6) | +| `REDIS_PASSWORD` | redis + alle Queue-Nutzer | ja | `REDIS_URL` wird im Compose daraus gebildet, nicht separat setzen | +| `S3_ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET/REGION` | app, craftvia-worker, backup-worker, garage-provision | ja | Garage: `http://garage:3900`, Key `GK`+24 Hex, Secret 64 Hex, Region `us-east-1`. Ohne S3 speichert der Adapter nur Metadaten (Stub), in Prod also unbrauchbar | +| `GARAGE_RPC_SECRET`, `GARAGE_ADMIN_TOKEN` | garage, garage-provision | ja | je `openssl rand -hex 32` | +| `AUTH_SECRET` | app, Worker | ja | ≥ 32 Zeichen, sonst Fail-Secure-Abbruch (`src/server/env.ts`) | +| `PASSWORD_PEPPER` | app, migrate, backup-worker | ja | 64 Hex, **nicht rotierbar** | +| `MFA_ENC_KEY` | app, craftvia-worker | empfohlen | TOTP-Secrets at-rest (leer = aus `AUTH_SECRET`), nach dem Setzen nicht ändern | +| `BACKUP_ENC_KEY` | backup-worker | empfohlen | AES-256-GCM der Backup-Artefakte (leer = `AUTH_SECRET`) | +| `AUTH_URL`, `APP_BASE_URL` | app, Worker | ja | öffentliche URL | +| `SMTP_HOST/PORT/SECURE/USER/PASSWORD/FROM`, `MAIL_FROM_NAME`, `MAIL_REPLY_TO` | app, worker, craftvia-worker | für Mailversand | ohne vollständige Konfiguration bleiben Mails `pending` mit Begründung | +| `AI_EXTRACTION_PROVIDER`, `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` | app, craftvia-worker | optional | §7 | +| `TRANSCRIPTION_PROVIDER/API_URL/API_KEY/MODEL` | app, craftvia-worker | optional | §7 | +| `AI_MONTHLY_TOKEN_LIMIT` | app, craftvia-worker | optional | §7.2, Default 0 = unbegrenzt | +| `AI_GENERATION_RETENTION_DAYS` | craftvia-worker | optional | §7.3, Default 180 | +| `API_RATE_LIMIT_PER_MINUTE`, `API_FIELD_RATE_LIMIT_PER_MINUTE` | app | optional | §8, Default 300 / 1200 | +| `OFFLINE_MAX_DAYS` | app | optional | Offline-Bundle gilt nach N Tagen als veraltet (1–365, Default 7) | +| `CLAMAV_HOST`, `CLAMAV_PORT` | app | optional | zusätzlicher Malware-Scan per clamd INSTREAM (sonst Allowlist + Magic Bytes) | +| `PDF_CHROMIUM_PATH` | craftvia-worker | – | im Image/Compose fest `/usr/bin/chromium` | +| `BACKUP_LOCAL_DIR`, `BACKUP_S3_BUCKET` | app, backup-worker, garage-provision | optional | §11 | +| `RUN_DEMO_SEED`, `BOOTSTRAP_ADMIN*`, `BOOTSTRAP_TENANT_*` | migrate | – | §3 | + +### 4.2 Secrets-Register (Grundregeln) + +Werte liegen ausschließlich im Passwortmanager (je Umgebung eigener Ordner) plus versiegelter +Offline-Kopie und werden nur als Coolify-Env verteilt, **nie** im Repo, Image oder Backup-Bucket. +Test, Staging und Prod haben unterschiedliche Werte. + +| Secret | Rotierbar? | Folge einer Rotation | +|---|---|---| +| `AUTH_SECRET` | ja | alle Sessions ungültig, Nutzer loggen neu ein | +| `PASSWORD_PEPPER` | **nein** | erzwungener Passwort-Reset aller Konten | +| `MFA_ENC_KEY` | **nein** | alle Nutzer müssen MFA neu einrichten | +| `BACKUP_ENC_KEY` | bedingt | gilt nur für neue Artefakte, Altschlüssel bis Retention-Ende aufbewahren | +| `craftvia_app`-Passwort (`RLS_DATABASE_URL`) | ja | `ALTER ROLE … PASSWORD`, danach Env setzen und app + craftvia-worker neu starten | +| `POSTGRES_PASSWORD`, `REDIS_PASSWORD` | ja | koordiniert mit allen Diensten neu deployen | +| `GARAGE_*`, `S3_ACCESS_KEY/SECRET_KEY` | ja | neuen Key provisionieren (`garage-provision`), Env tauschen, alten Key entfernen | +| `ANTHROPIC_API_KEY`, `TRANSCRIPTION_API_KEY`, `SMTP_PASSWORD` | ja | Env tauschen, Dienste neu starten | + +## 5. Migrationen + +- Der `migrate`-Job führt bei **jedem** Deploy `npx prisma migrate deploy` aus. Das ist idempotent: nur neue + Migrationen werden angewandt. app und Worker starten erst nach erfolgreichem Abschluss + (`service_completed_successfully`). Der App-Container migriert nie selbst. +- Danach läuft `scripts/sync-role-permissions.ts`: additiv, zieht neu eingeführte Rechte für bestehende + Mandanten nach. Betroffene Nutzer sehen neue Rechte nach erneutem Login (JWT). +- Regeln für neue Migrationen (RLS für Tenant-Tabellen usw.): [MIGRATIONS.md](MIGRATIONS.md). +- Manuell (Coolify-Terminal des `migrate`-Containers oder `docker exec`): + `npx prisma migrate status` / `npx prisma migrate deploy`. +- **Vor** Migrationen mit Datenumbau: Cluster-Backup ziehen (§11.1). + +## 6. Row Level Security aktivieren + +Die Baseline-Migration legt die Rolle `craftvia_app` **NOLOGIN NOBYPASSRLS** an, vergibt die +Tabellenrechte und aktiviert je Tenant-Tabelle `ENABLE` + `FORCE ROW LEVEL SECURITY` mit Policy +`tenant_isolation` (`USING` + `WITH CHECK` auf `current_setting('app.tenant_id', true)`). +Mit `RLS_ENFORCED=true` verbinden sich app und craftvia-worker über `RLS_DATABASE_URL` als +`craftvia_app` und setzen `app.tenant_id` transaktionslokal (`src/server/db.ts`, `dbForTenant`, +`tenantTransaction`). Migrationen, Seed/Bootstrap, Login-Lookup, Mail- und Backup-Worker laufen weiter über die +Owner-`DATABASE_URL`. + +1. Passwort für die App-Rolle setzen (einmalig je Umgebung, Wert in den Passwortmanager): + ```bash + docker exec -it psql -U craftvia -d craftvia \ + -c "ALTER ROLE craftvia_app WITH LOGIN PASSWORD '';" + ``` +2. Env setzen: + `RLS_ENFORCED=true`, + `RLS_DATABASE_URL=postgresql://craftvia_app:@postgres:5432/craftvia?schema=public` +3. Neu deployen. Fehlt `RLS_DATABASE_URL` bei aktivem Flag, bricht der Prozess beim Start ab + (fail secure). +4. Nachweis: `npx tsx scripts/test-rls-enforcement.ts`. Der Test prüft Owner-Sicht, Isolation A/B, 0 Zeilen + ohne Kontext, `WITH CHECK` und `dbForTenant` end-to-end. Er gehört auch zum CI-Gate. + +**Warnungen:** +- Die Owner-Rolle in `DATABASE_URL` **muss** Superuser oder BYPASSRLS sein. Sonst sieht der Login keine + Nutzer, und Restore (`session_replication_role`) scheitert. +- Mehrschritt-Schreibvorgänge nur über `inTransaction(ctx, fn)`, direktes `ctx.db.$transaction` ist + unter `RLS_ENFORCED=true` nicht atomar (ARCHITEKTUR §4.8). +- Zeilen mit `tenant_id = NULL` (Plattform-Audit, Mail-Logs, Auth-Tokens) sind für `craftvia_app` + unsichtbar. Sie werden nur über den Owner-Client geschrieben. + +## 7. Worker, Chromium und KI-Provider + +### 7.1 craftvia-worker und Chromium + +- Image-Stage `worker` (`Dockerfile`): `node:22.14.0-slim` + Debian-Pakete `chromium`, `fonts-dejavu-core`, + `fonts-liberation`; `node_modules` inkl. `tsx`, generierter Prisma-Client, `scripts/`, `src/`, + `messages/`, `prisma/`. Start `npx tsx scripts/craftvia-worker.ts`. +- PDF-Rendering (`src/server/pdf/render.ts`): `playwright-core` startet `PDF_CHROMIUM_PATH` mit + `--no-sandbox --disable-dev-shm-usage`. Der Container braucht deshalb keine zusätzlichen Capabilities. + `shm_size: 1gb` ist als Reserve für fotoreiche Berichte gesetzt. Seiten laden keine Netzressourcen, alle Assets sind als `data:` + eingebettet. +- Concurrency: `report-pdf` 2, übrige Queues 4 je Worker-Prozess. Horizontal skalieren = weitere + `craftvia-worker`-Replicas (BullMQ verteilt). RAM-Limit im Compose 1,5 GB. +- Diagnose im Container: `chromium --version`; Logs zeigen `[worker] listening on ` bzw. + `[worker] job failed: …`. + +### 7.2 KI-Provider + +| Zweck | Env | Verhalten ohne Konfiguration | +|---|---|---| +| Auftragsimport-Extraktion (PDF/Bild) | `AI_EXTRACTION_PROVIDER=anthropic`, `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` (leer = `claude-opus-5`) | Import bleibt manuell erfassbar | +| Lotse (Berichtsentwurf, Vollständigkeitsprüfung) | `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` | kein Entwurf, UI funktioniert weiter | +| Transkription von Sprachnotizen | `TRANSCRIPTION_PROVIDER=openai-compatible`, `TRANSCRIPTION_API_URL` (Default OpenAI `/v1/audio/transcriptions`), `TRANSCRIPTION_API_KEY`, `TRANSCRIPTION_MODEL` (Default `whisper-1`) | Status `disabled` | + +- Jede KI-Nutzung wird in `AiGeneration` protokolliert (Art, Provider, Modell, Bezug, Tokens ein/aus, + auslösender Nutzer, Ein-/Ausgabe). +- **Kostenbremse:** `AI_MONTHLY_TOKEN_LIMIT` ist die Plattform-Vorgabe für Tokens (ein + aus) je Mandant je + Kalendermonat (UTC), `0` = unbegrenzt. Mandantenadministratoren können unter `/settings/lotse` einen + eigenen Wert setzen (`TenantSettings.aiMonthlyTokenLimit`; leer = Plattform-Vorgabe, `0` = unbegrenzt). + Ist das Kontingent aufgebraucht, lehnt der Lotse neue Entwürfe/Zusammenfassungen ab („Kontingent + aufgebraucht“), die Import-Extraktion fällt auf manuelle Erfassung zurück. Geprüft wird vor jedem + Aufruf – ein laufender Aufruf kann das Limit einmalig überschreiten. Transkription (Audio) liefert keine + Tokens und wird nicht gezählt. +- Datenschutz: Anbieter (Anthropic, Transkriptions-API) sind Auftragsverarbeiter, daher AVV und + Drittlandbewertung vor Aktivierung klären. Die Worker brauchen Egress (Netz `default`). + +### 7.3 Aufbewahrung KI-Protokoll + +`AI_GENERATION_RETENTION_DAYS` (Default 180): Der Job `ai-retention` (Queue gleichen Namens) läuft +täglich im `craftvia-worker` (BullMQ-Job-Scheduler `ai-retention-daily`, beim Worker-Start registriert, +idempotent auch bei mehreren Replikas). Er leert Ein- und Ausgaben (`input`/`output`) von +`AiGeneration`-Einträgen, die älter als die Frist sind, und entfernt den Personenbezug (`createdById`). +Metadaten (Art, Modell, Tokens, Zeitpunkt, Bezug) bleiben für Kosten- und Nachvollziehbarkeit erhalten; +je Mandant wird ein Audit-Eintrag `ai_generation_retention` geschrieben. Die Frist mit dem DSB abstimmen. +Die Variable muss im `craftvia-worker` gesetzt sein. + +## 8. Rate Limits + +| Bereich | Env | Default | Zählung | +|---|---|---|---| +| REST-API `/api/v1/**` | `API_RATE_LIMIT_PER_MINUTE` | 300 | je Nutzer pro Minute | +| Einsatz/Sync: `/api/v1/sync`, `/api/v1/uploads`, `/api/v1/field/**` | `API_FIELD_RATE_LIMIT_PER_MINUTE` | 1200 | je Nutzer pro Minute | +| Passwort-Reset, Alt-Passwort-Prüfung, E-Mail-Änderung | fest (`src/server/rate-limit.ts`) | 5–10 je Fenster | je IP und je Konto | + +Das Field-Limit ist höher, weil die PWA nach Offline-Phasen Outbox-Batches (≤ 50 Ops) und Fotos in +Schüben nachsendet. Wird es zu knapp gewählt, laufen die Clients in Retry/Backoff, und die Sync-Seite zeigt +Fehler. Limits je Prozess gelten pro App-Instanz. Bei mehreren Replicas multipliziert sich das +effektive Limit. + +## 9. Prebuilt-Images (Registry) + +Wenn der Host-Build in Coolify zu lange dauert, die Images auf einem Build-Host (amd64) bauen, in die +Registry pushen und in Coolify `docker-compose.coolify.prebuilt.yml` verwenden. + +| Image | Target | Dienste | +|---|---|---| +| `${REGISTRY}/craftvia-app:${IMAGE_TAG}` | `runner` | app | +| `${REGISTRY}/craftvia-migrate:${IMAGE_TAG}` | `migrate` | migrate, worker, backup-worker, garage-provision | +| `${REGISTRY}/craftvia-worker:${IMAGE_TAG}` | `worker` | craftvia-worker | +| `${REGISTRY}/craftvia-garage:${IMAGE_TAG}` | `garage` | garage | + +```bash +docker login +REGISTRY=registry.example.com/craftvia ALSO_MAIN=true ./scripts/build-and-push-images.sh +# worker-Image (bis das Skript es mitbaut): +docker build --platform linux/amd64 --target worker -t registry.example.com/craftvia/craftvia-worker:main . +docker push registry.example.com/craftvia/craftvia-worker:main +``` + +**Gotchas:** Coolify reicht `IMAGE_TAG` nicht zuverlässig in die Compose-Interpolation, daher immer auch +`:main` pushen. Coolify entfernt alte Container **vor** dem Pull: erst alle Images pushen, dann +Redeploy, sonst ist die Umgebung unten. Registry-Token mit Minimalrechten (`read/write:package`) +verwenden und nach Klartext-Nutzung widerrufen. + +## 10. Smoke nach Deploy + +1. `migrate` und `garage-provision` mit Exit 0 beendet. `app` ist healthy, `craftvia-worker`, `worker` und + `backup-worker` laufen, im Log steht `[worker] listening on report-pdf` usw. +2. `https://app.craftvia.example/login` lädt mit gültigem Zertifikat. `/sw.js` und `/site.webmanifest` sind ohne + Session erreichbar (PWA). +3. Login Backoffice → `/dashboard`, `/work-orders`, `/customers`, `/reports`. Login Monteur → `/m`. +4. Datei-Upload an einem Auftrag und Download über `/files/` (prüft Garage + S3-Keys). +5. Bericht freigeben → PDF erscheint am Auftrag (prüft Queue, craftvia-worker, Chromium). +6. Optional: Import-PDF hochladen → Extraktion (bei gesetztem API-Key). Sprachnotiz → Transkription. +7. Test-Mail (z. B. Passwort-Reset) kommt an bzw. steht nachvollziehbar auf `pending`. +8. Plattform-Login `/platform/login` → `/admin`, `/admin/backup` zeigt das Backup-Ziel. +9. Bei aktiver RLS: Login + Auftragsliste funktionieren (sonst prüfen: `RLS_DATABASE_URL`, Rolle hat LOGIN). + +Automatisierter HTTP-Smoke mit Session-Cookie (ohne Passworteingabe, braucht DB-Zugriff und +`AUTH_SECRET`): `BASE=https://app.craftvia.example npx tsx scripts/smoke-auth.ts` (z. B. im +`migrate`-Container oder von einem Admin-Host mit Tunnel zur DB). + +## 11. Backup & Restore + +Zwei Ebenen, **beide** sind nötig: DB **und** Objektspeicher. + +### 11.1 Ebene A: Cluster (gesamte Datenbank + Volumes) + +- **Postgres:** mindestens täglich `pg_dump -Fc` (Coolify Scheduled Task) in einen **separaten**, + verschlüsselten Speicher. Für PITR pgBackRest/wal-g mit WAL-Archiving und `repo-cipher-type=aes-256-cbc`. + Deckt auch die globalen Tabellen (Identity, Plattform-Admins, Kataloge) ab. + ```bash + docker exec pg_dump -U craftvia -d craftvia -Fc > craftvia-$(date +%F).dump + # Restore in leere DB (App + Worker gestoppt): + docker exec -i pg_restore -U craftvia -d craftvia --clean --if-exists < craftvia-YYYY-MM-DD.dump + ``` +- **Garage:** `garage_meta` (Bucket-/Key-/Layout-Definitionen, **kritisch**) und `garage_data` + sichern, z. B. mit restic (eigenes Repo, eigenes Passwort) oder als Volume-Snapshot bei gestopptem + `garage`. Ohne `garage_meta` sind die Objektdaten nicht adressierbar. +- **Volume `backups`:** enthält lokale App-Backup-Artefakte (Ebene B), mitsichern. +- **Host-Encryption:** Daten-Volumes auf LUKS bzw. provider-verschlüsseltem Block-Storage. Das + Boot-Unlock-Verfahren dokumentieren. +- **Restore-Test** mindestens quartalsweise in eine Wegwerf-Umgebung, Ergebnis protokollieren. + +### 11.2 Ebene B: Mandanten-Export/-Restore und DSGVO (Betreiber-Portal) + +- Ziel der Artefakte in `/admin/backup` wählbar (Lokal = Volume `/app/.backups` oder S3). Die + Konfiguration liegt verschlüsselt in der DB. Präzedenz: DB-Config → `S3_*`/`BACKUP_LOCAL_DIR` → lokaler Default. +- Export, Restore und DSGVO-Export je Mandant unter `/admin/[id]` (Plattform-Full-Admin + MFA-Step-up), ausgeführt + vom `backup-worker`. Die Artefakte sind mit `BACKUP_ENC_KEY` (AES-256-GCM) verschlüsselt, der Restore arbeitet + nur innerhalb von `tenant_id` und betrifft keine anderen Mandanten. `TENANT_MODELS` in `src/server/db.ts` und + `src/server/backup/topology.ts` müssen jede Tenant-Tabelle enthalten. + +### 11.3 Restore-Kohärenz (Vorbedingung) + +`PASSWORD_PEPPER`, `MFA_ENC_KEY` und `BACKUP_ENC_KEY` stehen **nicht** im Backup. Ein Restore in eine +Umgebung mit anderen Werten macht Logins (Pepper), MFA (`MFA_ENC_KEY`) bzw. das Entschlüsseln +der Artefakte (`BACKUP_ENC_KEY`) unmöglich. Vor jedem Restore die Secrets der Quellumgebung +bereitstellen oder einen Passwort-/MFA-Reset einplanen. Cross-Environment-Restores (prod → staging) +sind nur so lauffähig. + +## 12. Update & Rollback + +**Update (Standard):** +1. CI grün (Gate-Job: migrate, seed, tsc, lint, build, Tests). +2. Migrationen der Release sichten. Bei destruktiven Änderungen vorher `pg_dump` (§11.1). +3. Coolify-Redeploy (bzw. Images pushen, dann Redeploy). `migrate` läuft vor app und Workern. +4. Smoke (§10). Neue Env-Variablen aus den `.env.*.example`-Dateien vorher eintragen. + +**Rollback:** +- **Ohne Schemaänderung:** vorheriges Image-Tag als `:main` retaggen und pushen (Prebuilt) bzw. vorherigen Commit + deployen. Die Worker ziehen dasselbe Tag mit. +- **Mit Schemaänderung:** Prisma-Migrationen haben kein automatisches Down. Entweder Vorwärts-Fix + (neue Migration), oder App + Worker stoppen, DB aus dem Pre-Deploy-Dump wiederherstellen (§11.1), + dann den alten Stand deployen. `prisma migrate deploy` toleriert in der DB angewandte Migrationen, + die im alten Code fehlen. +- **Queues:** Beim Rollback können Jobs eines neueren Payload-Formats in Redis liegen. Vor dem Rollback + Worker-Logs prüfen, fehlgeschlagene Jobs nach dem Fix erneut anstoßen (z. B. „PDF erzeugen"). + +## 13. Go-Live-Checkliste + +- [ ] Frische, starke Secrets je Umgebung, im Passwortmanager + versiegelte Offline-Kopie +- [ ] `RUN_DEMO_SEED=false`, Bootstrap-Admin-Passwort geändert, `BOOTSTRAP_ADMIN=false` +- [ ] HTTPS aktiv, `AUTH_URL`/`APP_BASE_URL` korrekt +- [ ] `RLS_ENFORCED=true` + `RLS_DATABASE_URL`, Smoke mit aktiver RLS bestanden +- [ ] `craftvia-worker` läuft, Test-PDF erzeugt +- [ ] SMTP mit SPF/DKIM/DMARC der Absenderdomain +- [ ] KI: AVV geklärt, `AI_MONTHLY_TOKEN_LIMIT` und `AI_GENERATION_RETENTION_DAYS` festgelegt +- [ ] Backups Ebene A (Postgres + `garage_meta`/`garage_data` + `backups`) eingerichtet, Restore-Test dokumentiert +- [ ] Monitoring: Uptime-Check auf die App-URL, Log-Aggregation, Alarm bei Worker-Neustarts +- [ ] Firewall (80/443/SSH), SSH-Key-Login, unattended-upgrades diff --git a/scripts/bootstrap-admin.ts b/scripts/bootstrap-admin.ts index 850afca..7bad024 100644 --- a/scripts/bootstrap-admin.ts +++ b/scripts/bootstrap-admin.ts @@ -15,7 +15,7 @@ import { provisionTenant } from "@/server/provision"; * Beide mit denselben Zugangsdaten (BOOTSTRAP_ADMIN_*), analog zum Seed. * * Ausführung: im migrate-Job nach `prisma migrate deploy`, gesteuert per - * BOOTSTRAP_ADMIN=true (siehe docker-compose.coolify.yml, docs/DEPLOY-PROD-CONTABO.md). + * BOOTSTRAP_ADMIN=true (siehe docker-compose.coolify.yml, docs/_certvia-archiv/DEPLOY-PROD-CONTABO.md). * * Idempotent: provisionTenant und der platformAdmin.upsert nutzen upserts; ein bereits * gesetztes Passwort wird beim erneuten Lauf NICHT überschrieben. diff --git a/scripts/build-and-push-images.sh b/scripts/build-and-push-images.sh index 110308e..ec2b1d9 100755 --- a/scripts/build-and-push-images.sh +++ b/scripts/build-and-push-images.sh @@ -46,10 +46,12 @@ build_one() { build_one runner craftvia-app build_one migrate craftvia-migrate build_one garage craftvia-garage +# L10b: Hintergrund-Worker (BullMQ: Import-Extraktion, Transkription, Berichts-PDF mit Chromium, KI-Aufbewahrung) +build_one worker craftvia-worker echo echo ">> Push ..." -for name in craftvia-app craftvia-migrate craftvia-garage; do +for name in craftvia-app craftvia-migrate craftvia-garage craftvia-worker; do docker push "$REGISTRY/$name:$TAG" [ "$ALSO_MAIN" = "true" ] && docker push "$REGISTRY/$name:main" || true done diff --git a/scripts/garage-provision.ts b/scripts/garage-provision.ts index e1f4591..6cb56b9 100644 --- a/scripts/garage-provision.ts +++ b/scripts/garage-provision.ts @@ -2,7 +2,7 @@ import "dotenv/config"; /** * IM/Garage-Migration — idempotentes Provisioning eines Single-Node-Garage - * (docs/KONZEPT-garage-migration.md §6 Lane B, §7 Runbook). + * (docs/_certvia-archiv/KONZEPT-garage-migration.md §6 Lane B, §7 Runbook). * * Garage verwaltet Buckets/Keys/Rechte NICHT über die S3-API (`CreateBucket` gibt es * dort nicht), sondern out-of-band. Dieser Init-Job (Compose-Service „garage-provision", diff --git a/scripts/test-auth-selfservice.ts b/scripts/test-auth-selfservice.ts index c174d4d..3763270 100644 --- a/scripts/test-auth-selfservice.ts +++ b/scripts/test-auth-selfservice.ts @@ -24,7 +24,7 @@ import { closeMailProvider } from "../src/server/mail/provider-smtp"; * 9. Enumeration: unbekannte Adresse liefert kein Konto. * * Die Browser-Abläufe (Reset-Mail → Link → neues Passwort) stehen im Testplan - * von docs/SEC2-AUTH-SELFSERVICE.md. + * von docs/_certvia-archiv/SEC2-AUTH-SELFSERVICE.md. */ let failures = 0; diff --git a/scripts/test-garage-storage.ts b/scripts/test-garage-storage.ts index 119f6e3..27e1810 100644 --- a/scripts/test-garage-storage.ts +++ b/scripts/test-garage-storage.ts @@ -1,5 +1,5 @@ // Smoke-Test des S3-Objektspeichers gegen einen echten Garage-Node (Phase C/D der -// MinIO→Garage-Migration, docs/KONZEPT-garage-migration.md §9). +// MinIO→Garage-Migration, docs/_certvia-archiv/KONZEPT-garage-migration.md §9). // // Prüft den realen S3-Pfad (AWS SDK v3, path-style) über die exportierte // `resolveBackupStore()`: