Files
everythingbutwork/_bmad-output/planning-artifacts/architecture.md
T
dejanandClaude Opus 4.7 4f33509a0f
ci / quality (push) Has been cancelled
ci / build (push) Has been cancelled
Initial commit: everythingbutwork MVP
Internes Project-Management-Tool für ToolCTRL — ergänzend zu Plane (Tickets)
und Outline (Doku). Umfasst Puls-Übersicht, Tagesrhythmus (Lock-in + Reflexion),
Bereichs-Detail-Views (Entwicklung/Vertrieb/Marketing), Pflicht-Minimum-Check
und Blocker-Lifecycle gemäß Projektmanagement-Leitfaden v3.0.

Stack: pnpm-Monorepo · Vite + React 18 + Tailwind · Fastify + tRPC v11 +
Drizzle ORM · Postgres 16 · Authentik OIDC (Phase 2, Dev-Auto-Login aktiv).

Planungs-Artefakte unter _bmad-output/planning-artifacts/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:47:26 +02:00

1002 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<AppRouter>()`.
- **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/<job>` 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 `<entity>_id`.
- **Fremdschlüssel:** `<entity>_id` — `user_id`, `project_id`. Nicht `fk_user`.
- **Indizes:** `idx_<table>_<column>` — `idx_day_entry_user_id`. Eindeutige: `uniq_<table>_<column>`.
- **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`, `<verb>_at` (`reflected_at`, `published_at`). Type `timestamptz`. Default `now()`.
#### tRPC (Backend-API)
- **Router-Datei:** `apps/api/src/routers/<feature>.ts` — `day-entry.ts`, `project.ts`, `blocker.ts`.
- **Router-Variable:** `<feature>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/<feature>.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<Name>` 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/<feature>/<ComponentName>.tsx`.
- **Frontend-Routen-Komponenten:** `apps/web/src/routes/<route-name>.tsx`.
- **Backend-Router:** `apps/api/src/routers/<feature>.ts`.
- **Shared Schemas:** `packages/shared/schemas/<feature>.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/<journey>.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 `<UserProvider>` 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:** `<entity>.<verb>` (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<typeof submitReflectionInput>;
// 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<AppRouter>()
│ │ │ │ ├── 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 # <UserProvider>
│ │ │ ├── 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/<procedurePath>`.
- **Cron-Trigger:** `POST /internal/cron/<job>` 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>/`).
- **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/<feature>.ts`) übersetzt nur zwischen tRPC-Input und Domain-Logik. Keine Geschäftslogik.
- **Domain-Logik** lebt in `lib/<topic>.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.