Ein Schritt nimmt jetzt Bilder (JPEG, PNG, WebP, HEIC) und PDF auf, am Handy direkt aus der Kamera. Die Dateien liegen im Dateisystem unter ANHAENGE_PFAD, nach Kunde und Lauf gegliedert; in der Datenbank stehen nur Name, Typ, Groesse und Pruefsumme. Der Pfad entsteht aus der Kennung, nie aus dem Dateinamen vom Browser. Jeder Abruf prueft Recht und Mandantengrenze — auch der des Bildes selbst: der Route Handler ruft rechtErforderlich() selbst, weil die Huelle unter (app) Route Handler nicht abdeckt. Hochgeladen wird ueber eine Server Action statt ueber einen zweiten Endpunkt; der Archiv-Schreibschutz haengt an derselben einen Stelle wie alles andere am Lauf. Kein SVG: ein SVG ist ein Dokument, das Skripte ausfuehrt. In der Datenbank halten zwei CHECK-Bedingungen und zwei zusammengesetzte Fremdschluessel fest, was auch gilt, wenn jemand an der Service-Schicht vorbei schreibt — ein Anhang kann strukturell nicht an einem Schritt eines anderen Laufs oder bei einem fremden Kunden haengen. docs/betrieb/sicherung.md ist neu geschrieben: dort stand "Kein Upload-Verzeichnis, keine erzeugten Dateien", und eine Sicherung nach der alten Anleitung haette ab hier still Daten verloren. Sicherung und Ruecksicherung sind einmal wirklich gefahren, mit Pruefsummenvergleich. Gefunden in der Klickprobe am gebauten Stand und nur dort: zwei Felder "datei" in einem Formular, und FormData.get nahm das leere. Wer "Datei" waehlte, bekam "Keine Datei gewaehlt", waehrend "Foto" ging. Die Regel liegt jetzt als reine Funktion in src/lib, und tests/e2e/anhaenge.spec.ts laedt bewusst ueber den zweiten Knopf hoch. Closes #52 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
146 lines
5.9 KiB
YAML
146 lines
5.9 KiB
YAML
# Laufzettel – Betriebsstand.
|
||
#
|
||
# Die Datenbank ist bewusst von aussen NICHT erreichbar: kein ports:-Mapping und
|
||
# ein Netz ohne Gateway (internal: true). Nur web und migrate kommen ueber den
|
||
# Dienstnamen daran. Fuer die Entwicklung gibt es docker-compose.dev.yml.
|
||
#
|
||
# Sicherung trotz geschlossenem Port:
|
||
# docker compose exec -T postgres pg_dump -U laufzettel laufzettel > sicherung.sql
|
||
|
||
name: laufzettel
|
||
|
||
services:
|
||
postgres:
|
||
image: postgres:17-alpine
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_USER: ${POSTGRES_USER:-laufzettel}
|
||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in der .env}
|
||
POSTGRES_DB: ${POSTGRES_DB:-laufzettel}
|
||
# Absichtlich expose statt ports: im LAN und auf dem Host nicht erreichbar.
|
||
expose:
|
||
- "5432"
|
||
networks:
|
||
- intern
|
||
volumes:
|
||
- datenbank:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-laufzettel}"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
|
||
# Laeuft einmal beim Start und spielt nur die Migrationen ein.
|
||
# Der Seed wird bewusst nicht automatisch ausgefuehrt, siehe README.
|
||
migrate:
|
||
build:
|
||
context: .
|
||
target: bauen
|
||
restart: "no"
|
||
environment:
|
||
# Immer aus den Einzelteilen, **nie** aus einer DATABASE_URL der .env: dort
|
||
# steht die Adresse fuer die Entwicklung (localhost und ein offener Port),
|
||
# und im Container zeigt sie ins Leere. Wer den dokumentierten Weg ging,
|
||
# bekam einen Stand, der nicht startet — gefunden bei der Abnahme zu #019.
|
||
# Im Compose-Stand heisst die Datenbank immer `postgres`, das ist der
|
||
# Dienstname im Netz `intern`.
|
||
DATABASE_URL: postgresql://${POSTGRES_USER:-laufzettel}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-laufzettel}?schema=public
|
||
#
|
||
# Fuer die Migrationen braucht es nichts weiter — wohl aber fuer den Seed,
|
||
# der ueber denselben Dienst gefahren wird:
|
||
# `docker compose run --rm migrate pnpm db:seed` (siehe README).
|
||
#
|
||
# APP_SECRET verschluesselt das Bind-Passwort der Beispiel-Verzeichnis-
|
||
# quelle. Ohne es brach der Seed mit einem Stapelverlauf ab, und zwar
|
||
# nachdem er die Vorlagen schon angelegt hatte — gefunden bei der Abnahme
|
||
# zu #019: der dokumentierte Befehl scheiterte auf einer frischen
|
||
# Installation.
|
||
APP_SECRET: ${APP_SECRET:?APP_SECRET fehlt in der .env}
|
||
SEED_MODE: ${SEED_MODE:-INTERN}
|
||
SEED_ADMIN_EMAIL: ${SEED_ADMIN_EMAIL:-}
|
||
SEED_ADMIN_PASSWORD: ${SEED_ADMIN_PASSWORD:-}
|
||
SEED_ORG_NAME: ${SEED_ORG_NAME:-}
|
||
SEED_ORG_KUERZEL: ${SEED_ORG_KUERZEL:-}
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
networks:
|
||
- intern
|
||
command: ["pnpm", "exec", "prisma", "migrate", "deploy"]
|
||
|
||
web:
|
||
build:
|
||
context: .
|
||
target: laufzeit
|
||
args:
|
||
APP_VERSION: ${APP_VERSION:-0.1.0}
|
||
GIT_SHA: ${GIT_SHA:-}
|
||
restart: unless-stopped
|
||
environment:
|
||
# Immer aus den Einzelteilen, **nie** aus einer DATABASE_URL der .env: dort
|
||
# steht die Adresse fuer die Entwicklung (localhost und ein offener Port),
|
||
# und im Container zeigt sie ins Leere. Wer den dokumentierten Weg ging,
|
||
# bekam einen Stand, der nicht startet — gefunden bei der Abnahme zu #019.
|
||
# Im Compose-Stand heisst die Datenbank immer `postgres`, das ist der
|
||
# Dienstname im Netz `intern`.
|
||
DATABASE_URL: postgresql://${POSTGRES_USER:-laufzettel}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-laufzettel}?schema=public
|
||
APP_SECRET: ${APP_SECRET:?APP_SECRET fehlt in der .env}
|
||
APP_URL: ${APP_URL:?APP_URL fehlt in der .env}
|
||
AUTH_LOCAL_ENABLED: ${AUTH_LOCAL_ENABLED:-false}
|
||
OIDC_ISSUER: ${OIDC_ISSUER:-}
|
||
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
|
||
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
|
||
OIDC_NAME: ${OIDC_NAME:-authentik}
|
||
OIDC_GROUP_ADMIN: ${OIDC_GROUP_ADMIN:-}
|
||
OIDC_GROUP_EDITOR: ${OIDC_GROUP_EDITOR:-}
|
||
OIDC_GROUP_TECHNICIAN: ${OIDC_GROUP_TECHNICIAN:-}
|
||
OIDC_GROUP_VIEWER: ${OIDC_GROUP_VIEWER:-}
|
||
OIDC_CUSTOMER_GROUP_PREFIX: ${OIDC_CUSTOMER_GROUP_PREFIX:-}
|
||
# Der Verzeichnis-Abgleich laeuft im selben Prozess (#016). Hier steht er
|
||
# an, weil dieser Dienst der einzige ist, der dauerhaft laeuft.
|
||
ZEITPLAN_AKTIV: ${ZEITPLAN_AKTIV:-true}
|
||
# Anhaenge und Fotos liegen im Dateisystem, nicht in der Datenbank (#032).
|
||
# Der Pfad zeigt in das Volume `anhaenge` unten.
|
||
ANHAENGE_PFAD: /daten/anhaenge
|
||
ANHANG_MAX_MB: ${ANHANG_MAX_MB:-10}
|
||
ANHAENGE_JE_LAUF: ${ANHAENGE_JE_LAUF:-20}
|
||
# Standardmaessig nur auf localhost; davor gehoert ein Reverse Proxy.
|
||
ports:
|
||
- "${BIND_ADDR:-127.0.0.1}:${APP_PORT:-3000}:3000"
|
||
volumes:
|
||
# **Gehoert in jede Sicherung.** Eine Sicherung, die nur `pg_dump` macht,
|
||
# verliert ab hier Daten: in der Datenbank stehen nur die Metadaten der
|
||
# Anhaenge, die Dateien liegen hier. Siehe docs/betrieb/sicherung.md.
|
||
- anhaenge:/daten/anhaenge
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
migrate:
|
||
condition: service_completed_successfully
|
||
networks:
|
||
- intern
|
||
- extern
|
||
healthcheck:
|
||
# 127.0.0.1, nicht localhost: busybox-wget loest localhost zuerst nach ::1
|
||
# auf, Next lauscht aber nur auf 0.0.0.0:3000. Der Healthcheck schlug
|
||
# deshalb dauerhaft fehl, waehrend die App einwandfrei antwortete -
|
||
# gefunden bei der Abnahme zu #019.
|
||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/gesundheit"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
retries: 5
|
||
start_period: 20s
|
||
|
||
networks:
|
||
# Kein Gateway: die Datenbank kann weder heraus noch ist sie von aussen erreichbar.
|
||
intern:
|
||
internal: true
|
||
# Nur fuer web: Reverse Proxy, OIDC zu authentik, LDAP zum AD.
|
||
extern:
|
||
|
||
volumes:
|
||
datenbank:
|
||
# Die Dateien der Anhaenge (#032). Zusammen mit der Datenbank zu sichern –
|
||
# getrennt gesichert ergeben beide Haelften kein vollstaendiges Bild.
|
||
anhaenge:
|