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>
59 KiB
stepsCompleted, lastStep, status, completedAt, inputDocuments, workflowType, project_name, user_name, date, binding_frame, prd_status
| stepsCompleted | lastStep | status | completedAt | inputDocuments | workflowType | project_name | user_name | date | binding_frame | prd_status | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
8 | complete | 2026-05-25 |
|
architecture | projectmanagement | Dejancosic | 2026-05-25 | ToolCTRL Projektmanagement-Leitfaden v3.0 (Mai 2026) | 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:
- Puls (FR-1 bis FR-7): Aggregierte Unternehmenssicht — Bereichsstatus, Heute-Quote, Pflicht-Minimum-Übersicht, Jourfixe-Hinweis, bereichsübergreifende Blocker.
- Bereichs-Detail-Views (FR-8 bis FR-20): Drei fachspezifische Sichten — Entwicklung (Projekte+Meilensteine), Vertrieb (Kanban+gewichtete Pipeline), Marketing (Funnel+Kampagnen). Crosslink Marketing↔Vertrieb.
- 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
- Identität (Authentik OIDC). Affiziert: alle Routen, User-Mapping, Default-Bereich per Group-Claim (OQ-1).
- Audit-Trail. Affiziert: jede mutierende Operation (Update-Reflexion, Blocker-Lifecycle, Meilenstein-Statuswechsel, Pflicht-Minimum-Änderungen).
- 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.
- Status-Caches (
status_cache,trend_cache). Denormalisierungen, die aus letzter Reflexion abgeleitet sind. Konsistenz: bei jeder Reflexion Cache aktualisieren. - 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.
- 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/Berlinals Wahrheit. - 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 viapackages/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-clientdirekt 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
# 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 viadrizzle-kit generateunddrizzle-kit migrate. Migrationen im Repo eingecheckt. - Validation: Zod, im
packages/shareddefiniert; beidseitig (Backend tRPC-Input + Frontend Form-Resolver) genutzt. - Status-Caches:
Project.status_cache,Project.trend_cache,Project.pflicht_minimum_stateals reguläre Postgres-Spalten (jsonbfü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_dumpnightly 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-clientv6+ direkt im Fastify-Backend (Auth.js v5 hat keinen reifen Fastify-Adapter;openid-clientist die referenzierte Library hinter den meisten OIDC-Implementierungen). - Flow: Authorization Code Flow + PKCE.
- Session: Server-seitige Session im Postgres (
Session-Tabelle), Browser bekommt nursession_idals 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 mitauthentik_subals 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— wraptprotectedProcedure, filtert DayEntries: vorpublished_atnur eigene + (falls Bezug=Projekt) Projektverantwortliche/r; abpublished_atfür alle Kollegen.projectOwnerProcedure— wraptprotectedProcedure, prüft, dass aktueller User der P1 des ininputreferenzierten 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
AppRouterausapps/api/src/trpc.ts; Frontend importiert den Typ auspackages/sharedund nutztcreateTRPCReact<AppRouter>(). - Transport: HTTP/HTTPS,
POSTfür Mutations und Queries (tRPC-Standard). - Error Handling:
TRPCError-Subklassen mit deutschenmessage-Strings für menschenlesbare Fehler; Frontend zeigt sie via einem zentralenonError-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/actionfü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 viauseState/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 ausbmad-create-ux-design(OQ-2 im PRD). - Components: Headless-UI-Library (
@headlessui/reactoderradix-uiPrimitives) 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:16als 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 aufapi:3000.- Automatic HTTPS.
- Statische Web-Assets aus
- 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, setztpublished_atauf 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).
- Jobs im MVP:
- CI/CD: Gitea Actions (Gitea ist im Authentik-Stack vorhanden). Pipeline:
pnpm install- Typecheck (
tsc --noEmit) - Lint (
eslint) - Tests (
vitest run, Playwright nur bei Tag/Release) - Build (Vite + tsc)
- Docker-Images bauen und in lokale Registry pushen
- Deploy via
docker-compose pull && up -d
- Environment Config:
.env.local(dev, ignored),.env.production(prod, Docker-Secret-Mount). Validation viazodbeim 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 (
testcontainersoder 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 inTodo(3-5× mehr). Selbst nach 5 Jahren < 1 Mio. Zeilen — keine Partitionierung nötig.
Decision Impact Analysis
Implementation Sequence (Story-1 → Story-N):
- Repo-Bootstrap (Monorepo mit pnpm-Workspaces, Vite, Fastify, Drizzle, Tailwind).
- Drizzle-Schema + erste Migration (alle Entitäten aus Addendum §1.2).
- Fastify-Server mit Health-Endpoint.
- Authentik-OIDC-Login-Flow (
openid-client, Session in DB). - tRPC-Bootstrap mit
protectedProcedureund User-Context. - Frontend-Shell (React Router + Layout + Login-Redirect).
- Tagesrhythmus-Slice (Einstempeln + ToDos + Reflexion) — UJ-1.
- Puls-Seite.
- Bereichs-Detail-Views (Dev/Sales/Marketing).
- Pflicht-Minimum-Feature + Blocker.
- Background-Jobs (Cron-Endpoints, Cron-Definition im Docker-Compose).
- Projekt-Abschluss + Archiv.
- Brand-Theming (auf Basis der UX-Design-Tokens).
- 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. Nichtusers, nichtDayEntry. - 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. Nichtfk_user. - Indizes:
idx_<table>_<column>—idx_day_entry_user_id. Eindeutige:uniq_<table>_<column>. - Enums: als TypeScript-Union in Drizzle-Schema, in Postgres als
textmitcheck-Constraint (einfacher zu erweitern alspg_enum). - Zeitstempel:
created_at,updated_at,<verb>_at(reflected_at,published_at). Typetimestamptz. Defaultnow().
tRPC (Backend-API)
- Router-Datei:
apps/api/src/routers/<feature>.ts—day-entry.ts,project.ts,blocker.ts. - Router-Variable:
<feature>RouteralscamelCase—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(stattdelete),resolve,checkIn,submitReflection.
- Reads:
- 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, Dateinamekebab-case—format-werktag.tsexportiertformatWerktag(). - 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/undcomponents/cards/, sonderncomponents/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-camelCaseautomatisch viacolumnNameOption. - Datums-Format: ISO 8601 mit Zeitzone —
2026-05-25T14:30:00+02:00. Im Frontend mitIntl.DateTimeFormat('de-DE')formatiert (nicht moment, nicht dayjs —Intlreicht). - Werktage-Format: Pures
YYYY-MM-DD(kein Zeitanteil) fürDayEntry.date,AbsenceDay.date. In Drizzle alsdate-Typ (nichttimestamptz). - Geldbeträge:
value_euralsintegerin Cent (bigintwenn > 21 Mio. €). Frontend formatiert viaIntl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }). - Booleans: echtes
boolean. Nicht0/1, nicht"yes"/"no". - Null vs. Undefined: Datenbank speichert
null, TypeScript-Code nutztnullfür „explizit nicht gesetzt",undefinedfür „existiert nicht". Konsistent durchziehen. - IDs in der URL: UUIDs als Param —
/projekt/:idmitid= UUID-String. Keine numerischen IDs öffentlich.
Communication Patterns
Error Handling
- Backend:
TRPCErrormit klarencodes:UNAUTHORIZED— kein LoginFORBIDDEN— Login aber falsche Rolle (z. B. nicht P1 für Blocker-Auflösung)NOT_FOUND— Entität existiert nicht oder ist außerhalb der SichtBAD_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 zeigterror.messageals Toast (Voice & Tone NFR-12/13). BeiUNAUTHORIZED→ Redirect auf Login. - Logging: Backend loggt jeden
INTERNAL_SERVER_ERRORmit Stack viapinoLevelerror.BAD_REQUESTist Levelinfo, nichterror.
Loading States
- Server-State: ausschließlich React Query (
isPending,isError,data). Niemals eigeneuseState-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 aktuellemUser-Objekt. Geladen viaprotectedProcedure user.meeinmal beim Mount. - Brand/Theme: CSS-Variablen im
:root, kein State-Container. - Lokaler UI-State:
useState/useReducerin 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
beforeundafter(für Mutations). Erzeugt im Backend mittels eineswithAudit-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/sharedimportieren, 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:
// 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):
// ❌ 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/*).
- Auth-Routes: klassisches REST:
- Datenbank-Zugriff: ausschließlich aus
apps/api.apps/webgreift 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.useTodaysEntryruft 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 inaudit_event.
Data Boundary
- Schema in
packages/db/schema.tsist 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.tsführt Authorization Code Flow + PKCE durch. Discovery-Endpointhttps://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.tswerden 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.exampleim Root als Master-Template; jede App liest die für sie relevanten Variablen. Validation via Zod inapps/api/src/env.tsundapps/web/src/env.ts(Viteimport.meta.env). - Tests neben Code:
*.test.ts(x)für Unit. E2E zentral inapps/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 watchauf: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 --buildbaut alle 3 Container und startet sie.- Caddy serviert
apps/web/dist/statisch und reverse-proxyt/api/*+/trpc/*anapi: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 ausrouters/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 incrontab. - 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):
- OQ-1 Authentik-Group → Bereich-Mapping: Default-Strategie wird in der ersten Architektur-Story (
auth/oidc.ts) entschieden. Vorschlag: einAUTHENTIK_GROUP_TO_AREA-Mapping in der.env(z. B.entwicklung:dev,vertrieb:sales,marketing:marketing); Fallback aufallgemein, User kann im Profil manuell wechseln. Nicht-Blocker, in 1 Story lösbar. - 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 indocs/operations.mdbei MVP-Launch. - 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):
- Zentrale Log-Aggregation (Loki/Grafana) — sinnvoll, sobald > 3 Container-Instanzen.
- Error-Tracking (Sentry oder gleichwertig) — Post-MVP.
- CSP-Headers im Caddyfile feinjustieren — vor Production-Launch.
- Migrations-Rollback-Strategie: Drizzle hat kein eingebautes Rollback; Operational-Notes festhalten („immer Rollback-Migration als nächste Migration schreiben").
- 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
- Project context thoroughly analyzed
- Scale and complexity assessed
- Technical constraints identified
- Cross-cutting concerns mapped
Architectural Decisions
- Critical decisions documented with versions
- Technology stack fully specified
- Integration patterns defined
- Performance considerations addressed
Implementation Patterns
- Naming conventions established
- Structure patterns defined
- Communication patterns specified
- Process patterns documented
Project Structure
- Complete directory structure defined
- Component boundaries established
- Integration points mapped
- 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/webkennt die DB nicht; nur tRPC). - Bei Unsicherheit: PRD ist die Quelle der Wahrheit für was, dieses Dokument für wie.
First Implementation Priority:
# 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.