Loggt bei AccessDenied die empfangenen Gruppen + erwartete Admin-Gruppe; bei
leerer Gruppenliste Hinweis auf fehlendes 'groups'-Scope-Mapping. Erleichtert
die Diagnose der Authentik-Gruppensteuerung im Betrieb.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Das Projekt hatte kein public/-Verzeichnis; der Runner-Stage-COPY
(COPY /app/public ./public) brach mit '/app/public: not found' ab.
Leeres, getracktes public/ behebt das (Next.js bedient es problemlos).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
next build evaluiert beim 'Collecting page data' Server-Routen (u. a.
/api/auth/[...nextauth]); src/lib/env.ts validiert beim Import (Fail-Fast) und
brach ohne gesetzte Variablen ab. Platzhalter-Env (erfüllt das Zod-Schema) nur
für den Build ergänzt — Server-env wird nicht ins Bundle inlined, die builder-
Stage landet nicht im Runtime-Image; echte Werte kommen zur Laufzeit aus Compose.
Lokal verifiziert: next build läuft mit den Platzhaltern sauber durch (alle Routen).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wie im feuerwehr_dashboard:
- .npmrc mit registry=https://registry.npmjs.org/ (committet)
- Dockerfile deps-Stage: npm@11 pinnen + .npmrc kopieren; statt 'npm ci' nun
'npm install' und den committeten Lockfile NICHT verwenden (er wurde gegen einen
internen Mirror erzeugt -> apple-Artifactory-URLs -> auf dem Server nicht erreichbar).
Damit baut 'make up-core'/'make deploy' auf dem Server gegen die öffentliche Registry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ermöglicht Deploy ohne OSRM/Nominatim (deren Preprocessing/Import viel RAM
braucht; Austria-Extrakt OOM-te beim osrm-extract). App läuft mit
Haversine-Fallback; Geo später via 'make data' + 'make up' nachziehen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dashboard nutzt das Format postgresql://USER:PASS@HOST:PORT/DB. Angeglichen in
docker-compose.yml, .env.example (+ Host-Hinweis lokal=localhost / Container=postgres),
vitest.setup.ts und env.test.ts. Funktional identisch (pg/Drizzle akzeptieren beide),
aber konsistent mit dem bestehenden Setup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BLOCKING-Befunde aus "Tests & Sicherheitshaertung":
1) Coverage-Pfad war nie ausfuehrbar: @vitest/coverage-v8 fehlte in den
devDependencies, obwohl vitest.config coverage.provider "v8" setzt und
test:coverage "vitest run --coverage" aufruft. Paket passend zu vitest
^3.2 ergaenzt und installiert. coverage.include zog ganze Verzeichnisse
(src/lib/search, src/lib/geo) ein - inkl. DB-/HTTP-gebundener Wrapper
(Drizzle gegen Postgres, Nominatim/OSRM), die offline nicht laufen und
die globale Schwelle verwaesserten. Scope auf die REINE, offline
testbare Logik beschraenkt (perFile-Schwellen), I/O-Wrapper und reine
Typ-Module ausgenommen (per Integrations-/E2E-Tests abgesichert).
Fehlende Branch-Abdeckung in geo/eintreffzeit.ts mit Tests fuer
Einzel-Haversine-Fallback, reine Koordinaten-lose Liste und fehlende
OSRM-distances-Zeile geschlossen. npm run test:coverage: Exit 0,
Schwellen (Lines/Stmts/Funcs >=90, Branches >=80) erfuellt.
2) Driftschutz zu permissiv: isPublic() in tests/support/route-scan.ts
stufte ueber `route.startsWith(p)` jeden reinen String-Praefix als
oeffentlich ein (z. B. /loginhelp, /api/healthz, /api/authentication)
und liess solche Routen dem Auth-Gating-Driftcheck entkommen. Die
redundante dritte Bedingung entfernt; exakter Treffer und echtes
Unterpfad-Segment (p + "/") sind korrekt und ausreichend. Negativ-
Testfall ergaenzt.
Verifiziert (offline): tsc --noEmit (0), vitest run (233 passed,
7 DB-Tests deferred), test:coverage (Exit 0). DB-/Server-/Browser-
abhaengige Schritte deferred (kein Postgres/Server im Sandbox).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beweist die Auth-Gating-Garantie und härtet das System ab (Definition of
Done #1, #2, #3, #7, #8):
- Routen-Manifest (tests/e2e/routes.manifest.ts) als einzige Quelle der
Wahrheit; anonyme Seite -> Redirect /login, anonyme API -> 401.
- Kritische auth-gating.spec.ts: genau ein Fall je Manifest-Eintrag, ohne
Daten-Leak.
- Driftschutz (routes.manifest.spec.ts + tests/unit/routes-manifest.test.ts):
keine ungetestete neue Route unter src/app/**.
- Default-Deny-Beweis für Server Actions (server-actions-guard.spec.ts +
tests/unit/server-actions-guard.test.ts): jede "use server"-Funktion ruft
als erste Anweisung einen Guard; Login-Actions per Allowlist ausgenommen.
- Wiederverwendbare reine Scanner unter tests/support (route-scan, guard-scan)
— offline lauffähig, in Vitest und Playwright geteilt.
- rbac-scoping, search-eta, login-ratelimit, security-headers Specs (gegen
geseedeten Server; in der Sandbox deferred, per test.skip abgesichert).
- global-setup (Migration + Seed) und auth.setup (Login je Konto ->
storageState); Playwright-Projekte setup -> chromium verdrahtet.
- src/lib/security/headers.test.ts: statischer Beleg für CSP, HSTS,
X-Frame-Options DENY, nosniff, Permissions-Policy.
- vitest.config.ts: Coverage-Schwellen (>=90 %) für src/lib/search + src/lib/geo.
- package.json: Scripts test:unit, test:coverage, test:e2e, test:e2e:gating.
- docs/reference/sicherheitshaertung-checkliste.md: jeder Härtungspunkt mit
Test/Befehl und Negativ-Probe.
Offline verifiziert: tsc --noEmit (0), vitest run (229 passed / 7 db-skipped),
drizzle-kit check (ok), next build (exit 0), next lint (0 Fehler),
playwright --list (98 Tests, 15 Dateien). DB-/Server-/Browser-abhängige
E2E-Läufe sind deferred (kein Postgres/Server in der Sandbox).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zwei BLOCKING-Befunde für "Deployment (Docker + externes Traefik)":
1) pg-Laufzeit-Closure unvollständig: pg-types/lib/binaryParsers.js macht
eager require('pg-int8'), postgres-interval/index.js require('xtend/mutable').
Beide lagen NICHT im Runner-Image -> node docker/migrate.mjs crasht beim
Container-Start mit ERR_MODULE_NOT_FOUND, Deploy kaputt. Fix: COPY für
pg-int8 + xtend ergänzt. Neuer Test berechnet die reale Closure aus
node_modules und schlägt fehl, sobald ein Paket nicht ins Image kopiert
wird (schützt vor erneutem Brechen bei pg/drizzle-Updates).
2) RUN_SEED toter Pfad: entrypoint.sh rief docker/seed.mjs auf, das nie
existierte -> RUN_SEED=true no-opte still zu leerem Katalog. Fix:
scripts/build-seed-bundle.mjs bündelt src/db/seed (inkl. Schema + Daten,
pg/drizzle-orm extern) per esbuild zu selbstständigem docker/seed.mjs;
im builder erzeugt und ins Runner-Image kopiert. entrypoint.sh bricht
jetzt laut ab, wenn RUN_SEED=true und das Bundle fehlt, statt still zu
überspringen. docker/seed.mjs ist generiert -> gitignored.
Verifiziert offline: tsc --noEmit, vitest (deployment + seed-Daten, 36 grün),
Bundle baut + lädt sauber (externe Imports nur pg/drizzle-orm, exit 1 ohne
DATABASE_URL). docker build/run sind im Sandbox deferred (kein Docker/Postgres).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der Standalone-Runner `tsx src/db/seed/index.ts` (npm run db:seed)
importierte Laufzeit-Module ueber den tsconfig-Pfad-Alias `@/`
(`@/db/schema`, `@/lib/audit`). tsx loest `paths` nur versionsabhaengig
auf (erst ab v4.20); package.json pinnt jedoch `tsx ^4.19.2`, sodass
eine 4.19.x-Aufloesung mit ERR_MODULE_NOT_FOUND scheitert, bevor eine
DB-Verbindung aufgebaut wird. Der Offline-Unit-Test maskierte das, weil
er nur die `./data/*`-Dateien importiert und Vite den Alias aufloest.
Fix: index.ts und upsert.ts nutzen jetzt relative `.js`-Imports
(`../schema/index.js`, `../../lib/audit.js`, `./data/*.js`, `./upsert.js`)
analog zu scripts/seed-auth.ts und scripts/migrate.ts. Damit ist die
Aufloesung tsx-versionsunabhaengig und konsistent zur etablierten
Konvention der uebrigen per tsx ausgefuehrten Scripts.
Verifiziert offline: tsc --noEmit (0), 25 Seed-Unit-Tests gruen, und
das Seed-Modul-Importgraph laedt unter `node --import tsx/esm` ohne
Resolver-Fehler. Die tatsaechliche `npm run db:seed`-Ausfuehrung gegen
Postgres bleibt deferred (kein Postgres/Server im Sandbox).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detailseiten & Kontakt (WS8) – zwei BLOCKING-Befunde behoben:
- UUID-Validierung an der Grenze (Querschnittsstandard 4): Die drei
Detailseiten (fahrzeuge/geraete/wehren [id]) gaben den Route-Param `id`
ungeprüft an die DB (Postgres `uuid`). Ein malformter Pfad wie
/fahrzeuge/abc erzeugte `invalid input syntax for type uuid` -> 500.
Jetzt: uuidSchema.safeParse(id) -> notFound() (deutsche 404) vor dem
Query-Aufruf.
- e2e-Harness: detail-auth.spec.ts nutzte storageState
"tests/e2e/.auth/wehr-read.json" (existiert nicht, ENOENT -> ganzer
'Eingeloggt'-Block errort). Auf Projektkonvention umgestellt:
storageState: process.env.E2E_WEHR_READ_STATE ?? { cookies, origins }
+ test.skip ohne Fixture (analog verwaltung-scoping.spec.ts).
Zusätzlich nicht-UUID-Fall (/fahrzeuge/abc -> deutsche 404) abgesichert.
Verifiziert (offline): tsc --noEmit OK, vitest detail-Unit-Tests OK,
next build "Compiled successfully" + Typecheck OK. Build-Page-Data-Phase
und e2e deferred (kein Postgres/Server in der Sandbox).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- tests/e2e/verwaltung-fuhrpark.spec.ts: selectOption({ label: /HLF 2/ })
uebergab eine RegExp an Playwrights string-typisierte label-Option
(TS2345 -> tsc-Gate rot). Stattdessen erste echte Vorlage per
{ index: 1 } waehlen (Index 0 ist "Ohne Vorlage (frei)"). tsc clean.
- src/lib/validation/vehicle.ts: Pflichtmerkmale wurden nur pro Element
geprueft; ein komplett ausgelassenes Pflichtmerkmal (z.B. []) entging
der Validierung. buildMerkmalValuesSchema prueft jetzt auf Array-Ebene
per superRefine, dass jede pflicht-Definition einen gesetzten,
typgerechten, nicht-leeren Wert hat ("validieren, nicht vertrauen",
Querschnittsstandard 4). Tests ergaenzt (TDD).
- src/server/actions/brigade.ts: bei fehlgeschlagenem Geocoding
(geo.status !== "ok") werden lat/lng nun auf null gesetzt, analog zu
createBrigadeWithFirstAdmin, damit keine veralteten Koordinaten
zuruechbleiben (WS4-Konsistenz).
Verifikation offline: tsc --noEmit exit 0; vitest src/lib/validation
47/47 gruen. E2E (DB/Server) deferred (kein Postgres/Server im Sandbox).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resetUserPassword: laedt das Konto in der Transaktion und bricht bei
authTyp !== "local" ab (kein Hash, kein user.reset-Audit). Damit wird die
dokumentierte Invariante "nur lokale Wehr-Benutzer zuruecksetzen" auch
serverseitig erzwungen, nicht nur im UI. resetBrigadeUserPassword faengt
den Fehler als { ok: false, error } ab.
mergeMerkmal: loest PK-Kollisionen in vehicle_template_merkmale auf, indem
proposed-Zeilen geloescht werden, wenn das Ziel-Merkmal in derselben Vorlage
bereits existiert (zusammengesetzter PK template_id, merkmal_id). Das gesamte
Umhaengen ist zudem in try/catch gekapselt und liefert bei Fehlern eine klare
{ ok: false }-Meldung - analog zu promoteMerkmal.
Neue Unit-Tests (db/tx gemockt, kein Postgres noetig) decken beide Pfade ab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Platform-Admin-only Oberflächen und Domänenlogik:
- codes.ts erweitert um allradCode/normalizeCode/codesMatch (Allrad-Infix
kanonisch; Suche importiert weiterhin expandNameQuery). Pure-Unit-Tests.
- slug.ts (Idempotenz-Key-Erzeugung) + Tests.
- audit.ts: writeAudit mit EINER Signatur und optionalem typisierten tx.
- provisioning.ts: createBrigadeWithFirstAdmin (Geocoding inline, argon2id,
Audit brigade.create/user.create) + resetUserPassword (Audit user.reset).
- Zod-Validierung: merkmal/template/equipment-category/brigade (+ Tests).
- Server Actions (jede mit Guard als erster Anweisung, default-deny):
merkmale (CRUD, Delete blockiert bei Referenz), proposals (promote/merge mit
Typ-Kompatibilität), templates (Merkmale/Vorgabewerte/Aliasse), equipment-
categories, brigades (Bereitstellung/Reset). Audit in allen Schreib-Actions.
- (admin)-Route-Group: Layout mit requirePlatformAdmin als erster Zeile,
AdminNav, DataTable, loading/error; Seiten für Merkmale (+Editor), Vorschläge
(Merge), Vorlagen (+Detail mit Merkmal-/Alias-Editor und Allrad-Hinweis),
Geräte-Kategorien (+Detail), Wehren (Liste/neu/Detail mit Passwort-Reset),
paginierter Audit-Viewer mit Filter. Jede Seite ruft zusätzlich den Guard.
- i18n: admin-Strings in zentraler de.ts.
- Playwright-Specs (deferred, nicht ausgeführt): admin-gating,
admin-merkmal-proposal, admin-brigade-provision.
Schema NICHT neu definiert — nur importiert. codes.ts ist hier Eigentümer.
Offline-Verifikation: tsc --noEmit grün; eslint grün; vitest run grün
(119 passed, 7 DB-roundtrip skipped); next build Exit 0; drizzle-kit check ok.
DB-/Server-/Browser-abhängige Schritte deferred (kein Postgres/Server im
Sandbox).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implementiert die Startseite mit Tabs (Fahrzeuge/Geräte/Wehren), Namens-/
Funkrufnamen-Suche und ein dynamisch aus dem aktiven Merkmal-Katalog erzeugtes
Filter-UI (Slider/Multi-Select/Tri-State Switch) plus Status-Filter.
Kern:
- src/lib/search/types.ts: uuid-IDs durchgängig (SearchHit, FacetDef, FilterValue).
- src/lib/search/parse-params.ts: typisiertes Parsen von f.<uuid>=… (number lo..hi,
enum CSV, boolean ja/nein) + q + bereit; Ungültiges wird still verworfen.
- src/lib/search/facets.ts: lädt nur status='active', geltungsbereich in (typ,'both'),
typ<>'text'; min/max je number, Optionen sortiert je enum.
- src/lib/search/query-vehicles.ts: Name+Funkrufname (OR) + Status + UND-verknüpfte
EXISTS-Filter je merkmal_id; Allrad-Regel via expandNameQuery; keine Sortierung.
- src/lib/search/query-equipment.ts: wie Fahrzeuge, ohne Allrad, mit categoryId.
- src/lib/search/query-brigades.ts: Name/Ort/PLZ, nur aktive Wehren.
- src/lib/admin/codes.ts: gemeinsame Allrad-Namensregel (HLFA->HLF, Allrad impliziert);
Eigentum Admin-WS, hier rein/testbar bereitgestellt und importiert.
- src/lib/db/indexes-trgm.sql: nur pg_trgm-GIN-Indizes auf vehicles.name/funkrufname
(idempotent); merkmal_values-Indizes bleiben Eigentum des DB-WS.
UI:
- src/components/search: SearchTabs, SearchBar (debounced q), FilterPanel (dispatch +
Status-Switch), useSearchParams (router.replace ohne Reload, atomares setParams),
StandortBar; facets/{NumberRange,Enum,Boolean}; results/{ResultList,Vehicle,Equipment,
Brigade}Row mit Empty-State und offenem ETA-Slot.
- src/app/(app)/{page,fahrzeuge,geraete,wehren}: Server Components mit requireSession()
als erster Zeile (default-deny in der Tiefe zusätzlich zum Layout-Gate). /fahrzeuge
sortiert bei gesetztem Standort via searchHitsToGeoCandidates + orderByEintreffzeit.
Tests:
- Units (ohne DB): codes, parse-params, query-vehicles (SQL-Render via PgDialect).
- tests/e2e/search.spec.ts geschrieben (deferred — kein Server/DB in Sandbox).
Verifiziert offline: tsc --noEmit (0 Fehler), eslint (0), drizzle-kit check (ok),
vitest src/lib (57 grün), next build (Compiled successfully, Routen registriert).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
placeholder und aria-label im StandortInput waren als deutsche
Strings hardcodet, während alle übrigen UI-Texte über t() aus
@/lib/i18n/de laufen. Das verletzte den i18n-Querschnittsstandard
des Repos.
- search.adresse ("Adresse") und search.adressePlaceholder
("Adresse oder Ort") in de.ts ergänzt
- standort-input.tsx nutzt jetzt t("search.adressePlaceholder")
und t("search.adresse")
- Unit-Test für die neuen i18n-Keys ergänzt
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behebt zwei BLOCKING-Befunde aus dem Review zu "Projekt-Fundament &
Design-System":
1. Route-Namens-Mismatch (Default-deny-Kerngarantie): Login-Seite lag unter
(auth)/anmelden, der gesamte downstream Auth-/Gating-Vertrag im Plan
erwartet aber /login (NextAuth pages.signIn, requireSession-Redirect,
PUBLIC_ALLOWLIST, Middleware-Matcher, auth-gating.spec toHaveURL(/\/login/),
Datei-Layout (auth)/login/...). Verzeichnis nach (auth)/login umbenannt;
/login als kanonischen Pfad im Guard-Slot-Kommentar von (app)/layout.tsx
dokumentiert, damit Workstream 3 dieselbe Route verwendet.
2. Fehlende Content-Security-Policy in SECURITY_HEADERS: Plan Z.1314 fordert
CSP mit default-src 'self', img-src 'self' data: blob:, worker-src
'self' blob:, frame-ancestors 'none', form-action 'self'; die
security-headers.spec prueft frame-ancestors 'none'. CSP ergaenzt, in
Produktion strikt, im Dev-Modus gelockerte script-src/connect-src
(unsafe-eval + ws:) fuer Next.js-HMR via NODE_ENV.
Verifikation: tsc --noEmit, next lint, next build (Route /login, kein
/anmelden) gruen; CSP zur Laufzeit fuer prod/dev geprueft.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phased, dependency-ordered plan across 11 workstreams (foundation,
schema, auth, admin taxonomy, brigade area, search, geo/ETA, detail,
deployment, seed, tests/security) with exact file paths, code/schema
snippets, ordered tasks and per-task verification. Includes cross-cutting
standards, definition-of-done, and risks. Produced by a fan-out design +
adversarial critique + synthesis workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bundesland confirmed: Niederösterreich; spec references seed catalog
- Vorlagen list corrected to NÖ HLF system + aliases + Allrad rule
- Geräte-Kategorien derived from Beladelisten
- Allrad designation is HLFA n (A infixed), not 'HLF n A'
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Synthesized from the 11 PDFs in unterlagen/: 11 Fahrzeug-Vorlagen
(HLF 1, HLF 1 W, HLF 2-4, VRF, VF, ALF, SSTF, WLF, MTF) with technical
specs and standard Beladung, plus a derived typed Merkmal-Katalog to
seed the dynamic attribute system. Corrects RL numbers that the source
filenames got wrong (FA 04 = VRF, FA 07 = HLF 4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Login-only mutual-aid platform for Austrian volunteer fire brigades to
list vehicles/equipment, searchable by other brigades and sorted by
fastest-arriving (drive-time ETA). Next.js + PostgreSQL/Drizzle + Auth.js
(Authentik OIDC + local argon2id), dynamic admin-curated Merkmal system,
self-hosted OSRM/Nominatim, Docker Compose behind external Traefik.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>