--- stepsCompleted: [1, 2, 3, 4, 5, 6, 7, 8] lastStep: 8 status: 'complete' completedAt: '2026-05-25' inputDocuments: - prds/prd-projectmanagement-2026-05-25/prd.md - prds/prd-projectmanagement-2026-05-25/addendum.md - prds/prd-projectmanagement-2026-05-25/.decision-log.md - prds/prd-projectmanagement-2026-05-25/inputs/ToolCTRL_Projektmanagement-Leitfaden_v3.0.pdf - prds/prd-projectmanagement-2026-05-25/inputs/unternehmens-cockpit.html - prds/prd-projectmanagement-2026-05-25/extract-prototype-html.md workflowType: 'architecture' project_name: 'projectmanagement' user_name: 'Dejancosic' date: '2026-05-25' binding_frame: 'ToolCTRL Projektmanagement-Leitfaden v3.0 (Mai 2026)' prd_status: 'final' --- # Architecture Decision Document — ToolCTRL Unternehmens-Cockpit _Dieses Dokument wächst kollaborativ Schritt für Schritt. Sektionen werden im Verlauf der Discovery angehängt._ --- ## Project Context Analysis ### Requirements Overview **Funktionale Anforderungen (49 FRs in 10 Gruppen):** Das Cockpit ist eine Drei-Tier-Übersichtsanwendung mit drei klar getrennten Sichten: 1. **Puls** (FR-1 bis FR-7): Aggregierte Unternehmenssicht — Bereichsstatus, Heute-Quote, Pflicht-Minimum-Übersicht, Jourfixe-Hinweis, bereichsübergreifende Blocker. 2. **Bereichs-Detail-Views** (FR-8 bis FR-20): Drei fachspezifische Sichten — Entwicklung (Projekte+Meilensteine), Vertrieb (Kanban+gewichtete Pipeline), Marketing (Funnel+Kampagnen). Crosslink Marketing↔Vertrieb. 3. **Tagesrhythmus** (FR-21 bis FR-33): Zweistufiger Workflow — Morgen-Einstempeln+ToDos, Abend-Reflexion mit Ampel/Trend, nächster-Werktag-Publikation. Querschnittsbereiche: Streak (F), Pflicht-Minimum (G), Blocker (H), Projekt-Abschluss (I), Navigation (J). **Nicht-funktionale Anforderungen (16 NFRs):** Architekturtreibend sind primär: - **NFR-1/2** (Performance): Puls < 1,5 s, Update-Speichern < 500 ms — moderate Anforderungen bei 6-15 Concurrent Users; kein Skalierungsdruck. - **NFR-5** (Auth): Pflicht-Integration mit **Authentik** (OIDC) — vorhandene Infrastruktur, kein eigenes Identity-Management. - **NFR-7** (Audit): Mutationsereignisse mit `actor` / `timestamp` / `payload` — Audit-Tabelle Pflicht. - **NFR-10** (Plattform): Desktop-first, mobil-lesbar — keine Mobile-App im MVP. - **NFR-12/13** (Voice & Tone): Mikrocopy-Regeln im UI-Schicht verankern. - **NFR-14/15** (Daten): Postgres + tägliches Backup — Pflicht-Stack. - **NFR-16** (Erweiterbarkeit): Datenmodell muss Phase-2-Erweiterungen ohne Migrationen > 2 h tragen (Plane/Outline-API-Sync, Mobile, weitere Bereiche, Kommentare). **Skalierung & Komplexität:** - **Primärdomäne:** Full-Stack-Web-Anwendung (React-Frontend, DB-backed Backend, OIDC). - **Komplexitätsstufe:** mittel — kein Hyperscale, aber nicht-triviales Datenmodell mit einer interessanten Sichtbarkeitslogik (private DayEntry vor `published_at` → konsolidiert ab nächstem Werktagsmorgen). - **Architektonisch geschätzte Komponenten:** ~6–8 (Frontend-SPA, Auth-Adapter, REST-API, DB-Schicht, Aggregations-/Caching-Schicht, optionaler Worker für Publikation/Streak, Audit-Logger, Hosting/Deployment). ### Technische Constraints & Abhängigkeiten **Hart vorgegeben (Pflicht-Stack):** - **Frontend:** React (PRD-Default, Team-Kompetenz). - **Persistenz:** Postgres (PRD NFR-14). - **Auth:** Authentik OIDC auf `auth.tool-ctrl.de` (PRD NFR-5, Addendum §1.3). - **Hosting:** vermutlich identische interne Infrastruktur wie Plane/Outline/Gitea (PRD §1.1 Tool-Aufteilung; Addendum §1.1). - **Sprache:** Deutsch durchgängig im UI (PRD NFR-11). **Methodisch verbindlich (Leitfaden v3.0):** - Cockpit muss die Tool-Aufteilung respektieren — kein Spiegel von Plane, kein Steckbrief-Editor (PRD Anti-Ziele A3/A4/A5). - Pflicht-Minimum-Check (Leitfaden §9) ist Erstklass-Feature, nicht optional. **Phase-2-Vorbereitung (architektonisch zu berücksichtigen):** - Plane-API-Sync (Pflicht-Punkt 3 Auto-Validierung). - Outline-API-Sync (Pflicht-Punkt 1 Auto-Validierung). - Lessons-Learned-Vorlage automatisch in Outline anlegen beim Abschluss. **Externe Systeme im laufenden MVP:** - Authentik (Pflicht, kritisch). - Plane/Outline (nur Verlinkung, keine API-Aufrufe im MVP — aber URL-Felder schon im Schema). ### Cross-Cutting Concerns 1. **Identität (Authentik OIDC).** Affiziert: alle Routen, User-Mapping, Default-Bereich per Group-Claim (OQ-1). 2. **Audit-Trail.** Affiziert: jede mutierende Operation (Update-Reflexion, Blocker-Lifecycle, Meilenstein-Statuswechsel, Pflicht-Minimum-Änderungen). 3. **Sichtbarkeitsmodell der DayEntries.** Sonderfall: pre-publication privat (nur Self + Projekt-P1); post-publication für alle Kollegen. Erfordert Filter-Logik in der API-Schicht, kann nicht reine Row-Level-Security sein. 4. **Status-Caches (`status_cache`, `trend_cache`).** Denormalisierungen, die aus letzter Reflexion abgeleitet sind. Konsistenz: bei jeder Reflexion Cache aktualisieren. 5. **Pflicht-Minimum-State.** Pro Projekt ein JSON-Block mit 6 Booleans. Punkt 5 (Status-Ampel aktuell) wird zeitabhängig (≤ 7 Werktage) → muss periodisch oder lazy berechnet werden. 6. **Werktags-/Tageszeitlogik.** Streak (FR-34), „Heute fehlt"-Liste (FR-35), Tages-Status (FR-33), Publikation (FR-29) — alle abhängig von einer konsistenten Werktagsdefinition (Mo-Fr, ohne Feiertage im MVP). Server-Zeit `Europe/Berlin` als Wahrheit. 7. **Internationalisierung.** Bewusst keine — Deutsch durchgängig (NFR-11). Brand-konsistente UI-Texte (NFR-12/13) als Constraint im Frontend. ### Architekturrelevante Open Questions aus PRD - **OQ-1** Authentik-Group-Claim → Cockpit-Bereich-Mapping. Architektur entscheidet Default-Strategie (z. B. erste passende Group, manuelle Korrektur im Profil). - **OQ-5** Archivierungs-Policy: Historie für immer behalten oder nach X Monaten löschen? Hat Auswirkungen auf Datenwachstum, Backup-Strategie, evtl. Soft-Delete vs. Hard-Delete. - **OQ-6** Pflicht-Minimum-Punkt 1 — manuelle Checkbox vs. Outline-API-Polling. PRD setzt manuelle Checkbox im MVP; Architektur baut so, dass spätere API-Validierung ohne Schemaänderung möglich ist. --- ## Starter Template Evaluation ### Primary Technology Domain Full-stack Web-Anwendung mit React-SPA-Frontend, Fastify-Backend, Postgres-Persistenz und OIDC-Auth. Self-hosted in der ToolCTRL-internen Infrastruktur. ### Constraint: React (SPA), nicht Next.js Per Team-Vorgabe wird **React als SPA** eingesetzt, **kein Next.js**. Das schließt Next.js-zentrierte Starter (T3 Stack, RedwoodJS, Blitz) aus und führt zu einer zweiteiligen Architektur: ein Vite-React-Frontend und ein separates Fastify-Backend. ### Starter Options Considered | Option | Bewertung | |---|---| | T3 Stack (Next.js) | ❌ Ausgeschlossen — Team-Vorgabe React-SPA, kein Next.js | | Custom Vite + React + Fastify-Backend Monorepo | ✅ **Gewählt** — passt zur React-SPA-Vorgabe; kein passender vorgefertigter Starter, also definieren wir die Struktur selbst | | Create React App (CRA) | ❌ Nicht mehr aktiv gewartet (2026), Vite ist Standard | | Remix | ❌ Auch ein Full-Stack-React-Framework wie Next.js — gleicher Ausschlussgrund | ### Selected Starter: Custom Monorepo (Vite + React + Fastify) Es gibt keinen einzelnen `npm create …`-Befehl, der genau diesen Schnitt liefert. Wir initialisieren die Repo-Struktur selbst und ziehen die Tools über pnpm rein. **Rationale:** - Erfüllt die Team-Vorgabe **React-SPA, kein Next.js**. - tRPC v11 funktioniert unverändert über den Fastify-Adapter — End-to-End-Typsicherheit bleibt erhalten (Frontend und Backend teilen sich den `AppRouter`-Typ via `packages/shared`). - Sauberer Schnitt zwischen Frontend (`apps/web`) und Backend (`apps/api`) erlaubt unabhängige Deploys und Background-Job-Endpoints im Backend ohne Frontend-Build. - Drizzle bleibt als ORM — leichter, schnellere Migrationen (NFR-16: max 2 h). - Authentik-OIDC über `openid-client` direkt im Fastify (Auth.js v5 hat keinen ausgereiften Fastify-Adapter, Express-Adapter wäre eine Brücke). - Tailwind im Vite-Frontend für Brand-Token-Theming. ### Repo-Struktur (Monorepo, pnpm workspaces) ``` cockpit/ ├── apps/ │ ├── web/ # Vite + React + TypeScript + Tailwind │ │ ├── src/ │ │ │ ├── routes/ # React Router v7 Routes │ │ │ ├── components/ │ │ │ ├── lib/trpc.ts # tRPC-Client │ │ │ └── main.tsx │ │ └── vite.config.ts │ └── api/ # Node + Fastify + tRPC server + Drizzle │ ├── src/ │ │ ├── routers/ # tRPC-Router (procedures) │ │ ├── auth/ # Authentik OIDC via openid-client │ │ ├── cron/ # Background-Job-Endpoints │ │ ├── server.ts │ │ └── trpc.ts │ └── tsconfig.json ├── packages/ │ ├── db/ # Drizzle-Schema + Migrationen │ │ ├── schema.ts │ │ ├── drizzle.config.ts │ │ └── migrations/ │ └── shared/ # AppRouter-Typ, Zod-Schemata, Konstanten │ └── src/ ├── docker-compose.yml ├── Caddyfile ├── pnpm-workspace.yaml └── package.json ``` ### Initialization Commands ```bash # Repo anlegen mkdir cockpit && cd cockpit pnpm init git init # Workspaces mkdir -p apps/web apps/api packages/db packages/shared echo 'packages:\n - "apps/*"\n - "packages/*"' > pnpm-workspace.yaml # Frontend cd apps/web pnpm create vite . --template react-ts pnpm add react-router @trpc/client @trpc/react-query @tanstack/react-query \ tailwindcss postcss autoprefixer zod pnpm add -D @types/react @types/react-dom # Backend cd ../api pnpm init pnpm add fastify @fastify/cookie @trpc/server openid-client \ drizzle-orm pg pino zod pnpm add -D typescript tsx @types/node @types/pg drizzle-kit # Shared cd ../../packages/shared pnpm init pnpm add zod ``` ### Architectural Decisions Provided by Custom Starter - **Language:** TypeScript (strict mode, ES2022 target). - **Frontend Framework:** React 18+ via Vite 5+. - **Frontend Routing:** React Router v7. - **Backend Framework:** Fastify 5+. - **API Layer:** tRPC v11 (typsicher, Frontend ↔ Backend per `AppRouter`-Typ-Sharing). - **ORM:** Drizzle (Postgres-Adapter). - **Auth:** `openid-client` (Authentik OIDC, Authorization Code Flow + PKCE), Session-Cookies (HttpOnly, SameSite=Lax), Session-Store in Postgres. - **Styling:** Tailwind CSS. - **Build:** Vite für Frontend, `tsx` (Dev) + `tsc` (Prod) für Backend. - **Validation:** Zod (im `shared`-Package, beidseitig genutzt). - **State Management:** React Query (Server-State), React Context + `useState`/`useReducer` (Client-State). **Note:** Die erste Implementation-Story (in `bmad-create-epics-and-stories`) ist das Bootstrap dieser Monorepo-Struktur mit obigen Befehlen. **Bewusst NICHT im Starter:** - Realtime/WebSockets (kein Realtime-Bedarf im MVP). - i18n (Deutsch durchgängig, NFR-11). - SSR (per Vorgabe SPA). - Background-Worker-Service — wir nutzen System-Cron → Fastify-Endpoint (siehe Step 4). --- ## Core Architectural Decisions ### Decision Priority Analysis **Critical Decisions (blocken Implementierung — alle entschieden):** - Backend-Framework, ORM, Auth-Flow, Repo-Struktur, Hosting, Background-Jobs. **Important Decisions (formen die Architektur — alle entschieden):** - Authorization-Pattern, Status-Cache-Strategie, Logging, Tests, CI/CD, Reverse-Proxy. **Deferred Decisions (Post-MVP):** - Monitoring/Alerting jenseits Healthcheck (Loki/Grafana — wenn das Tool wirklich genutzt wird). - Rate-Limiting (im internen Tool aktuell nicht nötig). - Outline-/Plane-API-Sync (Phase 2 — Skeleton im Schema vorhanden, Umsetzung später). ### Data Architecture - **DB-Engine:** PostgreSQL 16+ (verifiziert für `pg_cron`-Verfügbarkeit, JSONB-Support). - **ORM:** Drizzle (vom Starter) — Schema in `packages/db/schema.ts`, Migrationen via `drizzle-kit generate` und `drizzle-kit migrate`. Migrationen im Repo eingecheckt. - **Validation:** Zod, im `packages/shared` definiert; beidseitig (Backend tRPC-Input + Frontend Form-Resolver) genutzt. - **Status-Caches:** `Project.status_cache`, `Project.trend_cache`, `Project.pflicht_minimum_state` als reguläre Postgres-Spalten (`jsonb` für den Pflicht-Minimum-State). **Kein Redis** — nicht nötig bei 6–15 Usern. - **Encryption at Rest:** Postgres-Volume-Encryption via Docker-Volume auf verschlüsseltem Host-Filesystem. - **Backup:** `pg_dump` nightly via System-Cron im Postgres-Container, Dumps auf separates Volume; Retention 14 Tage. Restore-Drill quartalsweise (siehe Operative Notes). ### Authentication & Security - **Provider:** Authentik (`auth.tool-ctrl.de`) als externer OIDC-IdP. - **Client-Library:** `openid-client` v6+ direkt im Fastify-Backend (Auth.js v5 hat keinen reifen Fastify-Adapter; `openid-client` ist die referenzierte Library hinter den meisten OIDC-Implementierungen). - **Flow:** Authorization Code Flow + PKCE. - **Session:** Server-seitige Session im Postgres (`Session`-Tabelle), Browser bekommt nur `session_id` als HttpOnly + SameSite=Lax + Secure Cookie. **Keine JWTs im Cookie** — einfacher zu invalidieren, weniger Tokens im Umlauf. - **User-Mapping:** beim ersten Login wird `User`-Zeile angelegt mit `authentik_sub` als externer Schlüssel; Name/Email aus dem ID-Token-Claim; Default-Bereich aus dem Authentik-Group-Claim (Mapping in Implementation festzulegen — siehe OQ-1). - **Authorization-Pattern (tRPC-Procedures):** - `publicProcedure` — nur Login-Routen. - `protectedProcedure` — Standard, prüft Session. - `dayEntryVisibleProcedure` — wrapt `protectedProcedure`, filtert DayEntries: vor `published_at` nur eigene + (falls Bezug=Projekt) Projektverantwortliche/r; ab `published_at` für alle Kollegen. - `projectOwnerProcedure` — wrapt `protectedProcedure`, prüft, dass aktueller User der P1 des in `input` referenzierten Projekts ist (z. B. für Blocker-Auflösung FR-44, Projekt-Abschluss FR-45). - **CSRF-Schutz:** SameSite=Lax-Cookies + `Origin`-Header-Prüfung im Fastify-Pre-Handler. Kein separates Anti-CSRF-Token nötig, da keine Cross-Origin-Requests vorgesehen. - **TLS:** über Caddy als Reverse-Proxy (Automatic HTTPS via Let's Encrypt). - **Rate-Limiting:** im MVP **nicht eingebaut**. Authentik selbst limitiert Login-Versuche. Falls später nötig: `@fastify/rate-limit`. ### API & Communication Patterns - **API-Layer:** tRPC v11 — Backend exportiert `AppRouter` aus `apps/api/src/trpc.ts`; Frontend importiert den Typ aus `packages/shared` und nutzt `createTRPCReact()`. - **Transport:** HTTP/HTTPS, `POST` für Mutations und Queries (tRPC-Standard). - **Error Handling:** `TRPCError`-Subklassen mit deutschen `message`-Strings für menschenlesbare Fehler; Frontend zeigt sie via einem zentralen `onError`-Hook in React Query als Toast (Voice & Tone: NFR-12/13). - **API-Doku:** Tdie tRPC-Typen sind die Dokumentation. Keine separate OpenAPI/Swagger-Schicht im MVP. - **Versionierung:** keine — interner Monolith, Frontend und Backend immer im Gleichschritt deployt. - **Inter-Service-Kommunikation:** entfällt — Monolith. ### Frontend Architecture - **Framework:** React 18+ als SPA via Vite 5+. - **Routing:** React Router v7 (Data Router API, `loader`/`action` für serverseitige Datenvorbereitung sofern sinnvoll; Standard sind aber tRPC + React Query). - **Server-State:** React Query via `@trpc/react-query`. Cache-Invalidierung pro tRPC-Procedure-Key. - **Client-State:** React Context für `user` (aktueller eingeloggter User) und Theme-/Brand-Tokens. Lokaler UI-State via `useState`/`useReducer`. **Kein globaler Store (Redux/Zustand)** — bewusst nicht nötig. - **Forms:** React Hook Form + Zod-Resolver (Schemata aus `packages/shared`). - **Styling:** Tailwind CSS mit Custom-Theme in `tailwind.config.ts` — Brand-Tokens (weiß primär, schwarz sekundär) als CSS-Variablen + Tailwind-Erweiterungen. Konkrete Token-Werte aus `bmad-create-ux-design` (OQ-2 im PRD). - **Components:** Headless-UI-Library (`@headlessui/react` oder `radix-ui` Primitives) für komplexe Interaktionen (Modals, Dropdowns, Tabs). Konkrete Wahl in UX-Phase. - **Performance:** Code-Splitting per Route via React Router lazy-Loading; Tailwind purging im Vite-Build; Bundle-Größe-Budget < 300 kB initial. ### Infrastructure & Deployment - **Hosting:** **Bestehende ToolCTRL-interne Infrastruktur** — gleicher Docker-Host wie Plane/Outline/Authentik/Gitea. - **Container:** Multi-Stage-Dockerfiles pro App. - `apps/web` → Vite-Build → Static-Files-Serving via Caddy. - `apps/api` → Node 22+ Container (LTS), startet Fastify auf Port 3000. - `postgres:16` als separates Container (oder bestehende Postgres-Instance, falls vorhanden — in Implementation entscheiden). - **Reverse-Proxy:** **Caddy 2.x** mit Subdomain `cockpit.tool-ctrl.de`. Caddyfile: - Statische Web-Assets aus `apps/web/dist`. - `/api/*` und `/trpc/*` reverse-proxy auf `api:3000`. - Automatic HTTPS. - **Background-Jobs:** **System-Cron im API-Container** (oder Host-Crontab) → ruft authentifizierten Fastify-Endpoint `POST /internal/cron/` mit Bearer-Token aus Env-Variable auf. - Jobs im MVP: - `publish-day-entries` — täglich 00:05 Europe/Berlin, setzt `published_at` auf alle DayEntries des Vortags. - `recompute-pflicht-minimum-5` — stündlich, prüft pro Projekt, ob letzter Status-Update ≤ 7 Werktage alt. - `recompute-streaks` — täglich 00:10, aktualisiert User-Streaks. - `pg-dump-backup` — täglich 02:00. - Rationale für die Wahl: User hat "weiß nicht" geantwortet → Architekt wählt den einfachsten, wartbarsten Pfad. Cron + Endpoint hat den besten Debug- und Observability-Workflow (Logs im API-Container, Endpoint manuell anstoßbar). - **CI/CD:** **Gitea Actions** (Gitea ist im Authentik-Stack vorhanden). Pipeline: 1. `pnpm install` 2. Typecheck (`tsc --noEmit`) 3. Lint (`eslint`) 4. Tests (`vitest run`, Playwright nur bei Tag/Release) 5. Build (Vite + tsc) 6. Docker-Images bauen und in lokale Registry pushen 7. Deploy via `docker-compose pull && up -d` - **Environment Config:** `.env.local` (dev, ignored), `.env.production` (prod, Docker-Secret-Mount). Validation via `zod` beim Backend-Startup (Fail-Fast bei fehlenden Env-Variablen). - **Logging:** strukturierte JSON-Logs via `pino` → stdout → Docker-Logs. Aggregation (Loki) Post-MVP. - **Monitoring:** `GET /api/health` (Postgres-Ping + Authentik-Reachability) + Docker-Healthcheck. Externe Uptime-Überwachung optional Post-Launch. - **Scaling:** kein Skalierungspfad nötig (≤ 30 User in 12 Monaten — A-3). ### Testing-Strategie - **Unit-/Integration-Tests:** Vitest. Schwerpunkt auf tRPC-Procedures (Backend) und Zod-Schemas/Pure-Funktionen (Frontend). DB-nahe Tests gegen eine Test-Postgres-Instance (`testcontainers` oder lokaler Docker). - **E2E:** Playwright auf die kritischen User Journeys UJ-1 (Tagesrhythmus), UJ-2 (Jourfixe), UJ-5 (Projekt anlegen). Laufen in CI nur auf Tag/Release. - **Test-Daten:** Seed-Skript in `packages/db/seed.ts`, idempotent. Erzeugt 5 Test-User, 3 Projekte pro Bereich, einige DayEntries. ### Operative Notes - **Werktage / Zeitzone:** Server-Zeit `Europe/Berlin`. Werktage hart in einer Konstante (`MO-FR`), keine Feiertags-Erkennung im MVP (OQ aus PRD, Phase 2). - **Backup-Restore-Drill:** quartalsweise — Dump auf separaten Container restoren, Smoke-Test der App gegen die Kopie. - **Datenwachstum:** Schätzung 10 DayEntries × 15 User × 220 Werktage = 33.000 Zeilen/Jahr in `DayEntry`; vergleichbar in `Todo` (3-5× mehr). Selbst nach 5 Jahren < 1 Mio. Zeilen — keine Partitionierung nötig. ### Decision Impact Analysis **Implementation Sequence (Story-1 → Story-N):** 1. Repo-Bootstrap (Monorepo mit pnpm-Workspaces, Vite, Fastify, Drizzle, Tailwind). 2. Drizzle-Schema + erste Migration (alle Entitäten aus Addendum §1.2). 3. Fastify-Server mit Health-Endpoint. 4. Authentik-OIDC-Login-Flow (`openid-client`, Session in DB). 5. tRPC-Bootstrap mit `protectedProcedure` und User-Context. 6. Frontend-Shell (React Router + Layout + Login-Redirect). 7. Tagesrhythmus-Slice (Einstempeln + ToDos + Reflexion) — UJ-1. 8. Puls-Seite. 9. Bereichs-Detail-Views (Dev/Sales/Marketing). 10. Pflicht-Minimum-Feature + Blocker. 11. Background-Jobs (Cron-Endpoints, Cron-Definition im Docker-Compose). 12. Projekt-Abschluss + Archiv. 13. Brand-Theming (auf Basis der UX-Design-Tokens). 14. CI/CD-Pipeline + Deployment. **Cross-Component Dependencies:** - Auth (Schritt 4) ist Voraussetzung für jede `protectedProcedure` (alle Folgesteps). - Tagesrhythmus (Schritt 7) erzeugt DayEntries, die der Puls (Schritt 8) und Bereichs-Detail-Views (Schritt 9) anzeigen. - Pflicht-Minimum-Feature (Schritt 10) braucht Projekt-Entität (Schritt 9). - Background-Jobs (Schritt 11) brauchen alle vorherigen Schemata und sind orthogonal zur UI. --- ## Implementation Patterns & Consistency Rules > Diese Regeln sind **bindend** für alle Code-erzeugenden Agenten und Menschen. Sie verhindern Konflikte, wenn parallel an mehreren Stories gearbeitet wird. ### Naming Patterns #### Database (Drizzle + Postgres) - **Tabellen:** Singular, `snake_case`, englisch — `user`, `project`, `day_entry`, `todo`, `blocker`. Nicht `users`, nicht `DayEntry`. - **Spalten:** `snake_case` — `user_id`, `created_at`, `pflicht_minimum_state`. Domain-Begriffe dürfen deutsch sein (`abwesend_bis`, `arbeitsmodus`), Audit-Felder bleiben englisch. - **Primärschlüssel:** immer `id` (UUID v7 — zeitgeordnet, indextauglich). Nicht `_id`. - **Fremdschlüssel:** `_id` — `user_id`, `project_id`. Nicht `fk_user`. - **Indizes:** `idx__` — `idx_day_entry_user_id`. Eindeutige: `uniq_
_`. - **Enums:** als TypeScript-Union in Drizzle-Schema, in Postgres als `text` mit `check`-Constraint (einfacher zu erweitern als `pg_enum`). - **Zeitstempel:** `created_at`, `updated_at`, `_at` (`reflected_at`, `published_at`). Type `timestamptz`. Default `now()`. #### tRPC (Backend-API) - **Router-Datei:** `apps/api/src/routers/.ts` — `day-entry.ts`, `project.ts`, `blocker.ts`. - **Router-Variable:** `Router` als `camelCase` — `dayEntryRouter`, `projectRouter`. - **Procedure-Naming:** Verb-Substantiv in `camelCase` — `dayEntry.checkIn`, `dayEntry.submitReflection`, `project.archive`, `blocker.resolve`. - Reads: `list`, `byId`, `mine`, `forProject`. - Mutations: `create`, `update`, `archive` (statt `delete`), `resolve`, `checkIn`, `submitReflection`. - **Procedure-Inputs:** immer Zod-Schema, importiert aus `packages/shared/schemas/.ts`. Nicht inline. - **Procedure-Outputs:** immer explizit typisiert via Zod (vermeidet versehentliche Schema-Drift). #### Frontend (React) - **Komponenten:** PascalCase — `DayEntryForm`, `ProjectCard`, `BlockerList`. Dateiname matched: `DayEntryForm.tsx`. - **Hooks:** `use` camelCase — `useCurrentUser`, `useTodaysEntry`, `useProjectAmpel`. - **Utility-Funktionen:** `camelCase`, Dateiname `kebab-case` — `format-werktag.ts` exportiert `formatWerktag()`. - **Konstanten:** `SCREAMING_SNAKE_CASE` — `MAX_TODO_TEXT_LENGTH = 200`, `WERKTAGE = ['MO','DI','MI','DO','FR']`. - **Route-Pfade:** `kebab-case`, deutsch wo Domain-Begriff — `/puls`, `/bereich/entwicklung`, `/mein-heute`, `/projekt/:id`. - **CSS-Klassen via Tailwind:** keine eigenen Klassennamen außer Brand-Token-Variablen (`bg-brand-primary`). #### Files & Folders - **Frontend-Komponenten:** `apps/web/src/components//.tsx`. - **Frontend-Routen-Komponenten:** `apps/web/src/routes/.tsx`. - **Backend-Router:** `apps/api/src/routers/.ts`. - **Shared Schemas:** `packages/shared/schemas/.ts`. - **DB-Schema:** alle Tabellen in einer Datei: `packages/db/schema.ts`. Bei > 500 Zeilen splitten nach Feature. ### Structure Patterns - **Tests** liegen **neben dem Code**: `DayEntryForm.tsx` ↔ `DayEntryForm.test.tsx`. Backend: `day-entry.ts` ↔ `day-entry.test.ts`. - **E2E-Tests:** zentral in `apps/web/e2e/.spec.ts` (UJ-bezogen — `uj-1-tagesrhythmus.spec.ts`). - **Komponenten-Organisation:** nach **Feature**, nicht nach Typ. Nicht `components/forms/` und `components/cards/`, sondern `components/day-entry/`, `components/project/`. - **Shared-Utility-Funktionen:** `apps/web/src/lib/` (Frontend), `apps/api/src/lib/` (Backend). Wirklich shared zwischen beiden → `packages/shared/`. - **Konfig-Dateien** im Repo-Root (`tsconfig.json`, `.eslintrc.cjs`, `prettier.config.js`, `tailwind.config.ts`, `drizzle.config.ts`). Pro App nur überschreibende Configs. ### Format Patterns - **JSON-Felder über die Wire:** `camelCase` (tRPC serialisiert TS-Property-Namen 1:1). Drizzle mapt DB-`snake_case` → TS-`camelCase` automatisch via `columnName` Option. - **Datums-Format:** **ISO 8601 mit Zeitzone** — `2026-05-25T14:30:00+02:00`. Im Frontend mit `Intl.DateTimeFormat('de-DE')` formatiert (nicht moment, nicht dayjs — `Intl` reicht). - **Werktage-Format:** Pures `YYYY-MM-DD` (kein Zeitanteil) für `DayEntry.date`, `AbsenceDay.date`. In Drizzle als `date`-Typ (nicht `timestamptz`). - **Geldbeträge:** `value_eur` als `integer` in Cent (`bigint` wenn > 21 Mio. €). Frontend formatiert via `Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' })`. - **Booleans:** echtes `boolean`. Nicht `0`/`1`, nicht `"yes"`/`"no"`. - **Null vs. Undefined:** **Datenbank speichert `null`**, **TypeScript-Code nutzt `null` für „explizit nicht gesetzt", `undefined` für „existiert nicht"**. Konsistent durchziehen. - **IDs in der URL:** UUIDs als Param — `/projekt/:id` mit `id` = UUID-String. Keine numerischen IDs öffentlich. ### Communication Patterns #### Error Handling - **Backend:** `TRPCError` mit klaren `code`s: - `UNAUTHORIZED` — kein Login - `FORBIDDEN` — Login aber falsche Rolle (z. B. nicht P1 für Blocker-Auflösung) - `NOT_FOUND` — Entität existiert nicht oder ist außerhalb der Sicht - `BAD_REQUEST` — Zod-Validation gescheitert (tRPC mappt das automatisch) - `INTERNAL_SERVER_ERROR` — alles Unerwartete - **Error-Message:** **immer deutsch**, menschenlesbar, nicht technisch. Nicht „NULL constraint violated on `user_id`", sondern „Bitte wähle einen Verantwortlichen aus". - **Frontend:** zentraler `onError`-Hook in der tRPC-Client-Provider-Config zeigt `error.message` als Toast (Voice & Tone NFR-12/13). Bei `UNAUTHORIZED` → Redirect auf Login. - **Logging:** Backend loggt jeden `INTERNAL_SERVER_ERROR` mit Stack via `pino` Level `error`. `BAD_REQUEST` ist Level `info`, nicht `error`. #### Loading States - **Server-State:** ausschließlich React Query (`isPending`, `isError`, `data`). Niemals eigene `useState`-Booleans für Server-Loading. - **Skeleton-Pattern:** Listen und Cards zeigen Skeletons (`SkeletonCard`), nicht „Lädt …"-Text. - **Optimistic Updates:** für Tagesupdate-Reflexion und Blocker-Auflösung — fühlt sich schneller an. Für Projekt-Anlegen nicht (User braucht Server-bestätigte ID). - **Mutation-Loading:** Button wird disabled mit kleinem Spinner. Kein Modal-Overlay. #### State Management - **Server-State:** React Query via tRPC. Cache-Key = Procedure-Path + Input. Invalidierung pro Procedure. - **Auth-State:** React Context `` mit aktuellem `User`-Objekt. Geladen via `protectedProcedure user.me` einmal beim Mount. - **Brand/Theme:** CSS-Variablen im `:root`, kein State-Container. - **Lokaler UI-State:** `useState`/`useReducer` in der Komponente. Wenn State zwischen Sibling-Komponenten gebraucht wird → in Parent hochziehen, kein Context. - **Forms:** React Hook Form pro Form, Zod-Resolver mit Schema aus `packages/shared`. #### Events / Audit - **AuditEvent-Naming:** `.` (lowercase, dot-separated) — `day_entry.reflection_submitted`, `blocker.resolved`, `project.archived`. - **Payload:** strukturiertes JSONB mit `before` und `after` (für Mutations). Erzeugt im Backend mittels eines `withAudit`-Middleware-Wrappers um tRPC-Mutations. ### Process Patterns - **Werktag-Berechnungen:** **Alle Datumslogik im Backend**, niemals im Frontend. Frontend bekommt fertige Felder (`isWorkday`, `nextWorkday`, `streakDays`) aus dem API. - **Sichtbarkeitsfilter (DayEntry):** **immer im Backend in der `dayEntryVisibleProcedure`**, nie als Frontend-Filter. Frontend zeigt, was die API liefert. - **Pflicht-Minimum-Berechnung:** **lazy im Backend** beim Lesen des Projekts (keine periodische Re-Berechnung der Punkte 1-4; Punkt 5 wird im Cron-Job aktualisiert). - **Status-Cache-Aktualisierung:** bei jeder Reflexions-Mutation in derselben DB-Transaktion. Niemals separat. ### Enforcement Guidelines **Alle Code-erzeugenden Agenten MÜSSEN:** - TypeScript strict mode beachten; keine `any`. Bei wirklich unbekannter Form: `unknown` + Zod-Validation. - Zod-Schemas aus `packages/shared` importieren, niemals duplizieren. - Bei DB-Änderung **immer** eine Drizzle-Migration committen (kein Schema-Drift). - Tests neben Code schreiben (`*.test.ts`). - Voice & Tone aus PRD NFR-12/13 in jeder neuen UI-Copy. - Audit-Wrapper für jede schreibende tRPC-Procedure verwenden. **Pattern-Verstöße werden in PR-Review angesprochen.** Ein zentrales `CONVENTIONS.md` im Repo-Root spiegelt diese Sektion und ist Pflicht-Lesen vor Story-Bearbeitung. ### Pattern Examples **Good:** ```ts // packages/shared/schemas/day-entry.ts export const submitReflectionInput = z.object({ dayEntryId: z.string().uuid(), reflectionText: z.string().max(1000), ampel: z.enum(['green','amber','red']), trend: z.enum(['besser','gleich','schlechter']).nullable(), blockerText: z.string().optional(), url: z.string().url().optional(), }); export type SubmitReflectionInput = z.infer; // apps/api/src/routers/day-entry.ts export const dayEntryRouter = router({ submitReflection: protectedProcedure .input(submitReflectionInput) .mutation(async ({ ctx, input }) => { /* … */ }), }); ``` **Anti-Pattern (verboten):** ```ts // ❌ Inline-Zod im Procedure, kein shared Schema .input(z.object({ id: z.string(), text: z.string() })) // ❌ Englisches Error-Message throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid input' }); // ❌ snake_case-Component export const day_entry_form = () => { … } // ❌ Server-Loading via useState const [loading, setLoading] = useState(false); // statt React Query's isPending ``` --- ## Project Structure & Boundaries ### Complete Project Directory Structure ``` cockpit/ ├── README.md ├── CONVENTIONS.md # Spiegel der Patterns-Sektion. Pflicht-Lesen. ├── package.json # Root package.json (private, workspaces) ├── pnpm-workspace.yaml ├── pnpm-lock.yaml ├── tsconfig.base.json # Geerbt von allen Paketen ├── .eslintrc.cjs ├── .prettierrc ├── .editorconfig ├── .gitignore ├── .env.example # Template für alle Apps ├── docker-compose.yml # web + api + postgres + caddy ├── docker-compose.dev.yml # Override für lokale Entwicklung ├── Caddyfile ├── .gitea/ │ └── workflows/ │ ├── ci.yml # Typecheck, Lint, Test, Build │ └── deploy.yml # Manuelle Deploy-Trigger ├── apps/ │ ├── web/ # ───── FRONTEND (Vite + React SPA) ───── │ │ ├── package.json │ │ ├── vite.config.ts │ │ ├── tsconfig.json │ │ ├── tailwind.config.ts # Brand-Tokens hier (weiß primär, schwarz sekundär) │ │ ├── postcss.config.js │ │ ├── index.html │ │ ├── Dockerfile # Multi-Stage Vite-Build → Caddy serves dist/ │ │ ├── public/ │ │ │ ├── favicon.svg │ │ │ └── assets/ # Brand-Logos (von ToolCTRL Brand Identity) │ │ ├── src/ │ │ │ ├── main.tsx # ReactDOM root, Router, tRPC Provider │ │ │ ├── App.tsx # Outer Layout + Auth-Gate │ │ │ ├── styles/ │ │ │ │ └── globals.css # Tailwind + CSS-Variablen │ │ │ ├── lib/ │ │ │ │ ├── trpc.ts # createTRPCReact() │ │ │ │ ├── router.tsx # React Router v7 routes config │ │ │ │ ├── format.ts # Datums-/EUR-Formatierung via Intl │ │ │ │ └── voice.ts # UI-Strings (NFR-12/13 Voice & Tone) │ │ │ ├── context/ │ │ │ │ └── user-context.tsx # │ │ │ ├── routes/ # React-Router-Page-Components │ │ │ │ ├── login.tsx │ │ │ │ ├── puls.tsx # FR-1 bis FR-7 │ │ │ │ ├── bereich.$slug.tsx # FR-6 → FR-8…FR-20 │ │ │ │ ├── projekt.$id.tsx # FR-10 + FR-38 Pflicht-Minimum │ │ │ │ ├── mein-heute.tsx # UJ-1, FR-22…FR-25 │ │ │ │ ├── reflexion.tsx # FR-26…FR-28 │ │ │ │ ├── profil.tsx │ │ │ │ ├── archiv.tsx # FR-47, FR-48 │ │ │ │ └── 404.tsx │ │ │ ├── components/ │ │ │ │ ├── layout/ │ │ │ │ │ ├── TopNav.tsx # FR-48 │ │ │ │ │ ├── AuthGate.tsx # OIDC-Redirect │ │ │ │ │ └── ToastProvider.tsx # Error-Toast (NFR-12/13) │ │ │ │ ├── puls/ │ │ │ │ │ ├── BereichOverviewStrip.tsx # FR-1, FR-1a │ │ │ │ │ ├── BlockerList.tsx # FR-2 │ │ │ │ │ ├── HeuteQuote.tsx # FR-3 │ │ │ │ │ ├── JourfixeHint.tsx # FR-4 │ │ │ │ │ ├── KeyMetrics.tsx # FR-5 │ │ │ │ │ └── PflichtMinimumOverview.tsx # FR-7 │ │ │ │ ├── bereich/ │ │ │ │ │ ├── EntwicklungDetail.tsx # FR-8 … FR-11 │ │ │ │ │ ├── VertriebDetail.tsx # FR-12 … FR-16 │ │ │ │ │ ├── MarketingDetail.tsx # FR-17 … FR-20 │ │ │ │ │ └── CrosslinkBanner.tsx # FR-19 │ │ │ │ ├── projekt/ │ │ │ │ │ ├── ProjektCard.tsx # FR-9 │ │ │ │ │ ├── ProjektDetail.tsx # FR-10 │ │ │ │ │ ├── MeilensteinChecklist.tsx # FR-11 │ │ │ │ │ ├── PflichtMinimumPanel.tsx # FR-38 - FR-40 │ │ │ │ │ ├── DeadlineProgressBar.tsx │ │ │ │ │ └── AmpelTrendBadge.tsx # FR-9 Ampel + Trend │ │ │ │ ├── deal/ │ │ │ │ │ ├── DealKanban.tsx # FR-12 │ │ │ │ │ ├── DealCard.tsx # FR-13 │ │ │ │ │ └── VertriebKpiHeader.tsx # FR-14 │ │ │ │ ├── gtm/ │ │ │ │ │ └── GtmStepList.tsx # FR-15 │ │ │ │ ├── campaign/ │ │ │ │ │ ├── LeadFunnel.tsx # FR-17 │ │ │ │ │ └── CampaignList.tsx # FR-18 │ │ │ │ ├── day-entry/ │ │ │ │ │ ├── CheckInButton.tsx # FR-21 │ │ │ │ │ ├── TodoEditor.tsx # FR-22, FR-25 │ │ │ │ │ ├── ReflectionForm.tsx # FR-26 │ │ │ │ │ ├── ConsolidatedDayEntry.tsx # FR-29 (Vortags-Eintrag) │ │ │ │ │ └── DayStatusBadge.tsx # FR-33 │ │ │ │ ├── streak/ │ │ │ │ │ ├── StreakBadge.tsx # FR-34 │ │ │ │ │ ├── FehltHeuteList.tsx # FR-35 │ │ │ │ │ └── AbwesendDialog.tsx # FR-37 │ │ │ │ ├── blocker/ │ │ │ │ │ ├── BlockerCard.tsx # FR-43 │ │ │ │ │ ├── BlockerCreateDialog.tsx # FR-41 │ │ │ │ │ └── BlockerResolveDialog.tsx # FR-44 (nur P1) │ │ │ │ ├── abschluss/ │ │ │ │ │ ├── ProjektAbschlussDialog.tsx # FR-45, FR-46 │ │ │ │ │ └── ArchivList.tsx # FR-47 │ │ │ │ └── ui/ # Primitive Building-Blocks │ │ │ │ ├── Button.tsx │ │ │ │ ├── Card.tsx │ │ │ │ ├── SkeletonCard.tsx │ │ │ │ ├── Dialog.tsx │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── Form/ # React Hook Form Wrappers │ │ │ ├── hooks/ │ │ │ │ ├── useCurrentUser.ts │ │ │ │ ├── useTodaysEntry.ts │ │ │ │ └── useToast.ts │ │ │ └── e2e/ # Playwright-Specs (UJ-1, UJ-2, UJ-5) │ │ │ ├── uj-1-tagesrhythmus.spec.ts │ │ │ ├── uj-2-jourfixe.spec.ts │ │ │ └── uj-5-projekt-anlegen.spec.ts │ ├── api/ # ───── BACKEND (Fastify) ───── │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── Dockerfile # Multi-Stage Node 22+ + Cron │ │ ├── crontab # System-Cron-Definitionen (FR-29, Streak, Pflicht-Min-5) │ │ ├── src/ │ │ │ ├── server.ts # Fastify-App-Bootstrap │ │ │ ├── env.ts # Zod-validierte Env-Vars │ │ │ ├── trpc.ts # createTRPCBuilder + Context │ │ │ ├── context.ts # Per-Request-Context (user, db, req) │ │ │ ├── auth/ │ │ │ │ ├── oidc.ts # openid-client Setup, Authentik │ │ │ │ ├── session.ts # Session-Store in Postgres │ │ │ │ ├── login-route.ts # GET /api/auth/login, /callback, /logout │ │ │ │ └── middleware.ts # session-cookie → ctx.user │ │ │ ├── procedures/ │ │ │ │ ├── public.ts # publicProcedure │ │ │ │ ├── protected.ts # protectedProcedure │ │ │ │ ├── day-entry-visible.ts # dayEntryVisibleProcedure (Sichtbarkeitsfilter) │ │ │ │ └── project-owner.ts # projectOwnerProcedure (P1-Check) │ │ │ ├── middleware/ │ │ │ │ └── with-audit.ts # Audit-Wrapper für Mutations │ │ │ ├── routers/ # tRPC-Router pro Feature │ │ │ │ ├── _app.ts # appRouter = mergeRouters(...) │ │ │ │ ├── user.ts # user.me, user.list, user.markAbsent │ │ │ │ ├── project.ts # FR-8 … FR-11, FR-45, FR-47 │ │ │ │ ├── milestone.ts # FR-11 │ │ │ │ ├── deal.ts # FR-12 … FR-14 │ │ │ │ ├── gtm.ts # FR-15 │ │ │ │ ├── campaign.ts # FR-17 … FR-18 │ │ │ │ ├── crosslink.ts # FR-19 │ │ │ │ ├── day-entry.ts # FR-21 … FR-33 │ │ │ │ ├── todo.ts # FR-22 … FR-25 │ │ │ │ ├── streak.ts # FR-34, FR-35 │ │ │ │ ├── blocker.ts # FR-41 … FR-44 │ │ │ │ ├── pflicht-minimum.ts # FR-38 … FR-40 │ │ │ │ └── puls.ts # FR-1 … FR-7 Aggregate │ │ │ ├── cron/ # Background-Jobs (POST /internal/cron/*) │ │ │ │ ├── publish-day-entries.ts # FR-29 │ │ │ │ ├── recompute-pflicht-min-5.ts# FR-38 Punkt 5 │ │ │ │ ├── recompute-streaks.ts # FR-34 │ │ │ │ └── pg-dump-backup.ts # NFR-15 │ │ │ ├── lib/ │ │ │ │ ├── werktag.ts # Werktags-Logik (Mo-Fr Europe/Berlin) │ │ │ │ ├── ampel.ts # Ampel-Aggregations-Algorithmus (FR-1a) │ │ │ │ ├── pflicht-minimum.ts # Pflicht-Minimum-Berechnung │ │ │ │ └── logger.ts # pino-Instanz │ │ │ └── health.ts # GET /api/health │ │ └── test/ │ │ ├── fixtures/ # Test-User, Test-Projekte │ │ └── helpers/ # testcontainers-Postgres ├── packages/ │ ├── db/ # ───── DRIZZLE-SCHEMA & MIGRATIONEN ───── │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── drizzle.config.ts │ │ ├── schema.ts # ALLE Tabellen (siehe Addendum §1.2) │ │ ├── seed.ts # Test-Daten (5 User, 3 Projekte/Bereich) │ │ ├── migrations/ # drizzle-kit generierte SQL-Files │ │ │ ├── 0000_init.sql │ │ │ └── meta/ │ │ └── client.ts # createDbClient(connStr) – verwendet von api │ └── shared/ # ───── SHARED TYPES & SCHEMAS ───── │ ├── package.json │ ├── tsconfig.json │ └── src/ │ ├── index.ts # Re-exports │ ├── schemas/ # Zod-Schemas (Backend + Frontend) │ │ ├── user.ts │ │ ├── project.ts │ │ ├── milestone.ts │ │ ├── deal.ts │ │ ├── campaign.ts │ │ ├── day-entry.ts │ │ ├── todo.ts │ │ ├── blocker.ts │ │ └── pflicht-minimum.ts │ ├── enums.ts # Ampel, Trend, Stage, Status … │ ├── constants.ts # MAX_TODO_TEXT_LENGTH, JOURFIXE_DAY etc. │ └── types/ │ └── app-router.ts # export type AppRouter (re-exported from api) └── docs/ # Optionale Devs-Dokus, nicht User-facing ├── operations.md # Backup-Drill, Restore-Verfahren, Cron-Übersicht └── adr/ # Architecture Decision Records, später optional ``` ### Architectural Boundaries #### API Boundary - **Externe API:** keine. Cockpit ist intern. - **Interne API (Frontend ↔ Backend):** - **Auth-Routes:** klassisches REST: `GET /api/auth/login` (Redirect zu Authentik), `GET /api/auth/callback`, `POST /api/auth/logout`, `GET /api/health`. - **Geschäfts-API:** **ausschließlich tRPC** über `POST /api/trpc/`. - **Cron-Trigger:** `POST /internal/cron/` mit Bearer-Token-Header (nur über internes Netz erreichbar; Caddy blockt externen Zugriff auf `/internal/*`). - **Datenbank-Zugriff:** ausschließlich aus `apps/api`. `apps/web` greift **niemals** direkt auf die DB zu. #### Component Boundary (Frontend) - **Page-Components** (`routes/`) holen ihre Daten via tRPC + React Query. Sie übergeben die Daten an **Feature-Components** (`components//`). - **Feature-Components** sind nicht-routing-bewusst, nehmen Props, geben Callbacks zurück. Sie nutzen `ui/`-Primitives und domain-spezifische Hooks. - **UI-Primitives** (`components/ui/`) sind atomar, kennen weder Domain noch tRPC. - **Hooks** (`hooks/`) kapseln wiederverwendbare Logik (z. B. `useTodaysEntry` ruft mehrere tRPC-Procedures und merged sie). - **Context** ist Notfall, nicht Standard. Nur für `user`, `theme`, `toast`. #### Service Boundary (Backend) - **Router-Schicht** (`routers/.ts`) übersetzt nur zwischen tRPC-Input und Domain-Logik. Keine Geschäftslogik. - **Domain-Logik** lebt in `lib/.ts` (`werktag.ts`, `ampel.ts`, `pflicht-minimum.ts`). - **Datenzugriff** über Drizzle-Client direkt im Router/Lib — kein separates Repository-Pattern (Overkill). - **Audit-Spur** via `withAudit(...)`-Middleware um jede Mutation. Schreibt parallel zur Mutation in derselben Transaktion in `audit_event`. #### Data Boundary - **Schema in `packages/db/schema.ts`** ist Single Source of Truth. - Drizzle-Typen werden automatisch im API verwendet. - Frontend kennt **keine DB-Typen**, nur tRPC-Output-Typen (via Drizzle inferierte TS-Typen, die durch tRPC + Zod laufen). - **Migrationen** sind die Schnittstelle für Schema-Änderungen. Hand-Manipulation der DB außerhalb von Migrationen verboten. ### Requirements to Structure Mapping | Feature-Gruppe (PRD) | FRs | Frontend | Backend | |---|---|---|---| | A: Unternehmens-Puls | FR-1 … FR-7 | `routes/puls.tsx`, `components/puls/` | `routers/puls.ts`, `lib/ampel.ts` | | B: Bereich Entwicklung | FR-8 … FR-11 | `components/bereich/EntwicklungDetail.tsx`, `components/projekt/` | `routers/project.ts`, `routers/milestone.ts` | | C: Bereich Vertrieb | FR-12 … FR-16 | `components/bereich/VertriebDetail.tsx`, `components/deal/`, `components/gtm/` | `routers/deal.ts`, `routers/gtm.ts` | | D: Bereich Marketing | FR-17 … FR-20 | `components/bereich/MarketingDetail.tsx`, `components/campaign/` | `routers/campaign.ts`, `routers/crosslink.ts` | | E: Tagesrhythmus | FR-21 … FR-33 | `routes/mein-heute.tsx`, `routes/reflexion.tsx`, `components/day-entry/` | `routers/day-entry.ts`, `routers/todo.ts` | | F: Streak | FR-34 … FR-37 | `components/streak/` | `routers/streak.ts`, `cron/recompute-streaks.ts` | | G: Pflicht-Minimum | FR-38 … FR-40 | `components/projekt/PflichtMinimumPanel.tsx` | `routers/pflicht-minimum.ts`, `lib/pflicht-minimum.ts`, `cron/recompute-pflicht-min-5.ts` | | H: Blocker | FR-41 … FR-44 | `components/blocker/` | `routers/blocker.ts`, `procedures/project-owner.ts` | | I: Projekt-Abschluss | FR-45 … FR-47 | `components/abschluss/`, `routes/archiv.tsx` | `routers/project.ts` (archive-Procedures) | | J: Navigation | FR-48 … FR-49 | `components/layout/TopNav.tsx`, `lib/router.tsx` | — | ### Cross-Cutting Concerns Mapping | Concern | Frontend | Backend | |---|---|---| | Auth (Authentik OIDC) | `context/user-context.tsx`, `components/layout/AuthGate.tsx` | `auth/oidc.ts`, `auth/session.ts`, `auth/middleware.ts` | | Audit-Trail | — (nicht UI-relevant) | `middleware/with-audit.ts` | | Werktags-/Zeit-Logik | nur Anzeige (`lib/format.ts`) | `lib/werktag.ts` (ist die Wahrheit) | | Sichtbarkeit DayEntry | — (Frontend zeigt, was API liefert) | `procedures/day-entry-visible.ts` | | Voice & Tone | `lib/voice.ts`, alle UI-Komponenten | TRPCError-Messages in Routern (deutsch) | | Brand-Theme | `tailwind.config.ts`, `styles/globals.css` | — | ### Integration Points #### Internal Communication - Frontend ↔ Backend: tRPC über HTTPS (Same-Origin via Caddy). - Backend ↔ Postgres: Drizzle-Client über Connection-Pool (`pg`). - Cron ↔ Backend: Lokaler HTTP-Call vom Cron-Daemon im selben Container (oder Host-Cron) an `localhost:3000/internal/cron/*` mit Bearer-Token. #### External Integrations (MVP) - **Authentik** (OIDC): `auth/oidc.ts` führt Authorization Code Flow + PKCE durch. Discovery-Endpoint `https://auth.tool-ctrl.de/application/o/cockpit/.well-known/openid-configuration`. - **Plane/Outline:** **nur als URL-Referenzen**. Keine API-Calls im MVP (PRD §8). Phase-2-Hook: `lib/plane-client.ts`, `lib/outline-client.ts` werden in Phase 2 ergänzt, ohne Schema-Änderung. #### Data Flow (Hot-Path: Tagesrhythmus) ``` Browser ── Click "Einstempeln" ──▶ tRPC: dayEntry.checkIn │ ▼ Fastify routers/day-entry.ts │ (protectedProcedure) │ ▼ withAudit wrapper │ ▼ DB-Transaktion (Drizzle): INSERT DayEntry, INSERT Todos[], INSERT audit_event │ ▼ Response ──▶ React Query Cache-Invalidierung ──▶ UI rerender (Skeleton → echte Daten) ``` ### File Organization Patterns - **Configuration im Root:** `tsconfig.base.json`, `.eslintrc.cjs`, `prettier.config.js`, `tailwind.config.ts`, `drizzle.config.ts`. Pro App nur Overrides (extends Root). - **Environment:** `.env.example` im Root als Master-Template; jede App liest die für sie relevanten Variablen. Validation via Zod in `apps/api/src/env.ts` und `apps/web/src/env.ts` (Vite `import.meta.env`). - **Tests neben Code:** `*.test.ts(x)` für Unit. E2E zentral in `apps/web/e2e/`. - **Static Assets:** `apps/web/public/` (Vite-Standard). Brand-Logos hier. ### Development Workflow Integration - **Lokaler Dev-Server:** - `pnpm --filter web dev` → Vite auf `:5173`. - `pnpm --filter api dev` → Fastify + `tsx watch` auf `:3000`. - Postgres via `docker compose -f docker-compose.dev.yml up postgres`. - Vite-Proxy schickt `/api/*` an `:3000`. - **Build:** - `pnpm --filter web build` → `apps/web/dist/` (static). - `pnpm --filter api build` → `apps/api/dist/` (Node-Server-Bundle). - **Deployment:** - `docker compose up -d --build` baut alle 3 Container und startet sie. - Caddy serviert `apps/web/dist/` statisch und reverse-proxyt `/api/*` + `/trpc/*` an `api:3000`. - Postgres als persistenter Container mit eigenem Volume. --- ## Architecture Validation Results ### Coherence Validation ✅ **Decision Compatibility:** Vite 5+ und React 18+ bilden ein etabliertes SPA-Fundament; Fastify 5+ trägt Node-22-Workloads; tRPC v11 hat einen offiziellen Fastify-Adapter; Drizzle ORM ist Postgres-nativ; `openid-client` v6+ ist die Referenz-Library für OIDC-Flows und arbeitet sauber mit Authentik. Alle Versionen sind 2026 stabil und parallel im Ökosystem ausgereift. **Pattern Consistency:** Die Naming-Konventionen ziehen sich durch DB (`snake_case`), Backend (`camelCase` tRPC-Procedures, deutsche Domain-Begriffe in Feldnamen wo sinnvoll), Frontend (PascalCase-Komponenten, kebab-case-Routen mit deutschen Domain-Pfaden). Error-Handling-Pattern (TRPCError + zentraler Toast) ist im Code-Stack durchsetzbar. State-Management-Pattern (React Query + Context + lokal) ist konsistent. **Structure Alignment:** Die Monorepo-Struktur unterstützt die Stack-Wahl (pnpm-Workspaces sind 2026-Standard für Node-Monorepos). `packages/shared` ist Pflicht für End-to-End-Typsicherheit von tRPC. `packages/db` ist Single Source of Truth — Backend importiert, Frontend nicht. Boundaries sind technisch erzwingbar via TS-Paths und ESLint-Regeln. ### Requirements Coverage Validation ✅ **Functional Requirements (49 FRs):** Alle 49 FRs sind in der Mapping-Tabelle (§ Project Structure → Requirements to Structure Mapping) konkret auf Frontend-Komponenten und Backend-Router gemappt. Keine FR ohne Heimat. Stichproben: - **FR-1a (Ampel-Aggregation-Algorithmus)** → `apps/api/src/lib/ampel.ts` (Domain-Logik), aufgerufen aus `routers/puls.ts`. - **FR-26 (Reflexions-Formular)** → `apps/web/src/components/day-entry/ReflectionForm.tsx` + `routers/day-entry.ts:submitReflection`. - **FR-29 (Publikation am nächsten Werktagsmorgen)** → `apps/api/src/cron/publish-day-entries.ts` + System-Cron-Definition in `crontab`. - **FR-38–40 (Pflicht-Minimum-Check)** → `routers/pflicht-minimum.ts` + `lib/pflicht-minimum.ts` (Backend), `components/projekt/PflichtMinimumPanel.tsx` (Frontend). - **FR-44 (nur P1 löst Blocker)** → `procedures/project-owner.ts` (`projectOwnerProcedure`) wrapt die Blocker-Resolve-Mutation. **Non-Functional Requirements (16 NFRs):** | NFR | Architektur-Antwort | Status | |---|---|---| | NFR-1 (Puls < 1,5 s) | Server-Komponenten-frei (SPA), tRPC + React Query, Postgres-Indizes auf `day_entry(date, user_id)`, `project(area_id, archiviert)` | ✅ Plan adressiert | | NFR-2 (Save < 500 ms) | Single tRPC-Roundtrip, In-Memory Postgres-Connection-Pool, kein externer Dienst im Hot-Path | ✅ Plan adressiert | | NFR-3 (99 % Mo-Fr 8–19) | Docker-Healthcheck + Caddy-Restart-Policy, pg-dump-backup | ✅ | | NFR-4 (Wartungsfenster) | manuelle `docker compose down/up`, gating via `Caddy` | ✅ | | NFR-5 (Authentik OIDC) | `auth/oidc.ts` mit `openid-client` v6+, Code-Flow + PKCE | ✅ | | NFR-6 (keine externe PII) | Datenmodell enthält nur intern bekannte Felder | ✅ Konzeptuell adressiert | | NFR-7 (Audit-Trail) | `withAudit`-Middleware, `audit_event`-Tabelle | ✅ | | NFR-8 (Brand) | Tailwind-Theme-Tokens, finale Werte aus UX-Phase | ⚠️ verweist auf OQ-2 | | NFR-9 (≤ 3 Klicks ab Puls zum Update) | `routes/mein-heute.tsx` ist Direktlink in TopNav (FR-48) | ✅ | | NFR-10 (Desktop-first, mobil-lesbar) | Tailwind Responsive Utility-Classes, kein Custom-Mobile-Layout im MVP | ✅ | | NFR-11 (Deutsch durchgängig) | `lib/voice.ts` zentralisiert UI-Strings, keine i18n-Lib | ✅ | | NFR-12/13 (Voice & Tone) | `lib/voice.ts` + Error-Messages deutsch in Routern; bindend per CONVENTIONS.md | ✅ | | NFR-14 (Postgres) | Postgres 16+, Drizzle-Adapter | ✅ | | NFR-15 (Daily Backup) | `cron/pg-dump-backup.ts` täglich 02:00 | ✅ | | NFR-16 (≤ 2 h Migration) | Drizzle migrations sind klein, generierte SQL-Files, lokal testbar | ✅ | ### Implementation Readiness Validation ✅ **Decision Completeness:** Alle kritischen Entscheidungen sind mit Tech-Stack-Wahl, Versions-Hinweis und konkretem Library-Pfad (Bsp. `openid-client` v6+, `Fastify 5+`, `Drizzle ORM`) dokumentiert. Background-Job-Mechanik festgelegt (System-Cron → Bearer-geschützter Endpoint). Hosting festgelegt (ToolCTRL-interne Infra + Caddy). **Structure Completeness:** Project-Tree umfasst alle Apps (`apps/web`, `apps/api`), alle Packages (`db`, `shared`), Konfig-Dateien, Cron-Container-Skripte, Caddyfile, docker-compose. Pro FR-Gruppe ist ein konkretes Verzeichnis benannt. **Pattern Completeness:** Naming-, Struktur-, Format-, Kommunikations- und Prozess-Pattern sind in `CONVENTIONS.md` reflektiert. Anti-Pattern sind explizit aufgelistet. ### Gap Analysis Results **Critical Gaps:** keine. Die Architektur ist implementierungsbereit. **Important Gaps (sollten vor MVP-Bau geschlossen werden):** 1. **OQ-1 Authentik-Group → Bereich-Mapping**: Default-Strategie wird in der ersten Architektur-Story (`auth/oidc.ts`) entschieden. Vorschlag: ein `AUTHENTIK_GROUP_TO_AREA`-Mapping in der `.env` (z. B. `entwicklung:dev,vertrieb:sales,marketing:marketing`); Fallback auf `allgemein`, User kann im Profil manuell wechseln. *Nicht-Blocker, in 1 Story lösbar.* 2. **OQ-5 Archivierungs-Policy**: PRD-Default jetzt fixiert → **Soft-Delete via `archiviert`-Flag + `archiviert_am`-Zeitstempel; keine automatische Löschung; manuell auf Anfrage durch DB-Admin**. Aufnahme in `docs/operations.md` bei MVP-Launch. 3. **OQ-2 Brand-Tokens**: kommen aus `bmad-create-ux-design` (Folge-Skill). Tailwind-Config wird vor dem Story-Bau für Theme befüllt. **Nice-to-have Gaps (Post-MVP):** 1. **Zentrale Log-Aggregation** (Loki/Grafana) — sinnvoll, sobald > 3 Container-Instanzen. 2. **Error-Tracking** (Sentry oder gleichwertig) — Post-MVP. 3. **CSP-Headers** im Caddyfile feinjustieren — vor Production-Launch. 4. **Migrations-Rollback-Strategie**: Drizzle hat kein eingebautes Rollback; Operational-Notes festhalten („immer Rollback-Migration als nächste Migration schreiben"). 5. **Postgres-Restore-Drill** — quartalsweise (steht in operations.md). ### Validation Issues Addressed - *OQ-5 Archivierungs-Policy* — in der Validation auf Soft-Delete via `archiviert`-Flag fixiert, da das Schema-konform und am wenigsten destruktiv ist. ### Architecture Completeness Checklist **Requirements Analysis** - [x] Project context thoroughly analyzed - [x] Scale and complexity assessed - [x] Technical constraints identified - [x] Cross-cutting concerns mapped **Architectural Decisions** - [x] Critical decisions documented with versions - [x] Technology stack fully specified - [x] Integration patterns defined - [x] Performance considerations addressed **Implementation Patterns** - [x] Naming conventions established - [x] Structure patterns defined - [x] Communication patterns specified - [x] Process patterns documented **Project Structure** - [x] Complete directory structure defined - [x] Component boundaries established - [x] Integration points mapped - [x] Requirements to structure mapping complete ### Architecture Readiness Assessment **Overall Status:** **READY FOR IMPLEMENTATION** Alle 16 Checklist-Items sind erfüllt. Keine kritischen Gaps. Drei Important Gaps (OQ-1, OQ-5, OQ-2) sind entweder bereits in dieser Validierung mit einem Default belegt (OQ-5) oder werden im Lauf der ersten Stories bzw. in der folgenden UX-Phase aufgelöst (OQ-1, OQ-2) und sind kein Blocker für Story-Schneidung. **Confidence Level:** **hoch.** Die Architektur entspricht einer ausgereiften, im Ökosystem 2026 gut belegten Schnitt-Strategie für interne Tools dieser Größe. **Key Strengths:** - End-to-End-Typsicherheit (Drizzle → tRPC → React Query) reduziert Klassen von Bugs. - Klar getrennte Wahrheits-Ebenen — Plane (Arbeit), Outline (Gedächtnis), Cockpit (Übersicht) — werden architektonisch nicht verletzt. - Pflicht-Minimum-Check als Erstklass-Feature ist sauber gekapselt (eigene Lib + eigener Router + eigener Cron). - Sichtbarkeitslogik der DayEntries hat eine eigene Procedure (`dayEntryVisibleProcedure`), wodurch die Privacy-Regel nicht als Anti-Muster über die Codebase verteilt wird. **Areas for Future Enhancement:** - Plane- und Outline-API-Integration (Phase 2) ist im Schema vorbereitet, aber nicht implementiert. - Mobile-schreibbar (Phase 2). - Reaktionen/Kommentare auf Tagesupdates (Phase 2). - Reminders per Element/Matrix/Mail (Phase 2). - Volltextsuche (FR-49, Phase 2). ### Implementation Handoff **AI Agent Guidelines:** - Folge allen Entscheidungen in diesem Dokument exakt. - Nutze die Patterns in CONVENTIONS.md konsequent — Anti-Patterns sind verboten. - Respektiere die Boundaries (`apps/web` kennt die DB nicht; nur tRPC). - Bei Unsicherheit: PRD ist die Quelle der Wahrheit für *was*, dieses Dokument für *wie*. **First Implementation Priority:** ```bash # Story 1: Repo-Bootstrap mkdir cockpit && cd cockpit pnpm init mkdir -p apps/web apps/api packages/db packages/shared echo 'packages:\n - "apps/*"\n - "packages/*"' > pnpm-workspace.yaml # Frontend bootstrappen cd apps/web && pnpm create vite . --template react-ts # Backend, packages/db, packages/shared per Hand (s. Initialization Commands oben) ``` Story-Sequenz im Detail siehe Sektion „Decision Impact Analysis → Implementation Sequence" weiter oben.