Commit Graph

3 Commits

Author SHA1 Message Date
Matthias Hochmeister
e97e16d254 Workstream 6: Admin-Panel — Taxonomie & Bereitstellung (Phase 4)
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>
2026-06-09 10:30:52 +02:00
Matthias Hochmeister
0a7173ef38 Workstream 5: Dynamische Suche & Filter (Phase 3)
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>
2026-06-09 10:04:53 +02:00
Matthias Hochmeister
ae5d3589c3 Workstream 3: Authentifizierung & Zugriffskontrolle (Phase 2)
Einheitliche Auth.js-v5-Sitzung (role + brigadeId) über Authentik-OIDC
(Platform-Admins) und Credentials/argon2id (Wehr-Konten). Default-deny
dreifach: Middleware + Layout-Guard im (app)-Segment + 401/403 in API-Routen
und Guards in Server Actions.

- src/lib/auth/roles.ts: Rollen als Single Source of Truth (aus DB-Enum
  abgeleitet) + canAccessBrigade-Wehr-Scoping.
- src/lib/auth/password.ts: argon2id mit OWASP-Minima (Node-only).
- src/lib/auth/rate-limit.ts: Sliding Window (5 Fehlversuche/15 min) auf
  login_attempts; greift im authorize-Callback (beide Login-Pfade).
- src/auth.config.ts: Edge-sicher (kein @/db, kein argon2), Cookie-secure/
  __Secure- umgebungsabhaengig (isHttps).
- src/auth.ts: Credentials + DB-Lookup + Rate-Limit + Authentik-signIn-Gate.
- src/middleware.ts: Allowlist inkl. api/auth, api/health, login, Statics.
- src/lib/auth/guards.ts: requireSession/requireRole/requirePlatformAdmin/
  requireWehrAdmin/requireOwnBrigade + API-Varianten (401/403 ohne Daten-Leak).
- (app)/layout.tsx: requireSession() als erste Zeile aktiviert.
- (auth)/login: page + login-form + Server Actions (Zod, Authentik + lokal).
- api/auth/[...nextauth]/route.ts; api/health/route.ts (anonym 200).
- scripts/seed-auth.ts: idempotenter Erst-Admin-Seed.
- Typen: src/types/next-auth.d.ts.
- Tests: Unit (password/roles/rate-limit) gruen; E2E-Gating-Spec geschrieben
  (deferred, kein Server/DB in Sandbox).

Offline verifiziert: tsc --noEmit, next lint, next build, drizzle-kit check,
vitest (13 Unit-Tests) je ohne Fehler; Edge-Safety-Grep ohne DB/argon2-Import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:17:02 +02:00