Initial commit: everythingbutwork MVP
ci / quality (push) Has been cancelled
ci / build (push) Has been cancelled

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>
This commit is contained in:
dejanandClaude Opus 4.7 committed 2026-05-25 22:47:26 +02:00
commit 4f33509a0f
131 files changed
+34339

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
+24
View File
@@ -0,0 +1,24 @@
# --- Postgres ---
DATABASE_URL=postgres://cockpit:cockpit_dev@localhost:55432/cockpit
# --- Authentik OIDC (Story 1.3) ---
# Anlage als „Provider" und „Application" in Authentik unter https://auth.tool-ctrl.de
# → siehe docs/operations.md (kommt in Story 1.7)
AUTHENTIK_ISSUER=https://auth.tool-ctrl.de/application/o/cockpit/
AUTHENTIK_CLIENT_ID=
AUTHENTIK_CLIENT_SECRET=
AUTHENTIK_REDIRECT_URI=http://localhost:3000/api/auth/callback
# --- Session ---
SESSION_COOKIE_NAME=cockpit_session
SESSION_TTL_HOURS=24
SESSION_COOKIE_SECRET=please-change-me-in-prod-at-least-32-chars
# --- Cron Bearer (Story 2.6 + 5.1 + 6.3) ---
CRON_BEARER_TOKEN=please-change-me-in-prod
# --- App ---
NODE_ENV=development
LOG_LEVEL=info
PORT=3000
PUBLIC_BASE_URL=http://localhost:5173
+47
View File
@@ -0,0 +1,47 @@
# Gitea Actions CI für das Cockpit-Repo.
# Läuft bei Push und PR.
name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: pnpm/action-setup@v4
with:
version: 10.28.0
- name: Install
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm typecheck
- name: Lint
run: pnpm lint
- name: Format check
run: pnpm format:check
- name: Test
run: pnpm test
build:
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: pnpm/action-setup@v4
with:
version: 10.28.0
- run: pnpm install --frozen-lockfile
- run: pnpm --filter web build
- run: pnpm --filter api build
+48
View File
@@ -0,0 +1,48 @@
# Dependencies
node_modules/
.pnpm-store/
# Build outputs
dist/
build/
*.tsbuildinfo
# Environment
.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
pnpm-debug.log*
# Editor / OS
.DS_Store
.idea/
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
# Testing
coverage/
playwright-report/
test-results/
# Database
*.sqlite
*.sqlite-journal
# Misc
.cache/
.turbo/
# Brand-Assets (binär, im Repo Pull-Quelle: original Designer-Übergabe)
ToolCTRL Brand Identity/
# Lokale Claude-Code-Konfiguration
.claude/
# Lokale Hilfs-Dateien
/tmp-seed*.sql
+11
View File
@@ -0,0 +1,11 @@
node_modules
dist
build
.next
coverage
pnpm-lock.yaml
_bmad
_bmad-output
.claude
ToolCTRL Brand Identity
pnpm-workspace.yaml
+9
View File
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf"
}
+103
View File
@@ -0,0 +1,103 @@
# CONVENTIONS — ToolCTRL everythingbutwork
> **Bindend** für alle Code-erzeugenden Agenten und Menschen. Spiegelt Architektur-Spec § Implementation Patterns. Pflicht-Lesen vor jeder Story.
## Naming
### Datenbank (Drizzle + Postgres)
- Tabellen: Singular, `snake_case` (englisch) — `user`, `project`, `day_entry`, `todo`. Domain-Begriffe in Spalten dürfen deutsch sein (`abwesend_bis`, `arbeitsmodus`).
- Primärschlüssel: immer `id` (UUID v7).
- Fremdschlüssel: `<entity>_id` — `user_id`, `project_id`.
- Indizes: `idx_<table>_<column>`; unique: `uniq_<table>_<column>`.
- Enums: TypeScript-Union in Drizzle + `text` mit `check`-Constraint in Postgres.
- Zeitstempel: `created_at`, `updated_at`, `<verb>_at` als `timestamptz`, Default `now()`.
### tRPC Backend
- Router-Datei: `apps/api/src/routers/<feature>.ts` — `day-entry.ts`, `project.ts`.
- Router-Variable: `<feature>Router` (camelCase) — `dayEntryRouter`.
- Procedure-Naming: `<feature>.<verbNoun>` (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 aus `@cockpit/shared/schemas/<feature>`.
### Frontend (React)
- Komponenten: PascalCase. Dateiname matched (`DayEntryForm.tsx`).
- Hooks: `use<Name>` camelCase.
- Konstanten: `SCREAMING_SNAKE_CASE`.
- Route-Pfade: kebab-case, deutsch wo Domain — `/puls`, `/mein-heute`, `/bereich/entwicklung`.
- Komponenten-Organisation: **nach Feature**, nicht nach Typ.
## Struktur
- Tests **neben** dem Code: `Foo.tsx` ↔ `Foo.test.tsx` (Backend: `foo.ts` ↔ `foo.test.ts`).
- E2E: zentral in `apps/web/e2e/<journey>.spec.ts`.
- Shared zwischen Frontend + Backend → `packages/shared/`. Backend-only Helper → `apps/api/src/lib/`.
- Konfig-Dateien im Repo-Root, pro App nur Overrides.
## Format
- JSON-Felder über die Wire: `camelCase` (Drizzle mapt DB-`snake_case` → TS-`camelCase`).
- Datumsformat: **ISO 8601 mit Zeitzone**. Reine Werktage `YYYY-MM-DD` (Typ `date`).
- Geldbeträge: Cent als `integer` in DB; Frontend formatiert via `Intl.NumberFormat('de-DE')`.
- Booleans: echtes `boolean`. Nie `0/1` oder `"yes"/"no"`.
- Null vs. Undefined: DB nutzt `null`. Code: `null` = explizit nicht gesetzt, `undefined` = existiert nicht.
- IDs in URL: UUIDs (`/projekt/:id`), keine numerischen IDs öffentlich.
## Errors
- Backend: `TRPCError` mit klaren Codes — `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BAD_REQUEST`, `INTERNAL_SERVER_ERROR`.
- Message: **immer deutsch**, menschenlesbar. Nicht „NULL constraint violated" sondern „Bitte wähle einen Verantwortlichen aus".
- Frontend: zentraler `onError`-Hook in tRPC-Provider zeigt `error.message` als Toast.
- Logging: `INTERNAL_SERVER_ERROR` mit Stack → `pino.error`. `BAD_REQUEST` → `pino.info`.
## State (Frontend)
- Server-State: **ausschließlich React Query** via tRPC (`isPending`, `isError`, `data`). **Nie** eigene `useState`-Booleans für Server-Loading.
- Loading: Skeleton-Komponenten, keine Spinner-Only.
- Optimistic Updates für Reflexion, Todo-Toggle, Blocker-Auflösung. Sonst nicht.
- Lokaler UI-State: `useState`/`useReducer` in der Komponente. Hochziehen, wenn Sibling-Sharing nötig — kein Context.
## Audit & Authorization
- Mutations: `withAudit()`-Middleware um jede schreibende Procedure. Schreibt `audit_event` in derselben Transaktion.
- AuditEvent-Naming: `<entity>.<verb>` (lowercase, dot-separated) — `day_entry.reflection_submitted`, `blocker.resolved`.
- Procedure-Typen: `publicProcedure`, `protectedProcedure`, `dayEntryVisibleProcedure`, `projectOwnerProcedure`.
## Werktag / Zeit
- Server-Zeit: **`Europe/Berlin`**.
- Werktage: Mo-Fr (MVP, ohne Feiertage).
- **Alle Datumslogik im Backend** — Frontend bekommt fertige Felder (`isWorkday`, `nextWorkday`, `streakDays`).
## Voice & Tone
- UI-Strings zentral in `apps/web/src/lib/voice.ts`.
- Tonalität: locker, direkt, deutsch, kurze Imperative — „Stark.", „Wie läuft es?", „Du hast heute noch nicht gepingt.".
- Keine Corporate-Sprech, keine englischen Floskeln (Fachbegriffe aus Leitfaden erlaubt).
- Error-Messages im Backend ebenfalls deutsch.
## Tests
- Unit & Procedures: **Vitest**.
- E2E: **Playwright** auf UJ-1, UJ-2, UJ-5 (CI nur auf Tag/Release).
- A11y: `axe-core` in Vitest pro Komponente, `@axe-core/playwright` für E2E, Lighthouse-A11y ≥ 95 im CI.
- Tests neben Code (`*.test.ts(x)`).
## DB-Änderungen
- **Immer** Drizzle-Migration committen — kein Schema-Drift.
- Migration entstehen je Story-Bedarf, nicht upfront.
- TypeScript strict, kein `any`. Bei unklarer Form: `unknown` + Zod-Validation.
## Don'ts
- ❌ Inline-Zod im Procedure (gehört nach `@cockpit/shared/schemas`).
- ❌ Englisches Error-Message.
- ❌ snake_case-Komponentennamen.
- ❌ Server-Loading via `useState`.
- ❌ Drag-and-Drop außerhalb des Sales-Kanban.
- ❌ Pulsing-/Spin-Animationen auf dem Puls.
- ❌ Auto-Refresh / Background-Polling.
+29
View File
@@ -0,0 +1,29 @@
# Caddy 2.x — Reverse-Proxy für das Cockpit.
# Lokal: http://localhost
# Produktion: cockpit.tool-ctrl.de
{$COCKPIT_HOST:cockpit.tool-ctrl.de} {
encode zstd gzip
# /internal/* darf nur über das interne Netz erreichbar sein.
@internal path /internal/*
respond @internal 403
# API + tRPC → Backend
@api path /api/* /trpc/*
reverse_proxy @api api:3000
# Static frontend
root * /srv
try_files {path} /index.html
file_server
# Security-Headers (CSP wird in Story 6.x feinjustiert)
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
-Server
}
}
+44
View File
@@ -0,0 +1,44 @@
# ToolCTRL everythingbutwork
Internes Web-Tool zur täglichen Übersicht über alle Projekte und Bereiche. Ergänzt **Plane** (Tickets) und **Outline** (Doku) als drittes Werkzeug gemäß ToolCTRL Projektmanagement-Leitfaden v3.0.
## Stack
- **Frontend:** Vite + React 18 + TypeScript + Tailwind (kommt in Story 1.5)
- **Backend:** Fastify + tRPC v11 + Drizzle (kommt in Story 1.4 + 1.2)
- **DB:** Postgres 16+ (kommt in Story 1.2)
- **Auth:** Authentik OIDC (kommt in Story 1.3)
- **Hosting:** Docker + Caddy auf interner ToolCTRL-Infrastruktur
## Verzeichnisstruktur
```
.
├── apps/
│ ├── web/ # Vite + React SPA
│ └── api/ # Fastify Backend
├── packages/
│ ├── db/ # Drizzle-Schema + Migrationen
│ └── shared/ # Zod-Schemas + Konstanten + Typen
├── _bmad-output/ # Planungs-Artefakte (PRD, Architektur, UX, Epics)
└── CONVENTIONS.md # Bindende Code-Konventionen (Pflicht-Lesen)
```
## Loslegen
```bash
pnpm install
pnpm --filter web dev # Frontend auf http://localhost:5173
pnpm --filter api dev # Backend auf http://localhost:3000
pnpm typecheck # Alle Workspaces typecheken
pnpm lint # ESLint
pnpm format # Prettier
```
## Planungs-Artefakte
- [PRD](./_bmad-output/planning-artifacts/prds/prd-projectmanagement-2026-05-25/prd.md) — 49 Functional Requirements, 16 NFRs
- [Architektur](./_bmad-output/planning-artifacts/architecture.md) — Stack, Schnitt, Patterns
- [UX-Spec](./_bmad-output/planning-artifacts/ux-design-specification.md) — Brand-Tokens, Komponenten, User Journeys
- [Epics & Stories](./_bmad-output/planning-artifacts/epics.md) — 6 Epics, 32 Stories
- [Konventionen](./CONVENTIONS.md) — Naming, Struktur, Formate, Errors
File diff suppressed because it is too large. Load diff
+966
View File
@@ -0,0 +1,966 @@
---
stepsCompleted: [1, 2, 3, 4]
status: 'complete'
completedAt: '2026-05-25'
inputDocuments:
- prds/prd-projectmanagement-2026-05-25/prd.md
- prds/prd-projectmanagement-2026-05-25/addendum.md
- architecture.md
- ux-design-specification.md
- prds/prd-projectmanagement-2026-05-25/.decision-log.md
workflowType: 'epics-and-stories'
project_name: 'ToolCTRL Unternehmens-Cockpit'
user_name: 'Dejancosic'
date: '2026-05-25'
---
# ToolCTRL Unternehmens-Cockpit - Epic Breakdown
## Overview
Dieses Dokument zerlegt das PRD (49 FRs, 16 NFRs), die Architektur (komplett, Vite + React + Fastify + tRPC + Drizzle + Postgres + Authentik) und die UX-Spec (15 Custom-Components, Brand-Tokens, 5 User Journeys) in implementierbare Epics und Stories.
## Requirements Inventory
### Functional Requirements
> 49 FRs aus PRD §6, organisiert in 10 Feature-Gruppen (A–J).
**A: Unternehmens-Puls (Startseite)**
- **FR-1** Übersichtsleiste aller Areas (Entwicklung, Vertrieb, Marketing) inkl. aggregiertem Ampelstatus und Anzahl heute-fehlender Updates pro Area.
- **FR-1a** Ampel-Aggregation Algorithmus pro Area: Rot wenn ≥ 1 Projekt rot; Gelb wenn ≥ 1 Projekt gelb und kein rotes; Grün wenn alle grün; Grau wenn keine aktiven Projekte. Pflicht-Minimum-Verstöße heben Ampel mindestens auf Gelb.
- **FR-2** Bereichsübergreifende Liste aller offenen Blocker, sortiert nach Alter.
- **FR-3** Heute-Quote „X von Y Mitarbeitern haben heute gepingt" mit Klick auf Liste der fehlenden Personen.
- **FR-4** Anzeige des nächsten Jourfixe-Termins (statisch konfigurierbar, Default Di 11:00).
- **FR-5** Pro Bereich definierter Key-Metrics-Katalog:
- Entwicklung: # Projekte gesamt / im Plan / gefährdet / blockiert / # überzogene Meilensteine.
- Vertrieb: gewichteter Pipeline-Wert (Σ value × probability) / # offene Deals / gewonnene Deals (Quartal, # + EUR) / # offene GTM-Schritte.
- Marketing: Reichweite (Periode) / # aktive Kampagnen / Leads / MQLs / Übergaben an Vertrieb.
- **FR-6** Klick auf Bereich-Kachel öffnet Bereichs-Detail-View.
- **FR-7** Pflicht-Minimum-Übersicht im Puls: Liste aller aktiven Projekte mit roten/gelben Pflicht-Minimum-Punkten.
**B: Bereichs-Detail Entwicklung**
- **FR-8** Liste aller aktiven Projekte, gruppiert nach Projektart (Kundenprojekt / Internes Projekt — Leitfaden §1).
- **FR-9** Pro Projekt-Kachel: Name, Verantwortliche/r (P1), Ampelstatus + Trend (↗︎/→/↘︎), Deadline-Progress-Bar, # offene Meilensteine, Pflicht-Minimum-Score (z. B. „5/6").
- **FR-10** Projekt-Detail mit Kopfzeile (Name, P1, Projektart, Arbeitsmodus, Ampel + Trend), Pflicht-Minimum-Anzeige, Verlinkungen (Outline-Steckbrief, Plane-Board, Risikoliste), optional DoD-Text, Meilenstein-Checkliste, Update-Feed.
- **FR-11** Meilensteine als grobe Cockpit-Sicht (Titel, Fälligkeit, Status: offen/erledigt/überzogen). Wahrheit bleibt im Outline-Steckbrief.
**C: Bereichs-Detail Vertrieb**
- **FR-12** 5-Spalten Kanban: Lead → Erstgespräch → Demo → Angebot → Abschluss, mit Drag-and-Drop.
- **FR-13** Deal-Karten: Name, Verantwortlicher, Wert (€), Stage-Wahrscheinlichkeit (0-100 %), Tage in aktueller Stage.
- **FR-14** Vertriebs-Header-KPIs: gewichteter Pipeline-Wert, gewonnene Deals (# + EUR, laufendes Quartal), aktiver Deal-Count, ⌀ Stage-Verweildauer.
- **FR-15** Pro Produkt eine sortierbare GTM-Schritt-Liste (Schritt, Verantwortlicher, Status).
- **FR-16** Vertrieb-Update-Feed (Filter Bezug = Deal oder Vertrieb-allgemein).
**D: Bereichs-Detail Marketing**
- **FR-17** Horizontaler Lead-Funnel mit 5 Stufen (Reichweite → Website → Leads → MQL → Übergabe) + Conversion-%.
- **FR-18** Liste aktiver Kampagnen mit Status (Aktiv / Pausiert / Beendet), Reichweite, Verantwortlicher.
- **FR-19** Crosslink-Banner Marketing↔Vertrieb: „Marketing hat X Leads übergeben in [Zeitraum], Vertrieb hat daraus Y Deals erzeugt (Z €)".
- **FR-20** Marketing-Update-Feed (Filter Bezug = Kampagne oder Marketing-allgemein).
**E: Tagesrhythmus**
- **FR-21** „Einstempeln"-Aktion auf Puls (großer CTA), öffnet ToDo-Eingabe für heute.
- **FR-22** ToDo-Liste: 1-N Items, pro Item Titel (max. 200 Z) + optionaler Bezug.
- **FR-23** Eingestempelt = Tag „begonnen", Person verschwindet aus „heute fehlt"-Liste.
- **FR-24** Sichtbarkeit innerhalb des Tages: nur binäre Anwesenheit für Kollegen, ToDo-Inhalte privat. P1 sieht ToDos seiner Projekte.
- **FR-25** ToDos editierbar bis zur Reflexion.
- **FR-26** Reflexions-Formular: ToDos abhaken/verwerfen (mit Grund), Freitext (max. 1000 Z), Ampel (Grün/Gelb/Rot mit Leitfaden-Semantik), Trend (Pflicht bei Bezug=Projekt), optional Blocker, optional URL.
- **FR-27** Reflexion → Tag „voll abgeschlossen", Streak +1.
- **FR-28** Reflexion 30 min editierbar; Reflexions-Erinnerung ab konfigurierbarer Zeit (Default 17:00).
- **FR-29** Konsolidierter Vortags-Eintrag wird am nächsten Werktag morgen publiziert (sichtbar für Kollegen).
- **FR-30** Feed-Sortierung: neueste oben. Pagination 50/Seite.
- **FR-31** Reflexion setzt automatisch Ampel + Trend des verlinkten Projekts/Deals/Kampagne.
- **FR-32** Rückwirkende Erfassung max. 1 Werktag zurück.
- **FR-33** Tages-Status pro Person: 🟢 voll / 🟡 halb / ⚪ nicht angefangen / 🟣 abwesend.
**F: Streak / Anwesenheit / Disziplin**
- **FR-34** Pro Person aktuelle Streak (aufeinanderfolgende „volle" Werktage).
- **FR-35** „Heute fehlt"-Liste auf Puls — nach 11:00 für nicht-eingestempelte, nach 17:00 für „halb"-Status.
- **FR-36** Werktage = Mo-Fr (Feiertage Phase 2).
- **FR-37** Person markiert sich für Tag/Zeitraum als „Abwesend" (Urlaub/Krankheit), zählt nicht gegen Streak.
**G: Pflicht-Minimum-Check (Leitfaden §9)**
- **FR-38** Pro aktivem Projekt 6-Punkte-Checkliste sichtbar:
1. Projektsteckbrief in Outline freigegeben → erfüllt wenn `outline_steckbrief_url` gepflegt UND als „freigegeben" markiert.
2. P1 gesetzt.
3. Plane-Board mit zerlegter Arbeit → erfüllt wenn `plane_project_url` gepflegt.
4. Meilensteine mit groben Terminen → erfüllt wenn ≥ 1 Meilenstein.
5. Status-Ampel aktuell → erfüllt wenn letzte Reflexion mit Projektbezug ≤ 7 Werktage alt.
6. Abschluss-Retrospektive (nur bei beendeten Projekten).
- **FR-39** MVP: Punkte 1, 2, 3, 4 manuell als Cockpit-Checkbox; Punkt 5 automatisch aus Reflexions-Zeitstempel; Punkt 6 im Abschluss-Workflow.
- **FR-40** Projekte mit unerfülltem Pflicht-Punkt prominent auf Puls.
**H: Blocker**
- **FR-41** Blocker entsteht inline in Reflexion oder via „Neuer Blocker"-Button.
- **FR-42** Blocker-Felder: Titel, Beschreibung, Verantwortlicher, Ersteller, Bereich(e), Datum offen, Status.
- **FR-43** Blocker erscheint auf Puls, in betroffenen Bereichs-Detail-Views und in Projekt-Detail.
- **FR-44** Auflösung mit Auflöser, Datum, Kommentar. Nur P1 des bezogenen Projekts darf auflösen; bei projektlosen Blockern auch Ersteller/Verantwortlicher.
**I: Projekt-Abschluss**
- **FR-45** P1 setzt Projekt auf „abgeschlossen".
- **FR-46** Beim Abschluss fordert Cockpit Lessons-Learned-URL an.
- **FR-47** Abgeschlossene Projekte verschwinden aus Puls, bleiben in Archiv-Sicht pro Bereich.
**J: Navigation, Suche & Filter**
- **FR-48** Globale TopNav: Puls / Entwicklung / Vertrieb / Marketing / Mein heute / Eigenes Profil / Archiv.
- **FR-49** Volltextsuche → Phase 2; MVP: Filter im Feed nach Bereich/Bezug.
### NonFunctional Requirements
> 16 NFRs aus PRD §7.
**Performance**
- **NFR-1** Puls-Seite < 1,5 s bei 6-15 Nutzern und 90 Tagen Historie.
- **NFR-2** Tagesupdate-Speichern < 500 ms.
**Verfügbarkeit**
- **NFR-3** Mo-Fr 8-19: 99 %.
- **NFR-4** Wartungsfenster außerhalb Kernzeit kommunizierbar.
**Security & Privacy**
- **NFR-5** Alle Routen hinter Authentik-OIDC.
- **NFR-6** Keine externe PII in Deal-Karten.
- **NFR-7** Audit-Trail für jede mutierende Operation (actor / timestamp / payload).
**Bedienbarkeit & Visual**
- **NFR-8** Brand-konform: weiß primär, schwarz sekundär, ToolCTRL Brand Identity.
- **NFR-9** Tagesupdate-Schreib-Workflow ≤ 3 Klicks ab Puls.
- **NFR-10** Desktop-first, mobil-lesbar (nicht mobil-schreibbar).
- **NFR-11** Deutsch durchgängig, keine i18n.
**Voice & Tone**
- **NFR-12** Tonalität locker/direkt/deutsch, kurze Imperative ("Stark.", "Wie läuft es?", "Du hast heute noch nicht gepingt.").
- **NFR-13** Keine Corporate-Sprech, keine englischen Floskeln (Fachbegriffe aus Leitfaden ausgenommen).
**Datenhaltung & Erweiterbarkeit**
- **NFR-14** Postgres als Persistenz.
- **NFR-15** Tägliches Backup.
- **NFR-16** Datenmodell erweiterbar ohne Migrationen > 2 h.
### Additional Requirements
> Aus Architektur-Spec abgeleitete technische Anforderungen.
- **AR-1** **Starter / Bootstrap**: Custom Monorepo (pnpm-Workspaces) mit `apps/web` (Vite + React + TS + Tailwind), `apps/api` (Fastify + tRPC + Drizzle), `packages/db` (Drizzle-Schema), `packages/shared` (Zod + Types). Erste Story nutzt die in der Architektur §"Initialization Commands" beschriebenen Setup-Befehle.
- **AR-2** **Datenmodell** in `packages/db/schema.ts` mit allen 14 Entitäten (User, Area, Project, Milestone, Deal, GtmStep, Campaign, LeadHandover, DayEntry, Todo, Blocker, AbsenceDay, AuditEvent, Session) gemäß Addendum §1.2 und Architektur §Project Structure. UUID v7 als ID-Primary.
- **AR-3** **Auth-Flow**: `openid-client` v6+ direkt im Fastify, Authorization Code + PKCE gegen Authentik. Server-Side-Session in Postgres, HttpOnly + SameSite=Lax + Secure Cookie. Default-Bereich aus Authentik-Group-Claim.
- **AR-4** **tRPC Procedure-Typen**: `publicProcedure`, `protectedProcedure`, `dayEntryVisibleProcedure` (Sichtbarkeitsfilter pre/post `published_at`), `projectOwnerProcedure` (P1-Check).
- **AR-5** **Audit-Middleware** (`withAudit`) um jede schreibende tRPC-Mutation, schreibt parallel in `audit_event`.
- **AR-6** **Background-Jobs** via System-Cron → authentifizierter Fastify-Endpoint (`POST /internal/cron/*` mit Bearer-Token):
- `publish-day-entries` (tägl. 00:05 Europe/Berlin)
- `recompute-pflicht-minimum-5` (stündlich)
- `recompute-streaks` (tägl. 00:10)
- `pg-dump-backup` (tägl. 02:00)
- **AR-7** **Werktag-Logik** zentral in `apps/api/src/lib/werktag.ts` (Mo-Fr Europe/Berlin). Frontend bekommt vorberechnete Werte, niemals eigene Datumsmagie.
- **AR-8** **Ampel-Aggregation** in `apps/api/src/lib/ampel.ts` (FR-1a-Algorithmus).
- **AR-9** **Pflicht-Minimum-Berechnung** in `apps/api/src/lib/pflicht-minimum.ts`, lazy beim Read.
- **AR-10** **Hosting**: Docker-Compose mit `web`-Container (Caddy-served `apps/web/dist`), `api`-Container (Node 22+), `postgres:16`-Container, `caddy`-Container als Reverse-Proxy auf `cockpit.tool-ctrl.de`.
- **AR-11** **CI/CD**: Gitea Actions Pipeline (Typecheck, Lint, Vitest, Build, Docker-Image-Push, Deploy-Trigger).
- **AR-12** **Tests**: Vitest (Unit + tRPC-Procedures), Playwright (E2E auf UJ-1, UJ-2, UJ-5). Lighthouse-A11y-Score ≥ 95.
- **AR-13** **Logging**: strukturierte JSON-Logs via `pino` → stdout.
- **AR-14** **Healthcheck**: `GET /api/health` (Postgres-Ping + Authentik-Reachability) für Docker-Healthcheck.
- **AR-15** **Konventionen-Datei** `CONVENTIONS.md` im Repo-Root (spiegelt Architektur §Implementation Patterns), Pflicht-Lesen vor Story-Bearbeitung.
- **AR-16** **Backup-Strategie**: `pg_dump` täglich auf separates Volume, Retention 14 Tage. Restore-Drill quartalsweise dokumentiert in `docs/operations.md`.
### UX Design Requirements
> Aus UX-Spec abgeleitete UI/Visual/A11y-Anforderungen.
**Brand-Tokens & Visual Foundation**
- **UX-DR1** Brand-Tokens als CSS-Variablen in `apps/web/src/styles/globals.css` definieren und in `tailwind.config.ts` als Theme spiegeln. Werte gemäß UX-Spec §Visual Design Foundation:
- `--color-bg: #ffffff`, `--color-surface: #fafafa`, `--color-surface-elevated: #ffffff`
- `--color-border: #e5e5e5`, `--color-border-strong: #d4d4d4`
- `--color-ink: #0a0a0a`, `--color-ink-muted: #525252`, `--color-ink-faint: #a3a3a3`
- `--color-brand: #0a0a0a`, `--color-brand-fg: #ffffff`
- Ampel: green `#15803d`/`#dcfce7`, amber `#a16207`/`#fef3c7`, red `#b91c1c`/`#fee2e2`
- Trend: up `#15803d`, flat `#525252`, down `#b91c1c`
- **UX-DR2** Typografie: Inter (UI, variable) + JetBrains Mono (Werte) selbst-gehostet via `@fontsource/inter` und `@fontsource/jetbrains-mono`. 7-Stufen-Type-Scale aus UX-Spec §Typography.
- **UX-DR3** Spacing-Skala, Radii und Schatten gemäß UX-Spec §Spacing & Layout — als Tailwind-Theme-Erweiterung.
- **UX-DR4** Container-Max-Width 1.280 px, 12-Spalten-Grid mit 24 px Gap; Puls auf Desktop 3-Spalten.
**Custom-Component-Library (`apps/web/src/components/ui/`)**
- **UX-DR5** `Button` mit 3 Varianten (primary/secondary/ghost) + Destructive + Icon-Only, 3 Größen (sm/md/lg).
- **UX-DR6** `Card` mit Compound-API (`Card.Root`, `Card.Header`, `Card.Body`, `Card.Footer`).
- **UX-DR7** `Input`, `Textarea` (mit Zeichen-Counter), `Select` (via Radix), `Checkbox` (via Radix mit animiertem Häkchen).
- **UX-DR8** `AmpelButton` (3-Wege-Toggle Grün/Gelb/Rot, große Klick-Targets), `TrendButton` (3-Wege ↗︎/→/↘︎).
- **UX-DR9** `Dialog` (via Radix mit Brand-Header und `radius-xl`), `Tooltip` (via Radix, schwarzer Bubble), `Toast` (via Sonner, Bottom-Right, schwarzer Pill).
- **UX-DR10** `SkeletonCard` — graue Card-Skelette identischer Geometrie wie echte Cards.
- **UX-DR11** `AmpelBadge` — Status-Pill mit Farbpunkt + Label + optionalem Trend-Pfeil.
- **UX-DR12** `PflichtMinimumIndicator` — sechs kleine Quadrate horizontal, gefüllt/leer.
- **UX-DR13** `KeyCaret` — SVG-Komponente für das Brand-`^`-Element, einbindbar in Header/Logo/Toast.
**Layout & Navigation**
- **UX-DR14** TopNav fest mit `cockpit ^` Logo links, Tabs (Puls / Entwicklung / Vertrieb / Marketing / Mein heute / Archiv), rechts Streak-Badge + Avatar. Aktiver Tab mit 2 px schwarzem Underline.
- **UX-DR15** Breadcrumbs in Detail-Views (`Puls › Entwicklung › Projekt X`), klickbar pro Segment.
- **UX-DR16** Tastatur-Shortcuts: `g p`, `g h`, `g e`, `g v`, `g m`, `g a`, `n` (Neuer Blocker), `?` (Cheatsheet), `Esc` (Modal schließen).
**Voice & Tone (Mikrocopy)**
- **UX-DR17** Zentrale Mikrocopy-Datei `apps/web/src/lib/voice.ts` mit allen UI-Strings. Beispiele: „Stark.", „Wie läuft es?", „Du hast heute noch nicht gepingt.", „Reflektiert.", „Blocker offen.". Pflicht-Anwendung in allen Komponenten.
**User-Journey-Flows**
- **UX-DR18** UJ-1 Tagesrhythmus: `/mein-heute` (Morgen-Einstempeln + ToDo-Editor) und `/reflexion` (Abend-Reflexion mit Tagesplan-Echo).
- **UX-DR19** UJ-2 Jourfixe: Puls-Layout muss alle 3 Bereiche in einem Scroll abdeckbar machen.
- **UX-DR20** UJ-5 Projekt anlegen: Modal mit Pflicht-Feldern (Name, P1, Projektart, Arbeitsmodus, Steckbrief-URL, Plane-URL).
**Responsive & Accessibility**
- **UX-DR21** Drei Sicht-Ebenen: Desktop (≥ 1280 px, 3-Spalten Puls), Tablet (≥ 768 px, 2-Spalten), Mobile (< 768 px, Single-Column + Bottom-Tab-Bar, lesbar).
- **UX-DR22** WCAG 2.1 AA durchgängig. Keyboard-First (Tab, Enter, Space, Esc). Sichtbarer Fokus-Ring (`outline: 2px solid var(--color-ink)`, 2 px Offset).
- **UX-DR23** Skip-Link „Zum Hauptinhalt springen" oben. Saubere Heading-Hierarchie. Live-Regionen für Toasts.
- **UX-DR24** `prefers-reduced-motion` → Fade-Animationen auf 0 ms.
- **UX-DR25** axe-core in Vitest pro Komponente; `@axe-core/playwright` für E2E. ESlint-`jsx-a11y` mit `recommended` aktiviert.
**Interaktions-Patterns**
- **UX-DR26** Optimistic Updates bei Checkbox-Abhaken und Ampel-Setzen (Rollback bei API-Fehler).
- **UX-DR27** Skeleton-Cards beim Initial-Load — nie leerer Bildschirm, nie Spinner alleine.
- **UX-DR28** Empty-States als Voice-konforme Tropfen-Information (z. B. „Hier erscheinen die Updates des Tages, sobald jemand reflektiert hat.").
- **UX-DR29** Reflexions-Erinnerung im Header ab 17:00 (dezenter Pill, klickbar).
- **UX-DR30** Vortrags-Reveal: Fade-In ≤ 200 ms beim ersten Laden am Werktag X+1.
### FR Coverage Map
| FR | Epic | Kurztitel |
|---|---|---|
| FR-1, FR-1a, FR-2, FR-3, FR-4, FR-5, FR-6, FR-7 | Epic 3 | Puls & Übersicht |
| FR-8, FR-9, FR-10, FR-11 | Epic 4 | Bereich Entwicklung |
| FR-12, FR-13, FR-14, FR-15, FR-16 | Epic 4 | Bereich Vertrieb |
| FR-17, FR-18, FR-19, FR-20 | Epic 4 | Bereich Marketing |
| FR-21 – FR-33 | Epic 2 | Tagesrhythmus |
| FR-34, FR-35, FR-36, FR-37 | Epic 2 + Epic 3 | Streak + Anwesenheit |
| FR-38, FR-39, FR-40 | Epic 5 | Pflicht-Minimum |
| FR-41, FR-42, FR-43, FR-44 | Epic 5 | Blocker |
| FR-45, FR-46, FR-47 | Epic 6 | Projekt-Abschluss |
| FR-48 | Epic 1 | Navigation (Top-Nav) |
| FR-49 | Phase 2 | Volltextsuche |
**NFR-Abdeckung:** NFR-5/7/8/9 ⇒ Epic 1 · NFR-1/2 ⇒ über alle Epics validiert · NFR-3/4/14/15/16 ⇒ Epic 6 · NFR-10/11/12/13 ⇒ Epic 1 + Epic 2 (Voice-Anker etabliert)
## Epic List
> 6 Epics, jedes liefert ein eigenständig nutzbares Stück. Reihenfolge folgt der in `architecture.md` § "Decision Impact Analysis → Implementation Sequence" festgelegten Story-Sequenz.
### Epic 1: Fundament — Login, Shell, Brand
**Ziel:** Eine Person kann sich via Authentik einloggen und landet in einer leeren, brand-konformen Cockpit-Shell mit funktionierender Navigation. Es passiert noch nichts Fachliches — aber das Fundament steht.
**Was nach Epic 1 nutzbar ist:**
- Login/Logout funktioniert
- TopNav steht (Puls-Tab, Profil, Streak-Badge-Slot leer)
- Brand-Theme aktiv (Farben, Typografie, KeyCaret-Logo)
- Healthcheck + Docker-Compose laufen lokal
**FRs:** FR-48
**NFRs:** NFR-5, NFR-7, NFR-8, NFR-9 (vorbereitet), NFR-11, NFR-12, NFR-13, NFR-14
**AR:** AR-1, AR-2 (Schema-Skeleton User+Session+AuditEvent), AR-3, AR-4 (Procedure-Typen-Skeleton), AR-5 (Audit-Wrapper), AR-10, AR-15, AR-13 (pino), AR-14 (Healthcheck)
**UX-DR:** UX-DR1, UX-DR2, UX-DR3, UX-DR4, UX-DR5, UX-DR6, UX-DR7, UX-DR13, UX-DR14, UX-DR15, UX-DR16, UX-DR17, UX-DR22, UX-DR23, UX-DR24, UX-DR25
### Epic 2: Tagesrhythmus — Einstempeln, ToDos, Reflexion
**Ziel:** Eine Person kann morgens einstempeln + ToDos schreiben, abends reflektieren mit Ampel/Trend, und am nächsten Werktag wird der konsolidierte Eintrag für Kollegen sichtbar. Das Herz des Cockpits.
**Was nach Epic 2 nutzbar ist:**
- Person kann den vollen Tagesrhythmus durchlaufen (UJ-1)
- Streak und Tages-Status erscheinen im Profil und in der „Heute fehlt"-Liste (basal — volle Puls-Visualisierung kommt in Epic 3)
- Cron-Job publiziert Vortags-Einträge
**FRs:** FR-21 – FR-33, FR-34, FR-36, FR-37
**AR:** AR-2 (Schema DayEntry, Todo, AbsenceDay), AR-4 (`dayEntryVisibleProcedure`), AR-5, AR-6 (publish-day-entries cron + recompute-streaks cron), AR-7
**UX-DR:** UX-DR8 (AmpelButton, TrendButton), UX-DR9 (Dialog für AbwesendDialog), UX-DR10 (SkeletonCard), UX-DR18 (`/mein-heute` + `/reflexion`), UX-DR26 (optimistic), UX-DR27 (Skeleton), UX-DR28 (Empty-State), UX-DR29 (17:00-Hinweis), UX-DR30 (Vortrags-Reveal Fade-In)
### Epic 3: Puls & Bereichs-Übersicht
**Ziel:** Eine Person öffnet das Cockpit und sieht in unter 60 Sekunden den Unternehmens-Puls: alle drei Bereiche mit aggregiertem Ampelstatus, Heute-Quote, bereichsübergreifende Blocker, Jourfixe-Hinweis. Vortrags-Einträge im Feed.
**Was nach Epic 3 nutzbar ist:**
- Puls-Seite ist die Heimat (UJ-2, UJ-3)
- „Heute fehlt"-Liste klickbar mit Personen
- Bereichs-Strip mit Ampel-Aggregation
- Feed konsolidierter Vortrags-Einträge
**FRs:** FR-1, FR-1a, FR-2, FR-3, FR-4, FR-5 (Schema-Layout, KPIs werden mit Echtdaten in Epic 4 gefüllt), FR-6, FR-7 (Pflicht-Minimum-Slot, gefüllt in Epic 5), FR-35
**AR:** AR-8 (Ampel-Aggregation), AR-7 (Werktage)
**UX-DR:** UX-DR11 (AmpelBadge), UX-DR19 (Puls-Layout 3-Spalten)
### Epic 4: Projekte, Deals, Kampagnen — Bereichs-Details
**Ziel:** Eine Person kann Projekte (Entwicklung), Deals (Vertrieb) und Kampagnen (Marketing) anlegen und pflegen. Bereichs-Detail-Views zeigen Listen, Kanban, Lead-Funnel, Crosslinks. Reflexionen (aus Epic 2) setzen Projekt-Ampeln korrekt.
**Was nach Epic 4 nutzbar ist:**
- Projekte/Deals/Kampagnen können angelegt, geändert und gelistet werden (UJ-5)
- Sales-Kanban funktioniert (Drag-and-Drop, Stage-Wechsel)
- Lead-Funnel + Crosslink-Banner zeigen Aggregate
- KPIs im Puls-Bereichs-Strip (FR-5) zeigen Echtdaten
**FRs:** FR-8, FR-9, FR-10, FR-11, FR-12, FR-13, FR-14, FR-15, FR-16, FR-17, FR-18, FR-19, FR-20, FR-31
**AR:** AR-2 (Schema Project, Milestone, Deal, GtmStep, Campaign, LeadHandover)
**UX-DR:** UX-DR9 (Dialog für Projekt-Anlegen), UX-DR20 (Projekt-Anlege-Modal)
### Epic 5: Pflicht-Minimum & Blocker
**Ziel:** Die 6 Leitfaden-Pflicht-Punkte pro Projekt werden sichtbar gemacht. Blocker können erfasst, gelistet und (nur durch P1) aufgelöst werden.
**Was nach Epic 5 nutzbar ist:**
- Pflicht-Minimum-Indikator in Projekt-Card und -Detail (FR-7 wird voll funktional auf dem Puls)
- Blocker-Lebenszyklus komplett (Erfassen via Reflexion-Inline oder „+ Neuer Blocker"-Button, Anzeige im Puls/Bereich/Projekt, Auflösung)
- Frühwarn-Mechanik aktiv
**FRs:** FR-38, FR-39, FR-40, FR-41, FR-42, FR-43, FR-44
**AR:** AR-4 (`projectOwnerProcedure`), AR-6 (recompute-pflicht-minimum-5 cron), AR-9
**UX-DR:** UX-DR12 (PflichtMinimumIndicator)
### Epic 6: Projekt-Abschluss, Archiv & Production
**Ziel:** Projekte können abgeschlossen werden (mit Lessons-Learned-Link), Archiv-Sicht existiert. Production-Pipeline ist scharf: CI/CD, tägliches Backup, Healthcheck-Monitoring, A11y-Tests im CI.
**Was nach Epic 6 nutzbar ist:**
- Volle Projekt-Lebensspanne von Anlage bis Lessons-Learned
- Cockpit ist deploybar und beobachtbar
- Backup + Restore-Drill dokumentiert
- A11y-Score ≥ 95 im CI gemessen
**FRs:** FR-45, FR-46, FR-47
**NFRs:** NFR-1, NFR-2, NFR-3, NFR-4, NFR-10 (Responsive-Validierung), NFR-15, NFR-16
**AR:** AR-6 (pg-dump-backup), AR-11 (Gitea Actions), AR-12 (Tests, Playwright), AR-14 (Healthcheck-Erweiterung), AR-16 (Backup + Restore-Drill)
**UX-DR:** UX-DR21 (Responsive-Sichtbarkeitsprüfung), UX-DR25 (axe-core + jsx-a11y in CI)
---
**Phase-2-Backlog (nicht im MVP, nicht in Epics):**
- FR-49 Volltextsuche
- Plane-/Outline-API-Sync
- Mobile schreibbar
- Reaktionen / Kommentare auf Updates
- Element/Matrix/Mail-Reminders
- Loki/Grafana, Sentry, Feiertags-Kalender, Dark Mode
---
## Epic 1: Fundament — Login, Shell, Brand
Eine Person kann sich via Authentik einloggen und landet in einer leeren, brand-konformen Cockpit-Shell mit funktionierender Navigation. Es passiert noch nichts Fachliches — aber das Fundament steht und alle weiteren Epics können darauf aufsetzen.
### Story 1.1: Repo-Bootstrap und Tooling
Als **Entwickler**,
ich möchte **ein lauffähiges Monorepo mit Vite-Frontend, Fastify-Backend, Drizzle-DB-Package und Shared-Package**,
sodass **alle Folge-Stories auf einem konsistenten Setup aufsetzen können**.
**Acceptance Criteria:**
**Given** ein leeres `cockpit/`-Repo,
**When** `pnpm install` und `pnpm typecheck` ausgeführt werden,
**Then** alle 4 Workspaces (`apps/web`, `apps/api`, `packages/db`, `packages/shared`) sind installiert und typecheckbar
**And** `pnpm --filter web dev` startet Vite auf Port 5173
**And** `pnpm --filter api dev` startet Fastify auf Port 3000
**And** `CONVENTIONS.md` ist im Repo-Root vorhanden und spiegelt die Patterns aus `architecture.md` § Implementation Patterns
**And** ESLint und Prettier sind konfiguriert und liefern keinen Fehler beim Start.
### Story 1.2: Postgres + Drizzle-Schema-Skeleton mit User, Session, AuditEvent
Als **Entwickler**,
ich möchte **eine laufende Postgres-Instanz und das Drizzle-Schema für die drei Fundament-Entitäten**,
sodass **Auth und Audit auf einer realen DB-Schicht funktionieren können**.
**Acceptance Criteria:**
**Given** Docker auf dem Dev-Host,
**When** `docker compose -f docker-compose.dev.yml up postgres` läuft,
**Then** Postgres 16 ist auf Port 5432 erreichbar
**And** `packages/db/schema.ts` definiert die Tabellen `user`, `session`, `audit_event` mit UUID-v7-IDs, snake_case-Spalten und `timestamptz`-Zeitstempeln
**And** `pnpm --filter db migrate:generate` erzeugt eine Migration `0000_init.sql`
**And** `pnpm --filter db migrate` führt sie aus und die drei Tabellen existieren in der DB.
### Story 1.3: Authentik-OIDC-Login-Flow
Als **Mitarbeiter**,
ich möchte **mich via Authentik (SSO) einloggen und nach dem Login automatisch auf der Cockpit-Heimseite landen**,
sodass **ich ohne separate Account-Anlage Zugang zum Cockpit bekomme**.
**Acceptance Criteria:**
**Given** ich öffne `cockpit.tool-ctrl.de` ohne Session,
**When** das Backend mich an `auth.tool-ctrl.de` weiterleitet und ich mich dort erfolgreich einlogge,
**Then** werde ich auf `/puls` zurückgeleitet
**And** ein HttpOnly + SameSite=Lax + Secure Cookie `session_id` ist gesetzt
**And** in der DB existiert eine `session`-Zeile mit `expires_at` 24 h in der Zukunft
**And** in der DB existiert eine `user`-Zeile mit `authentik_sub`, `name`, `email` und (sofern Group-Claim vorhanden) `default_area_id`
**And** ein erneutes Aufrufen jeder geschützten Route innerhalb der Session-Gültigkeit funktioniert ohne erneuten Login
**And** `POST /api/auth/logout` invalidiert die Session in der DB und löscht das Cookie.
### Story 1.4: tRPC-Bootstrap mit `protectedProcedure` und Audit-Wrapper
Als **Entwickler**,
ich möchte **die tRPC-v11-Infrastruktur mit Context, `protectedProcedure` und `withAudit`-Wrapper laufen haben**,
sodass **alle fachlichen Stories direkt typsicher Procedures bauen können**.
**Acceptance Criteria:**
**Given** Story 1.3 ist gemerged,
**When** ein Client `trpc.user.me` ohne Session aufruft,
**Then** liefert das Backend `TRPCError(UNAUTHORIZED)` mit deutscher Message
**And** mit gültiger Session liefert es das `User`-Objekt aus der DB
**And** `withAudit` wrapt jede Mutation und schreibt parallel in `audit_event` in derselben Transaktion (verifiziert via Test)
**And** der Frontend-tRPC-Client in `apps/web/src/lib/trpc.ts` ist konfiguriert mit zentralem `onError`-Toast und nutzt die `AppRouter`-Typdefinition aus `packages/shared`.
### Story 1.5: Brand-Tokens, Tailwind-Theme und Voice-Datei
Als **UI-Entwickler**,
ich möchte **die ToolCTRL-Brand-Tokens als CSS-Variablen und Tailwind-Theme-Erweiterung aktiv haben**,
sodass **alle weiteren UI-Komponenten brand-konform stylebar sind**.
**Acceptance Criteria:**
**Given** Story 1.1 ist mergeable,
**When** ich `apps/web/src/styles/globals.css` lade,
**Then** sind alle Farb-Tokens, Spacing-, Radii- und Schatten-Variablen aus der UX-Spec definiert
**And** `tailwind.config.ts` spiegelt diese Tokens als Theme-Erweiterung
**And** `@fontsource/inter` und `@fontsource/jetbrains-mono` sind installiert und geladen
**And** `apps/web/src/lib/voice.ts` existiert mit allen Mikrocopy-Strings aus UX-Spec § Voice & Tone als exportierte Konstanten
**And** eine Test-Route `/dev/tokens` (nur in `import.meta.env.DEV`) zeigt eine Palette aller Tokens als visuelle Referenz.
### Story 1.6: TopNav-Shell mit Logo, Tabs, Profil-Slot und Keyboard-Shortcuts
Als **Mitarbeiter**,
ich möchte **eine sichtbare, navigierbare Cockpit-Shell mit Brand-Logo, Hauptnavigation und Profil-Bereich**,
sodass **ich die App-Struktur sofort verstehe**.
**Acceptance Criteria:**
**Given** ich bin eingeloggt,
**When** ich `cockpit.tool-ctrl.de` öffne,
**Then** sehe ich oben links `cockpit ^` (KeyCaret-SVG + Lockup) und in der Mitte die TopNav-Tabs `Puls`, `Entwicklung`, `Vertrieb`, `Marketing`, `Mein heute`, `Archiv`
**And** rechts erscheint mein Avatar (Initialen) und ein Streak-Badge-Platzhalter
**And** der aktive Tab zeigt einen 2 px dicken schwarzen Underline
**And** Tastatur-Shortcuts `g p`, `g h`, `g e`, `g v`, `g m`, `g a` springen auf die jeweilige Route
**And** `?` öffnet einen Cheatsheet-Dialog (alle Shortcuts gelistet)
**And** WCAG 2.1 AA wird erfüllt (axe-core-Test grün), Fokus-Ring sichtbar auf jedem Tab.
### Story 1.7: Healthcheck, Docker-Compose und CI-Skeleton
Als **DevOps**,
ich möchte **`docker compose up` startet Web + API + Postgres + Caddy lokal und der Healthcheck antwortet**,
sodass **Deployments und CI auf einer reproduzierbaren Basis aufsetzen können**.
**Acceptance Criteria:**
**Given** `docker-compose.yml` und `Caddyfile` existieren,
**When** `docker compose up -d --build` läuft,
**Then** sind alle vier Container `web`, `api`, `postgres`, `caddy` healthy
**And** `GET /api/health` liefert `200 OK` mit JSON `{ "status": "ok", "db": "ok", "authentik": "ok" }`
**And** `GET /` liefert die SPA-Index (statisch via Caddy)
**And** ein minimales Gitea-Actions-Workflow `ci.yml` läuft Typecheck, Lint und Vitest auf jedem Push.
---
## Epic 2: Tagesrhythmus — Einstempeln, ToDos, Reflexion
Eine Person kann den vollen Tagesrhythmus durchlaufen: morgens einstempeln + ToDos schreiben, abends reflektieren, am nächsten Werktag wird der Eintrag konsolidiert. Das ist die Kern-Erfahrung des Cockpits.
### Story 2.1: DayEntry- und Todo-Schema, AbsenceDay, Werktag-Helper
Als **Entwickler**,
ich möchte **die DB-Tabellen für Tageseinträge, ToDos und Abwesenheiten plus die Werktag-Logik im Backend**,
sodass **die Tagesrhythmus-Procedures auf einer korrekten Datenbasis arbeiten**.
**Acceptance Criteria:**
**Given** Epic 1 ist abgeschlossen,
**When** die Migration `0001_day_rhythm.sql` läuft,
**Then** existieren in der DB die Tabellen `day_entry`, `todo`, `absence_day`, `blocker` gemäß Addendum § 1.2
**And** `apps/api/src/lib/werktag.ts` bietet `isWerktag(date)`, `nextWerktag(date)`, `werktagsDistance(a, b)` mit Mo-Fr-Logik in `Europe/Berlin`
**And** Vitest-Tests decken Grenzfälle ab (Wochenende, Tageswechsel um Mitternacht).
### Story 2.2: Einstempeln + ToDo-Eingabe auf „Mein heute"
Als **Mitarbeiter**,
ich möchte **mich morgens einstempeln und meine ToDos für heute eintippen**,
sodass **mein Tag begonnen ist und ich für Kollegen als „eingestempelt" sichtbar bin**.
**Acceptance Criteria:**
**Given** ich öffne `/mein-heute` als noch nicht eingestempelte Person,
**When** ich auf „Einstempeln" klicke und 3 ToDos eintippe (Enter erzeugt neue Zeile, optionaler Bezug per Inline-Select),
**Then** wird beim Klick auf „Eingestempelt" ein `day_entry` mit `status='begun'` und `checked_in_at` angelegt sowie 3 `todo`-Zeilen
**And** ich werde auf `/puls` weitergeleitet
**And** im „Heute fehlt"-Modul auf dem Puls erscheine ich nicht mehr (Kollegen sehen nur binäre Anwesenheit, nicht die ToDo-Inhalte — vgl. FR-24)
**And** ein „Eingestempelt."-Toast erscheint 2 s in Bottom-Right
**And** der Median-Speed-Test (manuelle Stoppuhr) zeigt ≤ 60 s für 3 ToDos (CM1a).
### Story 2.3: Tagsüber ToDos editieren, „Mein heute" als private Sicht
Als **Mitarbeiter**,
ich möchte **meine ToDos im Verlauf des Tages ergänzen oder ändern können**,
sodass **mein Tagesplan realitätsnah bleibt — und kein Kollege das mitliest, bevor ich reflektiert habe**.
**Acceptance Criteria:**
**Given** ich bin eingestempelt mit 3 ToDos,
**When** ich `/mein-heute` erneut öffne,
**Then** sehe ich meine ToDos editierbar (hinzufügen, ändern, löschen) bis zur Reflexion
**And** Edits werden gespeichert via tRPC-Mutation `todo.update`/`todo.create`/`todo.delete`
**And** ein anderer User, der dieselbe Person als „eingestempelt" sieht, kann **keine** ToDo-Inhalte abrufen (tRPC `dayEntry.todosForUser` liefert für andere User vor `published_at` nur `count`)
**And** die Projekt-P1 darf die ToDos sehen, sofern ein ToDo Bezug auf ihr/sein Projekt hat — verifiziert durch tRPC-Test mit zwei verschiedenen User-Sessions.
### Story 2.4: Abend-Reflexion mit Tagesplan-Echo, Ampel + Trend
Als **Mitarbeiter**,
ich möchte **abends meine ToDos durchgehen, abhaken oder verwerfen, einen Freitext schreiben und Ampel/Trend setzen**,
sodass **mein Tag offiziell als „voll" abgeschlossen gilt und der Status meines Projekts aktuell ist**.
**Acceptance Criteria:**
**Given** ich bin eingestempelt und öffne `/reflexion`,
**When** ich meine ToDos sehe (Tagesplan-Echo mit 200 ms Fade-In) und einige abhake, einige verwerfe (Inline-Grund-Feld) und einen Freitext mit ≤ 1000 Zeichen schreibe,
**Then** kann ich Ampel (🟢/🟡/🔴 als drei große Buttons) und Trend (↗︎/→/↘︎) setzen
**And** Trend ist Pflichtfeld, sofern mindestens ein ToDo Bezug=Projekt hat — Submit ohne Trend zeigt deutsche Inline-Fehlermeldung
**And** Submit setzt `day_entry.status='full'`, `reflected_at`, schreibt `reflection_text`, `ampel`, `trend`
**And** die Ampel + der Trend des verlinkten Projekts werden in derselben DB-Transaktion aktualisiert (`status_cache`, `trend_cache`)
**And** ein „Reflektiert."-Toast erscheint, ich werde auf `/puls` redirected
**And** Reflexion ist 30 min lang editierbar — danach erscheint sie unverändert.
### Story 2.5: Blocker inline in der Reflexion erfassen
Als **Mitarbeiter**,
ich möchte **bei der Reflexion einen Blocker mit einem Toggle eintragen**,
sodass **das Team meine Hindernisse am nächsten Werktagsmorgen sieht**.
**Acceptance Criteria:**
**Given** ich bin in der Reflexion,
**When** ich den „Blocker?"-Toggle aktiviere und Titel + Beschreibung eintippe,
**Then** wird beim Submit zusätzlich ein `blocker`-Datensatz erzeugt
**And** der Blocker ist mit `opened_by = mein User` und (sofern ToDo-Bezug=Projekt) `project_id` verknüpft
**And** der `day_entry.blocker_id` zeigt auf den neuen Blocker
**And** der Blocker erscheint **erst nach Publikation am nächsten Werktag** im bereichsübergreifenden Blocker-Modul des Puls (Verifizierung mit zweiter Session).
### Story 2.6: Cron „publish-day-entries" + Streak-Berechnung
Als **System**,
möchte ich **nachts um 00:05 Europe/Berlin die DayEntries des Vortags publizieren und Streaks neu berechnen**,
sodass **am Morgen X+1 die konsolidierten Vortrags-Einträge sichtbar werden und Streaks korrekt sind**.
**Acceptance Criteria:**
**Given** mehrere User haben am Vortag eingestempelt + reflektiert,
**When** der System-Cron `POST /internal/cron/publish-day-entries` mit Bearer-Token aufruft,
**Then** werden alle `day_entry`-Zeilen des Vortags mit `published_at = heute 00:00` versehen
**And** die nachfolgende tRPC-Procedure `dayEntry.feedForArea` liefert diese Einträge inklusive ToDos + Reflexion für alle Kollegen
**And** ein zweiter Cron-Aufruf `recompute-streaks` setzt `user.current_streak` korrekt (vollständige + halbe + abwesende Tage gemäß FR-33/34)
**And** Cron-Aufrufe ohne korrekten Bearer-Token liefern `401`.
### Story 2.7: Streak-Badge, Abwesenheit, „Halb"-Status
Als **Mitarbeiter**,
ich möchte **meine aktuelle Streak in der TopNav sehen und mich für Tage als abwesend markieren können**,
sodass **Urlaubstage nicht meine Streak brechen**.
**Acceptance Criteria:**
**Given** ich bin eingeloggt,
**When** ich die TopNav betrachte,
**Then** sehe ich rechts neben meinem Avatar einen Pill „N ^" (N = aktuelle Streak, KeyCaret als Suffix)
**And** ich kann via Profil-Modal einen Zeitraum als „abwesend" (Urlaub/Krank/Sonstiges) markieren
**And** Abwesenheits-Tage werden als `absence_day` gespeichert und im Streak-Cron als „nicht gegen die Serie zählend" behandelt
**And** wenn ich morgens nur einstempele aber nicht reflektiere, zeigt `dayEntry.todayStatus` für mich `status='begun'` und mein Tages-Status-Indikator wird gelb („halb")
**And** der nächste Streak-Cron würde diesen Tag nicht für die Serie zählen (verifiziert via Unit-Test in `werktag.ts`).
---
## Epic 3: Puls & Bereichs-Übersicht
Eine Person öffnet das Cockpit und sieht in unter 60 Sekunden alles Wesentliche: drei Bereiche mit Ampel-Aggregat, Heute-Quote, Blocker, Jourfixe-Hinweis, Vortrags-Feed.
### Story 3.1: Area-Schema, Aggregations-Algorithmus, Puls-Procedure
Als **Entwickler**,
ich möchte **die `area`-Tabelle und die `apps/api/src/lib/ampel.ts`-Aggregations-Logik plus eine tRPC-Procedure `puls.overview`**,
sodass **die Puls-Frontend-Seite alle Aggregat-Daten in einem Roundtrip bekommt**.
**Acceptance Criteria:**
**Given** die DB enthält drei `area`-Zeilen (dev, sales, marketing) und (zunächst leere) Projekt-Tabelle,
**When** `trpc.puls.overview` ohne Filter aufgerufen wird,
**Then** liefert es ein Objekt mit `areas: [{ id, name, ampel, ampel_count, missing_today_count }]`, `blockers: [...]`, `heuteQuote: { gepingt, gesamt, fehlende: [...] }`, `jourfixe: { nextDate, label }`
**And** die Ampel-Aggregation pro Bereich folgt FR-1a-Algorithmus (verifiziert via Vitest-Test mit Fixtures aus mehreren Status-Kombinationen)
**And** die Procedure liefert in < 200 ms bei 6-15 Usern und 90 Tagen Historie (Performance-Test mit `vitest --bench`).
### Story 3.2: Puls-Seite Layout mit Bereichs-Strip und Heute-Quote
Als **Mitarbeiter**,
ich möchte **auf `/puls` in einem Blick die drei Bereiche mit Ampelstatus und die Heute-Quote sehen**,
sodass **ich morgens innerhalb 60 s einen Überblick habe**.
**Acceptance Criteria:**
**Given** ich öffne `/puls`,
**When** die Seite geladen hat,
**Then** sehe ich oben einen Hero mit „Stand: Di, 25. Mai 2026"
**And** unter dem Hero ein 3-Spalten-Grid mit den drei Bereichen, jeweils mit AmpelBadge, Bereichsname, Anzahl fehlender Updates heute, und einer Klick-Aktion zur Bereichs-Detail-View
**And** rechts (oder darunter auf Tablet) ein „Heute fehlt"-Modul mit `X/Y eingestempelt`-Stat und Klick auf Detail-Liste der fehlenden Personen (mit Avatar + Name)
**And** wenn ich noch nicht eingestempelt bin, erscheint prominent oberhalb des Folds der CTA „Du hast heute noch nicht gepingt." mit „Einstempeln"-Primary-Button → führt zu `/mein-heute`
**And** Skeleton-Cards beim Initial-Load, Fade-In ≤ 200 ms beim Datenanstieg.
### Story 3.3: Blocker-Modul, Jourfixe-Hinweis, KPI-Slots
Als **Mitarbeiter**,
ich möchte **auf `/puls` die offenen Blocker, den nächsten Jourfixe und KPI-Slots pro Bereich sehen**,
sodass **ich den ganzen Unternehmens-Puls auf einer Seite verstehe**.
**Acceptance Criteria:**
**Given** ich öffne `/puls`,
**When** die Seite vollständig geladen ist,
**Then** sehe ich unter dem Bereichs-Strip ein Modul „Bereichsübergreifend offen" mit allen offenen Blockern sortiert nach Alter
**And** ein Modul „Jourfixe" zeigt den nächsten Termin (Default „Di, 11:00")
**And** pro Bereich erscheint eine KPI-Kachel mit Slots aus FR-5 (Anzahl Projekte, Status-Verteilung etc.) — Werte sind in Epic 3 ggf. noch 0 (echte Daten kommen in Epic 4)
**And** das Modul „Pflicht-Minimum" ist als Slot vorhanden, zeigt aber bis Epic 5 nur „Keine Projekte mit unerfülltem Pflicht-Minimum" als Empty-State.
### Story 3.4: Vortrags-Feed im Puls + „Heute fehlt"-Detail
Als **Mitarbeiter**,
ich möchte **am Morgen X+1 die konsolidierten Vortrags-Einträge meiner Kollegen im Feed sehen**,
sodass **ich weiß, was gestern lief**.
**Acceptance Criteria:**
**Given** am Tag X haben N Personen reflektiert und der Publikations-Cron lief um 00:05,
**When** ich am Tag X+1 morgens `/puls` öffne,
**Then** sehe ich unten einen „Gestern im Team"-Feed mit konsolidierten DayEntries (Avatar + Name + Bereich + Ampel + Trend + Reflexionstext + abgehakte/verworfene ToDos)
**And** der Feed sortiert neueste oben, paginiert 50/Seite mit „Mehr laden"
**And** Klick auf einen Eintrag öffnet die Detail-Sicht (read-only)
**And** das „Heute fehlt"-Modul klickt jetzt auf eine vollständige Liste der heute noch nicht eingestempelten Personen
**And** Performance-Budget < 1,5 s gesamtladezeit auf `/puls` mit 90 Tagen Historie (Lighthouse-Test).
---
## Epic 4: Projekte, Deals, Kampagnen — Bereichs-Details
Eine Person kann Projekte, Deals und Kampagnen anlegen und in den jeweiligen Bereichs-Detail-Views arbeiten. Die KPI-Slots im Puls werden mit Echtdaten gefüllt.
### Story 4.1: Project-, Milestone- und Verlinkungs-Schema, Anlage-Modal
Als **Projektverantwortliche/r (P1)**,
ich möchte **ein Projekt im Cockpit anlegen können mit allen Cockpit-Sicht-Feldern und Pflicht-Verlinkungen**,
sodass **das Projekt im Puls und in der Bereichs-Detail-View erscheint**.
**Acceptance Criteria:**
**Given** ich bin eingeloggt und in der Bereichs-Detail-View Entwicklung,
**When** ich auf „+ Neues Projekt" klicke,
**Then** öffnet sich ein Dialog mit den Pflichtfeldern Name, Verantwortliche/r (P1, User-Auswahl), Projektart (Kundenprojekt/Internes Projekt), Arbeitsmodus (Cycle/Kanban), Outline-Steckbrief-URL, Plane-Board-URL
**And** optional DoD-Text, Risikoliste-URL, Start, Deadline und 0..N grobe Meilensteine inline
**And** beim Submit werden `project`- und `milestone`-Zeilen geschrieben (Migration `0002_projects.sql`), audit-getracked
**And** das neue Projekt erscheint sofort im Bereich Entwicklung (FR-8/9) und im Puls-KPI-Slot.
### Story 4.2: Bereichs-Detail Entwicklung mit Projekt-Karten und Detail-View
Als **Mitarbeiter**,
ich möchte **die Liste aller Entwicklungs-Projekte in zwei Gruppen (Kundenprojekt/Internes Projekt) sehen und in die Detail-Sicht klicken**,
sodass **ich Status, Meilensteine und letzte Updates pro Projekt erfasse**.
**Acceptance Criteria:**
**Given** mindestens 2 Projekte (eines Kundenprojekt, eines internes) existieren,
**When** ich `/bereich/entwicklung` öffne,
**Then** sehe ich zwei Sektionen „Kundenprojekte" und „Interne Projekte" mit Projekt-Cards (Name, P1-Avatar, AmpelBadge + Trend, Deadline-Progress-Bar, # offene Meilensteine, Pflicht-Minimum-Score Slot)
**And** Klick auf Projekt-Card öffnet `/projekt/:id` mit der Detail-Sicht: Kopfzeile (Name, P1, Projektart, Arbeitsmodus, Ampel + Trend), Verlinkungen (Outline-Steckbrief, Plane-Board, optional Risikoliste), optional DoD-Text, Meilenstein-Checkliste, Update-Feed dieses Projekts
**And** Meilensteine sind inline editierbar (Status: offen/erledigt/überzogen, Fälligkeit ändern) und werden in der Cockpit-DB persistiert.
### Story 4.3: Deal-Schema, Sales-Kanban und Pipeline-KPIs
Als **Vertrieb**,
ich möchte **meine Deals als Kanban in 5 Stages sehen und per Drag-and-Drop verschieben**,
sodass **die Pipeline immer aktuell ist**.
**Acceptance Criteria:**
**Given** Migration `0003_sales.sql` ist gelaufen und mind. 3 Deals existieren,
**When** ich `/bereich/vertrieb` öffne,
**Then** sehe ich oben das KPI-Header-Modul mit gewichtetem Pipeline-Wert (Σ value × probability), aktiven Deals-Count, gewonnenen Deals des laufenden Quartals (# + EUR formatiert als `12.500,00 €`), ⌀ Stage-Verweildauer
**And** darunter ein 5-Spalten-Kanban (Lead → Erstgespräch → Demo → Angebot → Abschluss) mit Deal-Karten
**And** Drag-and-Drop einer Karte auf eine andere Spalte ändert `deal.stage` und setzt `entered_stage_at = now()` (optimistic UI + Rollback bei Fehler)
**And** unten ein gefilterter Vertrieb-Update-Feed (Bezug=Deal oder Vertrieb-allgemein).
### Story 4.4: GTM-Schritte und Vertrieb-Feed
Als **Vertrieb**,
ich möchte **pro Produkt eine sortierbare GTM-Schritt-Liste pflegen**,
sodass **die Markteinführung jedes Produkts strukturiert ist**.
**Acceptance Criteria:**
**Given** im Vertriebs-Detail existiert ein Bereich „GTM-Schritte pro Produkt",
**When** ich ein neues Produkt-Tab hinzufüge und Schritte (Titel + Verantwortlicher + Status) einpflege,
**Then** wird `gtm_step` in der DB persistiert mit `order`-Feld für die Reihenfolge
**And** Schritte sind per Drag-and-Drop oder Up/Down-Tastatur-Buttons umsortierbar (`order` wird neu vergeben in einer Transaktion)
**And** der Status (offen/in Arbeit/erledigt) ist inline änderbar.
### Story 4.5: Campaign-Schema, Lead-Funnel und Crosslink-Banner
Als **Marketing**,
ich möchte **den Lead-Funnel mit Conversion-Prozenten und die Kampagnen-Liste sehen, plus einen Crosslink-Banner zur Vertriebs-Übergabe**,
sodass **ich verstehe, wie viele Leads in echte Deals umgewandelt werden**.
**Acceptance Criteria:**
**Given** Migration `0004_marketing.sql` ist gelaufen und `lead_handover` enthält Daten,
**When** ich `/bereich/marketing` öffne,
**Then** sehe ich oben einen horizontalen 5-Stufen-Lead-Funnel (Reichweite → Website → Leads → MQL → Übergabe) mit Conversion-% zwischen den Stufen
**And** darunter eine Liste aktiver Kampagnen (Aktiv/Pausiert/Beendet, Reichweite, Verantwortlicher)
**And** ein Crosslink-Banner zeigt „Marketing hat X Leads übergeben in [letzten 30 Tagen], Vertrieb hat daraus Y Deals erzeugt (Z € im Quartal)"
**And** derselbe Banner spiegelt sich umgekehrt im Vertrieb-Detail (FR-19)
**And** unten ein gefilterter Marketing-Update-Feed.
---
## Epic 5: Pflicht-Minimum & Blocker
Die 6 Leitfaden-Pflicht-Punkte pro Projekt werden sichtbar gemacht. Blocker-Lebenszyklus ist komplett — inkl. der Regel „nur P1 darf auflösen".
### Story 5.1: Pflicht-Minimum-State, Lazy-Berechnung und Cron-Punkt-5
Als **Entwickler**,
ich möchte **die Pflicht-Minimum-Logik im Backend mit der korrekten Punkt-5-Aktualisierung**,
sodass **der Status pro Projekt stets korrekt aus den Daten abgeleitet wird**.
**Acceptance Criteria:**
**Given** Migration `0005_pflicht_minimum.sql` ergänzt `project.pflicht_minimum_state` (JSONB) und `project.steckbrief_freigegeben` (bool),
**When** `apps/api/src/lib/pflicht-minimum.ts` `computePflichtMinimum(projectId)` aufgerufen wird,
**Then** liefert es ein Objekt mit 6 Booleans gemäß FR-38 Definition (Punkte 1-4 manuell aus DB-Feldern, Punkt 5 berechnet aus letztem Reflexions-Zeitstempel)
**And** der Cron-Endpoint `POST /internal/cron/recompute-pflicht-minimum-5` setzt Punkt 5 auf `false` für alle Projekte mit letzter Reflexion > 7 Werktage zurück
**And** der Aggregations-Algorithmus aus FR-1a wird ergänzt: Pflicht-Minimum-Verstöße heben Ampel mindestens auf Gelb (Vitest-Test mit Fixtures).
### Story 5.2: Pflicht-Minimum-Indikator und -Panel in der UI
Als **P1 / P4**,
ich möchte **pro Projekt-Card und Projekt-Detail die 6 Pflicht-Punkte als visuelle Indikatoren sehen**,
sodass **ich sofort sehe, was bei diesem Projekt fehlt**.
**Acceptance Criteria:**
**Given** Stories 5.1 ist abgeschlossen und ein Projekt hat 4/6 Punkte erfüllt,
**When** ich `/bereich/entwicklung` öffne,
**Then** zeigt jede Projekt-Card unten rechts den `PflichtMinimumIndicator` mit 6 horizontalen Quadraten (gefüllt/leer) und Score „4/6"
**And** auf `/projekt/:id` erscheint das `PflichtMinimumPanel` mit allen 6 Punkten als Checkliste, jeweils mit Status, kurzer Beschreibung und (für Punkte 1-4) Quick-Action-Button („Steckbrief verlinken", „P1 setzen", „Plane-Board verlinken", „Meilenstein anlegen")
**And** Punkte 5 und 6 sind read-only mit Erklärungs-Tooltip
**And** Projekte mit ≥ 1 unerfülltem Punkt erscheinen auf `/puls` im Modul „Pflicht-Minimum" (vorher leerer Slot wird gefüllt).
### Story 5.3: Blocker-Lebenszyklus — Erfassen, Anzeige, P1-Auflösung
Als **Mitarbeiter**,
ich möchte **Blocker außerhalb der Reflexion erfassen können und P1 kann sie auflösen**,
sodass **das Team auf bereichsübergreifende Hindernisse reagieren kann**.
**Acceptance Criteria:**
**Given** der „+ Neuer Blocker"-Button in der TopNav (Shortcut `n`),
**When** ich auf ihn klicke und Titel + Beschreibung + Verantwortlichen + Bereich(e) + optionale Projekt-Verknüpfung eintrage,
**Then** wird `blocker` in der DB persistiert mit `opened_by = ich`
**And** der Blocker erscheint auf `/puls` im bereichsübergreifenden Modul, im jeweiligen Bereichs-Detail und (sofern projektgebunden) im Projekt-Detail
**And** auf der Blocker-Card erscheint ein „Auflösen"-Button **nur sichtbar, wenn ich P1 des bezogenen Projekts bin** (oder bei projektlosen Blockern Ersteller oder Verantwortlicher)
**And** ein nicht-P1-User, der versucht via direktem tRPC-Call aufzulösen, bekommt `TRPCError(FORBIDDEN)` mit deutscher Message
**And** beim Auflösen werden `resolved_by`, `resolved_at` und `resolution_text` persistiert; Blocker verschwindet aus den offenen Listen.
### Story 5.4: Blocker im Projekt-Detail und Audit der Auflösung
Als **P1 / P4**,
ich möchte **die Blocker-Historie pro Projekt einsehen können**,
sodass **ich nachvollziehe, was und wann gelöst wurde**.
**Acceptance Criteria:**
**Given** ein Projekt hat mehrere Blocker (einige offen, einige gelöst),
**When** ich `/projekt/:id` öffne,
**Then** sehe ich einen Tab oder Bereich „Blocker" mit zwei Listen: offene Blocker oben (mit „Auflösen"-Button für P1), gelöste Blocker darunter (mit Auflöser-Name + Datum + Kommentar)
**And** jede Blocker-Mutation (erstellen, lösen) hat einen entsprechenden `audit_event`-Eintrag (verifiziert per DB-Query in Test).
---
## Epic 6: Projekt-Abschluss, Archiv & Production
Projekte können abgeschlossen werden inkl. Lessons-Learned-Pflicht. Archiv-Sicht existiert. Production-Pipeline ist scharf: CI/CD, Backup, A11y-Tests.
### Story 6.1: Projekt-Abschluss-Workflow und Lessons-Learned-Pflicht
Als **P1**,
ich möchte **ein Projekt auf „abgeschlossen" setzen können — aber nur, wenn ich die Lessons-Learned-URL angegeben habe**,
sodass **die Leitfaden-Pflicht-Minimum-Position 6 garantiert erfüllt wird**.
**Acceptance Criteria:**
**Given** ich bin P1 eines aktiven Projekts und öffne den Projekt-Header,
**When** ich auf „Projekt abschließen" klicke,
**Then** öffnet sich ein Dialog, der die Lessons-Learned-URL als Pflichtfeld einfordert (URL-Format-Validierung via Zod)
**And** beim Submit wird `project.archiviert=true`, `archiviert_am=now()`, `lessons_learned_url` gesetzt und `pflicht_minimum_state.6=true` neu berechnet
**And** das Projekt verschwindet sofort aus `/bereich/entwicklung` und `/puls`
**And** Try-To-Submit ohne URL zeigt deutsche Inline-Fehlermeldung
**And** ein nicht-P1-User sieht den „Abschließen"-Button nicht (UI + tRPC-Guard).
### Story 6.2: Archiv-Sicht pro Bereich
Als **Mitarbeiter**,
ich möchte **abgeschlossene Projekte in einer Archiv-Sicht einsehen**,
sodass **vergangene Erkenntnisse auffindbar bleiben**.
**Acceptance Criteria:**
**Given** mindestens 2 Projekte sind abgeschlossen,
**When** ich `/archiv` öffne (oder via TopNav-Tab),
**Then** sehe ich pro Bereich (Tabs Entwicklung/Vertrieb/Marketing) eine Liste abgeschlossener Items sortiert nach `archiviert_am` (neueste oben)
**And** Klick auf ein Item öffnet die read-only Detail-Sicht mit allen Daten + Lessons-Learned-Link
**And** die Archiv-Sicht hat denselben Sub-Header wie die Bereichs-Detail-Views (Sticky), aber mit zusätzlichem „Archiv"-Marker.
### Story 6.3: Backup-Cron, Restore-Drill-Dokumentation
Als **DevOps**,
ich möchte **ein tägliches `pg_dump`-Backup mit 14-Tage-Retention und dokumentiertem Restore-Pfad**,
sodass **Datenverlust auch nach einem schweren Vorfall begrenzt bleibt**.
**Acceptance Criteria:**
**Given** der Cron-Endpoint `POST /internal/cron/pg-dump-backup` ist implementiert,
**When** die System-Cron-Definition den Endpoint täglich 02:00 aufruft,
**Then** wird ein gz-komprimiertes Dump-File auf einem separaten Docker-Volume `backups` geschrieben
**And** Dumps älter als 14 Tage werden automatisch gelöscht
**And** `docs/operations.md` enthält eine Restore-Anleitung in 5 Schritten (Stop API, Restore Dump, Migrate, Smoke-Test, Start API)
**And** ein quartalsweiser Restore-Drill ist als Issue im Plane geöffnet (Story-Akzeptanz prüft, dass die Aufgabe dokumentiert ist, nicht den Drill selbst).
### Story 6.4: CI-Pipeline mit Vitest, Playwright und Lighthouse-A11y
Als **Entwickler**,
ich möchte **eine CI-Pipeline, die Typecheck, Lint, Vitest, Playwright und Lighthouse-A11y-Test bei jedem PR ausführt**,
sodass **Qualität und Barrierefreiheit nicht erodieren**.
**Acceptance Criteria:**
**Given** `.gitea/workflows/ci.yml` erweitert die Pipeline,
**When** ein PR geöffnet wird,
**Then** läuft die Pipeline `pnpm install` → `pnpm typecheck` → `pnpm lint` → `pnpm test` (Vitest mit axe-core auf Komponenten-Snapshots) → `pnpm build` → Playwright-E2E auf den drei UJs (UJ-1, UJ-2, UJ-5) gegen eine ephemere Docker-Compose-Umgebung → Lighthouse-CI auf `/puls`, `/mein-heute`, `/projekt/:id`
**And** Lighthouse-A11y-Score < 95 lässt die Pipeline fehlschlagen
**And** jeder PR-Check zeigt einen Statusbericht mit den erfüllten/gefehlten Punkten.
### Story 6.5: Responsive-Audit und Mobile-Lesbarkeit
Als **Mitarbeiter**,
ich möchte **das Cockpit auch unterwegs am Handy lesen können** (nicht schreiben — das bleibt Desktop),
sodass **ich beim Pendeln zumindest sehe, wie es um die Projekte steht**.
**Acceptance Criteria:**
**Given** ich öffne `/puls` und `/projekt/:id` auf einem mobilen Browser (Viewport 375×667),
**When** die Seiten geladen sind,
**Then** sind alle Karten und Listen leserlich gestapelt (Single-Column), keine horizontalen Scrolls
**And** die TopNav wird zu einer Bottom-Tab-Bar mit 4 Items (Puls, Mein heute, Bereiche-Dropdown, Profil)
**And** der Einstempel-CTA auf `/mein-heute` zeigt einen sichtbaren Hinweis „Tagesupdate auf Desktop empfohlen" (Voice-konform)
**And** Playwright-Mobile-Viewport-Tests laufen für `/puls` und `/projekt/:id` und prüfen Layout-Integrität.
---
## Validation Summary
### FR Coverage Check
| FR | Story | Status |
|---|---|---|
| FR-1, 1a, 2-7 | 3.1-3.4 | ✅ |
| FR-8-11 | 4.1, 4.2 | ✅ |
| FR-12-16 | 4.3, 4.4 | ✅ |
| FR-17-20 | 4.5 | ✅ |
| FR-21-25 | 2.2, 2.3 | ✅ |
| FR-26-28, 31 | 2.4 | ✅ |
| FR-29, 30 | 2.6, 3.4 | ✅ |
| FR-32, 33 | 2.2, 2.4, 2.7 | ✅ |
| FR-34-37 | 2.7 | ✅ |
| FR-38-40 | 5.1, 5.2 | ✅ |
| FR-41-44 | 2.5, 5.3, 5.4 | ✅ |
| FR-45-47 | 6.1, 6.2 | ✅ |
| FR-48 | 1.6 | ✅ |
| FR-49 | — | ⏭ Phase 2 (explizit) |
**Coverage:** 48 von 49 FRs durch Stories abgedeckt; FR-49 (Volltextsuche) bewusst Phase 2 gemäß PRD § 8.
### NFR Coverage Check
- **Performance (NFR-1, 2):** 3.1 (Procedure-Budget), 3.4 (Lighthouse), 6.4 (CI-Performance-Gate)
- **Verfügbarkeit (NFR-3, 4):** 6.3 (Backup + Restore-Drill), 1.7 (Healthcheck)
- **Security (NFR-5, 6, 7):** 1.3 (Authentik), 1.4 (Audit-Wrapper)
- **Bedienbarkeit (NFR-8-10):** 1.5 (Brand-Tokens), 6.5 (Mobile-Audit)
- **Voice & Tone (NFR-11, 12, 13):** 1.5 (voice.ts) — alle UI-Stories nutzen die zentrale Datei
- **Daten (NFR-14, 15, 16):** 1.2 (Postgres+Drizzle), 6.3 (Backup), 1.1 (Drizzle-Migrationen)
### Story-Quality-Validation
- ✅ **Sized for single dev agent:** keine Story > 1 Arbeitstag in der KI-gestützten Entwicklung.
- ✅ **Given/When/Then-Format** auf jeder AK.
- ✅ **Keine Vorwärts-Abhängigkeiten** — jede Story baut auf vorherigen Stories (gleicher Epic) oder bereits abgeschlossenen Epics auf.
- ✅ **Inkrementelles Schema:** DB-Migrationen entstehen je Story-Bedarf, nicht upfront. (`0000_init`, `0001_day_rhythm` inkl. blocker, `0002_projects`, `0003_sales`, `0004_marketing`, `0005_pflicht_minimum`.)
- ✅ **Custom-Starter** ist Story 1.1 (Repo-Bootstrap), entspricht Architektur § Initialization Commands.
### Epic-Independence-Check
| Epic | Depends on | Liefert |
|---|---|---|
| 1 | — | Login + Shell + Brand |
| 2 | 1 | Tagesrhythmus inkl. Cron + Streak |
| 3 | 1, 2 | Puls-Übersicht mit Vortrags-Feed |
| 4 | 1 (unabhängig von 2/3 möglich) | Projekt-/Deal-/Kampagnen-Domain |
| 5 | 4 (Projekte) + 2 (Blocker-Inline) | Pflicht-Minimum + Blocker-Lifecycle |
| 6 | 4 (Archiv), 2 (Cron-Pattern) | Abschluss + Production-Pipeline |
✅ Jedes Epic liefert eigenständig nutzbaren Wert.
### File-Churn-Check
- Epic 2 und Epic 5 berühren beide `blocker`-Schema und -Komponente — bewusst getrennt: Epic 2 etabliert nur Inline-Erfassung in der Reflexion, Epic 5 fügt vollständigen Lifecycle (Standalone-Button + P1-Auflösung + Audit-Tab) hinzu. Aufteilung mitigiert Risiko, dass das volle Blocker-System verzögert wird, bevor der Tagesrhythmus läuft.
- Epic 3 und Epic 4 berühren beide `apps/api/src/routers/puls.ts` und KPI-Slots im Frontend — getrennt, weil Epic 3 die Aggregation für leere Bereiche etabliert (skelettiert) und Epic 4 die Domain-Daten dahinter liefert. Konsolidierung hätte ein zu großes Epic ohne Zwischen-Release-Wert ergeben.
### UX-DR-Coverage-Spot-Check
- UX-DR1-4 (Brand-Tokens, Typo, Spacing, Container) → Story 1.5
- UX-DR5-7 (Button, Card, Input, Textarea, Select, Checkbox) → Story 1.5 + 1.6 (verteilt, je nach Bedarf in Folge-Stories)
- UX-DR8 (AmpelButton, TrendButton) → Story 2.4
- UX-DR9 (Dialog, Tooltip) → Story 4.1 + 6.1
- UX-DR10 (SkeletonCard) → Story 3.2
- UX-DR11 (AmpelBadge) → Story 3.2 / 4.2
- UX-DR12 (PflichtMinimumIndicator) → Story 5.2
- UX-DR13 (KeyCaret) → Story 1.6 + 2.7
- UX-DR14-16 (Navigation, Breadcrumbs, Shortcuts) → Story 1.6
- UX-DR17 (voice.ts) → Story 1.5
- UX-DR18-20 (User Journeys) → Stories 2.2/2.4, 3.2, 4.1
- UX-DR21-25 (Responsive + A11y) → Story 6.4 + 6.5
- UX-DR26-30 (Interaktions-Patterns) → verteilt über alle UI-Stories
✅ Alle 30 UX-DRs sind in Stories adressiert.
### Validation Verdict
**🟢 READY FOR DEVELOPMENT**
- 49 FRs · 16 NFRs · 16 AR-Items · 30 UX-DRs → durch 6 Epics und 32 Stories abgedeckt
- Keine Vorwärts-Abhängigkeiten, keine Upfront-Big-Bang-Migrationen
- Epic-Reihenfolge entspricht der Architektur-Implementation-Sequence
- Bauen kann mit **Story 1.1 (Repo-Bootstrap)** sofort starten.
@@ -0,0 +1,160 @@
# Entscheidungs-Log – PRD projectmanagement
> Kanonisches Gedächtnis und Audit-Trail dieses PRD-Laufs. Jede Entscheidung, Änderung
> und jeder Override (auch headless) wird hier festgehalten, während das Gespräch läuft.
## Eintraege
- **2026-05-25** – Lauf gestartet. Intent: *Create*. Workspace: `prd-projectmanagement-2026-05-25/`.
Sprache: Deutsch (communication & document_output). Facilitator: Claude (bmad-prd).
- **2026-05-25** – Inputs eingelesen: `inputs/unternehmens-cockpit.html` (HTML-Prototyp,
1214 Zeilen, von Nicht-Techniker als Vibe-Code-Ausgangspunkt erstellt). Extract liegt in
`extract-prototype-html.md`. ToolCTRL Brand Identity referenziert (Pfad ausserhalb Repo,
nur als Referenz; konkrete Werte werden in der UX-Phase festgezogen).
- **2026-05-25** – Working Mode: **Fast Path**. Verbleibende Gaps werden gebündelt
abgefragt, dann Draft mit `[ASSUMPTION]`-Tags. User reviewt und iteriert.
- **2026-05-25** – Team-Skalierung: **6-15 Mitarbeiter** (gesamtes ToolCTRL-Team).
- **2026-05-25** – Auth-Strategie: **Authentik (auth.tool-ctrl.de) via OIDC**. Wird als
weitere App im bestehenden SSO-Setup registriert. Konsistent mit Plane, Outline, Gitea,
Kimai, Nextcloud, Element, Jitsi etc., die alle schon hinter Authentik laufen.
- **2026-05-25** – Plane/Outline-Integration: **Phase 2, nicht MVP**. Im MVP nur freie
URL-Links als Strings im Feld `Bezug`. Später API-Sync möglich (auch unter Authentik).
- **2026-05-25** – Tech-Stack-Default für PRD: **React (Frontend) + Postgres (Datenhaltung)**;
Backend-Wahl bleibt der Architektur-Phase überlassen. Tech-Details landen in
`addendum.md`, das PRD selbst bleibt Capability-orientiert.
- **2026-05-25** – Areas-Scope MVP: **nur 3 Detail-Areas** (Entwicklung, Vertrieb,
Marketing). Buchhaltung & Design können Tagesupdates mit Bezug "allgemein" schreiben
und erscheinen im Puls als Update-Quote / Streak, bekommen aber keine eigene Detail-View.
Erweiterung in Phase 2.
- **2026-05-25** – Kern-Mechanik gegen Versandung: **kombinierter Ansatz** —
(a) Daily-Update-Pflicht mit öffentlicher "fehlt heute"-Liste & Streak,
(b) Deadline-/Meilenstein-Verfall als Bar-Visualisierung pro Projekt,
(c) bereichsübergreifende Blocker-Liste im Puls. Diese drei zusammen sind das "Warum
jetzt"-Kernversprechen des Produkts.
- **2026-05-25** – **Major Update: Leitfaden v3.0 als bindender Frame nachgereicht**
(`inputs/ToolCTRL_Projektmanagement-Leitfaden_v3.0.pdf`, Mai 2026).
Konsequenzen, die im PRD und Addendum eingepflegt sind:
- PRD-Frontmatter `binding_frame` gesetzt.
- Tool-Aufteilung verschärft: Plane=Arbeit, Outline=Gedächtnis, Cockpit=Übersicht
(Anti-Ziele A3, A4, A5 ergänzt).
- Ampel-Definitionen exakt aus Leitfaden §5: Grün/Gelb/Rot mit klarer
Semantik, plus Trend (besser/gleich/schlechter) + Freitext max 1000 Zeichen.
- **Neues Major-Feature**: Feature-Gruppe G "Pflicht-Minimum-Check" (FR-30/30a/30b)
leitet sich direkt aus Leitfaden §9 ab; im Puls (FR-7) sichtbar.
- Erfolgsmetrik M6 ergänzt (≥90% Projekte erfüllen alle 6 Pflicht-Punkte).
- Rollen umstrukturiert: §4.1 Leitfaden-Rollen (P1-P4) + §4.2 Querschnittsrollen
(Q1-Q5). Konsistent mit Leitfaden §7.
- Projektarten umbenannt: Kundenprojekt / Internes Projekt (statt
Umsatz/Intern aus Prototyp). Konsistent mit Leitfaden §1.
- Datenmodell-Skizze erweitert: `Project.kind`, `arbeitsmodus`, `dod_text`,
URL-Felder, `pflicht_minimum_state`, `Deal.probability`,
`Deal.won_at/won_value_eur`, neue `LeadHandover`-Entität.
- Addendum §7 "Leitfaden-Bezüge"-Tabelle ergänzt.
- **2026-05-25** – Reconciliation-Subagent Findings eingearbeitet:
- FR-13: `probability` ergänzt; FR-14: gewichteter Pipeline-Wert + Won-Deals-KPI;
- FR-19: Crosslink-Banner Marketing↔Vertrieb;
- NFR-12/13: neue Sektion **Voice & Tone** mit Prototyp-Anker-Phrasen;
- Vokabular im Addendum §6 um "Update-Serie", "Crosslink",
"Bereichsübergreifend offen", "Trend" erweitert.
- **2026-05-25** – Rubric-Walker Verdict: **good** (0 Critical, 0 High,
3 Medium, 5 Low). Findings teilweise sofort eingearbeitet:
- FR-1a: Ampel-Aggregations-Algorithmus pro Area expliziert.
- FR-5: Key-Metrics als verbindlicher Katalog pro Area (nicht mehr Beispiele).
- §5 UJ: Persona-Linkage via P1-P4 / Q1-Q5-IDs.
- FR-23: Sortierung neueste oben + Pagination 50/Seite.
- NFR-16: Bound "max. 2-stündige Migration".
- §10: Annahmen-Index ergänzt (A-1 bis A-6).
- OQ-4 als Phase-Blocker markiert.
Rest (OQ-3 Bestätigung, OQ-7 Pflicht/Optional Trend, "Bereich/Area"-Synonymie) →
Triage mit User.
- **2026-05-25** – Vorläufige Defaults für offene Fragen im PRD eingetragen:
OQ-3 Default = "rückwirkende Updates gesperrt"; OQ-4 Default = "jeder darf
Blocker als gelöst markieren"; OQ-6 Default = "manuelle Checkbox im MVP";
OQ-7 Default = "Trend optional". Alle bestätigungsbedürftig in Triage.
- **2026-05-25** – **Major Konzept-Erweiterung: Morgen-ToDos + Abend-Reflexion.**
User hat in der Triage einen bisher nicht erwähnten Kern-Workflow nachgereicht:
Tagesupdate ist nun ein zweistufiger Vorgang — morgens stempelt die Person sich
ein und trägt ihre ToDos für den Tag ein; abends ruft sie die Liste auf, hakt ab,
schreibt eine Reflexion, setzt Ampel + Trend. Kollegen sehen den konsolidierten
Eintrag erst am nächsten Werktagsmorgen. Innerhalb des laufenden Tages sehen
Kollegen nur binäre Anwesenheit, keine ToDo-Inhalte.
Konsequenzen im PRD:
- UJ-1 komplett umgeschrieben (Morgen/Abend/Nächster Morgen).
- Feature-Gruppe E umgebaut: 6.5.1 Morgen (FR-21-25), 6.5.2 Abend (FR-26-28),
6.5.3 Konsolidierung (FR-29-33). FRs danach von 26-39 auf 34-49 renumbered.
- Tages-Status pro Person (voll/halb/nicht angefangen/abwesend) (FR-33).
- Streak-Definition geändert: zählt nur volle Tage (FR-34).
- CM1 aufgesplittet in CM1a (Morgen ≤60s), CM1b (Abend ≤90s), CM1 (Gesamt ≤3 min).
- Sichtbarkeitsmodell: `published_at` = nächster Werktagsmorgen.
- Addendum: neues Datenmodell `DayEntry` ersetzt `Update`, neue Entität `Todo`,
Vokabular um Einstempeln/Reflexion/„Mein heute"/Konsolidierter Vortagseintrag
erweitert.
- Beschlossene OQs aus Triage: OQ-3 (1 Werktag zurück), OQ-4 (nur P1),
OQ-6 (manuelle Checkbox), OQ-7 (Trend Pflicht bei Projekt-Bezug).
- Neue OQs offen: OQ-8 (Erinnerungs-Zeit Reflexion, Default 17:00),
OQ-9 (≥1 ToDo Pflicht beim Einstempeln), OQ-10 (ToDos nach Reflexion noch
nachtragbar — Default: nein).
- **2026-05-25** – Polish-Pass durchgeführt durch zwei parallele Subagenten
(Struktur + Prosa) auf prd.md und addendum.md. Wichtigste Änderungen:
Vereinheitlichung „Area" → „Bereich"; CM-Tabelle nach Gesamtzeit zuerst,
dann Teilzeiten sortiert; Tippfehler bereinigt; Vokabular synchronisiert
(„Tagesupdate" durchgängig). Keine FRs/Anti-Ziele/Metriken inhaltlich
geändert. PRD: 29 206 → 28 633 Zeichen (–2%). Addendum: 9 500 → 9 350
Zeichen (–1,5%).
- **2026-05-25** – **Finalize abgeschlossen.** Frontmatter `status: final` in
prd.md und addendum.md gesetzt. Phase-Blocker-OQs (OQ-1 Authentik-Mapping,
OQ-2 Brand-Tokens, OQ-4 Blocker-Auflöser) sind im PRD entschieden oder an
konkrete Folge-Phase deferiert. Externe Handoffs (Confluence/Notion) sind im
Skill-Config leer — entfallen. Empfehlung für nächsten Schritt: parallel
bmad-create-architecture (technische Schnitt klar, Datenmodell skizziert)
und bmad-create-ux-design (Brand-Tokens, „Mein heute"-Konzept, Voice & Tone).
bmad-create-epics-and-stories danach.
- **2026-05-25** – **Architektur-Phase abgeschlossen.** Dokument liegt unter
`_bmad-output/planning-artifacts/architecture.md` (8 von 8 Schritten erledigt,
Status complete). Wichtigste Pivots:
- **Stack ist Vite + React (SPA), nicht Next.js** — auf Team-Wunsch
(T3-Stack-Vorschlag aus Step 3 verworfen).
- Monorepo (pnpm workspaces): `apps/web`, `apps/api`, `packages/db`, `packages/shared`.
- Backend: Fastify 5+ mit tRPC v11, `openid-client` v6+ für Authentik OIDC,
Drizzle ORM auf Postgres 16+, Session-Cookies (HttpOnly+SameSite=Lax) statt JWT.
- Background-Jobs: System-Cron → authentifizierter Fastify-Endpoint
(`/internal/cron/*` mit Bearer-Token), 4 Jobs definiert.
- Hosting: bestehende ToolCTRL-Infrastruktur, Caddy 2.x als Reverse-Proxy.
- 4 tRPC-Procedure-Typen: `public`, `protected`, `dayEntryVisible`, `projectOwner`.
- OQ-5 (Archivierungs-Policy) in der Architektur fixiert: Soft-Delete via
`archiviert`-Flag, kein automatisches Löschen.
- Verdict der Validierung: **READY FOR IMPLEMENTATION** (16/16 Checklist,
0 Critical, 3 Important nicht-blockierende Gaps).
- Folgeskill: `bmad-create-ux-design` (OQ-2 Brand-Tokens, „Mein heute"-Layout,
Voice & Tone), danach `bmad-create-epics-and-stories`.
- **2026-05-25** – **UX-Design-Phase abgeschlossen.** Dokument liegt unter
`_bmad-output/planning-artifacts/ux-design-specification.md` (14 von 14 Schritten
erledigt, status: complete). Hauptentscheidungen:
- **Brand-Tokens fixiert (OQ-2 geschlossen):** Weiß-Schwarz-Monochrom mit drei
Ampel-Akzentfarben für Status; konkrete Hex-Werte und Typo-Scale dokumentiert.
- **Typografie:** Inter (UI) + JetBrains Mono (Werte), selbst-gehostet via
`@fontsource`. Konsistent mit Brand-Marketing-Style.
- **Component-Library:** Tailwind + Radix UI Primitives + 15 hauseigene Brand-
konforme Komponenten in `components/ui/`. Kein shadcn-Bulk, kein Material UI.
- **Design-Richtung:** „Office Minimal" — ruhiges, typografie-getragenes Layout,
Brand-Reduktion als Feature. Drei Sichtebenen: Desktop (Volllayout), Tablet
(2-Spalten), Mobile (lesbar, schreiben auf Desktop empfohlen).
- **Voice & Tone konkretisiert:** „Stark.", „Wie läuft es?", „Du hast heute noch
nicht gepingt." als Mikrocopy-Anker; keine Emojis, kein Corporate-Sprech,
deutsch durchgängig.
- **Brand-Element `^`:** Das macOS-Ctrl-Caret als wiederkehrendes Element
(Logo-Lockup `cockpit ^`, Streak-Badges, Toast-Bestätigungen).
- **Mermaid-Diagramme** für UJ-1 bis UJ-5 in der Spec.
- **A11y-Strategie:** WCAG 2.1 AA, Keyboard-First, axe-core-Tests, Lighthouse-
Ziel ≥ 95.
- Folgeskill: **`bmad-create-epics-and-stories`** — übersetzt PRD + Architektur
+ UX-Spec in epische User-Stories mit Akzeptanzkriterien.
- **2026-05-25** – **Epics & Stories-Phase abgeschlossen.** Dokument liegt unter
`_bmad-output/planning-artifacts/epics.md` (4 von 4 Schritten erledigt,
status: complete). User-Direktive: „verkünstel dich nicht zu sehr in der
Planung — wir müssen mit dem Bauen anfangen." Daher schlanke Struktur:
**6 Epics** entlang der Architektur-Implementation-Sequence, **32 Stories**
insgesamt mit Given/When/Then-AKs. Schema wird inkrementell pro Story-Bedarf
migriert (kein Big-Bang). Coverage: 48/49 FRs, alle NFRs/AR/UX-DRs abgedeckt;
FR-49 (Volltextsuche) explizit Phase 2. **Verdict: READY FOR DEVELOPMENT —
bauen startet mit Story 1.1 (Repo-Bootstrap).**
@@ -0,0 +1,273 @@
---
title: Addendum – ToolCTRL Unternehmens-Cockpit
status: final
created: 2026-05-25
updated: 2026-05-25
binding_frame: ToolCTRL Projektmanagement-Leitfaden v3.0 (Mai 2026)
---
# Addendum zum PRD – ToolCTRL Unternehmens-Cockpit
> Inhalte, die im Hauptdokument den Lesefluss stören würden, aber für Architektur, UX-Design und das spätere Verständnis von Entscheidungen wichtig sind.
---
## 1. Technische Orientierung (vorläufig)
### 1.1 Stack-Default
- **Frontend:** React.
- **Datenhaltung:** Postgres.
- **Backend:** offen — Node/Express oder Fastify (passt zur React-Welt), Python/FastAPI, oder .NET. Entscheidung in Architektur-Phase.
- **Auth:** Authentik (auth.tool-ctrl.de) via OIDC. Cockpit als weitere App im Authentik-Provider; Token-Validierung im Backend.
- **Hosting:** wahrscheinlich gleiche interne Infrastruktur wie Plane/Outline/Gitea.
### 1.2 Datenmodell-Skizze (nicht normativ)
```
User
id, authentik_sub, name, email, default_area_id, active, abwesend_bis
Area
id, name, kind (dev|sales|marketing|allgemein), order
Project
id, area_id, name,
kind (kundenprojekt|internes_projekt) # Leitfaden §1
verantwortliche_user_id # Leitfaden §7 (P1)
arbeitsmodus (cycle|kanban) # Leitfaden §5
outline_steckbrief_url
steckbrief_freigegeben (bool, manuell)
plane_project_url
risikoliste_url (nullable)
dod_text (nullable, Freitext) # Leitfaden §4
start, deadline,
status_cache (green|amber|red|null)
trend_cache (besser|gleich|schlechter|null)
pflicht_minimum_state (jsonb mit 6 booleans) # Leitfaden §9
archiviert (bool), archiviert_am, lessons_learned_url (nullable)
created_by, created_at
Milestone
id, project_id, title, due, status (open|done|overdue), description
Deal
id, area_id (=sales), name, owner_id, value_eur,
probability (0-100), # für gewichtete Pipeline
stage (lead|erstgespraech|demo|angebot|abschluss),
entered_stage_at,
won_at (nullable), won_value_eur (nullable)
GtmStep
id, product, title, owner_id, status (open|in_progress|done), order
Campaign
id, area_id (=marketing), name, status (active|paused|done),
reach, owner_id
LeadHandover # Crosslink Marketing → Sales
id, campaign_id, dealkette_deal_ids (array),
übergeben_am, übergeben_von
DayEntry # ersetzt das alte "Update"
id, user_id, date,
status (begun|full|absent), # begun = nur Einstempeln; full = Einstempeln + Reflexion
checked_in_at, # Zeitstempel Morgen-Einstempeln
reflected_at (nullable), # Zeitstempel Abend-Reflexion
reflection_text (nullable, max 1000 Zeichen),
ampel (green|amber|red|null),
trend (besser|gleich|schlechter|null),
url (nullable),
blocker_id (nullable),
edit_locked_at (nullable), # 30 Min nach Reflexion
published_at # nächster Werktag 00:00 — ab dann für Kollegen sichtbar
Todo
id, day_entry_id, user_id, date,
title (max 200), order,
bezug_kind (project|deal|campaign|allgemein|null),
bezug_id (nullable),
status (open|done|dropped),
drop_reason (nullable, Freitext),
created_at, completed_at, dropped_at
# Hinweis: ein Tagesupdate (DayEntry) hat 0..N Todos plus einen einzigen
# Reflexionsblock. Vor `published_at` sehen nur die Person selbst und
# Projektverantwortliche (für Todos mit Projekt-Bezug) die Inhalte.
Blocker
id, title, description, area_id, project_id (nullable),
owner_id, opened_by, opened_at,
resolved_by, resolved_at, resolution_text
AbsenceDay
id, user_id, date, reason (urlaub|krank|sonstiges)
AuditEvent
id, actor_user_id, action, entity_kind, entity_id, payload_jsonb, created_at
```
### 1.3 Auth-Flow
1. User öffnet `cockpit.tool-ctrl.de`.
2. Backend prüft Session-Cookie; bei Fehlen Redirect auf Authentik-Authorize-Endpoint.
3. Authentik liefert OIDC-Code, Cockpit-Backend tauscht ihn gegen Token (Authorization Code Flow + PKCE).
4. Backend legt User in eigener Tabelle an, falls erstmaliger Login (Mapping per `sub`).
5. Default-Area wird aus Authentik-Group-Claim gemappt.
### 1.4 Phase 2 – Plane/Outline-Integration (Skizze)
- **Plane:** API-Token; bei Eingabe einer Plane-URL holt das Backend Titel, Status und Owner und cached sie. Pflicht-Minimum-Punkt 3 (Plane-Board) wird damit automatisch validiert.
- **Outline:** bei Eingabe einer Steckbrief-URL prüft das Backend per Outline-API, ob das Dokument existiert und welchen Status es hat (Draft/Published). Pflicht-Minimum-Punkt 1 wird damit automatisch validiert.
- **Lessons-Learned-Vorlage:** beim Projekt-Abschluss erzeugt das Cockpit eine vorbefüllte Outline-Seite gemäß Leitfaden-Vorlage.
---
## 2. Verworfene Alternativen
### 2.1 „Bleiben wir bei Plane allein, sparen uns das Cockpit"
- **Verworfen, weil:** Plane ist ein Tickets-Tool. Es zwingt nicht zu täglichen Updates und aggregiert nicht über Bereiche hinweg. Der Schmerz „wir sehen nicht, was läuft" bleibt ungelöst. Das Cockpit ist im Leitfaden §5 zudem explizit vorgesehen.
### 2.2 „Zurück zu Jira+Confluence"
- **Verworfen, weil:** schon einmal aufgegeben. Plane/Outline sind die strategisch gewollten Nachfolger.
### 2.3 „Slack/Element-Channel mit Daily-Standup-Bot"
- **Verworfen, weil:** Chat skaliert nicht zu Übersicht. Visualisierung (Puls, Funnel, Kanban) und Aggregation fehlen; Suche und Filter sind schwach.
### 2.4 „SaaS-Lösung kaufen (Range, Geekbot, Standuply, etc.)"
- **Verworfen, weil:** externe SaaS bedeutet weiteren Anbieter, Datenabfluss und Auth-Insel. ToolCTRL bevorzugt Self-Hosting hinter Authentik. Keine der untersuchten Lösungen trifft die ToolCTRL-Spezifika (Pflicht-Minimum-Check aus Leitfaden, Crosslinks, GTM-Schritte).
### 2.5 „localStorage / Single-Page, kein Backend"
- **Verworfen, weil:** Multi-User braucht Server-Persistenz.
### 2.6 „Alle 5 Bereiche mit Detail-Views im MVP"
- **Verworfen, weil:** Buchhaltung und Design haben keine etablierten KPI-Strukturen. Detail-Views wären leere Skelette. Phase 2 nach Definition.
### 2.7 „Steckbrief-Editor ins Cockpit holen — One-Stop-Shop"
- **Verworfen, weil:** Leitfaden §8 ist explizit: „Wir führen kein zusätzliches Projektmanagement-Tool ein." Outline ist das Gedächtnis. Doppel-Storage erodiert die Tool-Aufteilung.
### 2.8 „Plane-Issues spiegeln, damit man im Cockpit Tickets sieht"
- **Verworfen, weil:** Plane ist „jederzeit die Wahrheit" (Leitfaden §5). Ein Spiegel driftet auseinander. Wir verlinken auf Plane.
### 2.9 „CRUD für Projekte komplett aus dem Cockpit raus, nur API-Sync"
- **Verworfen, weil:** im MVP gibt es noch keinen API-Sync. Pragmatik vor Reinheit: das Cockpit pflegt eine reduzierte Sicht (Name, Verantwortliche/r, URLs, grobe Meilensteine), bis Phase 2 kommt. Die Doppelpflege wird offen kommuniziert und gemessen (CM4).
---
## 3. In-Depth-Personas (für UX-Phase)
### 3.1 Lukas, Entwickler, 28
- Macht morgens Code, will das Cockpit in unter 30 s pro Tag bedienen.
- Wechselt häufig zwischen 2–3 Projekten am Tag.
- Lästert intern, wenn das Tool ihn „checken" lässt. Sichtbar zu machen, dass Kollege X seit 5 Tagen blockt, empfindet er als hilfreich.
### 3.2 Dejan, Owner / Auftraggeber (P4), oft auch P1, 35
- Will im Jourfixe nicht mehr fragen „wie sieht's bei X aus" — sondern direkt klicken.
- Liest den Puls 2× täglich, schreibt selbst 1× Tagesupdate mit Bezug „Allgemein" oder zu einem Projekt.
- Größte Sorge: dass das Cockpit-Projekt das gleiche Schicksal erleidet wie Jira/Confluence zuvor.
### 3.3 Sales-Kollege (Q2), 30+
- Lebt im Kanban-Tab; bewegt 5–10 Karten pro Woche.
- Schreibt Tagesupdates ungern, wenn das Schreiben träge ist — daher CM1-Schwellwert (≤ 60 s Median).
- Will den eigenen Funnel-Snapshot stolz zeigen können — Marketing-Crosslink (FR-19) ist UI-relevant.
### 3.4 Marketing-Kollege (Q3)
- Aktualisiert wöchentlich Kampagnen, weniger täglich.
- Die tägliche Update-Pflicht ist die ungewohnteste Anforderung — UX muss kurze „heute kein Fortschritt, aber dran"-Updates erlauben.
### 3.5 Designerin / Buchhaltung (Q4, Q5)
- Schreiben Tagesupdates mit Bezug „Allgemein". Die UI darf sie nicht zwingen, ein Projekt zu wählen.
- Bereich = ihr eigener (Authentik-Group), Bezug-Default = „Allgemein".
---
## 4. Mechanik-Detail: Wie wir „Versandung" wirklich messen
Vier Indikatoren zusammen:
1. **Update-Quote pro Person/Tag** — direkter Disziplin-Indikator. Sinkt sie unter 70 %, ist die Cockpit-Kultur erodiert.
2. **Median-Update-Alter pro Projekt** — steigt der Median über 7 Werktage, hat das Projekt aufgehört zu leben.
3. **Anteil Meilensteine mit Status `overdue`** — steigt er über 30 %, sind Planung und Realität auseinandergedriftet.
4. **Pflicht-Minimum-Erfüllungsquote** (M6) — die Leitfaden-Brücke. Sinkt sie, geht es nicht nur am Status, sondern an der Methodik selbst.
Diese vier werden im Puls aggregiert und in der 4-Wochen-Retro besprochen.
---
## 5. Off-Brand-Risiken des Prototyps
Der HTML-Prototyp (`inputs/unternehmens-cockpit.html`) zeigt das gewünschte Konzept gut, trifft aber visuell nicht die Markenrichtung:
- Hintergrund: Hellgrau (`#eceef3`) — soll weiß sein.
- Header: dunkles Navy — passt nicht zu „weiß primär, schwarz sekundär".
- Akzentfarbe: Indigo (`#4f46e5`) — durch ToolCTRL-Brand-Akzent ersetzen.
- Ampelfarben (Grün/Amber/Rot): inhaltlich nötig, an die Brand-Palette adaptieren, ohne Funktion zu verlieren.
Die UX-Phase muss das Re-Skinning explizit durchführen — der Prototyp ist Logik-Referenz, keine Visual-Referenz. Die Voice-Anker aus dem Prototyp („Stark.", „Wie läuft es?", „Welt der Übersicht", „Willkommen, [Name]") sind zu bewahren — siehe NFR-12/13 im PRD.
---
## 6. Konzept-Vokabular
| Begriff | Bedeutung im Produkt | Quelle |
|---|---|---|
| **Puls** | Startseite — Unternehmens-Übersichtspuls. | Prototyp |
| **Area / Bereich** | Fachliche Spielwiese: Entwicklung, Vertrieb, Marketing. | Prototyp + Leitfaden |
| **Tagesupdate / DayEntry** | Kombinierter Tageseintrag einer Person: Einstempeln + ToDos + Reflexion + Ampel + Trend. | Leitfaden §5 + ToolCTRL-Workflow |
| **Einstempeln** | Morgendliche Anwesenheits-Aktion plus Anlegen der ToDo-Liste für den Tag. | ToolCTRL-Workflow |
| **ToDo** | Selbstgesetzter Tagesplan-Punkt mit optionalem Bezug, abends abhakbar. | ToolCTRL-Workflow |
| **Reflexion** | Abendlicher zweiter Touchpoint: ToDos abhaken, Freitext, Ampel, Trend. | ToolCTRL-Workflow |
| **„Mein heute"** | Persönliche Tagessicht (eigene ToDos, eigener Status) — privat während des Tages. | Cockpit-Konzept |
| **Konsolidierter Vortagseintrag** | Die für Kollegen sichtbare Version eines DayEntry, publiziert am nächsten Werktagsmorgen. | Cockpit-Konzept |
| **Bezug** | Worauf sich ein Tagesupdate bezieht: Projekt / Deal / Kampagne / Allgemein. | Prototyp |
| **Ampel** | Grün (im Plan) / Amber (Abweichung, Plan greift) / Rot (Abweichung, kein Plan). | Leitfaden §5 |
| **Trend** | besser / gleich / schlechter. | Leitfaden §5 |
| **Blocker** | Strukturiertes Hindernis, bereichsübergreifend sichtbar. | Prototyp |
| **Update-Serie / Streak** | Aufeinanderfolgende Werktage mit ≥ 1 Tagesupdate. | Prototyp |
| **Jourfixe** | Wöchentliches Treffen, vom Puls aus moderiert (Default Di 11:00). | ToolCTRL |
| **Crosslink** | Lead-Übergabe-Banner Marketing → Vertrieb. | Prototyp |
| **Bereichsübergreifend offen** | Label für die aggregierte Blocker-Liste im Puls. | Prototyp |
| **Pflicht-Minimum** | Die 6 Pflichtpunkte pro Projekt. | Leitfaden §9 |
| **Projektsteckbrief** | Eckdaten-Dokument in Outline. Das Cockpit verlinkt darauf. | Leitfaden §3 |
| **Zieldreieck** | Leistung & Umfang · Zeit · Aufwand. | Leitfaden §1 |
| **Meilenstein** | Überprüfbares Zwischenergebnis am Phasen-Ende. | Leitfaden Anhang |
| **Baseline** | Freigegebener Bezugsstand (Steckbrief nach Freigabe). | Leitfaden Anhang |
| **Scope Creep** | Schleichende, unkontrollierte Umfangserweiterung. | Leitfaden Anhang |
| **Definition of Done (DoD)** | Festlegung, wann etwas wirklich „fertig" ist. | Leitfaden Anhang |
| **Module / Issue / Sub-Issue** | Plane-Hierarchie der Arbeitszerlegung. | Leitfaden §4 |
| **Cycles / Kanban** | Zwei Arbeitsmodi der Umsetzungsphase. | Leitfaden §5 |
| **Retrospektive / Lessons Learned** | Strukturierter Rückblick am Projektende. | Leitfaden §6 |
| **Deal-Stage** | Lead → Erstgespräch → Demo → Angebot → Abschluss. | Prototyp |
| **GTM-Schritt** | Go-to-Market-Aufgabe pro Produkt. | Prototyp |
| **Lead-Funnel** | Reichweite → Website → Leads → MQL → Übergabe. | Prototyp |
| **Projektart** | Kundenprojekt oder Internes Projekt. | Leitfaden §1 |
| **Projektverantwortliche/r (P1)** | Eine Person pro Projekt, führt das Zieldreieck. | Leitfaden §7 |
---
## 7. Leitfaden-Bezüge (was setzt was um)
Tabelle zur Nachvollziehbarkeit, welche PRD-Anforderung auf welchen Leitfaden-Paragraphen zurückgeht. Hilft Architektur- und Story-Phase, die Methodik-Anbindung zu prüfen.
| Leitfaden-Abschnitt | Im PRD umgesetzt durch |
|---|---|
| §1 Was ein Projekt ist · Projektarten | FR-8 (Projektart-Gruppierung); Datenmodell `Project.kind`. |
| §1 Zieldreieck | Implizit in Ampel + Trend + Meilenstein-Status (kein eigenes UI). |
| §2 Vier-Phasen-Zyklus | FR-35-37 (Abschluss-Workflow); UJ-5 (Anlegen). |
| §3 Projektsteckbrief | A4 (kein Editor); FR-10 (Verlinkung); FR-30 Pflicht-Punkt 1. |
| §3 SMART-Ziele | nicht explizit im Cockpit (lebt in Outline); im Leitfaden §3 als Pflicht-Inhalt des Steckbriefs referenziert. |
| §3 Stakeholder | nicht im MVP; bleibt im Outline-Steckbrief. |
| §4 Arbeit zerlegen | A5 (kein Spiegel des Plane-Boards); FR-30 Pflicht-Punkt 3. |
| §4 Meilensteine | FR-11 Cockpit-Sicht; FR-30 Pflicht-Punkt 4. |
| §4 Aufwand schätzen | Out of Scope — Kimai + Plane decken ab. |
| §4 Risiken | FR-10 (Risikoliste-URL verlinken); Liste bleibt in Outline. |
| §5 Arbeitsmodus (Cycles/Kanban) | Datenmodell `Project.arbeitsmodus`; angezeigt in FR-10. |
| §5 Rhythmus → tägliche Updates im Cockpit | gesamte Feature-Gruppe E (Einstempeln · ToDos · Reflexion). |
| §5 Status sichtbar machen (Ampel + Trend + 2-3 Sätze) | FR-26 Reflexions-Formular (Ampel, Trend, Freitext ≤ 1000 Zeichen). |
| §5 Mit Änderungen umgehen | Out of Scope MVP; ändert nur den Outline-Steckbrief. |
| §5 Reviews an Meilensteinen | FR-11 Meilenstein-Status; UJ-3 Verfall-Erkennung. |
| §6 Abschluss / Lessons Learned | FR-35-37; Pflicht-Punkt 6. |
| §7 Rollen | §4.1 PRD (Projektverantwortliche/r, Team, PO, Auftraggeber). |
| §8 Tool-Aufteilung | A3-A5 PRD; gesamtes Strategie-Framing. |
| §9 Pflicht-Minimum | Feature-Gruppe G (FR-30); M6; Puls-Sichtbarkeit. |
---
*Ende Addendum.*
@@ -0,0 +1,332 @@
# HTML-Prototyp Extract
Quelle: `inputs/unternehmens-cockpit.html` (1.214 Zeilen, Single-File-Vanilla-JS-Prototyp, deutsche Sprache, `TODAY="2026-05-21"` hart kodiert, Persistenz via `localStorage` Key `toolctrl_unternehmens_cockpit_v0_2`).
Titel der App: **tool-ctrl · Unternehmens-Cockpit — v0 Prototyp**
Marker im UI: `v0 PROTOTYP` Pill im Header.
---
## Top-Level-Navigation / Hauptbereiche
Im Header gibt es **zwei Top-Level-Views** (umschaltbar via `nav button`):
1. **Cockpit** (Default) — zeigt Unternehmens-Puls + Detail eines ausgewählten Bereichs.
2. **Tagesupdate** — ein Formular zum Erfassen eines neuen Tagesupdates.
Innerhalb der Cockpit-View gibt es **drei Bereiche** (Areas) als Sub-Navigation via "Area-Tiles":
- **Entwicklung**
- **Vertrieb**
- **Marketing**
Header enthält außerdem:
- Brand-Marker (Dot + Titel + v0-Pill).
- Live-Datum "Heute · Mo, 21. Mai 2026".
- Button **"Demodaten zurücksetzen"** (cleart `localStorage` + lädt `SEED_UPDATES`).
Davor: **Intro / Splash-Screen** (`#intro`) — animierter HUD-Kreis, "Willkommen, Haris", User-Pill (HA · Vertrieb & Management · grüner Online-Dot), Enter-Button (oder Enter-Taste), dann fade-out.
---
## Module / Features (gruppiert)
### Unternehmens-Puls (oberster Karten-Block im Cockpit)
- **Zweck:** schneller Überblick über alle drei Bereiche auf einen Blick + offene Querschnitts-Themen.
- **Datenfelder pro Area-Tile:** Name, Status-Dot (green/amber/red), Status-Pill ("Im Plan" / "Gefährdet" / "Blockiert"), zwei Key-Metrics (variabel pro Bereich), Updates-Heute-Counter `sub/exp` + Progress-Bar.
- **Area-Tile-Metriken:**
- Entwicklung: `Projekte im Plan` (z. B. 4/6), `Meilensteine gesamt` (% erledigt).
- Vertrieb: `Aktive Deals` (#), `Pipeline-Wert` (EUR).
- Marketing: `Kampagnen live` (#), `Leads an Vertrieb` (# letzter Funnel-Step).
- **UI-Patterns:** 3-Spalten-Grid mit Tiles (klickbar, `on`-State markiert ausgewählte Area), darunter zweigeteilte Foot-Sektion.
- **Foot-Sektion:**
- Links: "X von Y Tagesupdates sind heute eingetragen" + Big-Progress-Bar + **Jourfixe-Hinweis** ("Nächstes Jourfixe — Di, 26. Mai, 11:00 · übergeordnete Themen aller Bereiche", berechnet via `nextTuesday()`).
- Rechts: **"Bereichsübergreifend offen"** — rote Chips für alle aktuellen Blocker (dedupliziert per area+blocker-Text, abgeschnitten >52 Zeichen). Bei keiner Blockade grüner Text "Aktuell keine offenen Blocker."
- **Interaktionen:** Klick auf Area-Tile setzt `state.selected` und scrollt nach oben; Klick auf roten Blocker-Chip navigiert zur betroffenen Area.
- **Status:** fertig und voll funktional.
### Bereichs-Detail: Entwicklung
- **Zweck:** Konsolidierte Sicht aller Entwicklungsprojekte mit Meilensteinen und Tagesupdates.
- **Layout:** `dhead` (Bereichs-Kopf) + 2-Spalten-Grid: **Projekt-Rail links (262px)** + **Projekt-Detail rechts**.
- **Bereichs-Kopf (`dhead`):** Status-Dot, Bereichsname "Entwicklung", Pill, Leitung (Person), Updates-Heute, Blurb-Text ("Alle 6 Produkt- und Infrastrukturprojekte mit Status, Meilensteinen und Tagesupdates — Projekt links wählen.").
- **Bereichs-Stats (4 Tiles):** `Projekte` (6 · "3 Umsatz · 3 Intern"), `Im Plan` (X/6), `Meilensteine` (X/Y + Bar), `Updates heute` (X/Y + Bar).
- **Projekt-Rail:** gruppiert in **"Umsatz"** und **"Intern"**; jedes `pitem` zeigt: Status-Dot, Projektname, nächster Meilenstein-Name, Due-Text (`in N Tagen` / `heute fällig` / `N Tage überfällig` mit warn/over-Färbung), `done/total` Meilensteine, mini-Progress-Bar. `on`-State für selektiertes Projekt.
- **Projekt-Detail (rechte Spalte):** eigener `dhead` mit Projektname (h1), Status-Pill, Gruppe-Tag ("Umsatz"/"Intern"), Leitung. Dann 4 Stats: `Status` (Pill, "aus dem letzten Update"), `Nächster Meilenstein` (Name + Datum + due-Text), `Fortschritt` (X/Y + Bar), `Updates heute` (X/Y + "Update-Serie"-Streak-Counter).
- **Darunter 2-Spalten-Grid (1fr 290px):**
- **Tagesupdates-Feed** (links): chronologisch nach Datum gruppiert, "Heute"-Marker; jedes Update zeigt Avatar mit Initialen + Personen-Farbe, Name, Rolle, Status-Pill, Text, optional Blocker-Box (rot). "+ Update hinzufügen" Button (oben rechts) prefillt Formular mit Area+Ref.
- **Meilensteine-Liste** (rechts): Checkliste mit Box, Name, Fälligkeitsdatum; Zustände `done` (grün gefüllt, durchgestrichen), `next` (lila Outline), normal.
- **Status:** fertig.
### Bereichs-Detail: Vertrieb
- **Zweck:** Deal-Pipeline + Go-to-Market-Fortschritt je Produkt.
- **Bereichs-Stats (4):** `Aktive Deals`, `Pipeline-Wert` (EUR Summe), `Gewichtet` (Σ value × Stage-Wahrscheinlichkeit), `Gewonnen` (# + EUR).
- **Crosslink-Banner:** "X Leads hat Marketing diesen Monat in die Pipeline übergeben — Marketing-Status ansehen" (Pfeil-Icon, klickt zu Marketing).
- **Deal-Pipeline-Board (Kanban-Style):** 5 Spalten = Stages `Lead`, `Erstgespräch`, `Demo`, `Angebot`, `Abschluss`. Spaltenkopf: Stage-Label + "N Deals · €Summe". Deal-Karten: Status-Dot, Kundenname, EUR-Wert, Produkt-Tag (Caps, z. B. "HIVEGRID"), Next-Step-Text.
- **Vertriebsfortschritt je Produkt (GTM):** pro Produkt eine Karte mit Name, "X/Y Schritte", Progress-Bar, Schritt-Liste mit Box (Zustände `done`/`next`/`blocked`/`open`) + Tags ("als Nächstes" / "blockiert").
- **Tagesupdates-Feed** (gleicher Feed-Style wie Entwicklung, aber mit `upd-ref`-Badge = Referenz zu Deal/Kampagne/"(allgemein)").
- **Status:** fertig.
### Bereichs-Detail: Marketing
- **Zweck:** Lead-Funnel + Kampagnen-Übersicht.
- **Bereichs-Stats (4):** `Reichweite` (#), `Leads gesamt`, `An Vertrieb` (Übergabe-Counter), `Kampagnen live` (X von Y).
- **Crosslink-Banner:** "Die N übergebenen Leads landen im Vertrieb — aktuelle Pipeline: €X" (klickt zu Vertrieb).
- **Lead-Funnel-Visualisierung:** 5 Stufen (Reichweite → Website-Besucher → Leads → Qualifiziert (MQL) → An Vertrieb übergeben). Pro Zeile: Label + Sub-Label, horizontale gefüllte Bar (Breite relativ zum Maximum, Wert eingebrannt), Conversion-% zur Vorstufe ("X% Übergang"). Letzte Zeile = "handover" mit grünem Gradient.
- **Kampagnen-Liste:** flache Liste, pro Zeile: Name + Channel (Sub-Text), Verantwortlicher, Leads-Count, Status-Pill (`läuft`, `geplant`, `ausgewertet`, `blockiert`, `pausiert` — je eigene Farbe).
- **Tagesupdates-Feed** (gleicher Feed-Style).
- **Status:** fertig.
### Tagesupdate-Formular (View 2)
- **Zweck:** strukturierte Eingabe eines täglichen Status-Updates.
- **Layout:** 2-Spalten (Formular-Karte 1fr + Side-Nudge 300px).
- **Formular-Felder:**
- `Wann` — statisch "Heute — Mo, 21. Mai 2026" (read-only Box).
- `Wer bist du?` — `<select>` aller TEAM-Mitglieder mit "Name — Rolle".
- `Bereich` — `<select>` Entwicklung/Vertrieb/Marketing.
- `Bezug` (Projekt, Deal oder Kampagne) — dynamische `<select>` Optionen je nach Bereich; Marketing/Vertrieb haben zusätzlich `(allgemein)`.
- `Was hast du heute geschafft?` — `<textarea>` (Placeholder mit Beispiel).
- `Wie läuft es?` — 3 Status-Optionen (Cards mit Dot + Titel + Sub): **Im Plan** (grün, "läuft nach Plan") / **Gefährdet** (amber, "gerät in Verzug") / **Blockiert** (rot, "brauche Hilfe").
- `Etwas, das dich blockiert oder wobei du Hilfe brauchst?` — optionale `<textarea>`.
- Fehler-Slot (`#f-err`) + Submit-Button "Tagesupdate absenden".
- **Validierung:** Person, Bereich, Text und Status sind Pflicht; Ref defaults zu `(allgemein)`.
- **Submit-Verhalten:** pusht in `updates`-Array, persistiert in `localStorage`, springt zurück in Cockpit-View des gewählten Bereichs, zeigt Toast "Update von X erfasst · Bereich".
- **Side-Nudge-Karte (rechts):**
- "Heute noch offen" — pro fehlendem Update ein roter Chip "Person · Bereich" (Klick prefillt Formular). Wenn alle abgegeben: "Alle Tagesupdates sind da. Stark." (grün).
- Trennlinie + "Der Rhythmus" — Erklärtext zum täglichen Update + Jourfixe-Termin (Di 11:00).
- **Status:** fertig und voll funktional.
### Intro / Splash-Screen
- **Zweck:** Willkommens-Animation beim Aufruf.
- **Inhalt:** "Willkommen, Haris", User-Avatar-Pill mit Online-Dot, Enter-Button.
- **Interaktion:** Klick auf Enter-Button oder Enter-Taste blendet aus.
- **Status:** dekorativ, kosmetisch.
### Toast
- Globale Toast-Komponente (`#toast`) am unteren Bildschirmrand für Bestätigungs-Messages (Update erfasst, Demodaten zurückgesetzt). Auto-Hide nach 3,2 s.
---
## KPI-Visualisierungen
Im Prototyp verwendete Visualisierungen:
- **Status-Ampel-Dots** (`.sdot`) in green/amber/red — überall, kleinste Atom-Komponente.
- **Status-Pills** (`.pill`) mit Label "Im Plan" / "Gefährdet" / "Blockiert".
- **Progress-Bars** (mehrere Varianten):
- `.tbar` (mini, in Area-Tiles für Updates-Quote heute).
- `.bigbar` (Puls-Foot für gesamte Update-Quote).
- `.ministat-bar` (in Stat-Kacheln für Meilenstein- und Update-Progress).
- `.pi-bar` (in Projekt-Rail-Items für Meilenstein-Fortschritt).
- `.sprod-bar` (in GTM-Produkt-Karten).
- **Meilenstein-Checkliste** (`.ms` Items) mit drei Box-Zuständen: done (grün/check), next (lila Outline mit Glow), blocked (roter Outline mit Glow); normal-State leere Box.
- **Kanban-Pipeline-Board** mit 5 Spalten (Deal-Stages).
- **Horizontaler Funnel** (5 Stufen, absteigende Bar-Breite proportional zum Wert, Conversion-% zwischen Stufen).
- **Streak-Counter** (Update-Serie Tage am Stück, im Projekt-Detail).
- **Due-Date-Färbung**: warn (amber, ≤4 Tage) / over (rot, überfällig).
- **Gewichteter Pipeline-Wert** (Σ value × p) als KPI im Vertrieb.
- **Conversion-Raten** (% zwischen Funnel-Stufen).
- **Status-Rollup-Logik** (`rollup()`): aggregiert Einzel-Status zu Area-Status (≥2 rot → red, ≥1 rot oder ≥3 amber → amber, sonst green).
Explizit **NICHT vorhanden:** Burn-down-Charts, Status-Pie-Charts, Zeitreihen-Charts, Trend-Linien, prozentuale Donuts, Heatmaps. Alle "Charts" sind 1D-Bars oder Listen.
---
## UI-Patterns (mehrfach auftauchend)
- **Card** (`.card`) — weiße Box mit `border-radius:14px` + Schatten; Basis-Container für alles.
- **Section-Title** (`.sec-title`) — Header in Cards mit optionalem Counter-Badge (`.cnt`) und rechts-bündigem Action-Button.
- **Sect-Label** (Caps + letter-spacing) — kleine Sektionsüberschrift.
- **Stat-Tile** (`.stat`) — kleine Metric-Box mit Label/Wert/Sub/optional Bar; meist 4er-Grid (`.stats`).
- **Chip** (`.chip`) — runder farbiger Tag, hier in red-Variante für Blocker.
- **Status-Pill** (`.pill`) — Pille in 3 Farben (green/amber/red).
- **Group-Tag** (`.grp-tag`) — kleines neutrales Caps-Label (z. B. "UMSATZ", "INTERN").
- **Avatar mit Initialen** (`.avatar`) — runder gefärbter Kreis mit 2-Buchstaben-Initialen, Farbe pro Person.
- **Crosslink-Banner** (`.crosslink`) — gerahmte klickbare Zeile mit Pfeil-Icon für Bereich-zu-Bereich-Verweise.
- **Feed-Item** (`.upd`) — Avatar + Name/Rolle/Ref/Status + Text + optional Blocker-Box; gruppiert nach `.fdate` (Datums-Header mit "Heute"-Highlight).
- **Blocker-Box** (`.blocker`) — rote bezugnehmende Sub-Box innerhalb eines Feed-Items.
- **Rail-Item** (`.pitem`) — sidebar list item mit `on`-Indikator (linker Strich) und Hover-State.
- **Status-Option-Card** (`.sopt`) — Radio-Karten-Pattern für Status-Wahl im Formular.
**Keine Modals.** Es gibt im Prototyp keine Dialoge, Popovers, Tooltips oder Dropdowns außer nativen `<select>`-Elementen und Tabs.
---
## Interaktionen / Klick-Flows
Implementierte Mock-Interaktionen (alle via Vanilla JS / `wire()`):
1. **Top-Nav umschalten** (Cockpit ↔ Tagesupdate) via `[data-nav]`.
2. **Area-Tile-Klick** wechselt aktiven Bereich (`state.selected`), scrollt nach oben.
3. **Projekt-Rail-Item-Klick** wechselt aktives Dev-Projekt (`state.devProject`).
4. **Blocker-Chip-Klick** (Puls-Foot) → Sprung in betroffenen Bereich.
5. **Crosslink-Banner-Klick** → Sprung in verlinkten Bereich.
6. **"+ Update hinzufügen"-Button** in Feed/Projekt-Detail → prefillt Formular (Area + ggf. Ref) und fokussiert Text-Feld.
7. **"Missing"-Chip-Klick** (Side-Nudge) → prefillt Formular mit Person + Area.
8. **Status-Option-Klick** im Formular setzt `state.formStatus` (immediate render).
9. **Submit-Button**: validiert, fügt Update zu `updates`-Array hinzu, persistiert in `localStorage`, navigiert zurück + Toast.
10. **"Demodaten zurücksetzen"** im Header: bestätigt + ersetzt updates mit `SEED_UPDATES`.
11. **Intro-Splash**: Enter-Button oder Enter-Taste blendet aus.
---
## Datenmodell (impliziert)
### `TEAM` (Mitglied)
- `id` (string slug, z. B. "haris")
- `name` (string)
- `role` (string, z. B. "Vertrieb & Management", "Senior Software Engineer", "Junior Software Engineer", "Junior Infrastructure/Software Engineer", "Designer")
- `color` (hex, Avatar-Farbe)
Es gibt 5 fest hinterlegte Personen. Keine echten User-Accounts/Auth.
### `AREA` (Bereich)
- `id` (slug: "entwicklung" | "vertrieb" | "marketing")
- `name` (string)
- `lead` (TEAM-id, Bereichsleitung)
- `members` (array of TEAM-ids)
- `blurb` (string, Bereichsbeschreibung)
### `DEV_PROJECT`
- `id` (slug)
- `name` (string)
- `group` ("Umsatz" | "Intern")
- `lead` (TEAM-id)
- `members` (array of TEAM-ids)
- `milestones`: array von Objekten `{ name:string, done:boolean, due:ISO-Date }`
### `Milestone`
- `name` (string)
- `done` (boolean)
- `due` (ISO-Date)
### `STAGE` (Vertriebs-Pipeline-Stufe)
- `id` (slug)
- `label` (string: "Lead", "Erstgespräch", "Demo", "Angebot", "Abschluss")
- `p` (Abschlusswahrscheinlichkeit, float 0–1: 0.10/0.25/0.50/0.75/0.90)
### `DEAL`
- `customer` (string)
- `product` (string, derzeit "HiveGrid" oder "Asset Control")
- `stage` (STAGE-id)
- `value` (number, EUR)
- `status` ("green" | "amber" | "red")
- `next` (string, nächster Schritt / Notiz)
### `WON_DEAL`
- `customer`, `product`, `value`, `closed` (ISO-Date)
### `SALES_GTM[product]` — pro Produkt eine Liste von GTM-Schritten
- `name` (string)
- `state` ("done" | "next" | "blocked" | "open")
### `CAMPAIGN`
- `name` (string)
- `channel` (string, z. B. "LinkedIn + Google Ads", "E-Mail (monatlich)", "Event / Messe", "Lokalpresse", "Content / Website", "Google Ads")
- `status` ("laeuft" | "geplant" | "ausgewertet" | "blockiert" | "pausiert")
- `leads` (number)
- `owner` (TEAM-id)
### `FUNNEL` (5 ordered steps)
- `label` (string)
- `sub` (string, Erklärung)
- `value` (number)
Stages: Reichweite → Website-Besucher → Leads → Qualifiziert (MQL) → An Vertrieb übergeben.
### `UPDATE` (Tagesupdate, das zentrale Entity)
- `area` (AREA-id)
- `ref` (string — Bezug; Projektname, Kundenname, Kampagnenname oder "(allgemein)")
- `person` (TEAM-id)
- `date` (ISO-Date)
- `status` ("green" | "amber" | "red")
- `text` (string, freier Text "was geschafft")
- `blockers` (string, optional; leerer String wenn keiner)
Persistiert in `localStorage` Key `toolctrl_unternehmens_cockpit_v0_2`.
### State (UI)
- `view` ("cockpit" | "form")
- `selected` (AREA-id, aktive Area im Cockpit)
- `devProject` (DEV_PROJECT-id, aktives Projekt in Entwicklungs-Rail)
- `formPerson`, `formArea`, `formRef`, `formStatus`, `_text`, `_blockers` (Formular-State)
---
## Rollen / Rechte / Filter
- **Rollen** sind als freitextliche `role`-Strings hinterlegt (Vertrieb & Management, Senior/Junior Software Engineer, Junior Infrastructure/Software Engineer, Designer). **Keine** Permission-Logik, keine Rollen-Gates, kein Auth. Jeder kann sich beim Tagesupdate als beliebige Person ausgeben.
- **Tags:** Es gibt Projekt-Gruppen-Tags ("Umsatz"/"Intern"), Produkt-Tags an Deals, Channel-Texte an Kampagnen, aber **keine freien Tags** oder Tagging-System.
- **Filter:** Es gibt **keine expliziten Filter-UI-Elemente** (keine Suche, kein Datumsfilter, keine Personen-Filter). Filterung erfolgt nur implizit durch Area-Auswahl und Projekt-Auswahl.
---
## Vokabular / Begriffe
Wichtige Begriffe und ihre Verwendung:
- **Cockpit** — das Gesamtprodukt; auch Name der Default-View.
- **Unternehmens-Puls** ("Puls") — Overview-Block ganz oben mit den Area-Tiles.
- **Bereich** / **Areas** — Top-Level-Domänen (Entwicklung, Vertrieb, Marketing).
- **Tagesupdate** — täglicher Status-Eintrag pro Person/Bereich/Bezug.
- **Bezug** — die Sache, auf die sich ein Tagesupdate bezieht (Projekt, Deal, Kampagne, "(allgemein)").
- **Jourfixe** — wiederkehrendes Team-Meeting (Dienstag 11:00) für übergeordnete Themen.
- **Update-Serie** / **Streak** — aufeinanderfolgende Tage mit Updates für ein Projekt.
- **Blocker** — explizit benannte Hindernisse in Updates; werden bereichsübergreifend aggregiert.
- **Bereichsübergreifend offen** — Cross-Area-Blocker-Aggregation.
- **Ampel** (green/amber/red) — Status-System; Labels: **Im Plan** / **Gefährdet** / **Blockiert**.
- **Meilenstein** — DEV_PROJECT-Schritt mit Name + done + due-Datum.
- **Pflichtenheft** — projektspezifischer Begriff (Spec-Dokument).
- **Deal-Pipeline** — 5-stufiger Vertriebs-Funnel.
- **Stages:** Lead, Erstgespräch, Demo, Angebot, Abschluss.
- **Pipeline-Wert** / **Gewichteter Wert** — Summen-KPIs im Vertrieb.
- **GTM** / **Vertriebsfortschritt je Produkt** — Go-to-Market-Schrittliste pro Produkt.
- **Lead-Funnel** — 5-stufiger Marketing-Funnel.
- **MQL** ("Qualifiziert (MQL)") — Marketing-Qualified Lead.
- **Reichweite** — Top-of-Funnel-Metrik.
- **Übergabe / An Vertrieb übergeben** — Funnel→Pipeline-Transition.
- **Kampagne** — Marketing-Aktivität mit Channel und Status.
- **Kampagnen-Status:** läuft, geplant, ausgewertet, blockiert, pausiert.
- **Gruppen** (Projekt-Gruppen): **Umsatz** vs. **Intern**.
- **Crosslink** — Verweis zwischen Bereichen.
- **Demodaten** — die SEED-Beispieldaten.
- **Produkte (im Prototyp benannt):** **HiveGrid**, **Asset Control**, **Vereins-App**, **Server / Infrastruktur**, **ProdValid**, **PostValid**.
Hinweis: Im Code-Kommentar erwähnt: *"Das separate Entwicklungs-Cockpit (index.html) bleibt eigenstaendig bestehen; hier ist Entwicklung eine verdichtete Bereichssicht."* — deutet auf ein paralleles, detaillierteres Dev-Cockpit-Tool außerhalb dieses Prototyps hin.
---
## Lücken / Unklarheiten / Skelette
- **Keine echte Auth / Identität.** Es gibt keinen Login. Der Splash zeigt fix "Haris", aber das Formular lässt frei wählen, wer man ist. Keine Sessions, keine Audit-Trails.
- **Keine Persistenz auf Server.** Alles im `localStorage` des Browsers; multi-user-uneinsichtbar.
- **TODAY ist hart kodiert** (`"2026-05-21"`). Keine echte Zeit, keine Zeitzonen, kein Datepicker.
- **Keine Editier-/Löschfunktionen** für Updates, Projekte, Deals, Kampagnen, Meilensteine. Tagesupdates können nur hinzugefügt werden, nicht korrigiert.
- **Keine Erstellung neuer Entitäten im UI.** DEV_PROJECTS, DEALS, CAMPAIGNS, FUNNEL, SALES_GTM, TEAM, AREAS sind im JS-Code statisch hinterlegt — kein CRUD-UI.
- **Meilensteine ändern sich nicht** über Updates; `done`-Flag ist statisch im Seed-Datensatz.
- **Status pro Projekt** wird aus dem **letzten Update** abgeleitet (`devStatus(p)` nimmt `f[0].status`) — keine separate Status-Eingabe pro Projekt.
- **Keine echten Visualisierungen** außer Bars und Listen. Keine Library (Chart.js etc.).
- **Keine Integration zu Plane** (Tickets) oder **Outline** (Doku) sichtbar — der Prototyp ist standalone.
- **Plane/Outline werden nicht referenziert** in der HTML/JS — diese externen Tools sind im Prototyp gar nicht present, obwohl die User-Anfrage sie nennt.
- **Keine Kommentar- oder Reaktions-Funktion** auf Updates.
- **Keine Notifications** außer dem in-app Toast.
- **Keine Suche.**
- **Keine Filter, Sortierungen oder Datumsbereiche** auf Feeds.
- **Keine Pagination** — Feeds zeigen alle Updates.
- **Won-Deals** werden im KPI-Tile gezählt, aber **nirgendwo gelistet/detailliert** (kein Won-Deal-Board).
- **"Vereins-App", "ProdValid", "PostValid"** etc. sind als Projekte im Code, aber keine Detail-Pages außerhalb der Standard-Projekt-Detail-Ansicht.
- **"Pausiert"-Kampagnen-Status** ist im CSS und im Status-Mapping vorhanden, wird aber von keiner Beispiel-Kampagne genutzt — toter Code-Pfad.
- **"WON_DEALS"** Daten sind erfasst, aber nur als Aggregat sichtbar.
- **Side-Nudge "Der Rhythmus"** ist reiner Erklärtext — keine konfigurierbaren Rhythmen.
- **Mobile/Responsive:** das Layout ist auf 1280 px max-width + viele festgelegte Grid-Spalten ausgelegt; **keine Responsive-Anpassung** im CSS.
- **Lokalisierung:** komplett deutsch, hartkodiert (Wochentage, Monate).
- **Keine Kommentare/Threads, keine Anhänge, keine Datei-Uploads.**
- **Keine Roadmap/Timeline-Visualisierung** (Gantt o. ä.).
- **Keine Trend-Daten** über Zeit; alle KPIs sind Snapshots auf TODAY.
---
## Sektionen, die unfertig / Platzhalter sind
Im Sinne von "leere Skelette" gibt es im Prototyp eigentlich keine — alle gerenderten Bereiche sind mit Demo-Daten gefüllt. Die "Lücken" oben sind eher fehlende Konzepte / Features. Vereinzelte tote Code-Pfade:
- `cstatus.pausiert` Stil ohne Daten.
- `feed-empty` ("Noch keine Updates in diesem Bereich.") und Projekt-Feed-Empty ("Noch keine Updates für dieses Projekt.") nur bei leerem State.
- Submit-Fehler-Slot (`#f-err`) leer im Default-Render.
@@ -0,0 +1,404 @@
---
title: PRD – ToolCTRL Unternehmens-Cockpit
status: final
created: 2026-05-25
updated: 2026-05-25
owner: Dejancosic
audience: ToolCTRL (intern, alle Mitarbeiter)
binding_frame: ToolCTRL Projektmanagement-Leitfaden v3.0 (Mai 2026)
---
# PRD – ToolCTRL Unternehmens-Cockpit
> **Was das ist.** Eine interne Web-Anwendung als tägliches Übersichts-Cockpit über alle laufenden Projekte und Bereiche von ToolCTRL. Sie operationalisiert den **ToolCTRL Projektmanagement-Leitfaden v3.0** als drittes Werkzeug neben **Plane** (Arbeit) und **Outline** (Gedächtnis) — im Leitfaden §5 (Rhythmus) so vorgesehen.
>
> **Wofür dieses Dokument da ist.** Es legt fest, was das Cockpit im MVP können muss. Capability-orientiert; technische Tiefe lebt im `addendum.md`.
---
## 1. Vision & Problemkontext
### 1.1 Das echte Problem
ToolCTRL hat **kein Tool-Problem, sondern ein Durchziehen-Problem.** Dank KI-gestützter Entwicklung starten wir 10+ Projekte parallel. Die Anfangsphase ist schnell und motivierend. Sobald Reibung entsteht, versandet das Projekt: Updates bleiben aus, Meilensteine schleifen, Verantwortlichkeit löst sich auf, Markt-Releases bleiben aus.
Wir hatten Jira und Confluence im Einsatz und haben mittendrin damit aufgehört, weil wir „schneller ohne PM" sein wollten. Ergebnis: Wir bekommen nichts fertig. Wir holen die strukturierte Projektarbeit jetzt zurück — mit der **dreigeteilten Werkzeug-Logik des Leitfadens**:
| Ebene | Werkzeug | Funktion (Leitfaden §5, §8) |
|---|---|---|
| **Arbeit** | **Plane** | Module → Issues → Sub-Issues, Boards, Cycles. *„Plane ist jederzeit die Wahrheit über den aktuellen Stand."* |
| **Gedächtnis** | **Outline** | Projektsteckbrief, Risikoliste, Status-Notizen, Entscheidungen, Lessons Learned. |
| **Übersicht / Disziplin** | **Cockpit** (dieses Produkt) | Grober Überblick über alle Projekte, tägliche Updates und Check-ins, illustrierte Unternehmensbereiche. |
Das Cockpit ist die Antwort auf: *„Warum versanden unsere Projekte? Weil niemand täglich gezwungen ist, sichtbar zu machen, wo er steht — und weil das Leitfaden-Pflicht-Minimum schleichend erodiert."* Das Cockpit erzwingt Sichtbarkeit und macht Erosion früh erkennbar.
### 1.2 Vision in einem Satz
**Das Cockpit ist der Ort, an dem jeder Mitarbeiter in unter 60 Sekunden sieht, woran heute gearbeitet wird, was klemmt, welche Projekte aus dem Plan laufen und welche das Pflicht-Minimum des Leitfadens nicht (mehr) erfüllen — und an dem niemand sich tagelang wegducken kann.**
### 1.3 Warum jetzt
- „Kein PM" als Strategie ist gescheitert.
- Der Leitfaden v3.0 setzt die verbindliche Rahmen-Methodik — das Cockpit ist sein operatives Tagesgesicht.
- Plane (Tickets) und Outline (Doku) decken den *aggregierten Status* und das *tägliche Sichtbarmachen* nicht ab.
- Das Cockpit muss vor der nächsten Welle neuer Projekte stehen, damit wir nicht in dasselbe Muster fallen.
---
## 2. Ziele & Anti-Ziele
### 2.1 Ziele (was wir erreichen wollen)
- **G1** – Jeder aktive Mitarbeiter schreibt mindestens 1× pro Werktag ein Tagesupdate. Die Quote ist öffentlich sichtbar.
- **G2** – Jedes laufende Projekt hat jederzeit einen klar erkennbaren Ampelstatus (Leitfaden §5: Grün / Gelb / Rot) inkl. Trend und einen visuellen Deadline-/Meilenstein-Verlauf.
- **G3** – Bereichsübergreifende Blocker sind innerhalb von 24h für alle Beteiligten sichtbar.
- **G4** – Das Cockpit ist feste erste Anlaufstation des Jourfixe (Di 11:00) und der täglichen Routine.
- **G5** – Mehr Projekte erreichen die „Marktreife / Release"-Phase, weil Stagnation früh sichtbar wird.
- **G6** – Erfüllung des **Pflicht-Minimums** (Leitfaden §9) ist pro Projekt sofort sichtbar; nicht erfüllte Punkte sind im Puls aufgelistet.
### 2.2 Anti-Ziele (was wir explizit NICHT bauen)
- **A1** – Kein **Ticketsystem**. Tickets bleiben in Plane.
- **A2** – Keine **Doku**. Wissens-Seiten, Steckbriefe, Risikolisten, Lessons Learned bleiben in Outline.
- **A3** – **Kein zusätzliches Projektmanagement-Tool** (Leitfaden §8: „Wir führen kein zusätzliches Projektmanagement-Tool ein."). Cockpit hält sich strikt an die Tool-Aufteilung.
- **A4** – **Kein Steckbrief-Editor.** Steckbrief lebt in Outline (Leitfaden §3). Das Cockpit verlinkt und referenziert.
- **A5** – **Kein gespiegeltes Plane-Board.** Die Issue-Liste bleibt in Plane.
- **A6** – **Keine externe Kundenkommunikation** (kein CRM, kein Kundenportal).
- **A7** – **Keine Zeiterfassung** — Kimai existiert.
- **A8** – **Kein Werkzeug zum Verkauf.** Das Cockpit ist und bleibt rein intern.
- **A9** – **Kein konfigurierbares Reporting-/BI-Dashboard** im MVP. Nur die im PRD spezifizierten Aggregate.
---
## 3. Erfolgskriterien & Gegenmetriken
### 3.1 Erfolgsmetriken
| ID | Metrik | Zielwert nach 8 Wochen Live-Betrieb |
|---|---|---|
| M1 | Anteil Werktage mit ≥1 Tagesupdate pro aktiver Person | ≥ 80% |
| M2 | Mittlere Zeit von „Blocker eingetragen" bis „im Puls sichtbar" | < 5 Minuten |
| M3 | Anteil Projekte mit aktuellem Ampelstatus (Update ≤ 3 Werktage alt) | ≥ 90% |
| M4 | Anteil Jourfixes, die das Cockpit als Hauptview nutzen | 100% |
| M5 | Aktiv genutzte Bereiche (Dev / Sales / Marketing) bekommen wöchentlich Updates | 3/3 |
| M6 | Anteil aktiver Projekte, die alle 6 Leitfaden-§9-Pflicht-Punkte erfüllen | ≥ 90% |
### 3.2 Gegenmetriken (was wir nicht kaputt machen dürfen)
| ID | Gegenmetrik | Grenze |
|---|---|---|
| CM1 | Gesamt-Tageszeit im Cockpit pro Person (Median, Werktag) | ≤ 3 Minuten |
| CM1a | Zeit pro Morgen-Einstempeln + ToDos (Median) | ≤ 60 Sekunden |
| CM1b | Zeit pro Abend-Reflexion (Median) | ≤ 90 Sekunden |
| CM2 | Anteil inhaltsleerer Pflicht-Updates | qualitativ — Jourfixe-Diskussion |
| CM3 | Spannungs-/Lästergefühl durch „fehlt heute"-Anzeige | qualitativ — 4-Wochen-Retro |
| CM4 | Doppelpflege Cockpit ↔ Outline-Steckbrief | qualitativ — wenn Steckbrief-Felder im Cockpit dupliziert werden, justieren wir |
> [NOTE FOR PM] CM2/CM3/CM4 sind qualitativ und werden 4 Wochen nach Launch im Jourfixe reflektiert.
---
## 4. Nutzer & Rollen
### 4.1 Leitfaden-Rollen (§7) – pro Projekt
| Rolle | Aufgabe im Cockpit-Kontext |
|---|---|
| **Projektverantwortliche/r** (P1) | Pflegt Ampelstatus + Trend, achtet auf Pflicht-Minimum, moderiert Cockpit-Sicht im Jourfixe für ihr/sein Projekt. |
| **Team** (P2) | Schreibt Tagesupdates mit Projektbezug; setzt Blocker; aktualisiert Meilenstein-Status. |
| **Product Owner** (P3, bei Produktprojekten) | Lese-Schwerpunkt; nutzt das Cockpit zur Priorisierungs-Diskussion. |
| **Auftraggeber** (P4, intern = Geschäftsführung) | Lesender Hauptnutzer für Pflicht-Minimum und Puls. |
### 4.2 Querschnittsrollen – alle Mitarbeiter
Alle Mitarbeiter sind aktive Cockpit-Nutzer, unabhängig vom Leitfaden-Rolle-Hut:
| Querschnitt | Hauptnutzen am Cockpit |
|---|---|
| **Entwickler** (Q1) | Tägliche Updates mit Projektbezug, Meilenstein-Status, eigene Streak. |
| **Sales / Vertrieb** (Q2) | Pflegt Deal-Pipeline + GTM-Schritte, postet Vertriebs-Updates. |
| **Marketing** (Q3) | Pflegt Lead-Funnel + Kampagnen-Status, postet Marketing-Updates. |
| **Design** (Q4) | Postet allgemeine Tagesupdates, kann Projekte als Bezug wählen. |
| **Buchhaltung** (Q5) | Postet allgemeine Tagesupdates. |
Alle Rollen sehen alles (flaches internes Vertrauensmodell im MVP).
### 4.3 Identität & Authentifizierung
Login erfolgt ausschließlich über **Authentik** (auth.tool-ctrl.de) per OIDC. Default-Bereich wird aus Authentik-Group-Claim abgeleitet (Mapping in OQ-1). Projektverantwortliche/r wird pro Projekt im Cockpit als Attribut gesetzt.
---
## 5. User Journeys
### UJ-1 — Tagesrhythmus: Einstempeln, Arbeiten, Reflektieren
*Primär: Q1-Q5*
**Morgens — Einstempeln + ToDos**
1. Person öffnet das Cockpit, klickt „Einstempeln".
2. Trägt eine **ToDo-Liste für heute** ein: 1-N Items, jedes Item mit Titel und optionalem Bezug (Projekt/Deal/Kampagne/Allgemein).
3. Speichert → Person verschwindet aus „heute fehlt eingestempelt", gilt für die Streak als „heute begonnen".
**Abends — Reflexion**
4. Person ruft im Lauf des Tages oder spätestens abends ihre ToDo-Liste auf.
5. Hakt ab, was erledigt wurde.
6. Trägt im Freitext nach, was sonst noch passiert ist (max. 1.000 Zeichen).
7. Setzt **Ampel** und — falls Bezug=Projekt vorhanden — **Trend**.
8. Optional: Blocker → bereichsübergreifende Blocker-Liste im Puls.
9. Speichert → Tag gilt als „voll abgeschlossen", Streak+1.
**Nächster Morgen — Kollegen-Sichtbarkeit**
10. Ab dem nächsten Werktagsmorgen ist der konsolidierte Vortags-Eintrag (ToDos + Häkchen + Reflexion + Ampel + Trend) für alle Kollegen sichtbar.
11. Innerhalb des laufenden Tages sehen Kollegen nur die binäre Anwesenheit („eingestempelt: ja/nein"), nicht die ToDo-Inhalte. Reduziert In-Day-Surveillance.
### UJ-2 — Jourfixe-Moderation
*Primär: P1, P4 (dienstags 11:00)*
1. PM öffnet Cockpit-Puls.
2. Geht die 3 Bereiche durch — Ampel pro Bereich, Top-Projekt-Status, offene Blocker, **Pflicht-Minimum-Score**.
3. Klickt auf rote/gelbe Projekte → Detail-View mit letzten Updates, Ampel + Trend, Meilenstein-Status, Deadline-Bar.
4. Diskutiert Blocker mit Verantwortlichen.
### UJ-3 — Projekt-Verfall früh erkennen
*Primär: P1, P4*
1. Person scrollt durch den Puls.
2. Sieht in einem Bereich ein Projekt mit roter Ampel, ablaufender Deadline-Bar **oder** unerfülltem Pflicht-Minimum.
3. Klickt → Detail-View zeigt: letztes Update vor X Tagen, X von Y Meilensteinen fällig, welche Pflicht-Punkte offen sind.
4. Schreibt direkt einen Kommentar/eine Nachfrage als allgemeines Update mit Bezug=Projekt.
### UJ-4 — Onboarding eines neuen Mitarbeiters
*Primär: Q1-Q5*
1. Mitarbeiter wird in Authentik angelegt → Cockpit-Zugriff automatisch via OIDC.
2. Erster Login: leerer „Mein Status"-Bereich + Hinweis „Hier schreibst du dein erstes Tagesupdate".
3. Default-Bereich aus Authentik-Group.
4. Schreibt das erste Update, erscheint ab sofort in Anwesenheits-/Streak-Liste.
### UJ-5 — Projekt anlegen (Cockpit-Sicht)
*Primär: P1*
1. Person legt in Outline den Steckbrief an (Leitfaden §3).
2. Wechselt ins Cockpit → „Neues Projekt anlegen" — füllt Cockpit-Sichtdaten: Name, Verantwortliche/r, Projektart (Kundenprojekt/Internes Projekt), Arbeitsmodus (Cycle/Kanban), **Steckbrief-URL** (Outline), **Plane-Board-URL**, grobe Meilensteine.
3. Cockpit zeigt das neue Projekt in seinem Bereich und im Puls. Die Pflicht-Minimum-Häkchen werden initial bewertet (Steckbrief vorhanden ✓, Verantwortliche/r ✓ usw.).
> [ASSUMPTION] Authentik-Groups werden für die initiale Bereichszuweisung genutzt. Mapping legen wir in der Architektur-Phase fest.
---
## 6. Funktionale Anforderungen (Features)
> Feature-Gruppen mit global durchnummerierten FR-IDs. Stabil für Architektur- und Story-Phase.
### 6.1 Feature-Gruppe A: Unternehmens-Puls (Startseite)
**Zweck:** Eine Seite, auf der jeder in unter 60s sieht, was los ist.
- **FR-1** Übersichtsleiste aller Bereiche (MVP: Entwicklung, Vertrieb, Marketing) inkl. aggregiertem Ampelstatus und Anzahl heute-fehlender Updates pro Bereich.
- **FR-1a** **Ampel-Aggregation pro Bereich:** *Rot*, wenn ≥1 Projekt rot ist; *Gelb*, wenn ≥1 Projekt gelb und kein rotes; *Grün*, wenn alle Projekte grün; *Grau*, wenn keine Projekte aktiv. *Pflicht-Minimum-Verstöße eines Projekts heben die Ampel mindestens auf Gelb.*
- **FR-2** Bereichsübergreifende Liste aller offenen Blocker (sortiert nach Alter).
- **FR-3** Heute-Quote: „X von Y Mitarbeitern haben heute gepingt" mit Klick → Liste der fehlenden Personen.
- **FR-4** Anzeige des nächsten Jourfixe-Termins (statisch konfigurierbar; Default Di 11:00).
- **FR-5** Pro Bereich werden folgende Key-Metrics angezeigt (verbindlicher Katalog für MVP):
- **Entwicklung:** Anzahl Projekte gesamt | im Plan (grün) | gefährdet (gelb) | blockiert (rot) | überzogene Meilensteine
- **Vertrieb:** Pipeline-Wert gewichtet (Σ value × probability) | offene Deals | gewonnene Deals (Quartal, # + EUR) | offene GTM-Schritte
- **Marketing:** Reichweite (laufende Periode) | aktive Kampagnen | Leads (Periode) | MQLs (Periode) | Übergaben an Vertrieb (Periode)
- **FR-6** Klick auf Bereichs-Kachel öffnet die Bereichs-Detail-View.
- **FR-7** **Pflicht-Minimum-Übersicht** im Puls: Liste aller aktiven Projekte mit roten/gelben Pflicht-Minimum-Punkten (siehe FR-38).
### 6.2 Feature-Gruppe B: Bereichs-Detail Entwicklung
- **FR-8** Liste aller aktiven Projekte der Entwicklung, gruppiert nach **Projektart** (Kundenprojekt / Internes Projekt — Leitfaden §1).
- **FR-9** Pro Projekt-Kachel: Name, Verantwortliche/r (P1), Ampelstatus (Grün/Gelb/Rot) + Trend (↗︎ besser / → gleich / ↘︎ schlechter), Deadline-Progress-Bar (verstrichene Zeit / Gesamtzeit), Anzahl offener Meilensteine, Pflicht-Minimum-Score (z. B. „5/6").
- **FR-10** Klick → Projekt-Detail mit:
- Kopfzeile: Name, Verantwortliche/r, Projektart, Arbeitsmodus (Cycle/Kanban), Ampel + Trend
- Pflicht-Minimum-Anzeige (FR-38)
- **Verlinkungen (Pflicht-Felder):** Outline-Steckbrief-URL, Plane-Board-URL, optional Risikoliste-URL
- Optional Definition of Done (Freitext, Cockpit-Sichtfeld)
- Meilenstein-Checkliste (Cockpit-Sicht — siehe FR-11)
- Update-Feed des Projekts
- **FR-11** Meilensteine werden im Cockpit als grobe Sicht gepflegt (Titel, Fälligkeit, Status: offen/erledigt/überzogen). *Wahrheit ist der Steckbrief in Outline;* Cockpit-Meilensteine sind eine reduzierte Sichtkopie. Doppelpflege wird akzeptiert, bis Phase-2-API-Sync eingeführt wird.
### 6.3 Feature-Gruppe C: Bereichs-Detail Vertrieb
- **FR-12** 5-Spalten-Kanban: Lead → Erstgespräch → Demo → Angebot → Abschluss. Drag-and-Drop zwischen Spalten.
- **FR-13** Deal-Karten zeigen Name, Verantwortlicher, Wert (€), **Stage-Wahrscheinlichkeit (probability, 0-100%)**, Tage in aktueller Stage.
- **FR-14** **Vertriebs-Header-KPIs:** Pipeline-Wert gewichtet (Σ value × probability), gewonnene Deals (# + EUR, laufendes Quartal), aktiver Deal-Count, durchschnittliche Stage-Verweildauer.
- **FR-15** Pro Produkt eine sortierbare GTM-Schritt-Liste (Schritt, Verantwortlicher, Status).
- **FR-16** Vertrieb-Update-Feed (Filter auf Bezug=Deal oder Bezug=Vertrieb-allgemein).
### 6.4 Feature-Gruppe D: Bereichs-Detail Marketing
- **FR-17** Horizontaler Lead-Funnel mit 5 Stufen (Reichweite → Website → Leads → MQL → Übergabe) und Conversion-Prozenten zwischen den Stufen.
- **FR-18** Liste aktiver Kampagnen mit Status (Aktiv / Pausiert / Beendet), Reichweite, Verantwortlicher.
- **FR-19** **Crosslink-Banner Marketing↔Vertrieb:** Sichtbarer Banner in Marketing & Vertrieb: „Marketing hat X Leads übergeben in [Zeitraum], Vertrieb hat daraus Y Deals erzeugt (Z €)."
- **FR-20** Marketing-Update-Feed (Filter auf Bezug=Kampagne oder Bezug=Marketing-allgemein).
### 6.5 Feature-Gruppe E: Tagesrhythmus (Einstempeln · ToDos · Reflexion)
> Ein „Tagesupdate" besteht aus zwei Touchpoints am gleichen Tag: **Morgen-Einstempeln mit ToDo-Liste** und **Abend-Reflexion**. Beides zusammen wird am nächsten Werktagsmorgen als konsolidierter Vortags-Eintrag für Kollegen sichtbar.
#### 6.5.1 Morgen — Einstempeln & ToDos
- **FR-21** „Einstempeln"-Aktion auf der Puls-Seite (großer Button bei nicht eingestempelten Personen): öffnet die ToDo-Eingabe für heute.
- **FR-22** **ToDo-Liste** für den aktuellen Werktag:
- 1-N ToDo-Items
- pro Item: Titel (max. 200 Zeichen), optionaler Bezug (Projekt/Deal/Kampagne/Allgemein), optionale Bezugs-ID
- keine Ampel/Trend pro ToDo
- **FR-23** Beim Einstempeln gilt der Tag als „begonnen" — Person verschwindet aus der „heute fehlt"-Liste auf dem Puls.
- **FR-24** **Sichtbarkeit innerhalb des laufenden Tages:**
- Kollegen sehen nur „Person hat sich eingestempelt (ja/nein)" — keine ToDo-Inhalte.
- Die Person selbst sieht ihre ToDo-Liste in „Mein heute".
- Projektverantwortliche/r eines bezogenen Projekts darf die ToDos sehen, falls Bezug auf das Projekt verweist (operative Notwendigkeit).
- **FR-25** ToDos sind während des Tages bis zur Reflexion editierbar (hinzufügen / löschen / ändern).
#### 6.5.2 Abend — Reflexion
- **FR-26** **Reflexions-Formular**: Person ruft ihre ToDo-Liste auf und füllt aus:
- Pro ToDo: ☑️ erledigt / ☐ offen / ✖️ verworfen (mit optionalem Grund)
- **Reflexions-Freitext** (max. 1.000 Zeichen) — was sonst noch passiert ist
- **Status-Ampel** (Leitfaden §5 Wortlaut):
- 🟢 Grün – im Plan
- 🟡 Gelb – spürbare Abweichung, **aber** Maßnahmenplan greift
- 🔴 Rot – Abweichung **ohne** gesicherten Plan, braucht jetzt eine Entscheidung
- **Trend** (Pflicht bei Bezug=Projekt; sonst optional): ↗︎ besser / → gleich / ↘︎ schlechter
- **Blocker** (optional Freitext, erzeugt Blocker-Eintrag — siehe FR-41ff.)
- **URL** (optional, z. B. Plane-Ticket oder Outline-Seite)
- **FR-27** Nach Speichern der Reflexion ist der Tag „voll abgeschlossen". Person zählt für die Streak als voller Tag.
- **FR-28** Reflexion ist nach 30 Minuten nicht mehr editierbar. Erinnerung optional („Du hast heute noch nicht reflektiert"-Hinweis ab z. B. 17:00 Uhr — keine externe Push-Notification im MVP).
#### 6.5.3 Konsolidierung & Sichtbarkeit
- **FR-29** Am nächsten Werktagsmorgen wird der konsolidierte Vortags-Eintrag publiziert: ToDos + Häkchen + Reflexions-Freitext + Ampel + Trend werden für Kollegen sichtbar im Bereichs-Feed und im jeweiligen Bezugs-Feed.
- **FR-30** Default-Sortierung in Feeds: neueste oben. Pagination: 50 Items pro Seite oder „Mehr laden".
- **FR-31** Eine Reflexion setzt Ampelfarbe und Trend des verlinkten Projekts/Deals/Kampagne — zuletzt eingetragener Wert gewinnt (mehrere Bezüge möglich → jeder Bezug erhält die Werte).
- **FR-32** **Rückwirkende Erfassung:** Bis zu **einem Werktag zurück** dürfen Einstempeln + Reflexion nachgetragen werden. Ältere Tage sind nicht mehr erfassbar (außer als „Abwesend" markiert, siehe FR-37).
- **FR-33** **Tages-Status pro Person** (sichtbar im eigenen Profil und im Puls für andere):
- 🟢 voll (Einstempeln + Reflexion) — zählt für Streak
- 🟡 halb (nur Einstempeln, keine Reflexion) — zählt nicht für Streak, aber Person ist „nicht durchgefallen"
- ⚪ nicht angefangen (kein Einstempeln) — fällt aus Streak
- 🟣 abwesend
### 6.6 Feature-Gruppe F: Anwesenheit / Update-Serie (Streak) / Disziplin
- **FR-34** Pro Person wird die aktuelle **Update-Serie** (Streak) — aufeinanderfolgende Werktage mit Tages-Status „voll" — angezeigt. „Halbe" Tage brechen die Streak nicht, zählen aber nicht zur Serie.
- **FR-35** „Heute fehlt"-Liste auf der Puls-Seite (siehe FR-3); klickbar, zeigt Personen ohne Einstempeln am aktuellen Werktag nach 11:00 und Personen mit „halbem" Tag-Status nach 17:00.
- **FR-36** Werktage-Definition: Mo-Fr (Feiertage Phase 2).
- **FR-37** Eine Person kann sich für einen Tag (oder Zeitraum) als „Abwesend" markieren (Urlaub / Krankheit). Diese Tage zählen nicht gegen die Streak.
### 6.7 Feature-Gruppe G: **Pflicht-Minimum-Check (Leitfaden §9)**
- **FR-38** Pro aktivem Projekt zeigt das Cockpit die 6 Pflicht-Punkte des Leitfadens als sichtbare Checkliste:
1. Projektsteckbrief in Outline freigegeben → erfüllt, wenn `outline_steckbrief_url` gepflegt **und** als „freigegeben" markiert ist
2. Projektverantwortliche/r gesetzt → erfüllt, wenn P1 zugewiesen
3. Plane-Board mit zerlegter Arbeit → erfüllt, wenn `plane_project_url` gepflegt
4. Meilensteine mit groben Terminen → erfüllt, wenn ≥1 Meilenstein definiert
5. Status-Ampel aktuell → erfüllt, wenn letzte Reflexion mit Bezug=Projekt ≤ 7 Werktage alt
6. Abschluss-Retrospektive — *nur bei beendeten Projekten relevant; aktiv = n/a*
- **FR-39** Im MVP werden Pflicht-Punkte 1, 2, 3, 4 als manuelle Bestätigungs-Felder pro Projekt geführt (Cockpit-Sicht, Doppelpflege akzeptiert). Punkt 5 wird automatisch aus dem Reflexions-Zeitstempel abgeleitet. Punkt 6 wird beim Abschluss-Workflow gesetzt.
- **FR-40** Projekte mit ≥1 unerfülltem Pflicht-Punkt erscheinen prominent auf der Puls-Seite (siehe FR-7).
### 6.8 Feature-Gruppe H: Blocker
- **FR-41** Ein Blocker entsteht entweder als Feld in einer Reflexion oder separat über „Neuer Blocker"-Button.
- **FR-42** Blocker tragen: Titel, Beschreibung, Verantwortlicher (zur Lösung), Ersteller, Bereich(e), Datum offen, Status (offen / gelöst).
- **FR-43** Blocker erscheinen auf der Puls-Seite, in den betroffenen Bereichs-Detail-Views und in der Projekt-Detail-View (falls projektgebunden).
- **FR-44** Auflösung: Auflöser, Auflöse-Datum und Auflösungs-Kommentar werden persistiert. **Nur die/der Projektverantwortliche (P1) des bezogenen Projekts darf einen Blocker als gelöst markieren.** Bei Blockern ohne Projektbezug darf die/der Ersteller/in oder Verantwortliche/r des Blockers selbst auflösen.
### 6.9 Feature-Gruppe I: Projekt-Abschluss
- **FR-45** Projektverantwortliche/r kann ein Projekt im Cockpit auf „abgeschlossen" setzen.
- **FR-46** Beim Abschluss prüft das Cockpit Pflicht-Minimum-Punkt 6 (Lessons-Learned-URL) — wenn fehlend, fordert es die URL an.
- **FR-47** Abgeschlossene Projekte verschwinden aus dem Puls, bleiben aber in einer „Archiv"-Sicht pro Bereich lesbar.
### 6.10 Feature-Gruppe J: Navigation, Suche & Filter
- **FR-48** Globale Top-Navigation: Puls / Entwicklung / Vertrieb / Marketing / „Mein heute" / Eigenes Profil / Archiv.
- **FR-49** Volltextsuche über Updates → Phase 2. Im MVP genügt Filterung im Feed nach Bereich/Bezug.
---
## 7. Nicht-funktionale Anforderungen (NFRs)
### 7.1 Performance
- **NFR-1** Puls-Seite lädt vollständig in < 1,5 s bei 6-15 aktiven Nutzern und 90 Tagen Historie.
- **NFR-2** Tagesupdate-Speichern dauert < 500 ms (Server-Roundtrip).
### 7.2 Verfügbarkeit
- **NFR-3** Zielverfügbarkeit Mo-Fr 8-19 Uhr: 99% (kein 24/7-SLA).
- **NFR-4** Geplante Wartungsfenster außerhalb der Kernzeit kommunizierbar.
### 7.3 Sicherheit & Privacy
- **NFR-5** Alle Routen sind hinter Authentik-OIDC abgesichert; kein anonymer Zugriff.
- **NFR-6** Keine PII von Externen (Deal-Karten enthalten nur intern bekannte Firmennamen, keine Klar-Personendaten).
- **NFR-7** Audit-Spur: jedes mutierende Event (Update, Blocker, Meilenstein-Statuswechsel, Pflicht-Minimum-Änderung) hat `actor`, `timestamp`, `payload`.
### 7.4 Bedienbarkeit & Visual
- **NFR-8** Brand-konform: primär weiß, sekundär schwarz, gemäß ToolCTRL Brand Identity. Konkrete Design-Tokens in der UX-Phase.
- **NFR-9** Tagesupdate-Schreib-Workflow ist in ≤ 3 Klicks ab Puls erreichbar.
- **NFR-10** Desktop-first; mobile-Responsiveness ist Phase 2 (mobil-lesbar ist Pflicht, mobil-schreibbar nicht).
- **NFR-11** Sprache: Deutsch durchgängig im UI. Keine i18n im MVP.
### 7.5 Voice & Tone (Mikrocopy)
- **NFR-12** Tonalität ist **locker, direkt, deutsch, kurze Imperative.** Beispiele aus dem Prototyp als Voice-Anker:
- „Stark."
- „Wie läuft es?"
- „Du hast heute noch nicht gepingt."
- „Willkommen, [Name]"
- **NFR-13** Keine Corporate-Sprech-Phrasen („wir möchten Sie darauf hinweisen", „im Zuge der Optimierung"). Keine englischen Floskeln im Fließtext der UI (Fachbegriffe wie „Backlog", „Kanban" sind okay — sie kommen aus dem Leitfaden).
### 7.6 Datenhaltung
- **NFR-14** Persistenz in **Postgres**. Kein localStorage-only.
- **NFR-15** Tägliches Backup (Mechanik in Architektur-Phase).
### 7.7 Erweiterbarkeit
- **NFR-16** Datenmodell muss Phase-2-Erweiterungen ohne mehr als eine zweistündige Migration tragen: zusätzliche Bereiche (Design, Buchhaltung), Plane/Outline-API-Sync, Kommentare auf Updates, Mobile, Feiertags-Kalender.
---
## 8. Out of Scope (MVP) / Phase-2-Kandidaten
| Item | Begründung |
|---|---|
| Plane-API-Sync (Ticket-Status, Owner, Titel ziehen; Meilensteine auto-erkennen) | Komplexität verzögert MVP; URL-Link reicht zunächst. |
| Outline-API-Sync (Steckbrief-Status „freigegeben" auto-erkennen) | Selbe Begründung; manuelle Bestätigung reicht. |
| Outline-Embed | Verlinkung genügt. |
| Design-Bereich, Buchhaltung-Bereich (eigene Detail-Views) | Beide nutzen „Allgemein" im MVP. |
| Mobile-App / Responsive Mobile-UI (schreibend) | Desktop deckt 95% der täglichen Nutzung ab; mobil bleibt lesbar. |
| Volltextsuche über Updates | Filter im Feed reicht im MVP. |
| Reaktionen / Kommentare auf Updates | Nice-to-have, kein Kernproblem. |
| Reminders per Element/Matrix/Mail bei fehlendem Update | Cockpit-Visualisierung zuerst beweisen, dann pushen. |
| Konfigurierbare Reporting-/BI-Dashboards | Andere Werkzeuge geeigneter. |
| Zeiterfassung | Kimai existiert. |
| Mehrsprachigkeit | Nicht nötig. |
| Feiertags-Kalender (für Streak-Berechnung) | MVP: Mo-Fr ohne Feiertags-Sonderfall. |
---
## 9. Offene Fragen
| ID | Frage | Wer entscheidet | Bis wann | Phase-Blocker? |
|---|---|---|---|---|
| OQ-1 | Genaues Mapping Authentik-Groups → Cockpit-Bereiche? | Architektur-Phase | Vor MVP-Bau | ja |
| OQ-2 | Welche konkreten Brand-Tokens (Hex, Typografie, Logo-Verwendung) gelten 1:1? | UX-Phase | Vor UI-Bau | ja |
| OQ-5 | Archivierte Projekte: Historie für immer behalten oder nach X Monaten löschen? | Architektur + Product | Vor MVP-Bau | nein |
| OQ-8 | Erinnerungszeit für „du hast noch nicht reflektiert"-Hinweis (Default 17:00)? | Product (Dejan) | Vor Story-Bau | nein |
| OQ-9 | Was passiert, wenn eine Person die ToDo-Liste morgens leer lässt — ist „eingestempelt mit 0 ToDos" gültig oder muss ≥1 ToDo gesetzt sein? PRD-Default: ≥1 ToDo Pflicht. | Product (Dejan) | Vor MVP-Bau | nein |
| OQ-10 | Darf eine Person ToDos *nach* dem Abend-Reflektieren noch nachtragen (z. B. „bin um 10 noch zur Apotheke gefahren")? PRD-Default: nein, nach Reflexion ist der Tag geschlossen. | Product (Dejan) | Vor Story-Bau | nein |
**Beschlossene OQs (vorher offen, jetzt im PRD verankert):**
- ~~OQ-3~~ → Rückwirkende Erfassung **max 1 Werktag zurück** (FR-32).
- ~~OQ-4~~ → Nur P1 (oder Blocker-Ersteller/Verantwortlicher bei projektlosen Blockern) darf auflösen (FR-44).
- ~~OQ-6~~ → Pflicht-Minimum-Punkt 1 als manuelle Cockpit-Checkbox im MVP (FR-39).
- ~~OQ-7~~ → Trend **Pflicht bei Bezug=Projekt**, sonst optional (FR-26).
---
## 10. Annahmen-Index
Sammlung aller im PRD getroffenen Annahmen — zur expliziten Bestätigung im Architektur- bzw. UX-Lauf:
- **A-1** Authentik-OIDC-Integration ist technisch in < 1 Tag erledigt (Erfahrung aus Plane/Outline-Setup).
- **A-2** Authentik-Groups werden für die initiale Bereichszuweisung genutzt; Mapping wird in der Architektur-Phase festgelegt.
- **A-3** Team wächst in den nächsten 12 Monaten nicht über 30 Personen.
- **A-4** Postgres + ein leichter Backend-Layer reichen aus; konkrete Backend-Wahl in Architektur-Phase.
- **A-5** Pflicht-Minimum-Punkte 1-4 werden im MVP manuell im Cockpit bestätigt (kein Outline-/Plane-API-Sync).
- **A-6** Doppelpflege Cockpit-Meilenstein ↔ Outline-Steckbrief ist im MVP akzeptiert; wird beobachtet (CM4).
---
## 11. Risiken
| Risiko | Mitigation |
|---|---|
| Das Cockpit wird selbst zu einem „starten und abbrechen"-Projekt. | MVP-Scope hart klein halten. Erstes Release in 4 Wochen anpeilen. Selbst-Dogfooding ab Tag 1. **Das Cockpit selbst muss das Pflicht-Minimum erfüllen.** |
| Daily-Update-Pflicht erzeugt Lästergefühl. | CM2/CM3 messen; 4-Wochen-Retro; Mechanik justierbar. |
| Cockpit erodiert die Tool-Aufteilung des Leitfadens (wird Schatten-Plane oder Schatten-Outline). | A1-A5 + CM4 wachen darüber. Architektur-Phase prüft den Schnitt explizit. |
| Sales-Daten sind sensibel und sollen evtl. nicht jeder sehen. | MVP: flaches Vertrauen, alle sehen alles. Bei Bedarf in Phase 2 Rollen-Sichtbarkeit. |
| Pflicht-Minimum-Häkchen werden „durchgewinkt" und verlieren Bedeutung. | Jourfixe diskutiert Stichproben. Phase 2: Outline-/Plane-API-Validierung. |
---
## 12. Erfolgskontrolle & Review-Punkte
- **Woche 2 nach Launch** – kurzer Puls-Check im Jourfixe: M1 erreicht?
- **Woche 4 nach Launch** – Retro: CM2/CM3/CM4. Adjust falls nötig.
- **Woche 8 nach Launch** – Vollauswertung gegen M1-M6. Entscheidung: Phase 2 starten oder MVP weiter härten?
---
*Ende PRD. Begleitend: `addendum.md` (technische Tiefe, verworfene Alternativen, Leitfaden-Bezüge, In-Depth-Personas) und `.decision-log.md` (Audit-Trail).*
@@ -0,0 +1,248 @@
# Reconciliation: HTML-Prototyp ↔ PRD + Addendum
Quelle (Prototyp): `inputs/unternehmens-cockpit.html` (verdichtet in `extract-prototype-html.md`)
Geprüft gegen: `prd.md` + `addendum.md`
Datum: 2026-05-25
---
## Vollständig abgedeckt
### Hauptstruktur & Navigation
- **Cockpit als Default-View** → PRD §1.1 (Tabelle), §6.1 (Feature-Gruppe A: Puls als Startseite), FR-32 (Top-Nav inkl. "Puls")
- **Tagesupdate als zweite Top-Level-View** → FR-32 ("Neues Tagesupdate" in Top-Nav), FR-19 (Formular), UJ-1
- **Drei Areas: Entwicklung / Vertrieb / Marketing** → PRD §6.1 (FR-1), §6.2/§6.3/§6.4 (eigene Feature-Gruppen), Addendum §6 (Vokabular)
- **Live-Datum im Header** → implizit via "Heute"-Logik (FR-3, FR-25)
### Unternehmens-Puls
- **Aggregierter Ampelstatus pro Area** → FR-1
- **Bereichsübergreifend offene Blocker** → FR-2, FR-30, Risiko-Modell §10
- **Heute-Quote ("X von Y haben gepingt")** → FR-3
- **Jourfixe-Hinweis (Di 11:00)** → FR-4, Vision G4, Addendum §6
- **Key-Metrics pro Area-Tile (3-5 Metrics)** → FR-5
- **Klick auf Area-Tile öffnet Detail-View** → FR-6
### Entwicklung
- **Projekt-Liste mit Gruppierung Umsatz/Intern** → FR-7
- **Status-Pill je Projekt (abgeleitet aus letztem Update)** → FR-8, FR-23
- **Deadline/Meilenstein-Progress** → FR-8, FR-10
- **Projekt-Detail mit Meilenstein-Checkliste + Update-Feed** → FR-9
- **Meilensteine mit Titel/Fälligkeit/Status** → FR-10
- **CRUD für Projekte/Meilensteine** → FR-11 (Prototyp hatte nur Read; PRD erweitert bewusst)
### Vertrieb
- **5-Spalten Kanban (Lead → Erstgespräch → Demo → Angebot → Abschluss)** → FR-12, Addendum §6 (Deal-Stage)
- **Deal-Karten mit Name/Owner/Wert** → FR-13
- **GTM-Schritt-Liste pro Produkt** → FR-14, Addendum §6
- **Vertrieb-Update-Feed** → FR-15
- **Crosslink zu Marketing (Lead-Übergabe)** → implizit über FR-30 (Blocker bereichsübergreifend) + UJ-2, allerdings nicht prominent (siehe Gaps)
### Marketing
- **Lead-Funnel mit 5 Stufen + Conversion-Raten** → FR-16, Addendum §6
- **Kampagnen-Liste mit Status** → FR-17
- **Marketing-Update-Feed** → FR-18
### Tagesupdates
- **Formular mit Person/Bereich/Bezug/Freitext/Ampel/Blocker** → FR-19
- **Bezug = Projekt/Deal/Kampagne/Allgemein** → FR-19, Addendum §6
- **Chronologische Anzeige in Area- und Bezug-Feed** → FR-21
- **Update setzt Ampelfarbe des Referenzobjekts** → FR-23
- **Optionale URL als Zusatz-Bezug** → FR-22 (Erweiterung gegenüber Prototyp)
- **Edit-Fenster (Prototyp: kein Edit)** → FR-20 (PRD lockert bewusst auf 30min)
- **Validierung (Pflichtfelder)** → implizit in FR-19
### Disziplin / Streak
- **Streak-Counter pro Person/Projekt** → FR-24, Vokabular im Addendum
- **"Heute fehlt"-Liste / Side-Nudge** → FR-3, FR-25
- **Werktage-Definition** → FR-26
- **Abwesenheit / Urlaub-Marker** → FR-27 (Prototyp hatte das nicht — PRD erweitert)
### Blocker
- **Blocker als Feld in Update** → FR-28
- **Bereichsübergreifend sichtbar** → FR-2, FR-30
- **Standalone-Blocker-Erstellung** → FR-28 (PRD erweitert gegenüber Prototyp)
- **Blocker mit Owner/Status/Auflösung** → FR-29, FR-31
### Status-System (Ampel)
- **3 Status: Im Plan / Gefährdet / Blockiert** → FR-19, Addendum §6
- **green/amber/red Mapping** → Datenmodell-Skizze in Addendum §1.2
- **Status-Rollup auf Area** → FR-1 (aggregierter Status)
### Identität
- **Personen mit Name/Rolle** → PRD §4.1 (Rollen-Tabelle), Datenmodell User in Addendum §1.2
- **OIDC-Auth (Prototyp hatte keine Auth — bewusste Erweiterung)** → PRD §4.2, NFR-5, Addendum §1.3
### Datenmodell (impliziert)
- **TEAM/AREA/PROJECT/DEAL/CAMPAIGN/UPDATE/MILESTONE/STAGE/FUNNEL/GTM** → Addendum §1.2 mit allen wesentlichen Entitäten
---
## Bewusst weggelassen (mit Begründung im PRD)
- **localStorage-only Persistenz** → Addendum §2.5 (explizit verworfen, Postgres stattdessen, NFR-12)
- **Single-User-Modell ohne Auth** → PRD §4.2 (Authentik-OIDC), Addendum §2.5
- **Intro/Splash-Screen ("Willkommen, Haris")** → nicht in MVP-Scope; nicht explizit als Anti-Ziel formuliert, aber als kosmetische Prototypen-Animation klar nicht produktiv (vgl. Addendum §5: Prototyp = Logik-Referenz, kein Visual-Referenz)
- **"Demodaten zurücksetzen"-Button** → reines Prototypen-Feature; im MVP gegen echte Daten irrelevant
- **TODAY hartcodiert** → trivial weggelassen, MVP nutzt echtes Datum
- **Hardcoded Produkte (HiveGrid/Asset Control/etc.)** → werden zu echten DB-Datensätzen (Datenmodell Project/GtmStep)
- **Off-Brand Visuals (Indigo, Navy, Hellgrau)** → Addendum §5 explizit als Off-Brand-Risiko markiert; NFR-8 verlangt Brand-Konformität
- **Detail-Views für Design/Buchhaltung** → Addendum §2.6 (verworfen, weil keine KPIs definiert); PRD §8 Phase 2
- **Volltextsuche** → PRD §8, FR-33 als Phase 2
- **Mobile/Responsive** → PRD §8, NFR-10 (desktop-first)
- **Reaktionen/Kommentare auf Updates** → PRD §8
- **Plane-API-Sync / Outline-Embed** → PRD §8, Addendum §1.4 als Phase 2
- **Reminders per Mail/Element** → PRD §8
- **BI/Reporting-Dashboards** → PRD Anti-Ziel A6, §8
- **Zeiterfassung** → PRD Anti-Ziel A4
---
## Gaps (potenziell vergessen — Empfehlung: aufnehmen oder bewusst rejizieren)
### Mittlere Priorität
1. **Won-Deals als KPI im Vertriebs-Header**
- Prototyp: KPI-Tile "Gewonnen (# + EUR)" in Vertriebs-Bereichsstats.
- PRD: nicht erwähnt. FR-13 zeigt nur Deal-Karten, kein Won-Aggregat.
- Empfehlung: als KPI aufnehmen oder explizit rejizieren (z. B. "Won-Deals werden nicht separat geführt; abgeschlossene Deals verschwinden aus Kanban").
2. **Gewichteter Pipeline-Wert (Σ value × Stage-Wahrscheinlichkeit)**
- Prototyp: explizite KPI mit Wahrscheinlichkeiten pro Stage (0.10/0.25/0.50/0.75/0.90).
- PRD: nicht erwähnt; Stage hat keine Wahrscheinlichkeit im Datenmodell.
- Empfehlung: in FR-12/13 ergänzen oder als Phase 2 vermerken. Konsequenz für Datenmodell: Stage braucht `probability`-Feld.
3. **Crosslink-Banner Marketing↔Vertrieb (Lead-Übergabe)**
- Prototyp: prominenter Crosslink "X Leads hat Marketing diesen Monat in die Pipeline übergeben" auf Vertriebs- und Marketings-Detail-View.
- PRD: implizit via Funnel-letzte-Stufe (FR-16), aber kein expliziter Cross-Area-Link als FR.
- Empfehlung: als FR aufnehmen — der Crosslink ist eine sichtbare Verbindung, die "Areas reden miteinander" konkret macht. Vibe-relevant.
4. **Streak nicht nur pro Person, sondern pro Projekt ("Update-Serie")**
- Prototyp: Projekt-Detail zeigt eigenen Streak-Counter "Tage am Stück mit Update".
- PRD: FR-24 nennt Streak nur pro Person.
- Empfehlung: in FR-24 erweitern oder als separates FR ergänzen — Projekt-Streak ist ein gutes Versandungs-Frühwarnsystem (deckt sich mit Addendum §4 Indikator 2: "Median-Update-Alter pro Projekt").
5. **Bereichs-Stats-Kacheln in Detail-Views (4er-Grid mit "Projekte/Im Plan/Meilensteine/Updates heute")**
- Prototyp: jede Area-Detail-View hat 4 Stats-Tiles als sekundärer Übersichtskopf.
- PRD: nicht spezifiziert, nur einzeln in FR-5 (Puls). Detail-Views haben keine Stat-Tile-Anforderung.
- Empfehlung: in FR-7/12/16 als "Bereichskopf mit 4 Kennzahlen" ergänzen — sonst gehen Detail-Views in Wireframes verloren.
### Niedrigere Priorität (qualitativ / UX-Feinheiten)
6. **Due-Date-Färbung (warn = amber ≤4 Tage, over = rot überfällig)**
- Prototyp: konkretes UI-Pattern in Projekt-Rail und Meilensteinen.
- PRD: NFR-/FR-Ebene zu generisch — keine explizite Färblogik.
- Empfehlung: in UX-Phase festhalten; nicht zwingend im PRD, aber sollte als "Pattern" im Addendum §6 oder UX-Brief landen.
7. **Avatar mit Initialen + Personen-Farbe als wiederkehrendes UI-Atom**
- Prototyp: starkes Identitäts-Element, durchgängig in Feed-Items.
- PRD: nicht erwähnt — nur "Person" als Feld.
- Empfehlung: UX-Phase-Detail; jeder User braucht `color` im Datenmodell.
8. **"Bereichsübergreifend offen"-Chip-Liste als Puls-Element**
- Prototyp: explizit rechte Foot-Sektion des Puls.
- PRD: FR-2 nennt nur "Liste"; das Pattern als rote Chips mit Klick→Area ist nicht spezifiziert.
- Empfehlung: in FR-2 spezifizieren oder UX-Phase.
9. **Kampagnen-Status "pausiert"**
- Prototyp: 5 Status (läuft / geplant / ausgewertet / blockiert / pausiert).
- PRD/Addendum: FR-17 nennt nur 3 (Aktiv / Pausiert / Beendet); Addendum Datenmodell hat (active|paused|done).
- Empfehlung: Status-Set vereinheitlichen. Entweder Prototyp-Set übernehmen (granularer) oder explizit auf 3 Status reduzieren.
10. **Toast-Komponente für Bestätigungs-Feedback**
- Prototyp: explizit nach Update-Submit.
- PRD: nicht erwähnt.
- Empfehlung: UX-Phase; trivial, sollte aber bewusst entschieden sein.
11. **Side-Nudge "Der Rhythmus"-Erklärtext im Tagesupdate-Formular**
- Prototyp: rechte Spalte im Formular mit Erklärtext zum Rhythmus + Jourfixe.
- PRD: nicht spezifiziert; PRD-Tonalität ist eher knapp.
- Empfehlung: bewusst entscheiden — passt zur "Disziplin"-Vision und macht den Sinn des Updates explizit (gut für Onboarding, UJ-4).
### Konzeptionelle Lücken
12. **Vibes/Onboarding-Splash "Welt der Übersicht"**
- Prototyp: Splash-Screen mit personalisierter Begrüßung ("Willkommen, Haris").
- PRD: UJ-4 nennt nur "leerer Mein-Status-Bereich + Hinweis".
- Empfehlung: Tonalität (locker, persönlich, motivierend) bewusst entscheiden — wenn das Cockpit "Disziplin ohne Druck" sein soll, könnte ein personalisierter Empfang Wirkung haben. Im PRD nicht erwähnt.
13. **Kein expliziter "Tonalität / Voice"-Abschnitt im PRD**
- Prototyp-Texte sind eher locker-geschäftlich ("Alle Tagesupdates sind da. Stark.", "läuft nach Plan", "brauche Hilfe").
- PRD spricht nüchtern in Capability-Sprache; keine Voice-/Tonalitäts-Vorgabe.
- Empfehlung: kurzen "Voice & Tone"-Abschnitt im UX-Brief — siehe eigener Abschnitt unten.
---
## Vokabular-Konsistenz
| Begriff | Prototyp | PRD | Addendum | Status |
|---|---|---|---|---|
| **Puls / Unternehmens-Puls** | ✓ Hauptbegriff | ✓ FR-1..5 | ✓ §6 | konsistent |
| **Areas / Bereich** | ✓ beide | ✓ "Area" und "Bereich" gemischt | ✓ "Area / Bereich" | konsistent (Doppelbegriff) |
| **Ampel** | ✓ | ✓ ("Ampelstatus") | ✓ | konsistent |
| **Im Plan / Gefährdet / Blockiert** | ✓ | ✓ FR-19 | ✓ §6 | konsistent |
| **Bezug** | ✓ | ✓ FR-19 | ✓ §6 | konsistent |
| **Streak / Update-Serie** | ✓ beide nebeneinander | ✓ "Streak" (FR-24) | ✓ "Streak" | **Inkonsistenz:** "Update-Serie" als Synonym fehlt im PRD/Addendum |
| **Jourfixe** | ✓ | ✓ FR-4, G4 | ✓ §6 | konsistent |
| **Meilenstein** | ✓ | ✓ FR-8, FR-10 | ✓ §6 | konsistent |
| **Deal-Stage / Stages** | ✓ | ✓ FR-12 | ✓ "Deal-Stage" §6 | konsistent |
| **GTM / GTM-Schritt** | ✓ "GTM" und "Vertriebsfortschritt je Produkt" | ✓ FR-14 ("GTM-Schritt-Liste") | ✓ §6 | konsistent (PRD nutzt "GTM-Schritt" sauber) |
| **Lead-Funnel** | ✓ | ✓ FR-16 | ✓ §6 | konsistent |
| **MQL** | ✓ | ✓ FR-16 | ✓ §6 | konsistent |
| **Blocker** | ✓ | ✓ Feature-Gruppe G | ✓ §6 | konsistent |
| **Bereichsübergreifend offen** | ✓ | (sinngemäß FR-2 "bereichsübergreifende Liste") | – | **Begriff fehlt wörtlich** — sollte im PRD/Addendum als Label übernommen werden |
| **Crosslink** | ✓ | – | – | **Begriff fehlt** (Konzept ist nur implizit) |
| **Umsatz / Intern (Projekt-Gruppen)** | ✓ | ✓ FR-7 | ✓ §1.2 (`group umsatz|intern`) | konsistent |
| **Pipeline-Wert / Gewichteter Wert** | ✓ beide | – | – | **fehlt** (siehe Gap #2) |
| **Reichweite** | ✓ | ✓ FR-16 | – | konsistent |
| **Übergabe / An Vertrieb übergeben** | ✓ | ✓ FR-16 ("Übergabe") | ✓ "Übergabe" §6 | konsistent |
| **Kampagne / Kampagnen-Status** | ✓ 5 Status | ✓ 3 Status (FR-17) | ✓ 3 Status | **Inkonsistenz im Status-Set** (siehe Gap #9) |
| **Pflichtenheft** | ✓ Prototyp-Kontext | – | – | nicht übernommen; vermutlich projektspezifisch, kein Produkt-Vokabular |
| **Demodaten** | ✓ | – | – | bewusst weggelassen (Prototyp-Spezifikum) |
### Empfehlungen Vokabular
- "Bereichsübergreifend offen" als Label in FR-2 verankern.
- "Update-Serie" als deutscher Synonym-Begriff zu "Streak" im Addendum §6 ergänzen — der Prototyp nutzt beide nebeneinander.
- "Crosslink" als Konzept im Addendum §6 aufnehmen, falls Gap #3 angenommen wird.
- Kampagnen-Status-Set zwischen Prototyp (5) und Addendum (3) auflösen.
---
## Tonalität / Vibe
### Beobachtungen Prototyp
Der Prototyp hat einen sehr spezifischen Voice-Charakter:
- **Locker-geschäftlich:** "Alle Tagesupdates sind da. Stark." | "Aktuell keine offenen Blocker." | "Stark." als Erfolgs-Quittung.
- **Persönlich-direkt:** "Willkommen, Haris" | "Was hast du heute geschafft?" | "Wie läuft es?" | "Etwas, das dich blockiert oder wobei du Hilfe brauchst?"
- **Konkret statt abstrakt:** "in 3 Tagen", "heute fällig", "2 Tage überfällig" — Mensch-lesbare Datumsangaben statt ISO-Strings.
- **Bilder/Metaphern:** "Puls", "Welt der Übersicht" (im Header-Konzept), "Rhythmus" (Side-Nudge im Formular), "Areas" (englisch eingedeutscht).
- **Kein Corporate-Sprech:** keine "KPIs", keine "Stakeholder", keine "Initiativen" im UI-Copy.
### Was PRD/Addendum vom Vibe einfangen
- PRD hat in §1 selbst einen klaren "Voice": *"kein Tool-Problem, sondern ein Durchziehen-Problem"* | *"niemand sich tagelang wegducken kann"* | Vision §1.2 ist sehr direkt. **Das ist gut.**
- Anti-Ziele §2.2 sind in derselben direkten Tonalität gehalten.
- Persona-Beschreibungen in Addendum §3 fangen Tonalität ("Lästert intern", "will stolz zeigen") und Persönlichkeit gut ein.
### Was verloren geht
- **"Welt der Übersicht"** als emotionaler Header-Claim ist nicht im PRD prominent. Wenn dieser Anspruch ("Übersicht als Welt, in der man sich aufhält") tragend sein soll, gehört er in §1.2 (Vision) oder als eigener Abschnitt "Voice & Tone".
- **Personalisierter Empfang ("Willkommen, [Name]")** ist im Onboarding (UJ-4) zu nüchtern beschrieben — verliert den "Mensch wird begrüßt"-Vibe.
- **Konkrete Mensch-Sprache in UI-Copy** (z. B. "Wie läuft es?", "brauche Hilfe", "Stark.") ist nirgends als Voice-Anweisung dokumentiert. Risiko: ein Implementations-Team baut "Status auswählen" statt "Wie läuft es?" — funktional richtig, vibe-mäßig tot.
- **Rhythmus-Erklärtext** (Side-Nudge "Der Rhythmus") als pädagogisches Element ist nicht spezifiziert — das erzählt User aber, warum tägliches Update überhaupt Sinn macht.
- **Lockere Quittungen** ("Stark.", "Alle Tagesupdates sind da.") sind FR-frei und drohen, im Bau durch "Update erfolgreich gespeichert." ersetzt zu werden.
### Empfehlung
Ein expliziter Abschnitt **"Voice & Tone"** in PRD oder Addendum mit:
- Beispiel-Copy aus Prototyp als Referenz ("Stark.", "Wie läuft es?", "Willkommen, [Name]").
- Verbot von Corporate-Sprech in UI (keine "KPIs", keine "Submit"-Buttons — stattdessen "Tagesupdate absenden").
- Mensch-lesbare Datumsangaben Pflicht ("in 3 Tagen", nicht "2026-05-28").
- Personalisierung mit Vornamen wo sinnvoll.
- Header-Claim "Welt der Übersicht" oder vergleichbar als emotionale Klammer entscheiden (übernehmen oder bewusst rejizieren).
---
## Zusammenfassung
- **Vollständig abgedeckt:** ~50 Items (alle Haupt-Features, Datenmodell-Entitäten, Hauptbegriffe).
- **Bewusst weggelassen mit Begründung:** ~13 Items (Prototyp-Spezifika, Phase-2-Kandidaten, Off-Brand-Visuals, Anti-Ziele).
- **Gaps zur Diskussion:** 13 Items — davon 5 mittlere Priorität (Won-Deals, gewichteter Pipeline-Wert, Crosslinks, Projekt-Streak, Detail-View-Stats) und 8 UX-/Vibe-Lücken.
- **Vokabular:** überwiegend konsistent; vier kleinere Lücken (Update-Serie, Bereichsübergreifend offen, Crosslink, Kampagnen-Status-Set).
- **Tonalität:** PRD hat eigenen guten Voice für Strategie/Anti-Ziele, aber UI-Copy-Tonalität des Prototyps ("Stark.", "Wie läuft es?", personalisierter Empfang) ist nirgends als Voice-Anweisung dokumentiert. Empfehlung: "Voice & Tone"-Abschnitt nachziehen.
@@ -0,0 +1,128 @@
# PRD Quality Review — ToolCTRL Unternehmens-Cockpit
## Overall verdict
**Good.** Das PRD ist ein angemessen kalibriertes Internal-Tool-Dokument mit klarer These ("Durchziehen-Problem, nicht Tool-Problem"), ehrlichem Scope (3 Areas statt 5, harte Anti-Ziele, sauber gepflegte Out-of-Scope-Tabelle) und einem zur Stake-Höhe passenden Detaillierungsgrad. Es trägt die Entscheidungen sichtbar (Daily-Update-Pflicht, flaches Vertrauensmodell, Authentik-OIDC) und benennt die wirklichen Risiken — insbesondere das Meta-Risiko, dass das Cockpit selbst versanden könnte. Was am ehesten wackelt, ist die Done-ness-Schärfe einzelner FRs (mehrere Adjektive ohne Bound) und ein paar leise Vagheiten in den NFRs; downstream lassen sich die Stories dennoch sauber ableiten.
## Decision-readiness — strong
Das PRD legt seine Entscheidungen offen statt sie zu verstecken. § 1.1 nennt das Kernproblem in einem Satz ohne Hedge ("kein Tool-Problem, sondern ein Durchziehen-Problem"). § 4.1 deklariert das flache Vertrauensmodell ("Alle Rollen sehen alles") und § 10 bestätigt, dass diese Entscheidung bewusst gegen Sales-Sensibilität gewählt wurde — das ist gelebter Trade-off, nicht Smoothing. § 2.2 listet sechs Anti-Ziele, jedes mit Reibung zu einer plausiblen Alternative (Ticketsystem, Zeiterfassung, BI). § 3.2 führt drei Gegenmetriken, davon zwei (CM2/CM3) explizit qualitativ — mit `[NOTE FOR PM]`-Verankerung der 4-Wochen-Retro. § 9 enthält fünf echte Open Questions mit benannten Entscheidern und Fristen ("Vor MVP-Bau"), nicht rhetorisch.
Was die Decision-readiness vom Status `strong` hält statt sie zu trüben: Die Top-Entscheidung "Daily-Update-Pflicht mit öffentlicher fehlt-heute-Liste" wird zwar in § 1.1 und FR-3 begründet, aber der explizite Trade-off — Disziplin-Mechanik gegen das Risiko Lästergefühl — taucht erst verteilt in CM3 und § 10 Risiko 2 auf. Ein einziger Decision-Block "Wir haben uns für Pflicht-mit-Öffentlichkeitsdruck entschieden, obwohl …" würde das Bild schärfer machen, ist aber für ein Internal-Tool dieses Stake-Levels Kosmetik.
### Findings
- **low** Trade-off zur Pflicht-Mechanik liegt verstreut (§ 1.1, § 3.2 CM3, § 10) — *Fix:* Optional einen 3-Zeilen-Decision-Block "Pflicht vs. freiwillig" in § 1.1 oder § 2 mit explizitem Was-wir-aufgeben.
## Substance over theater — strong
Das PRD verzichtet konsequent auf Theater-Bausteine. Es gibt **keine** Differenzierungs-/Innovations-Sektion (richtig, weil intern). Die Personas in § 4.1 sind funktional (eine Tabelle, eine Spalte "Hauptnutzen") und werden in FRs und UJs auch tatsächlich gebraucht — UJ-1 lebt vom Entwickler, UJ-2 vom PM, UJ-4 vom Onboarding. Die In-Depth-Personas (Addendum § 3) sind in den Addendum verschoben, wo sie für die UX-Phase Sinn ergeben, ohne das Hauptdokument zu verstopfen.
Die Vision in § 1.2 ist produktspezifisch ("in unter 60 Sekunden", "niemand sich tagelang wegducken kann") — sie ließe sich nicht ohne Edit in ein anderes Cockpit-PRD übertragen. § 1.3 "Warum jetzt" liefert konkrete Vergangenheit (Jira/Confluence aufgegeben, "schneller ohne PM" gescheitert), keine generischen Plattitüden.
Die NFRs in § 7 sind teilweise hart (NFR-1 ≤ 1,5 s bei 6-15 Nutzern und 90 Tagen Historie; NFR-2 ≤ 500 ms; NFR-9 ≤ 3 Klicks; CM1 ≤ 60 s Median), teilweise weicher (NFR-3 99% in Mo-Fr 8-19 — passend für Internal; NFR-13 "tägliches Backup, Mechanik in Architektur-Phase"). NFR-8 "Brand-konform … konkrete Tokens in UX-Phase" ist ehrlich vertagt und nicht Theater.
### Findings
*(Keine. Substance-Bar wird für Internal-Stake-Level vollständig getragen.)*
## Strategic coherence — strong
Das PRD hat eine sichtbare These: **Versandung ist ein Sichtbarkeits-/Disziplin-Problem, kein Werkzeug-Problem.** Diese These zieht sich durch:
- § 1.1 framt das Problem
- § 2 G1 ("≥1 Tagesupdate pro Werktag") und G2 ("Ampelstatus jederzeit erkennbar") operationalisieren die These
- FR-1/FR-3 (Puls + Heute-Quote), FR-19 (Update-Formular), FR-24/FR-25 (Streak + Fehlt-Liste), FR-2 (Blocker-Liste) sind allesamt direkte Hebel auf die These
- M1-M3 messen genau die These, nicht Vanity (M1 Update-Quote, M2 Blocker-Time-to-Visible, M3 Ampel-Frische); M4-M5 messen Verankerung im Arbeitsalltag
- Die drei Mechanik-Indikatoren im Addendum § 4 (Update-Quote, Median-Update-Alter, Overdue-Meilensteine) sind die operative Übersetzung der These — selten so sauber.
Die Feature-Priorisierung folgt der These: 3 Detail-Areas statt 5, weil Buchhaltung/Design keine etablierten KPIs haben (Addendum § 2.6); Plane/Outline-API-Sync bewusst Phase 2 (§ 8) — weil API-Komfort die These nicht stützt, Sichtbarkeit aber sofort braucht. Gegenmetriken (§ 3.2) existieren und benennen explizit, dass die Pflicht-Mechanik kippen kann.
MVP-Scope-Kind ist "problem-solving" (nicht Plattform, nicht Revenue), und die Scope-Logik passt: Drei Bereiche, drei Mechaniken, eine Startseite. Kein Backlog mit Section-Headern.
### Findings
*(Keine substanziellen.)*
## Done-ness clarity — adequate
Mehrheit der FRs trägt mindestens eine testbare Konsequenz. Gute Beispiele:
- FR-3: "X von Y Mitarbeitern haben heute gepingt" mit klickbarer Fehlt-Liste — eindeutig testbar
- FR-12: "5-Spalten Kanban: Lead → Erstgespräch → Demo → Angebot → Abschluss. Drag-and-Drop" — Stages explizit
- FR-16: 5 Funnel-Stufen explizit benannt
- FR-20: "unveränderbar nach 30 Minuten" — präzise Schwelle
- FR-25: "Werktag, nach 11:00" — Bedingung scharf
Wo die Schärfe wackelt:
- **FR-1** "aggregiertem Ampelstatus" — die Aggregations-Regel ist undefiniert. Wird die Area-Ampel aus dem schlechtesten Projekt abgeleitet, aus der Mehrheit, gewichtet? Engineering muss raten oder rückfragen.
- **FR-8** "Ampelstatus (abgeleitet aus letztem Update)" + **FR-23** "zuletzt gewinnt" — die Regel ist hier definiert (zuletzt gewinnt), aber FR-1's Area-Aggregation bleibt offen.
- **FR-5** "3-5 Key-Metrics pro Area (z. B. offene Deals, aktive Kampagnen, Projekte im Plan vs. gefährdet)" — die "z. B."-Beispiele sind keine Spezifikation. Welche Metriken konkret pro Area? Das verlässt das PRD ohne Festlegung.
- **FR-11** "schreibbar durch jede Person, Audit via `created_by` / `updated_by`" — keine Acceptance dafür, was bei Lösch-/Archiv-Operationen passiert (OQ-5 deckt nur den Archiv-Sub-Fall ab).
- **FR-21** "chronologisch angezeigt" — neueste oben oder älteste oben? Pagination? Limit?
- **NFR-1** Bound präzise; **NFR-3** "99% in Mo-Fr 8-19" angemessen für Internal; **NFR-9** "≤ 3 Klicks" scharf — aber **NFR-8** "Brand-konform" ohne Tokens und **NFR-14** "ohne Migrations-Drama" ist Adjektiv, kein Bound. Für NFR-14 wäre eine Schema-Migration-Strategie-Erwartung ("additive Migrationen, keine Daten-Drops in Phase 2") konkreter.
- **OQ-4** (Wer darf Blocker als gelöst markieren?) sollte vor MVP-Bau beantwortet sein — sonst fehlt FR-31 die Berechtigungsregel.
Insgesamt ist Done-ness ausreichend für ein Internal-Tool dieser Größe, aber die Story-Phase wird an FR-1 (Area-Aggregation), FR-5 (Key-Metrics-Katalog) und FR-21 (Sort-/Pagination) rückfragen.
### Findings
- **medium** FR-1 Area-Ampel-Aggregationsregel fehlt (§ 6.1) — *Fix:* "Worst-of aller Projekt-Ampeln in der Area" oder Alternative festlegen; einen Satz hinzufügen.
- **medium** FR-5 Key-Metrics nur als "z. B."-Beispiele (§ 6.1) — *Fix:* Pro Area die 3-5 Metriken namentlich aufzählen, oder explizit als `[ASSUMPTION]` markieren und in OQ aufnehmen.
- **low** FR-21 Sortierung/Pagination der Feeds undefiniert (§ 6.5) — *Fix:* "Neueste zuerst, Pagination ≥ N Einträge" oder Äquivalent.
- **low** NFR-14 "ohne Migrations-Drama" ist Adjektiv (§ 7.6) — *Fix:* Konkretisieren als "Phase-2-Felder additiv migrierbar; kein Daten-Drop nötig".
- **low** OQ-4 (Blocker-Auflöser-Berechtigung) muss vor Story-Bau zu FR-31 gespiegelt werden (§ 9, § 6.7) — *Fix:* Antwort früh holen; danach FR-31 ergänzen.
## Scope honesty — strong
Sehr saubere Scope-Disziplin. § 2.2 nennt sechs Anti-Ziele jeweils mit der Alternative, gegen die abgegrenzt wird. § 8 ist eine zehnzeilige Out-of-Scope-Tabelle mit Begründung pro Item — kein "TBD", jeder Eintrag trägt eine Erklärung ("URL-Link reicht zunächst", "Visualisierung zuerst beweisen, dann pushen"). FR-26 (Feiertage), FR-33 (Volltextsuche) und NFR-10 (Mobile) sind explizit als Phase 2 markiert, nicht stillschweigend ausgelassen.
`[ASSUMPTION]`-Tags sind sparsam, aber an den richtigen Stellen: UJ-4 (Authentik-Group-Mapping), FR-33 (Phase 2-Charakter), § 10 (drei Top-Annahmen zu OIDC-Aufwand, Team-Größe, Backend-Stack). `[NOTE FOR PM]` in § 3.2 verankert die 4-Wochen-Retro für CM2/CM3.
§ 9 hat fünf Open Questions mit Entscheider und Frist. Open-Items-Density: ca. 5 OQs + 4 `[ASSUMPTION]` + 1 `[NOTE FOR PM]` = 10 Open-Items bei einem MVP-PRD mit 33 FRs und 14 NFRs — angemessen für ein 4-Wochen-MVP, das in die Architektur-Phase übergeht. Keines der Items blockiert den Green-Light-Beschluss zum Bau; OQ-3 und OQ-4 sollten allerdings vor Story-Erstellung beantwortet sein.
### Findings
- **low** Assumptions-Index fehlt am Ende des PRD (§ 11 endet ohne Annex) — *Fix:* Optional einen "Annahmen-Index"-Block am Ende, der die vier `[ASSUMPTION]`-Inlines auflistet. Für Internal-Stake nicht zwingend.
## Downstream usability — adequate
Glossar ist im Addendum § 6 vorhanden und sauber gepflegt (12 Begriffe: Puls, Area, Tagesupdate, Bezug, Ampel, Blocker, Streak, Jourfixe, Meilenstein, Deal-Stage, GTM-Schritt, Lead-Funnel). Die Begriffe werden im PRD konsistent verwendet — "Puls" durchgängig in § 6.1, FR-2, FR-25; "Area" durchgängig statt Mischung mit "Bereich" (Achtung: "Bereich" wird parallel benutzt, siehe Mechanical Notes).
FR-IDs sind kontinuierlich (FR-1 bis FR-33), keine Lücken, keine Duplikate. NFRs analog (NFR-1 bis NFR-14). UJs (UJ-1 bis UJ-4), Metriken (M1-M5), Gegenmetriken (CM1-CM3), Anti-Ziele (A1-A6), Open Questions (OQ-1 bis OQ-5) — alle ID-Räume sauber, lokal benannt, eindeutig.
UJs nennen Personas implizit ("PM", "Owner", "Mitarbeiter", "Person") — nicht via exakter Label aus § 4.1. UJ-2 sagt "PM" (§ 4.1 hat "Product / PM"), UJ-3 sagt "PM oder Owner" (§ 4.1 hat "Geschäftsführung / Owner"). Das ist keine Katastrophe, aber Downstream-Story-Erstellung wird einmal nachschauen müssen. Für ein Internal-Tool akzeptabel; bei chain-top-Status mit UX-Phase davor wäre eine Persona-Label-Säuberung nützlich.
Cross-References funktionieren: FR-3 referenziert klar FR-25, FR-2 spiegelt zu FR-30. Sektionen sind extrahierbar — z. B. § 6.5 (Tagesupdates) kann allein gelesen werden, ohne "siehe oben"-Verweise.
Das PRD ist explizit Chain-Top — § 0 sagt "Tech-Entscheidungen leben im begleitenden `addendum.md` und in der späteren Architektur-Phase", § 11 plant Phase-2-Review. Downstream-Usability ist damit eine relevante Dimension.
### Findings
- **medium** UJ-Persona-Linkage nicht via exakter § 4.1-Label (§ 5) — *Fix:* "PM" → "Product / PM"; "PM oder Owner" → "Product / PM oder Geschäftsführung / Owner". Klein, aber spart Reibung.
- **low** "Bereich" und "Area" werden parallel genutzt (z. B. FR-19 "Bereich", Addendum § 6 nennt beide als Synonym) — *Fix:* Glossar-Eintrag explizit als gleichwertig erklären (ist faktisch schon dort), oder konsequent eine Form wählen. Aktuell tolerierbar.
## Shape fit — strong
Das PRD ist als **Internal-Tool / Multi-Operator-Kapazitäts-Spec** geformt, was zum Produkt passt (6-15 Nutzer, kein Verkauf, mehrere Rollen mit unterschiedlichen Workflows). Personas und UJs sind vorhanden und tragen — sie sind nicht overhead, denn das Cockpit hat echte Rollendifferenz (Sales braucht Kanban, Dev braucht Meilensteine, GF liest). SMs sind operational/Quoten-basiert (M1 Update-Quote, M3 Ampel-Frische), nicht User-Acquisition — passt zum Internal-Stake.
Was richtig dosiert ist:
- Keine Differentiator-Sektion (brauchts nicht — kein Verkauf)
- Keine Investor-Pitch-Schicht
- Keine GTM-Strategie
- Personas knapp tabellarisch + In-Depth ins Addendum verschoben
- NFR-Tiefe auf Internal-Verfügbarkeit (Mo-Fr 8-19, 99%) statt 24/7-SLA
- Brand/UX explizit auf UX-Phase verschoben
Was nicht over-formalized ist: Keine UJ-Inflation (vier UJs decken die Hauptpfade — Update schreiben, Jourfixe, Verfall-Detect, Onboarding — und das genügt für ein Cockpit). Keine 10-Persona-Parade.
Was nicht under-formalized ist: Das PRD hat eine Vision, eine These, Erfolgsmetriken, Gegenmetriken, Open Questions, Risiken — alles vorhanden, ohne aufgebläht zu wirken.
### Findings
*(Keine.)*
## Mechanical notes
- **Glossar-Drift:** "Bereich" und "Area" werden synonym verwendet; das Glossar (Addendum § 6) führt sie als gleichwertig ("Area / Bereich"). Konsistent, aber nicht ganz sauber — z. B. FR-19 nutzt "Bereich", FR-1 nutzt "Areas" im Plural. Tolerierbar.
- **ID-Kontinuität:** FR-1 bis FR-33 lückenlos. NFR-1 bis NFR-14 lückenlos. UJ-1 bis UJ-4, M1-M5, CM1-CM3, A1-A6, G1-G5, OQ-1 bis OQ-5 — alle sauber. Keine Duplikate.
- **Cross-References:** FR-3 ↔ FR-25 konsistent. FR-2 ↔ FR-30 konsistent. § 9 OQ-3/OQ-4 referenzieren implizit FR-19/FR-31 — sollte vor Story-Bau gespiegelt werden.
- **Assumptions-Index-Roundtrip:** Vier inline `[ASSUMPTION]`-Tags (UJ-4, FR-33, § 10 dreifach). Kein konsolidierter Index am Ende. Für Internal-Stake nicht zwingend, aber bei Chain-Top-Lesung praktisch.
- **UJ-Persona-Linkage:** UJs nennen Rollen nicht via exakter § 4.1-Label — siehe Downstream-Findings.
- **Erforderliche Sektionen:** Vision, Ziele, Anti-Ziele, SMs, CMs, Personas, UJs, FRs, NFRs, Out-of-Scope, Open Questions, Risks/Assumptions, Review-Punkte — alle vorhanden. Glossar ins Addendum verlagert (akzeptabel; man könnte einen einzeiligen Verweis aus dem PRD ins Addendum-Glossar setzen).
- **Addendum-Aufgabentrennung:** Tech-Skizze, verworfene Alternativen, In-Depth-Personas, Mechanik-Detail, Off-Brand-Risiken, Glossar — sauber im Addendum, hält das PRD lesbar. Das ist eine starke Trennung.
@@ -0,0 +1,789 @@
---
stepsCompleted: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
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/unternehmens-cockpit.html
- prds/prd-projectmanagement-2026-05-25/inputs/ToolCTRL_Projektmanagement-Leitfaden_v3.0.pdf
- prds/prd-projectmanagement-2026-05-25/extract-prototype-html.md
- architecture.md
workflowType: 'ux-design'
project_name: 'ToolCTRL Unternehmens-Cockpit'
user_name: 'Dejancosic'
date: '2026-05-25'
binding_frame: 'ToolCTRL Projektmanagement-Leitfaden v3.0 (Mai 2026)'
prd_status: 'final'
architecture_status: 'complete'
---
# UX Design Specification — ToolCTRL Unternehmens-Cockpit
**Author:** Dejancosic
**Date:** 2026-05-25
> Dieses Dokument legt das Visual Design, die Komponenten-Bibliothek, die Layouts und die Mikrocopy fest. Es operationalisiert die UX-Anforderungen aus PRD (NFR-8, NFR-9, NFR-12/13) und liefert die Brand-Token-Werte (OQ-2), die in `tailwind.config.ts` einfließen.
---
<!-- UX-Content wird Schritt für Schritt angehängt -->
---
## Executive Summary
### Project Vision
Das **ToolCTRL Unternehmens-Cockpit** ist ein internes Web-Werkzeug, das jeden Werktag in unter 60 Sekunden sichtbar macht, woran das Team arbeitet, was klemmt und welche Projekte aus dem Plan laufen. Es operationalisiert den Projektmanagement-Leitfaden v3.0 als drittes Werkzeug neben Plane (Arbeit) und Outline (Gedächtnis) und ist die Antwort auf das wiederkehrende „Wir starten viel, wir bekommen nichts fertig"-Muster.
UX-Aufgabe: Eine Anwendung schaffen, die sich **unaufgeregt, lesbar und schnell** anfühlt — kein „Tool, das einen checkt", sondern ein gemeinsamer Spiegel des Tages. Die Anwendung muss Disziplin erzeugen, ohne Lästergefühl zu produzieren (PRD CM3).
### Target Users
Alle ToolCTRL-Mitarbeiter (6-15 Personen), aufgeteilt in zwei Sichtebenen:
**Leitfaden-Rollen (pro Projekt):**
- **P1 Projektverantwortliche/r** — pflegt Ampelstatus und Trend, moderiert Jourfixe, achtet auf Pflicht-Minimum.
- **P2 Team** — schreibt Tagesupdates, setzt Blocker, pflegt Meilenstein-Status.
- **P3 Product Owner** — Lese-Schwerpunkt, Priorisierungs-Diskussionen.
- **P4 Auftraggeber** (intern = Geschäftsführung) — Lese-Hauptnutzer für Puls und Pflicht-Minimum.
**Querschnittsrollen (alle Mitarbeiter):**
- **Q1 Entwickler** — Updates mit Projektbezug, viele Bezugswechsel pro Tag, möchte unter 30 s pro Touchpoint.
- **Q2 Sales** — lebt im Kanban, verschiebt 5–10 Deals pro Woche, schreibt Updates ungern wenn das System träge ist.
- **Q3 Marketing** — pflegt Lead-Funnel + Kampagnen, weniger täglicher Fortschritt → das System darf „heute kein Fortschritt, aber dran"-Updates nicht verteufeln.
- **Q4 Design / Q5 Buchhaltung** — schreiben „Allgemein"-Updates, brauchen keine Projektpflicht.
Stand der Nutzer-Reife: hoch tech-affin (alle haben ein Authentik-SSO-Login bereits gewohnheitsmäßig), Desktop-First-Nutzer, Deutsch als Arbeitssprache.
### Key Design Challenges
1. **Zwei Touchpoints pro Tag, beide unter 60 Sekunden.** Morgen-Einstempeln + ToDos und Abend-Reflexion müssen *sehr* schnell zu erledigen sein — sonst erodiert die Disziplinmechanik (CM1a/CM1b).
2. **Sichtbarkeitsmodell pre/post Publikation.** Innerhalb des Tages: ToDos sind privat, nur „eingestempelt: ja/nein" für Kollegen sichtbar. Ab nächstem Werktagsmorgen: konsolidierter Eintrag wird öffentlich. Die UI muss diese Privatheit ohne Erklärungstext verständlich machen.
3. **Pflicht-Minimum als Erstklass-Information.** Die 6 Leitfaden-Pflicht-Punkte pro Projekt müssen prominent und auf einen Blick lesbar sein — sie sind die Brücke zwischen Cockpit und Leitfaden, nicht eine Detailseite.
4. **Drei strukturell unterschiedliche Bereichs-Details.** Entwicklung (Projekte+Meilensteine), Vertrieb (Kanban+gewichtete Pipeline), Marketing (Lead-Funnel+Kampagnen) sehen visuell anders aus, gehören aber in dasselbe Layout-Skelett — das verlangt eine konsistente Schale mit feature-spezifischem Inhalt.
5. **Drei Status-Ampelfarben in einer monochrom-geprägten Brand.** Brand ist schwarz/weiß; Ampel braucht Grün/Gelb/Rot semantisch. Sie müssen funktional erkennbar bleiben, ohne die Brand-Sprache zu sprengen.
6. **Voice & Tone als Disziplin-Anker.** Locker und direkt (`„Stark."`, `„Wie läuft es?"`) — UI-Mikrocopy ist Pflicht-Vehikel, kein Beiwerk.
### Design Opportunities
1. **Ein einzelner „Puls"-Bildschirm als tägliche Heimat.** Wenn der Puls in einem Blick die Antwort auf „Was muss ich heute wissen?" liefert, gewinnt das Cockpit täglich. Konsequente Information-Density ohne Overload.
2. **„Mein heute" als persönliche Tagesoase.** Eine ruhige, private Sicht, die der einzige Ort ist, wo ToDos heute bearbeitbar sind — visuell von der öffentlichen Puls-Sicht trennen.
3. **Reflexion als geführter, kurzer Flow.** Statt eines Formulars: eine schrittweise Reflexion (ToDos abhaken → Freitext → Ampel + Trend → fertig), die sich wie ein Tagesabschluss anfühlt, nicht wie ein Pflicht-Formular.
4. **Markenmäßige Reduktion als Feature.** Brand ist monochrom — das passt zu „weniger ablenken, mehr Übersicht". Statt das als Limit zu sehen, ist es ein Wettbewerbsvorteil gegenüber bunten SaaS-Tools.
5. **Der `^`-Caret als wiederkehrender Mikro-Anker.** Das macOS-Ctrl-Symbol als kleines Brand-Element (im Logo, in Tab-Indikatoren, in Bestätigungs-Toasts) zieht eine subtile Identitätslinie durch die App.
6. **Sichtbar gemachte Streaks als sozialer Motivator.** Wer 14 Werktage in Folge eingestempelt ist, sieht das prominent im Profil und im „Heute fehlt"-Modul. Sozialer Druck ohne Public Shaming.
---
## Core User Experience
### Defining Experience
Die Kern-Erfahrung des Cockpits ist der **Tagesrhythmus**: Eine Person stempelt morgens ein und definiert ihre ToDos für den Tag; am Abend reflektiert sie, hakt ab, setzt Ampel und Trend; am nächsten Werktagsmorgen sehen Kollegen den konsolidierten Vortags-Eintrag. Diese Schleife wiederholt sich jeden Werktag und ist der zentrale Wertbeitrag des Produkts. Alles andere — Puls, Bereichs-Details, Pflicht-Minimum, Blocker — ist die Auswertungs- und Übersichts-Schicht über dieser täglichen Routine.
Der zweite zentrale Moment ist der **Jourfixe-Puls am Dienstag 11:00**: Die Übersichtsseite wird vom PM (P1/P4) als Hauptbildschirm aufgerufen, alle drei Bereiche werden in einer scroll-bereiten Sicht durchgegangen, rote/gelbe Projekte und offene Blocker werden direkt angesprochen.
### Platform Strategy
- **Plattform:** Web-Anwendung (SPA), self-hosted unter `cockpit.tool-ctrl.de`.
- **Primärer Input:** Maus + Tastatur. Touch-Geräte sind nicht primär, müssen aber das Tagesupdate-Schreiben nicht blockieren.
- **Bildschirmgröße:** Desktop-first (≥ 1280 px breit). Mobile/Tablet: lesbar (responsive Layout-Grid), nicht primär schreibbar (PRD NFR-10).
- **Offline:** keine Offline-Fähigkeit im MVP. Cockpit ist immer online (interne Infrastruktur).
- **Geräte-Capabilities:** kein Spezial-Hardware, kein Push, keine Kamera. Der Browser ist die einzige Bühne.
- **Browser-Support:** moderne Evergreen-Browser (Chrome, Firefox, Safari der letzten 2 Major-Versionen). Kein IE11.
### Effortless Interactions
Diese Dinge müssen sich **mühelos** anfühlen — wenn hier Reibung entsteht, scheitert das ganze Produkt:
- **Einstempeln** — Person klickt einen großen CTA auf der Puls-Seite, Modal öffnet sich mit Bereichs-Default und leerer ToDo-Liste. Tippen → Enter → nächster ToDo → Enter. Speichern mit Cmd/Ctrl+S oder Enter auf leerer Zeile. Ziel: ≤ 30 Sekunden für 3-5 ToDos.
- **ToDo abhaken in der Reflexion** — Single-Click auf die Checkbox neben jedem ToDo. Optionaler Verwerf-Modus (Rechtsklick oder Hover-Button) für „verworfen mit Grund".
- **Ampel + Trend setzen** — drei große farbige Buttons (Grün/Gelb/Rot), nicht ein winziges Dropdown. Trend als drei Pfeil-Buttons (↗︎ → ↘︎).
- **Blocker erfassen** — entweder inline beim Reflektieren (Toggle „Blocker?") oder über ein „+ Neuer Blocker"-Button im Header, der von jeder Seite aus erreichbar ist.
- **Projekt wechseln** — Bereichs-Detail hat eine Projekt-Rail oben mit Klick-zum-Wechsel; Projekt-Detail hat „← Bereich" als Breadcrumb.
- **Login** — automatischer Authentik-Redirect; nach erfolgreichem Login landet die Person auf der Puls-Seite. Kein Login-Formular im Cockpit selbst.
### Critical Success Moments
1. **Morgen-Eintritt:** Person öffnet das Cockpit (oder folgt einem Reminder-Link). Die Puls-Seite zeigt prominent oben „Du hast heute noch nicht gepingt." mit einem großen primären Button. Die Person ist in 3 Klicks im Einstempeln-Flow. **Failure-Mode:** Diesen Hinweis übersehen oder das Formular träge bedienen — die Streak wird gefährdet.
2. **Reflexions-Moment am Abend:** Ab 17:00 Uhr erscheint im Header ein dezenter Hinweis „Du hast heute noch nicht reflektiert." Die Person klickt, sieht die eigene ToDo-Liste vom Morgen wieder, hakt ab, schreibt 2–3 Sätze, setzt Ampel. **Failure-Mode:** Die Reflexion fühlt sich wie ein Bürokratie-Akt an — dann wird sie ausgelassen und der Tag bleibt „halb".
3. **Jourfixe-Aufruf am Dienstag:** PM öffnet Puls, scrollt einmal von oben nach unten, hat in 60 Sekunden Übersicht über alle Bereiche, Blocker und Pflicht-Minimum-Verstöße. **Failure-Mode:** Wenn Informationen verstreut auf mehreren Tabs liegen, wird die Jourfixe ineffizient.
4. **Nächster-Morgen-Reveal:** Person öffnet das Cockpit am Tag X+1, sieht im Area-Feed die konsolidierten Vortags-Einträge der Kollegen — ToDos, abgehakte Status, Reflexion, Ampel. Das schafft das Gefühl gemeinsamer Sichtbarkeit. **Failure-Mode:** Wenn die Publikation um 00:05 fehlschlägt (Cron-Job), erscheint der Tag X+1 leer — Vertrauen ins Cockpit erodiert.
5. **Pflicht-Minimum-Erstmal-Sichtbar-Werden:** Bei Projekt-Neuanlage wird in der Projekt-Detail-Sicht die 6-Punkte-Checkliste angezeigt; offene Punkte sind sofort visuell als „nicht erfüllt" markiert. **Failure-Mode:** Wenn die Checkliste versteckt in einem Tab liegt, vergessen Verantwortliche, sie zu pflegen — der zentrale Leitfaden-Hebel verschwindet.
### Experience Principles
1. **Disziplin durch Sichtbarkeit, nicht durch Reibung.** Das Cockpit erzwingt nichts mit roten Bannern oder Pflicht-Pop-ups. Es macht nur sichtbar, wer und welches Projekt fehlt. Sichtbarkeit ist der Hebel, nicht Druck.
2. **Privat tagsüber, öffentlich rückblickend.** „Mein heute" gehört der Person; der konsolidierte Vortagseintrag gehört dem Team. Diese Zeit-Asymmetrie ist UI-konstitutiv.
3. **Monochrome Ruhe, semantische Klarheit.** Weiß und Schwarz dominieren. Farbe (Grün/Gelb/Rot) trägt ausschließlich Bedeutung — niemals Dekoration.
4. **Drei-Klick-Maximum für Schreib-Touchpoints.** Einstempeln, Reflexion, Neuer Blocker sind von der Puls-Seite in ≤ 3 Klicks erreichbar (PRD NFR-9).
5. **Voice statt Decoration.** „Stark.", „Wie läuft es?", „Du hast heute noch nicht gepingt." als Mikrocopy-Anker. Kein Corporate-Sprech, keine Emojis im UI-Text, keine englischen Floskeln.
6. **Die Wahrheit lebt woanders.** Steckbrief in Outline, Issues in Plane, Tickets nicht im Cockpit. Verlinkungen sind eine UI-Erstklass-Komponente, nicht ein technischer Hinweis.
7. **Geschwindigkeit ist ein Feature.** Puls < 1,5 s, Update-Speichern < 500 ms. Performance-Wahrnehmung ist Teil des UX-Versprechens — träge wirkt wie respektlos gegenüber der Zeit der Mitarbeiter.
---
## Desired Emotional Response
### Primary Emotional Goals
Das Cockpit soll vier Gefühle gleichzeitig erzeugen:
- **Ruhe.** Beim Öffnen kein Lärm, keine bunten Banner, keine Notifications-Flut. Der erste visuelle Eindruck ist *aufgeräumt*.
- **Selbstwirksamkeit.** Nach dem Einstempeln das Gefühl: „Ich weiß, was ich heute will." Nach der Reflexion: „Ich weiß, was ich geschafft habe." Kein Pflichtgefühl, sondern stiller Stolz.
- **Mitsehen-Können.** Beim Puls-Scrollen: „Ah, so läuft's bei den anderen — gut, dass ich das ohne Fragen sehe." Geteilte Sichtbarkeit ohne Überwachungsgefühl.
- **Frühwarn-Vertrauen.** Wenn ein Projekt rot wird, vertraut man dem Cockpit, dass es das schon Tage vorher angedeutet hätte (gelb → rot Trend). Das Cockpit ist ein verlässlicher Frühwarn-Spiegel, kein Schock-Geber.
Was das Cockpit **nicht** erzeugen darf:
- ❌ **Lästergefühl** — das „fehlt heute"-Modul darf sich nicht wie ein Pranger anfühlen (PRD CM3).
- ❌ **Hektik** — keine roten Banner, keine Pulsing-Animationen, keine Pop-ups.
- ❌ **Bürokratie** — Reflexion soll wie ein Tagesabschluss-Ritual wirken, nicht wie ein Stundenzettel.
- ❌ **Spielerei** — keine Konfetti-Animationen für Streaks, keine Gamification-Badges. Subtil bleiben.
### Emotional Journey Mapping
Eine typische Werktag-Emotion einer Person (Q1 Entwickler oder Q2 Sales):
| Moment | Erwartete Emotion | UI-Antwort |
|---|---|---|
| **Login morgens** | Neugierig, leicht angespannt | Klarer Begrüßungs-Header, „Wie läuft es?" sanft, kein Druck |
| **„Du hast heute noch nicht gepingt"** | Erinnerung statt Anklage | Großer freundlicher CTA, keine roten Akzente |
| **ToDos schreiben** | Fokussierung | Minimalistisches Formular, eine Spalte, monospaced-Optionen |
| **Tagsüber im Cockpit reinschauen** | Schnelles Vergewissern | Puls in unter 60 s gelesen, Streak-Badge sichtbar, kein Drill-Down nötig |
| **Reflexion abends** | Ritualisches Abschließen | Geführter Flow: Checkboxen → Freitext → Ampel → fertig. Bestätigung mit „Stark." |
| **Morgens X+1: Vortags-Reveal** | Soziale Verbundenheit | Konsolidierte Vortageseinträge im Feed, ruhig präsentiert |
| **Wenn ein Projekt gelb wird** | Vorsichtige Wachsamkeit | Subtile Gelb-Akzente, klare Erläuterung („Maßnahmenplan greift") |
| **Wenn ein Projekt rot wird** | Handlungsbereitschaft | Klarer roter Akzent, Hinweis „braucht jetzt eine Entscheidung", direkter Link zum Projekt-Detail |
| **Wenn man die Streak verliert** | Verständnis, keine Strafe | Streak-Counter geht auf 0 ohne Drama; vorheriger Höchstwert wird im Profil als „bester Lauf" gespeichert |
### Micro-Emotions
Diese kleinen Momente machen den Unterschied zwischen „funktional" und „liebenswert":
- **„Stark."-Bestätigungs-Toast** nach erfolgreicher Reflexion. Drei Buchstaben, schwarzer Pill, 2 Sekunden sichtbar.
- **Tagesplan-Echo:** Beim Reflektieren werden die ToDos vom Morgen mit einer leichten Animation eingeblendet — die Person sieht das eigene morgendliche Ich wieder.
- **Subtile Ladekontrolle:** Skeleton-Boxen statt Spinner. Wartezeit fühlt sich wie „die App ist schon fast da" an, nicht wie „die App hängt".
- **Streak-Tag im Profil:** Eine schlichte Zahl mit dem `^`-Caret als Suffix. „14 ^". Keine Trophäen.
- **Reflexions-Erinnerung:** Ab 17:00 erscheint im Header ein dezenter Hinweis-Pill „Reflexion offen". Klick → direkt im Flow. Kein Notification-Lärm.
- **Vortags-Reveal:** Beim Laden des Feeds am Morgen X+1 erscheinen die neuen konsolidierten Einträge mit einer kurzen Fade-In-Animation (≤ 200 ms). Markiert visuell „diese sind neu seit gestern".
### Design Implications
Die emotionalen Ziele übersetzen sich in folgende UX-Pflichten:
- **Weißraum großzügig.** Cards mit viel Padding, klare vertikale Rhythmen — keine Information-Density um jeden Preis.
- **Typografie als Hauptkommunikator.** Hierarchie über Schriftgrade und Gewichte, nicht über Farben.
- **Animationen sparsam und schnell.** Höchstens 200 ms, ease-out, niemals länger oder bouncy. Animation als Bestätigung, nicht als Show.
- **Status-Farben nur funktional.** Grün/Gelb/Rot ausschließlich bei Ampel/Trend/Blocker. Keine farbigen Decorative-Akzente sonst.
- **Mikrocopy konsequent deutsch und kurz.** Keine englischen Floskeln, keine Höflichkeitsfloskeln („Bitte beachten Sie…").
- **Kein Drama bei Fehlern.** Fehler-Toasts dezent, in der unteren rechten Ecke, schwarz mit weißem Text.
### Emotional Design Principles
1. **Subtil schlägt laut.** Animationen, Farbakzente und Mikrocopy sind unaufdringlich; nichts ringt um Aufmerksamkeit.
2. **Ritual statt Pflicht.** Reflexion fühlt sich wie ein Abschluss-Ritual an, nicht wie ein Formular.
3. **Sichtbarkeit ist freundlich, nicht überwachend.** „Heute fehlt"-Anzeige als Information, nicht als Anklage.
4. **Geschwindigkeit ist Respekt.** Schnell laden, schnell reagieren — das vermittelt: Wir respektieren deine Zeit.
5. **Stille Anerkennung.** Streaks, „Stark."-Toasts, „bester Lauf"-Memory: kleine Anerkennungen, keine Trophäen-Show.
---
## UX Pattern Analysis & Inspiration
### Inspiring Products Analysis
Vier Produkte dienen als Pattern-Referenzen für das Cockpit. Wir kopieren nicht ihren Look, sondern lernen *welche UX-Muster sich für unsere Probleme bewährt haben*.
**Linear (linear.app)** — Issue-Tracker für Tech-Teams
- **Was übernehmen:** Aufgeräumte, monochrome Listen-Ansichten; Tastatur-Shortcuts als Erstklass-Interaktion; sehr schnelle Mutations-Feedback-Schleifen; Status-Icons als kleine Quadrate statt Text.
- **Was nicht:** Linear ist Ticket-Tool — wir sind explizit *kein* Ticket-Tool (PRD Anti-Ziel A1).
**Linear's Sub-Issue-System** — Mini-Hierarchie für Tagesplan
- Übertragbar als Inspiration für die ToDo-Liste: minimalistisches Inline-Editing, Tippen → Enter → nächste Zeile.
**Notion** (notion.so) — Doku & Wissens-Tool
- **Was übernehmen:** Klare Typografie-Hierarchie; reduzierte Farbpalette; Embed-/Verlinkungs-Patterns für externe Tools.
- **Was nicht:** Wir sind kein Doku-Tool (Outline-Job). Keine Notion-typischen Floating-Elements oder Slash-Commands.
**GitHub Projects (Roadmap-Sicht)** — Multi-Projekt-Übersicht
- **Was übernehmen:** Tabellen-Sicht über viele Items mit klar erkennbarem Status, Filter-Persistenz, Spalten-Sortierung.
- **Was nicht:** GitHub-typische Density (für uns zu viel auf einen Blick) und die Bunt-Tags-Kultur.
**Apple HIG (Human Interface Guidelines)** — System-Design-Sprache
- **Was übernehmen:** Klare Modal-Patterns, Toast-Position, Form-Stil, San-Serif Lesbarkeit, generöser Weißraum.
**Plane (plane.so)** — das parallel laufende Schwester-Tool
- **Was übernehmen:** UX-Konsistenz bei Iconografie und Statusfarben, damit der Wechsel Plane ↔ Cockpit nicht mental zerreisst.
- **Was nicht:** Plane's volle Issue-Listen — die spiegeln wir nicht (Anti-Ziel A5).
### Transferable UX Patterns
| Pattern | Quelle | Anwendung im Cockpit |
|---|---|---|
| **Inline-Add-on-Enter** | Linear, Notion | ToDo-Liste: Enter erzeugt neue ToDo-Zeile; Cmd+Enter speichert die ganze Liste. |
| **Tastatur-Shortcuts mit `?`-Help** | Linear, GitHub | `g p` → Puls, `g h` → Mein heute, `g a` → Archiv. `?` öffnet Cheatsheet. |
| **Optimistic Updates mit zarter Bestätigung** | Linear | Beim Abhaken eines ToDos: sofortige UI-Reaktion + leichtes Häkchen-Fade-In; Rollback bei API-Fehler. |
| **Skeleton-Cards** | Linear, Vercel | Statt Spinner: graue Card-Skelette mit identischem Layout wie die Echtdaten. |
| **Hover-Reveal-Actions** | Linear | Sekundäre Aktionen (Edit, Verwerfen, Löschen) erst beim Hover/Focus sichtbar. Reduziert visuelle Last. |
| **Linkurations-Cards** | Notion, Slack-Unfurls | Beim Verlinken einer Plane-URL erscheint im Update eine kleine Card mit Titel und Status (Phase 2 mit API; im MVP: nur URL). |
| **Sticky-Sub-Header** | GitHub Projects | Bei langer Bereichs-Detail-View bleibt der Bereichs-Header oben sticky, damit man immer weiß wo man ist. |
| **Status-Pill mit Trend-Pfeil** | GitHub PR-Status | Ampel-Pill mit anhängendem Trend-Pfeil als kompaktes Statussignal an Projekt-Karten. |
| **Profil-Streak-Stat** | Duolingo (abstrahiert) | Streak als schlichte Zahl im Profil, ohne Streak-Recovery-Käufe oder Drama. |
### Anti-Patterns to Avoid
Diese Patterns sind im Cockpit **verboten**, weil sie den Experience Principles widersprechen:
- ❌ **Roter Mahn-Banner oben.** Statt „Du hast heute noch nicht eingestempelt"-Banner: ein freundlicher CTA mit `^`-Caret und schwarzem Pill-Button. Niemals Alarmrot.
- ❌ **Notification-Badges mit Zahlen.** Wir zeigen Status, nicht Posteingänge. Keine roten Zähler.
- ❌ **Gamification-Trophäen.** Keine Badges, keine Levels, keine Konfetti-Animationen. Streak ist genug.
- ❌ **Pflicht-Pop-up beim Login** („Bitte stempel zuerst ein"). Stattdessen ist der Einstempel-CTA Teil der Puls-Seite, nicht ein Modal.
- ❌ **Bunte Status-Tags** in Marketing-/Vertrieb-Stil. Status-Farben bleiben monochrom + 3 Ampel-Töne.
- ❌ **Komplexe Filter-Sidebar**. Im MVP keine Multi-Facet-Filter. Wenn der Feed lang wird: Bereich-Filter + „nur meine" reicht.
- ❌ **Dichte Tabellen mit 10+ Spalten.** Karten/Lists, keine Excel-Sheets.
- ❌ **Pulsing-/Spin-Animationen** auf der Puls-Seite. Animation nur bei expliziter User-Aktion.
- ❌ **Auto-Refresh**. Daten werden bei Page-Navigation und manuellem Pull-to-Refresh-Equivalent neu geholt. Kein Background-Polling außer am User-aktiven Tab.
- ❌ **Drag-and-Drop überall**. Drag-and-Drop nur im Sales-Kanban (FR-12), nicht für ToDos oder Meilensteine.
### Design Inspiration Strategy
- **Atomare Inspiration, nicht Holistik.** Wir kopieren keine ganzen App-Looks; wir übernehmen einzelne Patterns (Inline-Add, Skeleton, Sticky-Sub-Header) und kombinieren sie mit der ToolCTRL-Brand.
- **Brand schlägt Pattern.** Wo ein Pattern (z. B. Notion-Slash-Commands) gegen die Brand-Reduktion arbeiten würde, lassen wir es weg.
- **Linear ist die nächste Konstante.** Im Zweifel folgen wir Linear-Patterns für Listenwesen und Mutations-Feedback, da Linear das am saubersten löst für Tech-Teams.
- **Eigene Patterns nur wo nötig.** Sichtbarkeitsmodell pre/post Publikation und Pflicht-Minimum-Panel sind eigene Erfindungen — die müssen wir selbst gut machen.
---
## Design System Foundation
### Design System Choice
**Tailwind CSS** (vom Architektur-Starter gesetzt) als Utility-First-Foundation, ergänzt um **Radix UI Primitives** für interaktive Komponenten (Dialog, Dropdown, Tabs, Tooltip) und eine **schlanke Custom-Component-Library** in `apps/web/src/components/ui/`.
**Keine vollständige Component-Library** (kein shadcn-Bulk-Install, kein Material-UI, kein Ant Design): Die Brand ist zu spezifisch und das Tooling-Volumen zu klein, um eine externe Library zu rechtfertigen.
### Rationale for Selection
- **Tailwind passt zur Brand:** Monochrome Palette, klare Spacing-Skala, kein Default-Theme das mit der Brand kämpft.
- **Radix UI Primitives liefern Accessibility-Korrektheit** ohne Visual-Design — wir styleen sie mit Tailwind nach unserer Sprache. Spart Bug-Klassen wie kaputte Focus-Traps in Modalen, falsche ARIA-Roles, etc.
- **Eigene Components statt shadcn:** shadcn copies kleine Library-Snippets ins Repo — bei unserer Größe ist es kontrollierbarer, ~15 Komponenten direkt im Brand-Stil zu schreiben, als 50 Snippets zu kopieren und Brand-anzupassen.
- **Keine Dark-Mode-Pflicht im MVP:** Das Cockpit ist Single-Theme (weiß primär). Phase 2 kann Dark Mode hinzufügen.
### Implementation Approach
```
apps/web/src/components/ui/ # Eigene Brand-konforme Komponenten
├── Button.tsx # 3 Varianten: primary, secondary, ghost
├── Card.tsx # Standard-Container mit großen Radii
├── Input.tsx # Text + Number + URL
├── Textarea.tsx
├── Select.tsx # nutzt Radix Select
├── Checkbox.tsx # nutzt Radix Checkbox
├── Toggle.tsx # 3-Wege-Toggle (Ampel)
├── Dialog.tsx # nutzt Radix Dialog
├── Tabs.tsx # nutzt Radix Tabs
├── Tooltip.tsx # nutzt Radix Tooltip
├── Toast.tsx # nutzt Sonner (toast lib)
├── SkeletonCard.tsx
├── AmpelBadge.tsx # Status-Pill mit Trend
├── PflichtMinimumIndicator.tsx # 6-Punkt-Block-Indikator
└── KeyCaret.tsx # Das `^`-Brand-Element
```
### Customization Strategy
1. **Brand-Tokens als CSS-Variablen im `:root`**, von Tailwind via Theme-Erweiterung konsumiert. Das erlaubt späteres Dark-Mode-Switching ohne Class-Renaming.
2. **Eine `tailwind.config.ts` als Source of Truth** für Spacing, Radii, Schriftgrade, Farben. Custom-Components verwenden `@apply`-Composition oder direkte Utility-Klassen.
3. **Radix-Komponenten werden gestyled, nicht überschrieben.** Tailwind-Classes auf Radix-Primitives (`<Dialog.Content className="…">`).
4. **Icon-Set:** **Lucide-React** als Default (offen, kostenlos, konsistent). Wo Brand-Icons aus `ToolCTRL Brand Identity/Brand EcoSystem/Ui icons Pattern` vorhanden sind (Home, Settings, Search, Trash, Zoom-in, etc.) — bevorzugt verwenden; Rest aus Lucide. Beide Sets sind monochrom-filled.
---
## Defining Experience (Detail)
### Defining Experience
Im Zentrum steht der **Tagesrhythmus-Doppeltakt**: ein zeitlich getrennter Schreib-Workflow (morgens setzen, abends reflektieren), der bewusst nicht als ein einziges Formular vereint wird, sondern als zwei eigene Touchpoints existiert — getrennt durch den Tag. Die Trennung ist kein technischer Workaround, sondern das UX-Versprechen: „Plan und Realität dürfen sich unterscheiden, und wir machen genau diese Differenz sichtbar."
Die zweite Erfahrungs-Säule ist der **Puls als gemeinsamer Spiegel**: ein einziger, dichter Bildschirm, der jeden Werktagsmorgen die Antwort liefert auf „Wie steht es heute um uns alle?".
### User Mental Model
Die Personen sollen das Cockpit so verstehen:
- **„Mein heute" ist mein Privates** — wie eine Tageskarte, die nur ich sehe.
- **Der Puls ist unser Gemeinsames** — wie ein Schwarzes Brett, das jeden Werktag aktuell ist.
- **Bereich = Familie meines Projekts/Deals/Kampagne** — Entwicklung, Vertrieb, Marketing als drei eigene Häuser unter einem Dach.
- **Plane ist Werkzeug, Outline ist Wissen, Cockpit ist Spiegel.** Das Cockpit interpretiert oder editiert nichts — es zeigt.
Was das Modell **vermeiden** muss: die Verwechslung „das Cockpit ist mein Ticket-Tool" (es ist nicht Plane) oder „das Cockpit ist mein Wiki" (es ist nicht Outline).
### Success Criteria
Die Kern-Erfahrung gilt als erfolgreich, wenn nach 8 Wochen Live-Betrieb gilt:
- ≥ 80 % der Werktage haben pro aktiver Person mindestens ein abgeschlossenes Tagesupdate (PRD M1).
- Median Einstempel-Zeit ≤ 60 s, Median Reflexions-Zeit ≤ 90 s (CM1a/CM1b).
- 100 % der Jourfixes nutzen den Puls als Hauptbildschirm (M4).
- Kein erhöhter Druck-/Lästergefühl im 4-Wochen-Retro (CM3).
### Novel UX Patterns
Diese Patterns sind im Cockpit **neu erfunden** (haben kein direktes Vorbild in der Inspirations-Bibliothek):
1. **Sichtbarkeits-Asymmetrie über Zeit:** ToDos sind tagsüber privat, am nächsten Werktagsmorgen publiziert — bewusst keine Realtime-Sichtbarkeit. Visuell muss klar sein: „Was du gerade tippst, sehen Kollegen ab morgen."
2. **Pflicht-Minimum-Indikator:** Sechs kleine Blöcke (•••••• statt einzelner Häkchen-Checkliste) zeigen pro Projekt den Erfüllungsstand auf einen Blick. Ein klick zum Detail.
3. **Tagesplan-Echo:** Bei der Abend-Reflexion erscheinen die morgens gesetzten ToDos zurück — Person konfrontiert sich mit dem eigenen morgendlichen Plan.
4. **Tagesstatus-Pill pro Person:** 🟢 voll, 🟡 halb, ⚪ nicht angefangen, 🟣 abwesend — eine vierwertige Statusanzeige, die sich klar von Projekt-Ampeln (3-wertig + grau) unterscheidet.
### Experience Mechanics
**Morgen-Einstempeln-Flow:**
1. Person öffnet Cockpit → Puls-Seite.
2. CTA „Einstempeln" prominent oberhalb des Folds. Bei nicht-eingestempelten Personen großer schwarzer Pill-Button mit `^`-Caret.
3. Klick öffnet `/mein-heute` (kein Modal — eigene Route, das `^` der Brand wird Teil des Headers).
4. Eine zentrale Spalte: leere ToDo-Liste mit Cursor auf Zeile 1.
5. Bereich ist vorausgewählt (Default = eigene Area). Bezugs-Auswahl pro ToDo erscheint inline rechts neben der Zeile, wenn die ToDo fokussiert ist (Hover-Reveal-Pattern).
6. Enter erzeugt nächste Zeile. Cmd/Ctrl+S oder Klick auf „Eingestempelt"-Button speichert.
7. Bestätigung: Subtiler „Eingestempelt."-Toast (2 s) + Redirect auf Puls. „Heute fehlt"-Modul auf Puls zeigt eine Person weniger.
**Abend-Reflexions-Flow:**
1. Ab 17:00 erscheint im Header-Bereich ein dezenter „Reflexion offen"-Pill.
2. Klick → Route `/reflexion` (oder `/mein-heute` mit Reflexions-Tab).
3. Person sieht die morgens definierte ToDo-Liste, Klick auf Checkbox-Icon hakt ab, Klick auf X-Icon verwirft (mit optionalem Grund im Inline-Field).
4. Unter der ToDo-Liste: ein Freitext-Bereich „Sonst noch was?" (1.000 Zeichen, monospaced-Counter zeigt verbleibende Zeichen unauffällig).
5. Drei große farbige Ampel-Buttons (Grün/Gelb/Rot), darunter drei Trend-Pfeile (Pflicht, wenn Bezug=Projekt).
6. Optionaler „Blocker?"-Toggle, öffnet ein Inline-Feld.
7. „Reflektiert."-Toast (2 s) + Redirect auf Puls.
**Vortags-Reveal:**
- Beim ersten Laden des Puls am Werktag X+1 erscheinen die konsolidierten Vortageseinträge im Feed mit kurzem Fade-In. Header-Hinweis (sehr dezent): „Stand: Mo, 24. Mai".
---
## Visual Design Foundation
### Color System
Konkrete Brand-Tokens, abgeleitet aus der ToolCTRL Brand Identity. Werden in `apps/web/src/styles/globals.css` als CSS-Variablen definiert und in `tailwind.config.ts` als Theme-Erweiterung gespiegelt.
**Neutrals (Hauptpalette):**
| Token | Hex | Verwendung |
|---|---|---|
| `--color-bg` | `#ffffff` | App-Hintergrund |
| `--color-surface` | `#fafafa` | Card-Hintergrund (sehr subtil von BG getrennt) |
| `--color-surface-elevated` | `#ffffff` | Modal-Hintergrund, schwebende Elemente (mit Schatten) |
| `--color-border` | `#e5e5e5` | Dünne Trennlinien, Card-Borders |
| `--color-border-strong` | `#d4d4d4` | Hover-Border, fokussierter Trennstrich |
| `--color-ink` | `#0a0a0a` | Haupttext (near-black, nicht reines #000 für weichere Lesbarkeit) |
| `--color-ink-muted` | `#525252` | Sekundärtext, Labels |
| `--color-ink-faint` | `#a3a3a3` | Tertiärtext, Platzhalter |
| `--color-brand` | `#0a0a0a` | Primärbutton-Hintergrund, Marken-Akzente (= ink) |
| `--color-brand-fg` | `#ffffff` | Text auf Brand-Flächen |
**Semantische Ampel-Farben** (auch hell genug für die Brand-Welt):
| Token | Hex (Foreground/Text) | Hex (Background/Surface) | Verwendung |
|---|---|---|---|
| `--color-ampel-green` | `#15803d` | `#dcfce7` | Status „Im Plan" |
| `--color-ampel-amber` | `#a16207` | `#fef3c7` | Status „Gefährdet" |
| `--color-ampel-red` | `#b91c1c` | `#fee2e2` | Status „Blockiert" |
**Trend-Indikatoren:**
| Token | Hex |
|---|---|
| `--color-trend-up` | `#15803d` (besser, ↗︎) |
| `--color-trend-flat` | `#525252` (gleich, →) |
| `--color-trend-down` | `#b91c1c` (schlechter, ↘︎) |
**Focus / Interaction:**
| Token | Hex | Verwendung |
|---|---|---|
| `--color-focus-ring` | `#0a0a0a` mit 20% opacity outline | Tastatur-Fokus-Indikator |
| `--color-hover-overlay` | `rgba(0,0,0,0.04)` | Hover-Layer auf Cards/Buttons |
### Typography System
**Schriftfamilie:**
- **Sans-Serif (UI):** `Inter` — selbst-gehostet via `@fontsource/inter`. Variable Font (300–900). Lehnt am Brand-Marketing-Sans an (geometrische Sans-Serif).
- **Monospace (Werte/Zeiten):** `JetBrains Mono` — selbst-gehostet via `@fontsource/jetbrains-mono`. Für Zeitstempel, EUR-Beträge, Datums-Anzeigen.
**Type-Scale** (basierend auf 16 px Body):
| Token | Größe / Line-Height / Weight | Verwendung |
|---|---|---|
| `text-xs` | 12 / 16 / 500 | Labels, Captions, Audit-Zeitstempel |
| `text-sm` | 14 / 20 / 400 | Sekundärtext, Meta-Information |
| `text-base` | 16 / 24 / 400 | Body, Form-Inputs |
| `text-lg` | 18 / 28 / 500 | Card-Titel |
| `text-xl` | 20 / 28 / 600 | Sektions-Header |
| `text-2xl` | 24 / 32 / 700 | Seitentitel |
| `text-3xl` | 32 / 40 / 700 | Hero im Puls („Stand: Di, 25. Mai") |
**Voice-Anker-Typografie:**
- „**Stark.**" (Toast): `text-base font-semibold` schwarzer Pill.
- „**Wie läuft es?**" (Reflexions-Header): `text-xl font-medium ink-muted`.
- „**Du hast heute noch nicht gepingt.**" (Puls-CTA-Banner): `text-lg font-medium ink` + Button daneben.
### Spacing & Layout Foundation
**Spacing-Skala** (Tailwind-Standard `0.25rem` = `4px`):
| Token | Wert | Anwendung |
|---|---|---|
| `space-1` | 4 px | Mikro-Padding |
| `space-2` | 8 px | Innen-Padding kleiner Pills |
| `space-3` | 12 px | Standard-Gap zwischen Inline-Elementen |
| `space-4` | 16 px | Card-Inner-Padding small |
| `space-6` | 24 px | Card-Inner-Padding default |
| `space-8` | 32 px | Sektions-Abstand |
| `space-12` | 48 px | Seitenrand-Padding |
| `space-16` | 64 px | Hero-Abstand |
**Radii:**
| Token | Wert | Verwendung |
|---|---|---|
| `radius-sm` | 6 px | kleine Pills, Inputs |
| `radius-md` | 10 px | Buttons, Tags |
| `radius-lg` | 16 px | Cards, Modal-Container |
| `radius-xl` | 24 px | Hero-Cards, Brand-konforme Big-Rounded-Boxes (vgl. Need-Help-PDF Brand-Stil) |
| `radius-full` | 9999 px | Pills, Avatars |
**Schatten (sehr subtil, near-black mit niedriger Opacity):**
| Token | Wert | Verwendung |
|---|---|---|
| `shadow-sm` | `0 1px 2px rgba(10,10,10,0.04)` | Cards in Default-State |
| `shadow-md` | `0 4px 12px rgba(10,10,10,0.06)` | Hover-State Cards |
| `shadow-lg` | `0 12px 32px rgba(10,10,10,0.10)` | Modale, Dropdowns |
**Grid & Container:**
- **Container-Max-Width:** 1.280 px, horizontal zentriert.
- **Container-Padding-X:** 24 px (mobile) → 48 px (≥ md).
- **Grid:** 12-Spalten-Grid mit 24 px Gap auf Desktop.
- **Puls-Layout:** 3-Spalten-Grid für die drei Bereiche; auf Tablet 2-Spalten; auf Mobile single-column-Stack.
### Accessibility Considerations
- **WCAG 2.1 AA** als Mindeststandard für Kontraste:
- `--color-ink` auf `--color-bg`: 19.5:1 ✅
- `--color-ink-muted` auf `--color-bg`: 8.1:1 ✅
- `--color-ampel-green` Text auf Green-BG: 5.5:1 ✅
- `--color-ampel-amber` Text auf Amber-BG: 5.8:1 ✅
- `--color-ampel-red` Text auf Red-BG: 6.1:1 ✅
- **Farbe ist nie alleinige Information.** Ampel hat zusätzlich Icon + Text-Label („Im Plan", „Gefährdet", „Blockiert"). Trend hat Pfeil-Icon zusätzlich zur Farbe.
- **Fokus-Indikator** auf jedem interaktiven Element — `outline: 2px solid var(--color-ink)` mit 2 px Offset.
- **Tastatur-Navigation:** Tab-Order folgt visueller Reihenfolge; alle interaktiven Elemente sind ohne Maus erreichbar.
- **Screenreader:** Radix-Primitives liefern korrekte ARIA-Roles automatisch. Custom-Components ergänzen `aria-label` wo nötig (z. B. Icon-Only-Buttons).
- **Reduzierte Bewegung:** `prefers-reduced-motion: reduce` schaltet Fade-In-Animationen auf 0 ms.
- **Schriftgrad-Mindest:** keine Schriftgröße kleiner als 12 px im UI.
---
## Design Direction Decision
### Design Directions Explored
Drei Visualrichtungen wurden konzeptionell erwogen:
1. **"Office Minimal" (gewählt)** — fast vollständig weiß-grau-schwarz, Typografie-getragen, einzelne semantische Farbakzente, sehr ruhig.
2. **"Brand Dark Hero"** — schwarzer Header und Hero-Bereich wie in den LinkedIn-Posts; Body auf Weiß. Dramatischer, marken-näher.
3. **"Colorful Status Pop"** — Brand monochrom, aber Ampeln und Bereichs-Akzente sehr farbig und groß. Spielerischer Versuch.
### Chosen Direction
**Office Minimal** — die Anwendung präsentiert sich als ruhiges, fast nüchternes Werkzeug, das durch Klarheit überzeugt, nicht durch visuelle Effekte.
### Design Rationale
- **Passt zum User-Kontext:** Internes Tool, das mehrmals täglich geöffnet wird. „Brand Dark Hero" ermüdet auf Dauer, „Colorful Status Pop" wird laut.
- **Passt zur Brand-Reduktion:** ToolCTRL ist visuell minimal — Office Minimal überträgt diese Reduktion in die App.
- **Diensthandelnde Klarheit:** Wer schnell wissen will, „läuft das Projekt?", soll die Antwort in 1 s lesen können — das geht am besten mit hohem Kontrast und wenig Visual-Noise.
- **Brand-Akzente bleiben übrig:** Der `^`-Caret als wiederkehrendes Element, der schwarze Pill-Button-Stil, große Radii — das bringt die Marken-DNA in die App, ohne sie aufzudrängen.
### Implementation Approach
- Weiß als Default-Background überall.
- Cards mit `radius-lg` (16 px), `shadow-sm`, `border: 1px solid var(--color-border)`.
- Primärbuttons schwarz mit weißem Text + `radius-md` (10 px).
- Sekundärbuttons mit transparentem Hintergrund + 1 px Border in `--color-ink`.
- Ghost-Buttons (z. B. „Abbrechen") ganz ohne Border, nur Text-Color.
- Ampel-Indikatoren als kleine farbige Punkte/Pills mit kontrastreichem Text auf zarter Background-Farbe.
- Brand-Element `^` als kleines Detail im Logo-Lockup oben links: `cockpit ^`.
- Hero-Texte (z. B. „Stand: Di, 25. Mai") in `text-3xl font-bold` schwarz auf weiß.
---
## User Journey Flows
### UJ-1 — Tagesrhythmus (Morgen → Abend → Nächster Morgen)
```mermaid
flowchart TD
A[Login via Authentik] --> B[Puls-Seite]
B --> C{Schon eingestempelt?}
C -- nein --> D[CTA Einstempeln klicken]
D --> E[/mein-heute öffnet sich]
E --> F[ToDos tippen, Enter erzeugt neue Zeile]
F --> G[Eingestempelt-Button]
G --> H[Stark.-Toast 2s]
H --> B
C -- ja --> I[Tagsüber arbeiten / weitere Cockpit-Lookups]
I --> J{Nach 17:00?}
J -- nein --> I
J -- ja --> K[Reflexion-offen-Pill im Header]
K --> L[/reflexion oder /mein-heute Tab Reflexion]
L --> M[ToDos abhaken / verwerfen]
M --> N[Freitext was sonst noch]
N --> O[Ampel + Trend setzen]
O --> P{Blocker?}
P -- ja --> Q[Blocker-Text inline]
P -- nein --> R[Reflektieren-Button]
Q --> R
R --> S[Reflektiert.-Toast]
S --> B
B --> T[Nächster Werktag 00:05: Cron publiziert]
T --> U[Vortrags-Reveal im Feed mit Fade-In]
```
**Touchpoints im Detail:**
- **Morgen (CM1a ≤ 60 s):** Person + Bereich (Default) + 3-5 ToDos + Submit. UI muss Tab-Reihenfolge perfekt einhalten — Erste ToDo-Zeile bekommt sofort Fokus.
- **Tagsüber:** Cockpit darf aufgerufen werden, aber zeigt der Person nur die eigene Sicht + den Puls; die ToDos sind nur für sie selbst sichtbar.
- **Abend (CM1b ≤ 90 s):** Tagesplan-Echo (ToDos vom Morgen erscheinen mit kurzem Fade-In), schnelles Abhaken via Klick, ein bis drei Sätze Freitext, drei große Buttons für Ampel, drei Pfeile für Trend, fertig.
- **Nächster Morgen:** Vortrags-Reveal als zarte UI-Akzentuierung; sonst bleibt das Cockpit ruhig.
### UJ-2 — Jourfixe-Moderation (Di 11:00, P1/P4)
```mermaid
flowchart TD
A[PM öffnet Cockpit am Di 11:00] --> B[Puls-Seite]
B --> C[Bereichs-Strip lesen: 3 Ampelstatus]
C --> D[Blocker-Liste lesen]
D --> E[Pflicht-Minimum-Übersicht lesen]
E --> F{Rote oder gelbe Projekte?}
F -- ja --> G[Klick auf rotes/gelbes Projekt]
G --> H[Projekt-Detail mit Updates + Meilensteinen]
H --> I[Diskussion mit Verantwortlichem]
I --> J{Weitere Projekte?}
J -- ja --> G
J -- nein --> K[Optional: Jourfixe-Meta-Update als allgemeines Update]
F -- nein --> K
K --> L[Jourfixe beendet]
```
### UJ-3 — Verfall früh erkennen (P1, P4)
```mermaid
flowchart TD
A[Person scrollt durch Puls] --> B[Projekt mit rotem Status auffällt]
B --> C[Klick auf Projekt-Card]
C --> D[Projekt-Detail zeigt: letzter Update vor X Tagen]
D --> E[Letzte Reflexionen mit Trend ↘︎]
E --> F[Meilenstein-Status: X von Y überzogen]
F --> G[Pflicht-Minimum-Panel zeigt unerfüllte Punkte]
G --> H[Person schreibt allgemeines Update mit Bezug auf Projekt]
```
### UJ-4 — Onboarding eines neuen Mitarbeiters
```mermaid
flowchart TD
A[Admin legt User in Authentik an + Group] --> B[User loggt sich erstmals ein]
B --> C[OIDC-Flow läuft, Backend legt User-Zeile an]
C --> D[Default-Bereich aus Authentik-Group gemappt]
D --> E[Puls-Seite: Hero-Hinweis Erstes Tagesupdate]
E --> F[User klickt Einstempeln]
F --> G[Schreibt erste ToDos]
G --> H[Erscheint in Anwesenheits- + Streak-Liste, Streak=1]
```
### UJ-5 — Projekt im Cockpit-Sicht anlegen (P1)
```mermaid
flowchart TD
A[P1 legt Steckbrief in Outline an] --> B[Wechsel ins Cockpit]
B --> C[Bereichs-Detail → Neues Projekt]
C --> D[Modal: Name, Verantwortliche/r, Projektart, Arbeitsmodus]
D --> E[Felder: Steckbrief-URL Pflicht, Plane-Board-URL Pflicht]
E --> F[Optional: DoD-Text, Risikoliste-URL]
F --> G[Optional: erste Meilensteine inline]
G --> H[Speichern → Projekt erscheint im Bereich + im Puls]
H --> I[Pflicht-Minimum-Indikator startet bei 4-6 erfüllten Punkten]
```
### Journey Patterns
- **„Puls-zurück" als roter Faden:** Nach jedem Schreib-Touchpoint kehrt die Person auf den Puls zurück. Das verankert den Puls als Heimat.
- **Toast als kleinster Erfolg:** Jede Mutation → kurze Toast-Bestätigung mit Voice-Anker (`Stark.`, `Reflektiert.`, `Blocker offen.`).
- **Inline-Eingabe statt Modal-Stapel:** Wo möglich, werden Bezüge inline gesetzt, nicht in einem zweiten Modal. Modals nur für Projekt-Anlage und Blocker-Auflösung.
- **Breadcrumb über Tabs:** Statt Tabs nutzen wir Breadcrumbs (Puls → Bereich → Projekt) für sauberere Navigation und einfachere Tastatursteuerung.
### Flow Optimization Principles
1. **First-Field-Focus.** Jedes Modal/jeder Schreib-Flow setzt den Cursor automatisch auf das erste sinnvolle Feld.
2. **Enter ist Submit, wo es Sinn ergibt.** ToDo-Liste: Enter → neue Zeile. Reflexion: Cmd/Ctrl+Enter → Submit.
3. **Esc schließt Modale ohne Speichern** (Standard für alle Radix-Dialoge).
4. **Keine Multi-Step-Wizards.** Selbst die Reflexion ist ein single-screen Flow mit gestaffeltem visuellem Aufbau, nicht „Schritt 1 von 3".
5. **Konsequente Rückkehr.** Nach Erfolg: zurück zum Puls. Nach Abbruch: zurück zur Vor-Seite via Browser-History (Esc + Back).
---
## Component Strategy
### Design System Components
Aus dem Tailwind + Radix-Stack stehen folgende Primitives ohne Eigenentwicklung zur Verfügung:
- **Radix UI:** Dialog, Dropdown Menu, Select, Checkbox, Tabs, Toast (via Sonner), Tooltip, Popover, Hover Card, Collapsible, Accordion, Avatar, Form, Label, Toggle, Toggle Group.
- **Lucide React:** ~1.500 monochrome Icons (Suche, Settings, Plus, Check, X, ChevronDown, ArrowUpRight, ArrowRight, ArrowDownRight, Calendar, Clock, User, Filter, etc.)
- **Sonner:** Toast-Library.
- **React Hook Form + Zod:** Form-Management.
- **@fontsource/inter + @fontsource/jetbrains-mono:** Selbst-hostbare Brand-Fonts.
### Custom Components
15 hauseigene Komponenten in `apps/web/src/components/ui/`:
| Komponente | Zweck | Verwendung in FRs |
|---|---|---|
| `Button` | 3 Varianten (primary/secondary/ghost), 3 Größen (sm/md/lg) | Überall |
| `Card` | Standard-Container mit `radius-lg`, `shadow-sm`, `border` | Puls, Bereiche, Projekte, Listen |
| `Input` | Text-Input mit Brand-Style | FR-21, FR-22, FR-26 |
| `Textarea` | Multi-Line-Eingabe, Counter unten rechts | FR-26 |
| `Select` | Auf Radix-Select, kompakter dropdown | FR-22 Bezug, Bereich |
| `Checkbox` | Auf Radix-Checkbox, animiertes Häkchen | FR-26 ToDo abhaken |
| `AmpelButton` | 3-Wege-Toggle Grün/Gelb/Rot, große Klick-Targets | FR-26 |
| `TrendButton` | 3-Wege-Toggle ↗︎/→/↘︎ | FR-26 |
| `Dialog` | Auf Radix-Dialog, mit Brand-Header und `radius-xl` | FR-31 Blocker, FR-45 Abschluss |
| `Tooltip` | Auf Radix-Tooltip, schwarzer Bubble mit weißem Text | Hover-Erläuterungen |
| `Toast` | via Sonner, schwarzer Pill mit weißem Text, Bottom-Right | Voice-Ankerbestätigungen |
| `SkeletonCard` | Graue Card-Skelette identischer Geometrie wie echte Cards | Loading-States überall |
| `AmpelBadge` | Status-Pill mit Farbpunkt + Label + optionalem Trend-Pfeil | FR-9, FR-21 |
| `PflichtMinimumIndicator` | Sechs kleine Quadrate horizontal, gefüllt/leer | FR-9 (Projekt-Card), FR-7 (Puls) |
| `KeyCaret` | Brand-`^`-Element als SVG, einbindbar in Header/Logo | TopNav, Toast-Bestätigung |
### Component Implementation Strategy
- **Compound-Components statt Prop-Explosion:** `<Card.Root>`, `<Card.Header>`, `<Card.Body>`, `<Card.Footer>` für flexible Layouts ohne 20 Props.
- **`cva` (class-variance-authority)** für Variant-Verwaltung (`Button.variants.primary`, `Button.variants.secondary`).
- **Storybook entfällt im MVP** — bei 15 Komponenten und einem Frontend-Team können wir per `apps/web/src/routes/dev/components.tsx` eine eigene „Component Gallery"-Seite bauen, die nur in dev-Mode sichtbar ist.
- **Snapshot-Tests via Vitest + jsdom** für Komponenten-Stabilität.
### Implementation Roadmap
1. **Sprint 1 — UI-Kit-Bootstrap:** Button, Card, Input, Textarea, KeyCaret, Toast, SkeletonCard. Component-Gallery-Route.
2. **Sprint 2 — Status-Spezifika:** AmpelButton, TrendButton, AmpelBadge, PflichtMinimumIndicator.
3. **Sprint 3 — Interaktion:** Dialog, Tooltip, Select, Checkbox.
4. **Sprint 4 — Feature-Komponenten:** Puls-Module, Bereichs-Details, Tagesrhythmus-Flow.
5. **Sprint 5 — Spezialfälle:** Sales-Kanban (Drag-and-Drop via dnd-kit), Lead-Funnel.
---
## UX Consistency Patterns
### Button Hierarchy
| Variant | Verwendung | Visual |
|---|---|---|
| **Primary** | Hauptaktion einer Seite (Einstempeln, Reflektieren, Speichern) | Schwarzer Hintergrund, weißer Text, `radius-md` |
| **Secondary** | Alternative Aktion (Abbrechen mit Aktion, Sekundäre Navigation) | Transparenter Hintergrund, 1 px schwarzer Border, schwarzer Text |
| **Ghost** | Tertiär (Abbrechen, Schließen, Edit-Hover-Actions) | Kein Border, kein Hintergrund, nur Text. Hover bekommt `--color-hover-overlay`. |
| **Destructive** | Löschen, Archivieren | Wie Primary, aber `--color-ampel-red`-Hintergrund (selten genutzt — Cockpit kennt fast keine Lösch-Aktionen) |
| **Icon-Only** | Inline-Aktionen (Edit, X) | Quadratisch 32×32, Ghost-Stil mit Lucide-Icon zentral. `aria-label` Pflicht. |
**Größenstaffel:** `sm` (28 px Höhe), `md` (36 px Höhe, Default), `lg` (44 px Höhe, für Haupt-CTAs auf dem Puls).
### Feedback Patterns
- **Sofortige optimistic Reaktion** bei Mutations (Checkbox abhaken, Ampel setzen). Bei Server-Fehler: Rollback + roter Toast „Speichern fehlgeschlagen — bitte nochmal versuchen.".
- **Toast bei jeder Mutation** mit Voice-Anker. Toast erscheint bottom-right, 2 s sichtbar, ohne Dismiss-Button (außer Fehler-Toasts haben ein X).
- **Skeleton beim Initial-Load.** Niemals leerer Bildschirm, niemals Spinner als Alleinstellungsmerkmal.
- **Empty-States als Tropfen-Information:** Wenn ein Feed leer ist, nicht „Keine Daten" — sondern „Hier erscheinen die Updates des Tages, sobald jemand reflektiert hat.". Voice-konform.
- **Error-States immer mit Aktion.** Statt nur „Fehler beim Laden": „Konnte Projekt nicht laden. [Erneut versuchen]".
### Form Patterns
- **Labels über dem Feld**, nicht inline-floating.
- **Pflichtfelder mit `*`** im Label, deutlich aber dezent.
- **Validation onBlur**, nicht onChange — verhindert nervige Rotmeldungen während Tippen.
- **Submit-Button disabled** während Pending-Request mit Mini-Spinner statt komplettem Loading-Overlay.
- **Form-Errors unter dem Feld**, schwarzer Text, kein Rot (außer Border des Feldes wird rot).
- **Eine Spalte Form-Layout**, kein Side-by-Side außer Datum/Uhrzeit-Paare.
### Navigation Patterns
- **TopNav fest:** `cockpit ^` Logo links · Tabs (Puls / Entwicklung / Vertrieb / Marketing / Mein heute / Archiv) · rechts: Streak-Badge + Avatar.
- **Active-Indicator:** Aktiver Tab bekommt einen 2 px dicken schwarzen Underline. Kein farbiges Highlight.
- **Breadcrumbs in Detail-Views:** „Puls › Entwicklung › Projekt X" (Klickbare Pfad-Segmente).
- **Browser-Back funktioniert immer.** React-Router-History-Pflege.
- **Keyboard-Shortcuts:** `g p` Puls, `g h` Mein heute, `g e` Entwicklung, `g v` Vertrieb, `g m` Marketing, `g a` Archiv. `?` öffnet Cheatsheet. `n` öffnet „Neuer Blocker"-Dialog. Esc schließt.
### Additional Patterns
- **Status-Pill-Konvention:** Immer als `[● Label]` formatieren — kleiner farbiger Punkt + Label, in Pill mit `radius-full`.
- **Datum-Anzeige:** „Mo, 25. Mai" für Werktage; „vor 2 Tagen" für relative Zeitangaben; ISO-Format `2026-05-25` nur in technischen Audit-Anzeigen.
- **EUR-Anzeige:** Tausenderpunkt + Komma + Euro-Zeichen mit Leerzeichen, z. B. `12.500,00 €`. Niemals `12500€` oder `EUR 12500`.
- **Avatar-Konvention:** Initialen (max 2 Zeichen) in schwarzem Kreis mit weißer Schrift; Bilder optional aus Authentik-Profilbild Phase 2.
---
## Responsive Design & Accessibility
### Responsive Strategy
- **Desktop-first** (`min-width` queries).
- **Drei Sicht-Ebenen:**
- **Desktop (≥ 1280 px):** Volles 3-Spalten-Layout im Puls, alle Tabs in TopNav sichtbar.
- **Tablet (768–1279 px):** Puls auf 2-Spalten-Grid; TopNav klappt überlange Tabs in „Mehr"-Dropdown.
- **Mobile (< 768 px):** Single-Column-Stack; TopNav wird zu Bottom-Tab-Bar mit 4 Items (Puls / Mein heute / Bereiche-Dropdown / Profil). Reflexion und Einstempeln sind **lesbar**, das Schreiben wird ausdrücklich auf Desktop empfohlen (Info-Hinweis im Mobile-Layout).
### Breakpoint Strategy
| Breakpoint | Größe | Verwendung |
|---|---|---|
| `sm` | ≥ 640 px | Vereinzelte Layout-Anpassungen |
| `md` | ≥ 768 px | Tablet-Layout aktiviert |
| `lg` | ≥ 1024 px | Mehr Padding, größere Cards |
| `xl` | ≥ 1280 px | Desktop-Volllayout, 3-Spalten |
| `2xl` | ≥ 1536 px | Container bleibt bei 1280 px gecappt, mehr Außen-Padding |
### Accessibility Strategy
- **Standard:** WCAG 2.1 AA durchgängig.
- **Keyboard-First-Pflicht.** Alle interaktiven Elemente erreichbar via Tab, Aktivierung mit Enter/Space.
- **Focus-Ring sichtbar:** `outline: 2px solid var(--color-ink)` + 2 px Offset auf jedem fokussierten Element.
- **Skip-Link** ganz oben („Zum Hauptinhalt springen") für Screenreader.
- **Heading-Hierarchie sauber:** `<h1>` einmal pro Seite (Seitentitel), `<h2>` für Sektionen, `<h3>` für Sub-Sektionen.
- **Live-Regionen für Toast-Notifications:** `aria-live="polite"` für Erfolg, `aria-live="assertive"` für Fehler.
- **Alt-Texte** auf jedem nicht-dekorativen Bild und SVG. `<KeyCaret aria-hidden="true">` für reine Decorations.
- **Reduced-Motion:** Beim System-Setting `prefers-reduced-motion: reduce` werden Fade-Ins auf `duration: 0` gesetzt.
- **Dark-Mode-Support:** Phase 2.
- **Hochkontrast-Modus:** automatisch durch Brand-Kontrast (19:1 default) — kein Custom-High-Contrast-Theme nötig.
### Testing Strategy
- **axe-core integration** in Vitest-Tests pro Komponente.
- **Playwright-E2E-Tests** mit `@axe-core/playwright` auf die kritischen Routen (UJ-1, UJ-2, UJ-5).
- **Manuelle Keyboard-Audits** vor jedem Release: Tab-durchnavigieren der Puls-Seite + Mein-Heute + Reflexion.
- **Manuelle Screenreader-Audit** (VoiceOver oder NVDA) vor MVP-Launch auf die kritischen User Journeys.
### Implementation Guidelines
- **Tailwind-Plugins** für besseren Focus-Ring (`@tailwindcss/forms`) und Typografie (`@tailwindcss/typography`).
- **Eslint-Plugin-jsx-a11y** aktiviert mit `recommended` ruleset, blockt PRs bei A11y-Verstößen.
- **Lighthouse-Scoring-Ziel:** ≥ 95 in Accessibility-Score (gemessen auf jeder Route im CI).
+44
View File
@@ -0,0 +1,44 @@
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
BMad Method,bmad-investigate,Investigate,IN,Forensic case investigation calibrated to the input. Evidence-graded analysis with hypothesis tracking. Produces a structured case file.,,4-implementation,,,false,implementation_artifacts,investigation report,
BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,,1-analysis,,,false,planning_artifacts,brainstorming session
BMad Method,bmad-market-research,Market Research,MR,Market analysis competitive landscape customer needs and trends.,,,1-analysis,,,false,planning_artifacts|project-knowledge,research documents
BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
BMad Method,bmad-product-brief,Create Brief,CB,An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you.,,-A,1-analysis,,,false,planning_artifacts,product brief
BMad Method,bmad-prfaq,PRFAQ Challenge,WB,Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief.,,-H,1-analysis,,,false,planning_artifacts,prfaq document
BMad Method,bmad-prd,Create Edit and Review PRD,PRD,"Facilitated PRD workflow — create a new PRD via coached discovery, update an existing one against a change signal, or validate a finished PRD against a checklist with an HTML findings report.",,,2-planning,bmad-product-brief,,true,planning_artifacts,prd
BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,,2-planning,bmad-prd,,false,planning_artifacts,ux design
BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,,3-solutioning,,,true,planning_artifacts,architecture
BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report
BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,,4-implementation,,,true,implementation_artifacts,sprint status
BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,,4-implementation,bmad-sprint-planning,,false,,
BMad Method,bmad-create-story,Create Story,CS,Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.,create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story
BMad Method,bmad-create-story,Validate Story,VS,Validates story readiness and completeness before development work begins.,validate,,4-implementation,bmad-create-story:create,bmad-dev-story,false,implementation_artifacts,story validation report
BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,,4-implementation,bmad-create-story:validate,,true,,
BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,,4-implementation,bmad-dev-story,,false,,
BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,,4-implementation,,,false,,
BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite
BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective
BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,,anytime,,,false,project-knowledge,*
BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,,anytime,,,false,output_folder,project context
BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,,anytime,,,false,implementation_artifacts,spec and project implementation
BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,,anytime,,,false,planning_artifacts,change proposal
BMad Method,bmad-agent-tech-writer,Write Document,WD,"Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review.",write,,anytime,,,false,project-knowledge,document
BMad Method,bmad-agent-tech-writer,Update Standards,US,Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.,update-standards,,anytime,,,false,_bmad/_memory/tech-writer-sidecar,standards
BMad Method,bmad-agent-tech-writer,Mermaid Generate,MG,Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.,mermaid,,anytime,,,false,planning_artifacts,mermaid diagram
BMad Method,bmad-agent-tech-writer,Validate Document,VD,Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.,validate,[path],anytime,,,false,planning_artifacts,validation report
BMad Method,bmad-agent-tech-writer,Explain Concept,EC,Create clear technical explanations with examples and diagrams for complex concepts.,explain,[topic],anytime,,,false,project_knowledge,explanation
Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,,
Core,bmad-help,BMad Help,BH,,,,anytime,,,false,,
Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,,anytime,,,false,,
Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,,[path],anytime,,,false,,
Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes
Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,,[path],anytime,,,false,report located with target document,
Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",,[path],anytime,,,false,,
Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,,
Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s)
Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files
1 module skill display-name menu-code description action args phase preceded-by followed-by required output-location outputs
2 BMad Method _meta false https://docs.bmad-method.org/llms.txt
3 BMad Method bmad-investigate Investigate IN Forensic case investigation calibrated to the input. Evidence-graded analysis with hypothesis tracking. Produces a structured case file. 4-implementation false implementation_artifacts investigation report
4 BMad Method bmad-brainstorming Brainstorm Project BP Expert guided facilitation through a single or multiple techniques. 1-analysis false planning_artifacts brainstorming session
5 BMad Method bmad-market-research Market Research MR Market analysis competitive landscape customer needs and trends. 1-analysis false planning_artifacts|project-knowledge research documents
6 BMad Method bmad-domain-research Domain Research DR Industry domain deep dive subject matter expertise and terminology. 1-analysis false planning_artifacts|project_knowledge research documents
7 BMad Method bmad-technical-research Technical Research TR Technical feasibility architecture options and implementation approaches. 1-analysis false planning_artifacts|project_knowledge research documents
8 BMad Method bmad-product-brief Create Brief CB An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you. -A 1-analysis false planning_artifacts product brief
9 BMad Method bmad-prfaq PRFAQ Challenge WB Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief. -H 1-analysis false planning_artifacts prfaq document
10 BMad Method bmad-prd Create Edit and Review PRD PRD Facilitated PRD workflow — create a new PRD via coached discovery, update an existing one against a change signal, or validate a finished PRD against a checklist with an HTML findings report. 2-planning bmad-product-brief true planning_artifacts prd
11 BMad Method bmad-create-ux-design Create UX CU Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project. 2-planning bmad-prd false planning_artifacts ux design
12 BMad Method bmad-create-architecture Create Architecture CA Guided workflow to document technical decisions. 3-solutioning true planning_artifacts architecture
13 BMad Method bmad-create-epics-and-stories Create Epics and Stories CE 3-solutioning bmad-create-architecture true planning_artifacts epics and stories
14 BMad Method bmad-check-implementation-readiness Check Implementation Readiness IR Ensure PRD UX Architecture and Epics Stories are aligned. 3-solutioning bmad-create-epics-and-stories true planning_artifacts readiness report
15 BMad Method bmad-sprint-planning Sprint Planning SP Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story. 4-implementation true implementation_artifacts sprint status
16 BMad Method bmad-sprint-status Sprint Status SS Anytime: Summarize sprint status and route to next workflow. 4-implementation bmad-sprint-planning false
17 BMad Method bmad-create-story Create Story CS Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation. create 4-implementation bmad-sprint-planning bmad-create-story:validate true implementation_artifacts story
18 BMad Method bmad-create-story Validate Story VS Validates story readiness and completeness before development work begins. validate 4-implementation bmad-create-story:create bmad-dev-story false implementation_artifacts story validation report
19 BMad Method bmad-dev-story Dev Story DS Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed. 4-implementation bmad-create-story:validate true
20 BMad Method bmad-code-review Code Review CR Story cycle: If issues back to DS if approved then next CS or ER if epic complete. 4-implementation bmad-dev-story false
21 BMad Method bmad-checkpoint-preview Checkpoint CK Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs. 4-implementation false
22 BMad Method bmad-qa-generate-e2e-tests QA Automation Test QA Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that. 4-implementation bmad-dev-story false implementation_artifacts test suite
23 BMad Method bmad-retrospective Retrospective ER Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC. 4-implementation bmad-code-review false implementation_artifacts retrospective
24 BMad Method bmad-document-project Document Project DP Analyze an existing project to produce useful documentation. anytime false project-knowledge *
25 BMad Method bmad-generate-project-context Generate Project Context GPC Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects. anytime false output_folder project context
26 BMad Method bmad-quick-dev Quick Dev QQ Unified intent-in code-out workflow: clarify plan implement review and present. anytime false implementation_artifacts spec and project implementation
27 BMad Method bmad-correct-course Correct Course CC Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories. anytime false planning_artifacts change proposal
28 BMad Method bmad-agent-tech-writer Write Document WD Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review. write anytime false project-knowledge document
29 BMad Method bmad-agent-tech-writer Update Standards US Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions. update-standards anytime false _bmad/_memory/tech-writer-sidecar standards
30 BMad Method bmad-agent-tech-writer Mermaid Generate MG Create a Mermaid diagram based on user description. Will suggest diagram types if not specified. mermaid anytime false planning_artifacts mermaid diagram
31 BMad Method bmad-agent-tech-writer Validate Document VD Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority. validate [path] anytime false planning_artifacts validation report
32 BMad Method bmad-agent-tech-writer Explain Concept EC Create clear technical explanations with examples and diagrams for complex concepts. explain [topic] anytime false project_knowledge explanation
33 Core _meta false https://docs.bmad-method.org/llms.txt
34 Core bmad-brainstorming Brainstorming BSP Use early in ideation or when stuck generating ideas. anytime false {output_folder}/brainstorming brainstorming session
35 Core bmad-party-mode Party Mode PM Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate. anytime false
36 Core bmad-help BMad Help BH anytime false
37 Core bmad-index-docs Index Docs ID Use when LLM needs to understand available docs without loading everything. anytime false
38 Core bmad-shard-doc Shard Document SD Use when doc becomes too large (>500 lines) to manage effectively. [path] anytime false
39 Core bmad-editorial-review-prose Editorial Review - Prose EP Use after drafting to polish written content. [path] anytime false report located with target document three-column markdown table with suggested fixes
40 Core bmad-editorial-review-structure Editorial Review - Structure ES Use when doc produced from multiple subprocesses or needs structural improvement. [path] anytime false report located with target document
41 Core bmad-review-adversarial-general Adversarial Review AR Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews. [path] anytime false
42 Core bmad-review-edge-case-hunter Edge Case Hunter Review ECH Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven. [path] anytime false
43 Core bmad-distillator Distillator DG Use when you need token-efficient distillates that preserve all information for downstream LLM consumption. [path] anytime false adjacent to source document or specified output_path distillate markdown file(s)
44 Core bmad-customize BMad Customize BC Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required. anytime false {project-root}/_bmad/custom TOML override files
+232
View File
@@ -0,0 +1,232 @@
type,name,module,path,hash
"yaml","manifest","_config","_config/manifest.yaml","b13e521bbb5ebdd1a441988d30c424d7d782db134ab9ea1e00333066b496ee78"
"csv","documentation-requirements","bmm","bmm/1-analysis/bmad-document-project/documentation-requirements.csv","d1253b99e88250f2130516b56027ed706e643bfec3d99316727a4c6ec65c6c1d"
"csv","domain-complexity","bmm","bmm/3-solutioning/bmad-create-architecture/data/domain-complexity.csv","3dc34ed39f1fc79a51f7b8fc92087edb7cd85c4393a891d220f2e8dd5a101c70"
"csv","module-help","bmm","bmm/module-help.csv","b8c199e3bb160060887211772af2d21b785ce7a3d646699e39520f867af5400f"
"csv","project-types","bmm","bmm/3-solutioning/bmad-create-architecture/data/project-types.csv","12343635a2f11343edb1d46906981d6f5e12b9cad2f612e13b09460b5e5106e7"
"html","validation-report-template","bmm","bmm/2-plan-workflows/bmad-prd/assets/validation-report-template.html","a80becdd8205d2d7c18738855830b096172a496b96b5ca8a0820ec55c9bb8bf9"
"json","bmad-manifest","bmm","bmm/1-analysis/bmad-prfaq/bmad-manifest.json","7908cab6f0cd66f1f3427b4cc659975b4737bdc44f789b23c6a895fcd084c4bc"
"json","project-scan-report-schema","bmm","bmm/1-analysis/bmad-document-project/templates/project-scan-report-schema.json","8466965321f1db22f5013869636199f67e0113706283c285a7ffbbf5efeea321"
"md","architecture-decision-template","bmm","bmm/3-solutioning/bmad-create-architecture/architecture-decision-template.md","5d9adf90c28df61031079280fd2e49998ec3b44fb3757c6a202cda353e172e9f"
"md","artifact-analyzer","bmm","bmm/1-analysis/bmad-prfaq/agents/artifact-analyzer.md","7bdc44830f8d593346ec0ee15e36e1e431432fcc6c38b70bb861999315c9cfa4"
"md","brief-template","bmm","bmm/1-analysis/bmad-product-brief/assets/brief-template.md","bb28edf75d023067551c8d417b19b8803324e7acfb2ad0c80f882e9134f4c1f1"
"md","case-file-template","bmm","bmm/4-implementation/bmad-investigate/references/case-file-template.md","9dccb41f3f3fc796b42411966e51b32c4231aaceb4b6bb1880a16225a990002e"
"md","checklist","bmm","bmm/1-analysis/bmad-document-project/checklist.md","581b0b034c25de17ac3678db2dbafedaeb113de37ddf15a4df6584cf2324a7d7"
"md","checklist","bmm","bmm/4-implementation/bmad-correct-course/checklist.md","3e082b95def90ccb876e3101ce0bbaf797a0f03a9471e1347361897f27977327"
"md","checklist","bmm","bmm/4-implementation/bmad-create-story/checklist.md","b94e28e774c3be0288f04ea163424bece4ddead5cd3f3680d1603ed07383323a"
"md","checklist","bmm","bmm/4-implementation/bmad-dev-story/checklist.md","630b68c6824a8785003a65553c1f335222b17be93b1bd80524c23b38bde1d8af"
"md","checklist","bmm","bmm/4-implementation/bmad-qa-generate-e2e-tests/checklist.md","b58f810aeb1040c2f6758c88aa133afce72f8cc178d3d97ff0fbaa3d943057dc"
"md","checklist","bmm","bmm/4-implementation/bmad-sprint-planning/checklist.md","80b10aedcf88ab1641b8e5f99c9a400c8fd9014f13ca65befc5c83992e367dd7"
"md","compile-epic-context","bmm","bmm/4-implementation/bmad-quick-dev/compile-epic-context.md","5cfda02f252941e415b80c57b4528f46226b3cbf456ad45d78fcb5a7ef4816e2"
"md","customer-faq","bmm","bmm/1-analysis/bmad-prfaq/references/customer-faq.md","96f8565197649c58908a1d61b6cd805fd01f57da7945ba889c18d087ad597aeb"
"md","deep-dive-instructions","bmm","bmm/1-analysis/bmad-document-project/workflows/deep-dive-instructions.md","a79e24b93a25ab456062916a9dad7b6a16dc43ac0b4b555700d3c3751cff0d25"
"md","deep-dive-template","bmm","bmm/1-analysis/bmad-document-project/templates/deep-dive-template.md","6198aa731d87d6a318b5b8d180fc29b9aa53ff0966e02391c17333818e94ffe9"
"md","deep-dive-workflow","bmm","bmm/1-analysis/bmad-document-project/workflows/deep-dive-workflow.md","a64d98dfa3b771df2853c4fa19a4e9c90d131e409e13b4c6f5e494d6ac715125"
"md","discover-inputs","bmm","bmm/4-implementation/bmad-create-story/discover-inputs.md","dfedba6a8ea05c9a91c6d202c4b29ee3ea793d8ef77575034787ae0fef280507"
"md","epics-template","bmm","bmm/3-solutioning/bmad-create-epics-and-stories/templates/epics-template.md","a804f740155156d89661fa04e7a4264a8f712c4dc227c44fd8ae804a9b0f6b72"
"md","explain-concept","bmm","bmm/1-analysis/bmad-agent-tech-writer/explain-concept.md","6ea82dbe4e41d4bb8880cbaa62d936e40cef18f8c038be73ae6e09c462abafc9"
"md","full-scan-instructions","bmm","bmm/1-analysis/bmad-document-project/workflows/full-scan-instructions.md","2235945df2ae261265187447ce593238e132a026a9b88d3507a1f1d6808a0263"
"md","full-scan-workflow","bmm","bmm/1-analysis/bmad-document-project/workflows/full-scan-workflow.md","3bff88a392c16602bd44730f32483505e73e65e46e82768809c13a0a5f55608b"
"md","generate-trail","bmm","bmm/4-implementation/bmad-checkpoint-preview/generate-trail.md","4a5936d86fbe5a85285b4535097b1e2edda8849da35586f4b588a982d7224459"
"md","headless","bmm","bmm/2-plan-workflows/bmad-prd/references/headless.md","bb7ef8900d6af6a60eb6a72c596bb3c113dde2dfbee703b99bc4ea25eabba40e"
"md","headless-schemas","bmm","bmm/2-plan-workflows/bmad-prd/assets/headless-schemas.md","66a5b1f0ce22c7baf3a6b6a14feb7458491b1106bef437389cd6291f81eebd7f"
"md","index-template","bmm","bmm/1-analysis/bmad-document-project/templates/index-template.md","42c8a14f53088e4fda82f26a3fe41dc8a89d4bcb7a9659dd696136378b64ee90"
"md","instructions","bmm","bmm/1-analysis/bmad-document-project/instructions.md","9f4bc3a46559ffd44289b0d61a0f8f26f829783aa1c0e2a09dfa807fa93eb12f"
"md","internal-faq","bmm","bmm/1-analysis/bmad-prfaq/references/internal-faq.md","26eb83f844cda1ed8efb50f4703d61713ada8a64bd27eb387f759f858b5748de"
"md","mermaid-gen","bmm","bmm/1-analysis/bmad-agent-tech-writer/mermaid-gen.md","1d83fcc5fa842bc31ecd9fd7e45fbf013fabcadf0022d3391fff5b53b48e4b5d"
"md","prd-template","bmm","bmm/2-plan-workflows/bmad-prd/assets/prd-template.md","ebba04ba47740d9127e9bcb30f9a829c7018014208e015e8b727180d12694b14"
"md","prd-validation-checklist","bmm","bmm/2-plan-workflows/bmad-prd/assets/prd-validation-checklist.md","c5def5aa51aba5b7497f3552e06e86d86d15b9d94be26d056c0bbd1b9375c50a"
"md","press-release","bmm","bmm/1-analysis/bmad-prfaq/references/press-release.md","c406adb0e2d2cc326cbc45d0174f89d014523448ad82bc272293999d22aec596"
"md","prfaq-template","bmm","bmm/1-analysis/bmad-prfaq/assets/prfaq-template.md","b27e6964f0437ab4e78c8c0ffbe5052c28e3b3ef2fc811726cbb394d5a5c7559"
"md","project-context-template","bmm","bmm/3-solutioning/bmad-generate-project-context/project-context-template.md","54e351394ceceb0ac4b5b8135bb6295cf2c37f739c7fd11bb895ca16d79824a5"
"md","project-overview-template","bmm","bmm/1-analysis/bmad-document-project/templates/project-overview-template.md","a7c7325b75a5a678dca391b9b69b1e3409cfbe6da95e70443ed3ace164e287b2"
"md","readiness-report-template","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/templates/readiness-report-template.md","0da97ab1e38818e642f36dc0ef24d2dae69fc6e0be59924dc2dbf44329738ff6"
"md","research.template","bmm","bmm/1-analysis/research/bmad-domain-research/research.template.md","507bb6729476246b1ca2fca4693986d286a33af5529b6cd5cb1b0bb5ea9926ce"
"md","research.template","bmm","bmm/1-analysis/research/bmad-market-research/research.template.md","507bb6729476246b1ca2fca4693986d286a33af5529b6cd5cb1b0bb5ea9926ce"
"md","research.template","bmm","bmm/1-analysis/research/bmad-technical-research/research.template.md","507bb6729476246b1ca2fca4693986d286a33af5529b6cd5cb1b0bb5ea9926ce"
"md","SKILL","bmm","bmm/1-analysis/bmad-agent-analyst/SKILL.md","dff8fbd39de875ccc6735204561f2b7b877788843ecd6e8b6001e7ca7f39641e"
"md","SKILL","bmm","bmm/1-analysis/bmad-agent-tech-writer/SKILL.md","d39ffa2a931361e9fea11ef5eb96da0fc6b8dbab70dc200142404a50b78e7665"
"md","SKILL","bmm","bmm/1-analysis/bmad-document-project/SKILL.md","efe25c8c27116faeeef119078953c38e827db787338fdb2dca65c086f42e0c4b"
"md","SKILL","bmm","bmm/1-analysis/bmad-prfaq/SKILL.md","9e864f574cc2e1411284edb625a17268aab1f894c46baac2edc3bcf1383adaca"
"md","SKILL","bmm","bmm/1-analysis/bmad-product-brief/SKILL.md","6078a95cf9500c4c9010e834adc290fa2c793cd4e0e6f2d47e36dc477be24112"
"md","SKILL","bmm","bmm/1-analysis/research/bmad-domain-research/SKILL.md","53d2ee5ccbd1d73290e752cff0235bfdb4928dc5ee70f347992ef15621341124"
"md","SKILL","bmm","bmm/1-analysis/research/bmad-market-research/SKILL.md","720183e577a5f6f307f320703a5ec522190723e957cfe6ad7821d8d833dea0c7"
"md","SKILL","bmm","bmm/1-analysis/research/bmad-technical-research/SKILL.md","1c4b61afaeedd3191db65923253be56084c64e4f832f0e2b9179671f91b5bf20"
"md","SKILL","bmm","bmm/2-plan-workflows/bmad-agent-pm/SKILL.md","3a8550daac2df2f01a7c04b66148c1e30b50f81730f043415f2a1aba6314f8a4"
"md","SKILL","bmm","bmm/2-plan-workflows/bmad-agent-ux-designer/SKILL.md","3ac99856f0ee9bae3ba05cefab32e90920b4a1fdf0e8c4bf9c440be31f9e5a1f"
"md","SKILL","bmm","bmm/2-plan-workflows/bmad-create-prd/SKILL.md","143c5c2d85734021db343ff3dbf143e804eddbf9fb519fb824b70f1a9154767d"
"md","SKILL","bmm","bmm/2-plan-workflows/bmad-create-ux-design/SKILL.md","24a4123035194bd3517a059417dcd8114db90f99314ed1a51cfdbbaff02860b2"
"md","SKILL","bmm","bmm/2-plan-workflows/bmad-edit-prd/SKILL.md","31ecfe3c16513d994e44e648a7f2d4f4a6ccf297ca6c1826aa68ef7b053e7ff2"
"md","SKILL","bmm","bmm/2-plan-workflows/bmad-prd/SKILL.md","aac7b30d01f9d9275fb8339e65c58e4b21c4ae22ddc5810a05a464da5951dec0"
"md","SKILL","bmm","bmm/2-plan-workflows/bmad-validate-prd/SKILL.md","931439b8c84bc454d0596a1ad35d6583c1926c941f2928c54b31aea702c24b53"
"md","SKILL","bmm","bmm/3-solutioning/bmad-agent-architect/SKILL.md","1bb79ab9d9ae3180a6142d9d20c2a5cb15ad5d5f28e38a7f847295dd04d2299f"
"md","SKILL","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/SKILL.md","c683db5628781e6dba6fad388ed65cf7561a208e773f72c786918743e379abef"
"md","SKILL","bmm","bmm/3-solutioning/bmad-create-architecture/SKILL.md","b12711f1655cb809bb74a3e11e40943dce225ccba9d01974d5bf521a5867ba23"
"md","SKILL","bmm","bmm/3-solutioning/bmad-create-epics-and-stories/SKILL.md","9db05e4a63ead6e416009fdec250bb9b749b361331964b601559ffeedd2f1c0f"
"md","SKILL","bmm","bmm/3-solutioning/bmad-generate-project-context/SKILL.md","3d8c20e13e62981464365509fc5e1dfd31e70a94e8c08b048a3e3866ea00ff43"
"md","SKILL","bmm","bmm/4-implementation/bmad-agent-dev/SKILL.md","ab47a75e3031bc58a9783e4f8dece8d44dfdacb7ba74cd56f766b260f9993218"
"md","SKILL","bmm","bmm/4-implementation/bmad-checkpoint-preview/SKILL.md","fcdd92af6b97ae64f99a89c37deef0074a974ffa577263e6c79c4b6734c63bda"
"md","SKILL","bmm","bmm/4-implementation/bmad-code-review/SKILL.md","6801f6742156f125185b1eb6e10c58dfde4ff6545b2e6278acf68cbcfbe0abe4"
"md","SKILL","bmm","bmm/4-implementation/bmad-correct-course/SKILL.md","25be5dd528b5c3996e5fa02fc30b72377f688436742c6962f4a1e9a2dac46e55"
"md","SKILL","bmm","bmm/4-implementation/bmad-create-story/SKILL.md","81517ba8ef137a15002d6d21ef18a1e88190c74ac9e0c5b29227e4059870809a"
"md","SKILL","bmm","bmm/4-implementation/bmad-dev-story/SKILL.md","5e7d3bca5051ff8f009bcf608e3fdac3260c2aa6faa8df170a954fe03a4a03f8"
"md","SKILL","bmm","bmm/4-implementation/bmad-investigate/SKILL.md","79905849c7ce4ef4c1c647365c52837a8ede2af01a25ce6267dfe8ba64683652"
"md","SKILL","bmm","bmm/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md","ad6025f0279ef9fac2f6c76d8f612c37ccc23f8062b7ac0ac40708b49cc7db80"
"md","SKILL","bmm","bmm/4-implementation/bmad-quick-dev/SKILL.md","2b2e57e5327fc69962e6529a0c17498185b571646a9c7360e5c15be14ea7e63b"
"md","SKILL","bmm","bmm/4-implementation/bmad-retrospective/SKILL.md","997308d999545fa6c82b8652a2973e1d89d82cfbf892531e25e491291a95c33e"
"md","SKILL","bmm","bmm/4-implementation/bmad-sprint-planning/SKILL.md","21946cdaef8115deee6ce322c460e8af39368509d700c778cd7109f50a6821eb"
"md","SKILL","bmm","bmm/4-implementation/bmad-sprint-status/SKILL.md","2fe141bc2033ea341a73fb93349fcee0296d8d8714fcf984ab056bc0abae0a19"
"md","source-tree-template","bmm","bmm/1-analysis/bmad-document-project/templates/source-tree-template.md","109bc335ebb22f932b37c24cdc777a351264191825444a4d147c9b82a1e2ad7a"
"md","spec-template","bmm","bmm/4-implementation/bmad-quick-dev/spec-template.md","3ee15d5a63cf5eeee74149c590668fc61d0e44023eac12988a1ca2a9438a9d39"
"md","step-01-clarify-and-route","bmm","bmm/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md","b5eb9f0cecf2a462885b8e7b4c21abbaec1e95c8abbd76bc410d9090fd8379f0"
"md","step-01-discover","bmm","bmm/3-solutioning/bmad-generate-project-context/steps/step-01-discover.md","8b2c8c7375f8a3c28411250675a28c0d0a9174e6c4e67b3d53619888439c4613"
"md","step-01-document-discovery","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-01-document-discovery.md","c763b512d55906122433cb65c1bcd5b5b283e45eacdc07281c8ec7596b6b3980"
"md","step-01-gather-context","bmm","bmm/4-implementation/bmad-code-review/steps/step-01-gather-context.md","d0ee7558605e9d48b5b6f15d9b535542eb6d922613f529bb520326eacade4171"
"md","step-01-init","bmm","bmm/1-analysis/research/bmad-domain-research/domain-steps/step-01-init.md","efee243f13ef54401ded88f501967b8bc767460cec5561b2107fc03fe7b7eab1"
"md","step-01-init","bmm","bmm/1-analysis/research/bmad-market-research/steps/step-01-init.md","64d5501aea0c0005db23a0a4d9ee84cf4e9239f553c994ecc6b1356917967ccc"
"md","step-01-init","bmm","bmm/1-analysis/research/bmad-technical-research/technical-steps/step-01-init.md","c9a1627ecd26227e944375eb691e7ee6bc9f5db29a428a5d53e5d6aef8bb9697"
"md","step-01-init","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-01-init.md","0b257533a0ce34d792f621da35325ec11cb883653e3ad546221ee1f0dee5edcd"
"md","step-01-init","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-01-init.md","5119205b712ebda0cd241c3daad217bb0f6fa9e6cb41d6635aec6b7fe83b838a"
"md","step-01-orientation","bmm","bmm/4-implementation/bmad-checkpoint-preview/step-01-orientation.md","d9e3b949c36d49a025f3535773af2b51888fe4ce616b6d6d69683a122716b1d2"
"md","step-01-validate-prerequisites","bmm","bmm/3-solutioning/bmad-create-epics-and-stories/steps/step-01-validate-prerequisites.md","5c2aabc871363d84fc2e12fd83a3889e9d752b6bd330e31a0067c96204dd4880"
"md","step-01b-continue","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-01b-continue.md","4d42c6b83eaa720975bf2206a7eea1a8c73ae922668cc2ef03d34c49ab066c19"
"md","step-01b-continue","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-01b-continue.md","4bf216008297dcea25f8be693109cf17879c621865b302c994cdd15aa5124e5f"
"md","step-02-context","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-02-context.md","4381c5128de7d5c02ac806a1263e3965754bd2598954f3188219fbd87567e5c9"
"md","step-02-customer-behavior","bmm","bmm/1-analysis/research/bmad-market-research/steps/step-02-customer-behavior.md","bac4de244049f90d1f2eb95e2cc9389cc84966d9538077fef1ec9c35e4533849"
"md","step-02-design-epics","bmm","bmm/3-solutioning/bmad-create-epics-and-stories/steps/step-02-design-epics.md","7c66987808c1f84e853fe54b7aff26d209196d450b5644704110f124a15179bc"
"md","step-02-discovery","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-02-discovery.md","9ffd5b31cc869b564e4d78cdc70767f0fb1b04db4c40201ccfa9dde75739fa8d"
"md","step-02-domain-analysis","bmm","bmm/1-analysis/research/bmad-domain-research/domain-steps/step-02-domain-analysis.md","385a288d9bbb0adf050bcce4da4dad198a9151822f9766900404636f2b0c7f9d"
"md","step-02-generate","bmm","bmm/3-solutioning/bmad-generate-project-context/steps/step-02-generate.md","b1f063edae66a74026b67a79a245cec7ee85438bafcacfc70dcf6006b495e060"
"md","step-02-plan","bmm","bmm/4-implementation/bmad-quick-dev/step-02-plan.md","72f4df415adceaaf554166983559e058c6a019d783d0f87cf42c401db3c5f52c"
"md","step-02-prd-analysis","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md","38be2bf4b924c0b5b395b57d68f685d790ade7b1a6c10993d3c550675f87d954"
"md","step-02-review","bmm","bmm/4-implementation/bmad-code-review/steps/step-02-review.md","1883758e0e91fba439497d04417f03d778f3f6fb12754f49aa8cfa5e15489f70"
"md","step-02-technical-overview","bmm","bmm/1-analysis/research/bmad-technical-research/technical-steps/step-02-technical-overview.md","9c7582241038b16280cddce86f2943216541275daf0a935dcab78f362904b305"
"md","step-02-walkthrough","bmm","bmm/4-implementation/bmad-checkpoint-preview/step-02-walkthrough.md","66cf893f8f968ee81034e9ccd8c20415692c3a8c23a9a143c2245fe6c800acdc"
"md","step-03-competitive-landscape","bmm","bmm/1-analysis/research/bmad-domain-research/domain-steps/step-03-competitive-landscape.md","f10aa088ba00c59491507f6519fb314139f8be6807958bb5fd1b66bff2267749"
"md","step-03-complete","bmm","bmm/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md","e61463db76a8fa060411aa24127aee936d646b97564e9e2a883494ea50e68464"
"md","step-03-core-experience","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-03-core-experience.md","1f58c8a2f6872f468629ecb67e94f793af9d10d2804fe3e138eba03c090e00c5"
"md","step-03-create-stories","bmm","bmm/3-solutioning/bmad-create-epics-and-stories/steps/step-03-create-stories.md","c5b787a82e4e49ed9cd9c028321ee1689f32b8cd69d89eea609b37cd3d481afc"
"md","step-03-customer-pain-points","bmm","bmm/1-analysis/research/bmad-market-research/steps/step-03-customer-pain-points.md","5b2418ccaaa89291c593efed0311b3895faad1e9181800d382da823a8eb1312a"
"md","step-03-detail-pass","bmm","bmm/4-implementation/bmad-checkpoint-preview/step-03-detail-pass.md","d48163b9f305f15af57729a8443142e47beb6c3e977554afe12b39ee49cb9fc0"
"md","step-03-epic-coverage-validation","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md","7b187f03a47cba0325fcfd10240410db9c59d93768342fc2dd3de2a01ec23356"
"md","step-03-implement","bmm","bmm/4-implementation/bmad-quick-dev/step-03-implement.md","4d848865eafe5eeba7d83529c766bd410a9cc20a05de1d6a764954c7275b4749"
"md","step-03-integration-patterns","bmm","bmm/1-analysis/research/bmad-technical-research/technical-steps/step-03-integration-patterns.md","005d517a2f962e2172e26b23d10d5e6684c7736c0d3982e27b2e72d905814ad9"
"md","step-03-starter","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-03-starter.md","b7727e0f37bc5325e15abad1c54bef716d617df423336090189efd1d307a0b3f"
"md","step-03-triage","bmm","bmm/4-implementation/bmad-code-review/steps/step-03-triage.md","91eaa27f6a167702ead00da9e93565c9bff79dce92c02eccbca61b1d1ed39a80"
"md","step-04-architectural-patterns","bmm","bmm/1-analysis/research/bmad-technical-research/technical-steps/step-04-architectural-patterns.md","4636f23e9c585a7a0c90437a660609d913f16362c3557fc2e71d408d6b9f46ce"
"md","step-04-customer-decisions","bmm","bmm/1-analysis/research/bmad-market-research/steps/step-04-customer-decisions.md","f0bc25f2179b7490e7a6704159a32fc9e83ab616022355ed53acfe8e2f7059d5"
"md","step-04-decisions","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-04-decisions.md","7fc0ebb63ab5ad0efc470f1063c15f14f52f5d855da2382fd17576cf060a8763"
"md","step-04-emotional-response","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-04-emotional-response.md","75724811b170c8897e230a49e968e1db357fef3387008b0906b5ff79a43dbff9"
"md","step-04-final-validation","bmm","bmm/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md","6b83fcf177c24c6b43c2eb0df2efc1430cb0f357b3ab20ee8a5c5483c3fee079"
"md","step-04-present","bmm","bmm/4-implementation/bmad-code-review/steps/step-04-present.md","eee5687e9d7da9a0aeffd78180b8289fca804fc2d59896db5f65a8dd476e4869"
"md","step-04-regulatory-focus","bmm","bmm/1-analysis/research/bmad-domain-research/domain-steps/step-04-regulatory-focus.md","d22035529efe91993e698b4ebf297bf2e7593eb41d185a661c357a8afc08977b"
"md","step-04-review","bmm","bmm/4-implementation/bmad-quick-dev/step-04-review.md","e62a50a37ab20eb426f5abd44179dfac51548707b5e85e6fe7817c144cbe3dae"
"md","step-04-testing","bmm","bmm/4-implementation/bmad-checkpoint-preview/step-04-testing.md","28a56e868968ea2d18add0df8c4bccced0f94b698e218df3d45ddac072ce369c"
"md","step-04-ux-alignment","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-04-ux-alignment.md","f71e5f0d77615e885ae40fdee6b04c1dd6e472c871f87b515fe869cb5f6966fb"
"md","step-05-competitive-analysis","bmm","bmm/1-analysis/research/bmad-market-research/steps/step-05-competitive-analysis.md","17532051ad232cfc859f09ac3b44f9f4d542eb24cff8d07317126ccdff0d225a"
"md","step-05-epic-quality-review","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-05-epic-quality-review.md","d8a84e57f4e3a321734b5b5d093458ceb1e338744f18954c5a204f5ce3576185"
"md","step-05-implementation-research","bmm","bmm/1-analysis/research/bmad-technical-research/technical-steps/step-05-implementation-research.md","e2b8a2c79bcebadc85f3823145980fa47d7e7be8d1c112f686c6223c8c138608"
"md","step-05-inspiration","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-05-inspiration.md","b0cadcd4665c46d2e6e89bdb45ddfdd4e4aac47b901e59aa156b935878a2b124"
"md","step-05-patterns","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-05-patterns.md","3c80aba507aa46893ef43f07c5c321b985632ef57abc82d5ee93c3d9c2911134"
"md","step-05-present","bmm","bmm/4-implementation/bmad-quick-dev/step-05-present.md","ea49ce8703d4946ec54a59ff428d52ad64ab2649f49c43230bec1f3d09e85b55"
"md","step-05-technical-trends","bmm","bmm/1-analysis/research/bmad-domain-research/domain-steps/step-05-technical-trends.md","fd6c577010171679f630805eb76e09daf823c2b9770eb716986d01f351ce1fb4"
"md","step-05-wrapup","bmm","bmm/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md","c2d4dbc17fed4f8ecbfcb09a2f9c77f0d4562bd27397270e2c27ba37cf4fe073"
"md","step-06-design-system","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-06-design-system.md","1c71e452916c5b9ed000af4dd1b83954ae16887463c73776251e1e734e7d7641"
"md","step-06-final-assessment","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md","1b9c8c2a21950f85e052e05309bfcb3bfe38157ba6fca402ccf37444e7f1cee3"
"md","step-06-research-completion","bmm","bmm/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md","4f5d3158a8462656d6959d29544ed5c92cb925641e2205e47af05c124fcd7666"
"md","step-06-research-synthesis","bmm","bmm/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md","805f2cb7005a4fb0c4da52ea38d45de863f3e840e0f2de1456c414831cdbdf59"
"md","step-06-research-synthesis","bmm","bmm/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md","64d9303ef0c9ce839dc3b2d72e94e3e5b4673e82dc7fc9f1571bd0b03b3a43a7"
"md","step-06-structure","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-06-structure.md","f8333ca290b62849c1e2eb2f770b46705b09fe0322217b699b13be047efdd03e"
"md","step-07-defining-experience","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-07-defining-experience.md","17f78d679a187cfb703c2cd30eea84d9dd683f3708d24885421239338eea4edd"
"md","step-07-validation","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-07-validation.md","dde4cd93a3015011da9a8a3237aafa2d15d0935205686231e58bf7064f81cc28"
"md","step-08-complete","bmm","bmm/3-solutioning/bmad-create-architecture/steps/step-08-complete.md","6466e5196be63e9e21b81d6fed45f1ff547d6ac739271201d6180febc857473d"
"md","step-08-visual-foundation","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-08-visual-foundation.md","985b4da65435114529056f33ff583ec4d1b29feb3550494ae741b6dbb89798a9"
"md","step-09-design-directions","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-09-design-directions.md","07962c637e69a612a904efccf6188b7f08c9e484d4d7369c74cd0de7da0cb1e3"
"md","step-10-user-journeys","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-10-user-journeys.md","eabe15745e6b68df06833bca103c704d31094c8f070c84e35f1ee9b0c28d10bd"
"md","step-11-component-strategy","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-11-component-strategy.md","52a1d0230160124496467ddbe26dd9cc4ae7d9afceaea987aad658e1bb195f59"
"md","step-12-ux-patterns","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-12-ux-patterns.md","37215fe8ea33247e9a31b5f8b8fe3b36448d7f743c18803e4d5054c201348be8"
"md","step-13-responsive-accessibility","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-13-responsive-accessibility.md","cd4d4e2a307b4cbc805c6954860c93c14a11b74b1e206c45ff89f8b81ab03a62"
"md","step-14-complete","bmm","bmm/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md","47880f82ada9f2edff8be144328f86fb683fd78aba64ec0ea9dfbff71732be00"
"md","step-oneshot","bmm","bmm/4-implementation/bmad-quick-dev/step-oneshot.md","d68648848bb8277f5b762c3528c2aa42122268037b97d252a8f4faf4edb2830e"
"md","sync-sprint-status","bmm","bmm/4-implementation/bmad-quick-dev/sync-sprint-status.md","c0f2fdb940984206a4b7aa2467fbcb7c4811f648634e43a488593974e9efff5e"
"md","template","bmm","bmm/4-implementation/bmad-create-story/template.md","29ba697368d77e88e88d0e7ac78caf7a78785a7dcfc291082aa96a62948afb67"
"md","ux-design-template","bmm","bmm/2-plan-workflows/bmad-create-ux-design/ux-design-template.md","ffa4b89376cd9db6faab682710b7ce755990b1197a8b3e16b17748656d1fca6a"
"md","validate","bmm","bmm/2-plan-workflows/bmad-prd/references/validate.md","689387606f3df71641a619e42d2841c815de1e05f8ceb87d2198383e3cb0f98b"
"md","validate-doc","bmm","bmm/1-analysis/bmad-agent-tech-writer/validate-doc.md","3b8d25f60be191716266726393f2d44b77262301b785a801631083b610d6acc5"
"md","verdict","bmm","bmm/1-analysis/bmad-prfaq/references/verdict.md","1a1cbc34090114d3a2b928456edec1563b73d84fea66c58e98780bf680f172ec"
"md","web-researcher","bmm","bmm/1-analysis/bmad-prfaq/agents/web-researcher.md","6e9127bb9bd3e4b15c701e4ced9eef328769262cd34eadc221bebe954c1f3aef"
"md","write-document","bmm","bmm/1-analysis/bmad-agent-tech-writer/write-document.md","c0ddfd981f765b82cba0921dad331cd1fa32bacdeea1f02320edfd60a0ae7e6f"
"toml","customize","bmm","bmm/1-analysis/bmad-agent-analyst/customize.toml","7191f4a60ada7dbabe083699b2461f35778af7688924dc6ce05911cf1ffc9054"
"toml","customize","bmm","bmm/1-analysis/bmad-agent-tech-writer/customize.toml","bb828f2d26a136870099226f07c61297ce88ddd335823b7549592932bbe14a2e"
"toml","customize","bmm","bmm/1-analysis/bmad-document-project/customize.toml","3bbb5044fafdd865a5fe0863df116a479f30de7fb95c9cf4213f0b87ba1ba924"
"toml","customize","bmm","bmm/1-analysis/bmad-prfaq/customize.toml","8994f925bab65bd310e5ab38781286b6137f3e0c9e89f3ab8a2f480eb05e0de5"
"toml","customize","bmm","bmm/1-analysis/bmad-product-brief/customize.toml","cdd7b417de5b280ab5bd3af65f69654b05980b5efb48077563827c16391f42d6"
"toml","customize","bmm","bmm/1-analysis/research/bmad-domain-research/customize.toml","462ef14dc1e1f6d145fdfe25e315168fd1fa278ea5f87fe0fa86689234daec2f"
"toml","customize","bmm","bmm/1-analysis/research/bmad-market-research/customize.toml","f47af9158893dd3f7ab81ac03cb07d8c76342eead61ec7c9a8a463066b7db9af"
"toml","customize","bmm","bmm/1-analysis/research/bmad-technical-research/customize.toml","7f567fddb04674f817398addbc06e6d1620883d89dcc7ac643c5a56eef5ac337"
"toml","customize","bmm","bmm/2-plan-workflows/bmad-agent-pm/customize.toml","afc2250de25241ac3577c9827e1ee62d8c7a1913de8eb23aadfffb0aadbadc37"
"toml","customize","bmm","bmm/2-plan-workflows/bmad-agent-ux-designer/customize.toml","0d36c8ce31fe332770b2755a7e952dc102f184fb3275448bf6ac2ebc783ed03b"
"toml","customize","bmm","bmm/2-plan-workflows/bmad-create-prd/customize.toml","0c990af4c0a755402abbef893850bd542744a8b6f36c0587c9f3fb6dab6bf5e7"
"toml","customize","bmm","bmm/2-plan-workflows/bmad-create-ux-design/customize.toml","e51d02120e5e79788b183f1fb377f823f7626bac7702a6aa4dc6ddd0a3b00000"
"toml","customize","bmm","bmm/2-plan-workflows/bmad-edit-prd/customize.toml","35cc2dfb3eda73a0ae4dc987cac58867b25c43ce5c5a1bf552e215826ede6c22"
"toml","customize","bmm","bmm/2-plan-workflows/bmad-prd/customize.toml","c30f48939b558a0e7e4c0a4d4f6eb08e50bad4464ca64c652247d114a398d459"
"toml","customize","bmm","bmm/2-plan-workflows/bmad-validate-prd/customize.toml","bfb3c8af5b81b2cdb9bb84f844f1b0dafc40d8c283effa6ad48db966b9446cef"
"toml","customize","bmm","bmm/3-solutioning/bmad-agent-architect/customize.toml","32e8074896a569ce59e4db62ab3b4af396798b8fa50c67449a5d90c2a7a7e074"
"toml","customize","bmm","bmm/3-solutioning/bmad-check-implementation-readiness/customize.toml","571d81087b7a6d914cf7c2f21aa26354eac385bb2f5cfeabc591892cbb98d036"
"toml","customize","bmm","bmm/3-solutioning/bmad-create-architecture/customize.toml","618be49b0286f18dfb4b6bac3d96bf1cf062994fa5278cd327be9898e651b43c"
"toml","customize","bmm","bmm/3-solutioning/bmad-create-epics-and-stories/customize.toml","da9e43605bc6aec83173050b5d37ede6aebfa9a17c5959a5029681d73e744ba3"
"toml","customize","bmm","bmm/3-solutioning/bmad-generate-project-context/customize.toml","00b6e893b93a1be7c04408f1824dad72c2c43ee02ba29210725e34394bf8225e"
"toml","customize","bmm","bmm/4-implementation/bmad-agent-dev/customize.toml","b56c0af68e2234dc57d9ebe765049ebd9df9e24909d0d9b2b4b12ddefe5291a7"
"toml","customize","bmm","bmm/4-implementation/bmad-checkpoint-preview/customize.toml","1a1e0c3a8914adda3cec925eed51287e067c7f84120de64a8df53348e7b77de5"
"toml","customize","bmm","bmm/4-implementation/bmad-code-review/customize.toml","94ad662fdf3fda372c6e24b2cbda629d0b2aa851097d02afb5905c768b1a1cde"
"toml","customize","bmm","bmm/4-implementation/bmad-correct-course/customize.toml","78095d25afb5b2d4844956f61e9ac640a62d0245174fab9406e9c289f33fd84b"
"toml","customize","bmm","bmm/4-implementation/bmad-create-story/customize.toml","1204f6de064436922f2813069778eb4eaa5f1f945aa8ea5a5ffb87897a2ba8fe"
"toml","customize","bmm","bmm/4-implementation/bmad-dev-story/customize.toml","592fa7948c4614609a59b152518f518d35e0178d6aa7c22e169d78157fd9f5aa"
"toml","customize","bmm","bmm/4-implementation/bmad-investigate/customize.toml","7ec009a4a0a9b476de98de99938f58e319dc6f410b9176910c9ed725e80519e1"
"toml","customize","bmm","bmm/4-implementation/bmad-qa-generate-e2e-tests/customize.toml","04a99926b2d79318b927434c7344dc666f143026ed99c7fcc0d6e3d68263ae66"
"toml","customize","bmm","bmm/4-implementation/bmad-quick-dev/customize.toml","5ba463713de51762eb381be787cbd657932333b808be370d851fc3e40f139e2f"
"toml","customize","bmm","bmm/4-implementation/bmad-retrospective/customize.toml","0b032c342129732820ca2db386d2d5e26d033d8ac296388fc9f2e78765fab9fb"
"toml","customize","bmm","bmm/4-implementation/bmad-sprint-planning/customize.toml","f7bde2f792e8604f26122ee792c5493e270710296044b07680db7d90e886caf3"
"toml","customize","bmm","bmm/4-implementation/bmad-sprint-status/customize.toml","96261fd227befaa4685b0a5091d3e85299d4ae8e4404176b42f5ef2e2fb501bd"
"yaml","config","bmm","bmm/config.yaml","06e1233c58826b94feede08e24d01a7044ca8d0d25f4d3c26e5656dc3b46e257"
"yaml","sprint-status-template","bmm","bmm/4-implementation/bmad-sprint-planning/sprint-status-template.yaml","deeec135d875b107618dd41278349689b5f3dcb5894d7509909417a570f46fd9"
"toml","config","config.toml","config.toml","a5289f86cfb065a86da2b66e6e7af3f4ecf97d9ebe77236d9f346ae30ed75d1c"
"toml","config.user","config.user.toml","config.user.toml","3d482b5bce4aa49577c42895f9a4d79a6859a9b6df2740b8f7829dcce50f142c"
"csv","brain-methods","core","core/bmad-brainstorming/brain-methods.csv","0ab5878b1dbc9e3fa98cb72abfc3920a586b9e2b42609211bb0516eefd542039"
"csv","methods","core","core/bmad-advanced-elicitation/methods.csv","e08b2e22fec700274982e37be608d6c3d1d4d0c04fa0bae05aa9dba2454e6141"
"csv","module-help","core","core/module-help.csv","6677d8aaeaae3ed92185ea51094bfb08c5f2fb059faf88029da30d3313c36273"
"md","compression-rules","core","core/bmad-distillator/resources/compression-rules.md","86e53d6a2072b379864766681d1cc4e1aad3d4428ecca8c46010f7364da32724"
"md","distillate-compressor","core","core/bmad-distillator/agents/distillate-compressor.md","c00da33b39a43207a224c4043d1aa4158e90e41ab421fff0ea7cc55beec81ef8"
"md","distillate-format-reference","core","core/bmad-distillator/resources/distillate-format-reference.md","47b6734ab8c327215e3578f98c3b7ef3772332dcf6710d92f5599ffb4d03a085"
"md","round-trip-reconstructor","core","core/bmad-distillator/agents/round-trip-reconstructor.md","47c83f4a37249ddac38460d8c95d162f6fc175a8919888e8090aed71bd9383bc"
"md","SKILL","core","core/bmad-advanced-elicitation/SKILL.md","705cc827cab892855d07dcc926f5c9f749c3abab33e915d6ac1574aec174896b"
"md","SKILL","core","core/bmad-brainstorming/SKILL.md","f4a2c22b40ed34cdbd3282dd6161a3b869902f3bc75b58e181fc9faf78eedd9d"
"md","SKILL","core","core/bmad-customize/SKILL.md","6461073f9322f5f2ec64477d6ae5e79c601e36e3160e492fef4bc2d928d4a9a8"
"md","SKILL","core","core/bmad-distillator/SKILL.md","756ee0706ff6b8a3d5726b465e81ba244e4eaeba21b7de0d2390473acebb5ddc"
"md","SKILL","core","core/bmad-editorial-review-prose/SKILL.md","b3687fe80567378627bc2a0c5034ae8d65dfeedcf2b6c90da077f4feca462d0c"
"md","SKILL","core","core/bmad-editorial-review-structure/SKILL.md","164444359d74f695a84faf7ea558d0eef39c75561e6b26669f97a165c6f75538"
"md","SKILL","core","core/bmad-help/SKILL.md","a766c6bd76bcfc4a49a683417440d39978a2e10bb5618dfd469fff03f96b4b4d"
"md","SKILL","core","core/bmad-index-docs/SKILL.md","a855d7060414e73ca4fe8e1a3e1cc4d0f2ce394846e52340bdf5a1317e0d234a"
"md","SKILL","core","core/bmad-party-mode/SKILL.md","bd13fd6cb0b0c61d215e4c394cf4b7cac3a465394c3918f4dc14910513ddb898"
"md","SKILL","core","core/bmad-review-adversarial-general/SKILL.md","7bffc39e6dba4d9123648c5d4d79e17c3c5b1efbd927c3fe0026c2dbb8d99cff"
"md","SKILL","core","core/bmad-review-edge-case-hunter/SKILL.md","f49ed9976f46b4cefa1fc8b4f0a495f16089905e6a7bbf4ce73b8f05c9ae3ee6"
"md","SKILL","core","core/bmad-shard-doc/SKILL.md","3a1538536514725fd4f31aded280ee56b9645fc61d114fd94aacb3ac52304e52"
"md","splitting-strategy","core","core/bmad-distillator/resources/splitting-strategy.md","26d3ed05f912cf99ff9ebe2353f2d84d70e3e852e23a32b1215c13416ad708b5"
"md","step-01-session-setup","core","core/bmad-brainstorming/steps/step-01-session-setup.md","7fd2aed9527ccdf35fc86bd4c9b27b4a530b5cfdfb90ae2b7385d3185bcd60bc"
"md","step-01b-continue","core","core/bmad-brainstorming/steps/step-01b-continue.md","49f8d78290291f974432bc8e8fce340de58ed62aa946e9e3182858bf63829920"
"md","step-02a-user-selected","core","core/bmad-brainstorming/steps/step-02a-user-selected.md","7ff3bca27286d17902ecea890494599796633e24a25ea6b31bbd6c3d2e54eba2"
"md","step-02b-ai-recommended","core","core/bmad-brainstorming/steps/step-02b-ai-recommended.md","cb77b810e0c98e080b4378999f0e250bacba4fb74c1bcb0a144cffe9989d2cbd"
"md","step-02c-random-selection","core","core/bmad-brainstorming/steps/step-02c-random-selection.md","91c6e16213911a231a41b1a55be7c939e7bbcd1463bd49cb03b5b669a90c0868"
"md","step-02d-progressive-flow","core","core/bmad-brainstorming/steps/step-02d-progressive-flow.md","6b6fbbd34bcf334d79f09e8c36ed3c9d55ddd3ebb8f8f77aa892643d1a4e3436"
"md","step-03-technique-execution","core","core/bmad-brainstorming/steps/step-03-technique-execution.md","b97afefd4ccc5234e554a3dfc5555337269ce171e730b250c756718235e9df60"
"md","step-04-idea-organization","core","core/bmad-brainstorming/steps/step-04-idea-organization.md","acb7eb6a54161213bb916cabf7d0d5084316704e792a880968fc340855cdcbbb"
"md","template","core","core/bmad-brainstorming/template.md","5c99d76963eb5fc21db96c5a68f39711dca7c6ed30e4f7d22aedee9e8bb964f9"
"md","workflow","core","core/bmad-brainstorming/workflow.md","74c87846a5cda7a4534ea592ea3125a8d8a1a88d19c94f5f4481fb28d0d16bf2"
"py","analyze_sources","core","core/bmad-distillator/scripts/analyze_sources.py","31e2a8441c3c43c2536739c580cdef6abecb18ff20e7447f42dd868875783166"
"py","list_customizable_skills","core","core/bmad-customize/scripts/list_customizable_skills.py","8787f542930b927789e7fdf12bc5a67ff08e19865903a5ad05ff2cc8fc426b66"
"py","test_analyze_sources","core","core/bmad-distillator/scripts/tests/test_analyze_sources.py","d90525311f8010aaf8d7d9212a370468a697866190bae78c35d0aae9b7f23fdf"
"py","test_list_customizable_skills","core","core/bmad-customize/scripts/tests/test_list_customizable_skills.py","b55fc2e454f245753874f359c18ade9f3ad04debd66176c6e6bf3e403ca9c812"
"yaml","config","core","core/config.yaml","5fc4b2e796091877a445427b4e3a1f403daaa177193277b6fda26180a88f71ee"
"file",".gitignore","custom","custom/.gitignore","973b03a33f142c22cf9b65be285bebadd85790b6b55be04637d2f8c716f58fab"
"py","resolve_config","scripts","scripts/resolve_config.py","8e326149d9170477ecc21aa2aa2389d8fbaa5d1cd95db2de2ad33029ce8ae528"
"py","resolve_customization","scripts","scripts/resolve_customization.py","6dbf36a2fea13392426fdbaf4f074b6d9b93488a964d2d1bff2a5c1c3a1d506e"
1 type name module path hash
2 yaml manifest _config _config/manifest.yaml b13e521bbb5ebdd1a441988d30c424d7d782db134ab9ea1e00333066b496ee78
3 csv documentation-requirements bmm bmm/1-analysis/bmad-document-project/documentation-requirements.csv d1253b99e88250f2130516b56027ed706e643bfec3d99316727a4c6ec65c6c1d
4 csv domain-complexity bmm bmm/3-solutioning/bmad-create-architecture/data/domain-complexity.csv 3dc34ed39f1fc79a51f7b8fc92087edb7cd85c4393a891d220f2e8dd5a101c70
5 csv module-help bmm bmm/module-help.csv b8c199e3bb160060887211772af2d21b785ce7a3d646699e39520f867af5400f
6 csv project-types bmm bmm/3-solutioning/bmad-create-architecture/data/project-types.csv 12343635a2f11343edb1d46906981d6f5e12b9cad2f612e13b09460b5e5106e7
7 html validation-report-template bmm bmm/2-plan-workflows/bmad-prd/assets/validation-report-template.html a80becdd8205d2d7c18738855830b096172a496b96b5ca8a0820ec55c9bb8bf9
8 json bmad-manifest bmm bmm/1-analysis/bmad-prfaq/bmad-manifest.json 7908cab6f0cd66f1f3427b4cc659975b4737bdc44f789b23c6a895fcd084c4bc
9 json project-scan-report-schema bmm bmm/1-analysis/bmad-document-project/templates/project-scan-report-schema.json 8466965321f1db22f5013869636199f67e0113706283c285a7ffbbf5efeea321
10 md architecture-decision-template bmm bmm/3-solutioning/bmad-create-architecture/architecture-decision-template.md 5d9adf90c28df61031079280fd2e49998ec3b44fb3757c6a202cda353e172e9f
11 md artifact-analyzer bmm bmm/1-analysis/bmad-prfaq/agents/artifact-analyzer.md 7bdc44830f8d593346ec0ee15e36e1e431432fcc6c38b70bb861999315c9cfa4
12 md brief-template bmm bmm/1-analysis/bmad-product-brief/assets/brief-template.md bb28edf75d023067551c8d417b19b8803324e7acfb2ad0c80f882e9134f4c1f1
13 md case-file-template bmm bmm/4-implementation/bmad-investigate/references/case-file-template.md 9dccb41f3f3fc796b42411966e51b32c4231aaceb4b6bb1880a16225a990002e
14 md checklist bmm bmm/1-analysis/bmad-document-project/checklist.md 581b0b034c25de17ac3678db2dbafedaeb113de37ddf15a4df6584cf2324a7d7
15 md checklist bmm bmm/4-implementation/bmad-correct-course/checklist.md 3e082b95def90ccb876e3101ce0bbaf797a0f03a9471e1347361897f27977327
16 md checklist bmm bmm/4-implementation/bmad-create-story/checklist.md b94e28e774c3be0288f04ea163424bece4ddead5cd3f3680d1603ed07383323a
17 md checklist bmm bmm/4-implementation/bmad-dev-story/checklist.md 630b68c6824a8785003a65553c1f335222b17be93b1bd80524c23b38bde1d8af
18 md checklist bmm bmm/4-implementation/bmad-qa-generate-e2e-tests/checklist.md b58f810aeb1040c2f6758c88aa133afce72f8cc178d3d97ff0fbaa3d943057dc
19 md checklist bmm bmm/4-implementation/bmad-sprint-planning/checklist.md 80b10aedcf88ab1641b8e5f99c9a400c8fd9014f13ca65befc5c83992e367dd7
20 md compile-epic-context bmm bmm/4-implementation/bmad-quick-dev/compile-epic-context.md 5cfda02f252941e415b80c57b4528f46226b3cbf456ad45d78fcb5a7ef4816e2
21 md customer-faq bmm bmm/1-analysis/bmad-prfaq/references/customer-faq.md 96f8565197649c58908a1d61b6cd805fd01f57da7945ba889c18d087ad597aeb
22 md deep-dive-instructions bmm bmm/1-analysis/bmad-document-project/workflows/deep-dive-instructions.md a79e24b93a25ab456062916a9dad7b6a16dc43ac0b4b555700d3c3751cff0d25
23 md deep-dive-template bmm bmm/1-analysis/bmad-document-project/templates/deep-dive-template.md 6198aa731d87d6a318b5b8d180fc29b9aa53ff0966e02391c17333818e94ffe9
24 md deep-dive-workflow bmm bmm/1-analysis/bmad-document-project/workflows/deep-dive-workflow.md a64d98dfa3b771df2853c4fa19a4e9c90d131e409e13b4c6f5e494d6ac715125
25 md discover-inputs bmm bmm/4-implementation/bmad-create-story/discover-inputs.md dfedba6a8ea05c9a91c6d202c4b29ee3ea793d8ef77575034787ae0fef280507
26 md epics-template bmm bmm/3-solutioning/bmad-create-epics-and-stories/templates/epics-template.md a804f740155156d89661fa04e7a4264a8f712c4dc227c44fd8ae804a9b0f6b72
27 md explain-concept bmm bmm/1-analysis/bmad-agent-tech-writer/explain-concept.md 6ea82dbe4e41d4bb8880cbaa62d936e40cef18f8c038be73ae6e09c462abafc9
28 md full-scan-instructions bmm bmm/1-analysis/bmad-document-project/workflows/full-scan-instructions.md 2235945df2ae261265187447ce593238e132a026a9b88d3507a1f1d6808a0263
29 md full-scan-workflow bmm bmm/1-analysis/bmad-document-project/workflows/full-scan-workflow.md 3bff88a392c16602bd44730f32483505e73e65e46e82768809c13a0a5f55608b
30 md generate-trail bmm bmm/4-implementation/bmad-checkpoint-preview/generate-trail.md 4a5936d86fbe5a85285b4535097b1e2edda8849da35586f4b588a982d7224459
31 md headless bmm bmm/2-plan-workflows/bmad-prd/references/headless.md bb7ef8900d6af6a60eb6a72c596bb3c113dde2dfbee703b99bc4ea25eabba40e
32 md headless-schemas bmm bmm/2-plan-workflows/bmad-prd/assets/headless-schemas.md 66a5b1f0ce22c7baf3a6b6a14feb7458491b1106bef437389cd6291f81eebd7f
33 md index-template bmm bmm/1-analysis/bmad-document-project/templates/index-template.md 42c8a14f53088e4fda82f26a3fe41dc8a89d4bcb7a9659dd696136378b64ee90
34 md instructions bmm bmm/1-analysis/bmad-document-project/instructions.md 9f4bc3a46559ffd44289b0d61a0f8f26f829783aa1c0e2a09dfa807fa93eb12f
35 md internal-faq bmm bmm/1-analysis/bmad-prfaq/references/internal-faq.md 26eb83f844cda1ed8efb50f4703d61713ada8a64bd27eb387f759f858b5748de
36 md mermaid-gen bmm bmm/1-analysis/bmad-agent-tech-writer/mermaid-gen.md 1d83fcc5fa842bc31ecd9fd7e45fbf013fabcadf0022d3391fff5b53b48e4b5d
37 md prd-template bmm bmm/2-plan-workflows/bmad-prd/assets/prd-template.md ebba04ba47740d9127e9bcb30f9a829c7018014208e015e8b727180d12694b14
38 md prd-validation-checklist bmm bmm/2-plan-workflows/bmad-prd/assets/prd-validation-checklist.md c5def5aa51aba5b7497f3552e06e86d86d15b9d94be26d056c0bbd1b9375c50a
39 md press-release bmm bmm/1-analysis/bmad-prfaq/references/press-release.md c406adb0e2d2cc326cbc45d0174f89d014523448ad82bc272293999d22aec596
40 md prfaq-template bmm bmm/1-analysis/bmad-prfaq/assets/prfaq-template.md b27e6964f0437ab4e78c8c0ffbe5052c28e3b3ef2fc811726cbb394d5a5c7559
41 md project-context-template bmm bmm/3-solutioning/bmad-generate-project-context/project-context-template.md 54e351394ceceb0ac4b5b8135bb6295cf2c37f739c7fd11bb895ca16d79824a5
42 md project-overview-template bmm bmm/1-analysis/bmad-document-project/templates/project-overview-template.md a7c7325b75a5a678dca391b9b69b1e3409cfbe6da95e70443ed3ace164e287b2
43 md readiness-report-template bmm bmm/3-solutioning/bmad-check-implementation-readiness/templates/readiness-report-template.md 0da97ab1e38818e642f36dc0ef24d2dae69fc6e0be59924dc2dbf44329738ff6
44 md research.template bmm bmm/1-analysis/research/bmad-domain-research/research.template.md 507bb6729476246b1ca2fca4693986d286a33af5529b6cd5cb1b0bb5ea9926ce
45 md research.template bmm bmm/1-analysis/research/bmad-market-research/research.template.md 507bb6729476246b1ca2fca4693986d286a33af5529b6cd5cb1b0bb5ea9926ce
46 md research.template bmm bmm/1-analysis/research/bmad-technical-research/research.template.md 507bb6729476246b1ca2fca4693986d286a33af5529b6cd5cb1b0bb5ea9926ce
47 md SKILL bmm bmm/1-analysis/bmad-agent-analyst/SKILL.md dff8fbd39de875ccc6735204561f2b7b877788843ecd6e8b6001e7ca7f39641e
48 md SKILL bmm bmm/1-analysis/bmad-agent-tech-writer/SKILL.md d39ffa2a931361e9fea11ef5eb96da0fc6b8dbab70dc200142404a50b78e7665
49 md SKILL bmm bmm/1-analysis/bmad-document-project/SKILL.md efe25c8c27116faeeef119078953c38e827db787338fdb2dca65c086f42e0c4b
50 md SKILL bmm bmm/1-analysis/bmad-prfaq/SKILL.md 9e864f574cc2e1411284edb625a17268aab1f894c46baac2edc3bcf1383adaca
51 md SKILL bmm bmm/1-analysis/bmad-product-brief/SKILL.md 6078a95cf9500c4c9010e834adc290fa2c793cd4e0e6f2d47e36dc477be24112
52 md SKILL bmm bmm/1-analysis/research/bmad-domain-research/SKILL.md 53d2ee5ccbd1d73290e752cff0235bfdb4928dc5ee70f347992ef15621341124
53 md SKILL bmm bmm/1-analysis/research/bmad-market-research/SKILL.md 720183e577a5f6f307f320703a5ec522190723e957cfe6ad7821d8d833dea0c7
54 md SKILL bmm bmm/1-analysis/research/bmad-technical-research/SKILL.md 1c4b61afaeedd3191db65923253be56084c64e4f832f0e2b9179671f91b5bf20
55 md SKILL bmm bmm/2-plan-workflows/bmad-agent-pm/SKILL.md 3a8550daac2df2f01a7c04b66148c1e30b50f81730f043415f2a1aba6314f8a4
56 md SKILL bmm bmm/2-plan-workflows/bmad-agent-ux-designer/SKILL.md 3ac99856f0ee9bae3ba05cefab32e90920b4a1fdf0e8c4bf9c440be31f9e5a1f
57 md SKILL bmm bmm/2-plan-workflows/bmad-create-prd/SKILL.md 143c5c2d85734021db343ff3dbf143e804eddbf9fb519fb824b70f1a9154767d
58 md SKILL bmm bmm/2-plan-workflows/bmad-create-ux-design/SKILL.md 24a4123035194bd3517a059417dcd8114db90f99314ed1a51cfdbbaff02860b2
59 md SKILL bmm bmm/2-plan-workflows/bmad-edit-prd/SKILL.md 31ecfe3c16513d994e44e648a7f2d4f4a6ccf297ca6c1826aa68ef7b053e7ff2
60 md SKILL bmm bmm/2-plan-workflows/bmad-prd/SKILL.md aac7b30d01f9d9275fb8339e65c58e4b21c4ae22ddc5810a05a464da5951dec0
61 md SKILL bmm bmm/2-plan-workflows/bmad-validate-prd/SKILL.md 931439b8c84bc454d0596a1ad35d6583c1926c941f2928c54b31aea702c24b53
62 md SKILL bmm bmm/3-solutioning/bmad-agent-architect/SKILL.md 1bb79ab9d9ae3180a6142d9d20c2a5cb15ad5d5f28e38a7f847295dd04d2299f
63 md SKILL bmm bmm/3-solutioning/bmad-check-implementation-readiness/SKILL.md c683db5628781e6dba6fad388ed65cf7561a208e773f72c786918743e379abef
64 md SKILL bmm bmm/3-solutioning/bmad-create-architecture/SKILL.md b12711f1655cb809bb74a3e11e40943dce225ccba9d01974d5bf521a5867ba23
65 md SKILL bmm bmm/3-solutioning/bmad-create-epics-and-stories/SKILL.md 9db05e4a63ead6e416009fdec250bb9b749b361331964b601559ffeedd2f1c0f
66 md SKILL bmm bmm/3-solutioning/bmad-generate-project-context/SKILL.md 3d8c20e13e62981464365509fc5e1dfd31e70a94e8c08b048a3e3866ea00ff43
67 md SKILL bmm bmm/4-implementation/bmad-agent-dev/SKILL.md ab47a75e3031bc58a9783e4f8dece8d44dfdacb7ba74cd56f766b260f9993218
68 md SKILL bmm bmm/4-implementation/bmad-checkpoint-preview/SKILL.md fcdd92af6b97ae64f99a89c37deef0074a974ffa577263e6c79c4b6734c63bda
69 md SKILL bmm bmm/4-implementation/bmad-code-review/SKILL.md 6801f6742156f125185b1eb6e10c58dfde4ff6545b2e6278acf68cbcfbe0abe4
70 md SKILL bmm bmm/4-implementation/bmad-correct-course/SKILL.md 25be5dd528b5c3996e5fa02fc30b72377f688436742c6962f4a1e9a2dac46e55
71 md SKILL bmm bmm/4-implementation/bmad-create-story/SKILL.md 81517ba8ef137a15002d6d21ef18a1e88190c74ac9e0c5b29227e4059870809a
72 md SKILL bmm bmm/4-implementation/bmad-dev-story/SKILL.md 5e7d3bca5051ff8f009bcf608e3fdac3260c2aa6faa8df170a954fe03a4a03f8
73 md SKILL bmm bmm/4-implementation/bmad-investigate/SKILL.md 79905849c7ce4ef4c1c647365c52837a8ede2af01a25ce6267dfe8ba64683652
74 md SKILL bmm bmm/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md ad6025f0279ef9fac2f6c76d8f612c37ccc23f8062b7ac0ac40708b49cc7db80
75 md SKILL bmm bmm/4-implementation/bmad-quick-dev/SKILL.md 2b2e57e5327fc69962e6529a0c17498185b571646a9c7360e5c15be14ea7e63b
76 md SKILL bmm bmm/4-implementation/bmad-retrospective/SKILL.md 997308d999545fa6c82b8652a2973e1d89d82cfbf892531e25e491291a95c33e
77 md SKILL bmm bmm/4-implementation/bmad-sprint-planning/SKILL.md 21946cdaef8115deee6ce322c460e8af39368509d700c778cd7109f50a6821eb
78 md SKILL bmm bmm/4-implementation/bmad-sprint-status/SKILL.md 2fe141bc2033ea341a73fb93349fcee0296d8d8714fcf984ab056bc0abae0a19
79 md source-tree-template bmm bmm/1-analysis/bmad-document-project/templates/source-tree-template.md 109bc335ebb22f932b37c24cdc777a351264191825444a4d147c9b82a1e2ad7a
80 md spec-template bmm bmm/4-implementation/bmad-quick-dev/spec-template.md 3ee15d5a63cf5eeee74149c590668fc61d0e44023eac12988a1ca2a9438a9d39
81 md step-01-clarify-and-route bmm bmm/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md b5eb9f0cecf2a462885b8e7b4c21abbaec1e95c8abbd76bc410d9090fd8379f0
82 md step-01-discover bmm bmm/3-solutioning/bmad-generate-project-context/steps/step-01-discover.md 8b2c8c7375f8a3c28411250675a28c0d0a9174e6c4e67b3d53619888439c4613
83 md step-01-document-discovery bmm bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-01-document-discovery.md c763b512d55906122433cb65c1bcd5b5b283e45eacdc07281c8ec7596b6b3980
84 md step-01-gather-context bmm bmm/4-implementation/bmad-code-review/steps/step-01-gather-context.md d0ee7558605e9d48b5b6f15d9b535542eb6d922613f529bb520326eacade4171
85 md step-01-init bmm bmm/1-analysis/research/bmad-domain-research/domain-steps/step-01-init.md efee243f13ef54401ded88f501967b8bc767460cec5561b2107fc03fe7b7eab1
86 md step-01-init bmm bmm/1-analysis/research/bmad-market-research/steps/step-01-init.md 64d5501aea0c0005db23a0a4d9ee84cf4e9239f553c994ecc6b1356917967ccc
87 md step-01-init bmm bmm/1-analysis/research/bmad-technical-research/technical-steps/step-01-init.md c9a1627ecd26227e944375eb691e7ee6bc9f5db29a428a5d53e5d6aef8bb9697
88 md step-01-init bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-01-init.md 0b257533a0ce34d792f621da35325ec11cb883653e3ad546221ee1f0dee5edcd
89 md step-01-init bmm bmm/3-solutioning/bmad-create-architecture/steps/step-01-init.md 5119205b712ebda0cd241c3daad217bb0f6fa9e6cb41d6635aec6b7fe83b838a
90 md step-01-orientation bmm bmm/4-implementation/bmad-checkpoint-preview/step-01-orientation.md d9e3b949c36d49a025f3535773af2b51888fe4ce616b6d6d69683a122716b1d2
91 md step-01-validate-prerequisites bmm bmm/3-solutioning/bmad-create-epics-and-stories/steps/step-01-validate-prerequisites.md 5c2aabc871363d84fc2e12fd83a3889e9d752b6bd330e31a0067c96204dd4880
92 md step-01b-continue bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-01b-continue.md 4d42c6b83eaa720975bf2206a7eea1a8c73ae922668cc2ef03d34c49ab066c19
93 md step-01b-continue bmm bmm/3-solutioning/bmad-create-architecture/steps/step-01b-continue.md 4bf216008297dcea25f8be693109cf17879c621865b302c994cdd15aa5124e5f
94 md step-02-context bmm bmm/3-solutioning/bmad-create-architecture/steps/step-02-context.md 4381c5128de7d5c02ac806a1263e3965754bd2598954f3188219fbd87567e5c9
95 md step-02-customer-behavior bmm bmm/1-analysis/research/bmad-market-research/steps/step-02-customer-behavior.md bac4de244049f90d1f2eb95e2cc9389cc84966d9538077fef1ec9c35e4533849
96 md step-02-design-epics bmm bmm/3-solutioning/bmad-create-epics-and-stories/steps/step-02-design-epics.md 7c66987808c1f84e853fe54b7aff26d209196d450b5644704110f124a15179bc
97 md step-02-discovery bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-02-discovery.md 9ffd5b31cc869b564e4d78cdc70767f0fb1b04db4c40201ccfa9dde75739fa8d
98 md step-02-domain-analysis bmm bmm/1-analysis/research/bmad-domain-research/domain-steps/step-02-domain-analysis.md 385a288d9bbb0adf050bcce4da4dad198a9151822f9766900404636f2b0c7f9d
99 md step-02-generate bmm bmm/3-solutioning/bmad-generate-project-context/steps/step-02-generate.md b1f063edae66a74026b67a79a245cec7ee85438bafcacfc70dcf6006b495e060
100 md step-02-plan bmm bmm/4-implementation/bmad-quick-dev/step-02-plan.md 72f4df415adceaaf554166983559e058c6a019d783d0f87cf42c401db3c5f52c
101 md step-02-prd-analysis bmm bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md 38be2bf4b924c0b5b395b57d68f685d790ade7b1a6c10993d3c550675f87d954
102 md step-02-review bmm bmm/4-implementation/bmad-code-review/steps/step-02-review.md 1883758e0e91fba439497d04417f03d778f3f6fb12754f49aa8cfa5e15489f70
103 md step-02-technical-overview bmm bmm/1-analysis/research/bmad-technical-research/technical-steps/step-02-technical-overview.md 9c7582241038b16280cddce86f2943216541275daf0a935dcab78f362904b305
104 md step-02-walkthrough bmm bmm/4-implementation/bmad-checkpoint-preview/step-02-walkthrough.md 66cf893f8f968ee81034e9ccd8c20415692c3a8c23a9a143c2245fe6c800acdc
105 md step-03-competitive-landscape bmm bmm/1-analysis/research/bmad-domain-research/domain-steps/step-03-competitive-landscape.md f10aa088ba00c59491507f6519fb314139f8be6807958bb5fd1b66bff2267749
106 md step-03-complete bmm bmm/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md e61463db76a8fa060411aa24127aee936d646b97564e9e2a883494ea50e68464
107 md step-03-core-experience bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-03-core-experience.md 1f58c8a2f6872f468629ecb67e94f793af9d10d2804fe3e138eba03c090e00c5
108 md step-03-create-stories bmm bmm/3-solutioning/bmad-create-epics-and-stories/steps/step-03-create-stories.md c5b787a82e4e49ed9cd9c028321ee1689f32b8cd69d89eea609b37cd3d481afc
109 md step-03-customer-pain-points bmm bmm/1-analysis/research/bmad-market-research/steps/step-03-customer-pain-points.md 5b2418ccaaa89291c593efed0311b3895faad1e9181800d382da823a8eb1312a
110 md step-03-detail-pass bmm bmm/4-implementation/bmad-checkpoint-preview/step-03-detail-pass.md d48163b9f305f15af57729a8443142e47beb6c3e977554afe12b39ee49cb9fc0
111 md step-03-epic-coverage-validation bmm bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md 7b187f03a47cba0325fcfd10240410db9c59d93768342fc2dd3de2a01ec23356
112 md step-03-implement bmm bmm/4-implementation/bmad-quick-dev/step-03-implement.md 4d848865eafe5eeba7d83529c766bd410a9cc20a05de1d6a764954c7275b4749
113 md step-03-integration-patterns bmm bmm/1-analysis/research/bmad-technical-research/technical-steps/step-03-integration-patterns.md 005d517a2f962e2172e26b23d10d5e6684c7736c0d3982e27b2e72d905814ad9
114 md step-03-starter bmm bmm/3-solutioning/bmad-create-architecture/steps/step-03-starter.md b7727e0f37bc5325e15abad1c54bef716d617df423336090189efd1d307a0b3f
115 md step-03-triage bmm bmm/4-implementation/bmad-code-review/steps/step-03-triage.md 91eaa27f6a167702ead00da9e93565c9bff79dce92c02eccbca61b1d1ed39a80
116 md step-04-architectural-patterns bmm bmm/1-analysis/research/bmad-technical-research/technical-steps/step-04-architectural-patterns.md 4636f23e9c585a7a0c90437a660609d913f16362c3557fc2e71d408d6b9f46ce
117 md step-04-customer-decisions bmm bmm/1-analysis/research/bmad-market-research/steps/step-04-customer-decisions.md f0bc25f2179b7490e7a6704159a32fc9e83ab616022355ed53acfe8e2f7059d5
118 md step-04-decisions bmm bmm/3-solutioning/bmad-create-architecture/steps/step-04-decisions.md 7fc0ebb63ab5ad0efc470f1063c15f14f52f5d855da2382fd17576cf060a8763
119 md step-04-emotional-response bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-04-emotional-response.md 75724811b170c8897e230a49e968e1db357fef3387008b0906b5ff79a43dbff9
120 md step-04-final-validation bmm bmm/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md 6b83fcf177c24c6b43c2eb0df2efc1430cb0f357b3ab20ee8a5c5483c3fee079
121 md step-04-present bmm bmm/4-implementation/bmad-code-review/steps/step-04-present.md eee5687e9d7da9a0aeffd78180b8289fca804fc2d59896db5f65a8dd476e4869
122 md step-04-regulatory-focus bmm bmm/1-analysis/research/bmad-domain-research/domain-steps/step-04-regulatory-focus.md d22035529efe91993e698b4ebf297bf2e7593eb41d185a661c357a8afc08977b
123 md step-04-review bmm bmm/4-implementation/bmad-quick-dev/step-04-review.md e62a50a37ab20eb426f5abd44179dfac51548707b5e85e6fe7817c144cbe3dae
124 md step-04-testing bmm bmm/4-implementation/bmad-checkpoint-preview/step-04-testing.md 28a56e868968ea2d18add0df8c4bccced0f94b698e218df3d45ddac072ce369c
125 md step-04-ux-alignment bmm bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-04-ux-alignment.md f71e5f0d77615e885ae40fdee6b04c1dd6e472c871f87b515fe869cb5f6966fb
126 md step-05-competitive-analysis bmm bmm/1-analysis/research/bmad-market-research/steps/step-05-competitive-analysis.md 17532051ad232cfc859f09ac3b44f9f4d542eb24cff8d07317126ccdff0d225a
127 md step-05-epic-quality-review bmm bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-05-epic-quality-review.md d8a84e57f4e3a321734b5b5d093458ceb1e338744f18954c5a204f5ce3576185
128 md step-05-implementation-research bmm bmm/1-analysis/research/bmad-technical-research/technical-steps/step-05-implementation-research.md e2b8a2c79bcebadc85f3823145980fa47d7e7be8d1c112f686c6223c8c138608
129 md step-05-inspiration bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-05-inspiration.md b0cadcd4665c46d2e6e89bdb45ddfdd4e4aac47b901e59aa156b935878a2b124
130 md step-05-patterns bmm bmm/3-solutioning/bmad-create-architecture/steps/step-05-patterns.md 3c80aba507aa46893ef43f07c5c321b985632ef57abc82d5ee93c3d9c2911134
131 md step-05-present bmm bmm/4-implementation/bmad-quick-dev/step-05-present.md ea49ce8703d4946ec54a59ff428d52ad64ab2649f49c43230bec1f3d09e85b55
132 md step-05-technical-trends bmm bmm/1-analysis/research/bmad-domain-research/domain-steps/step-05-technical-trends.md fd6c577010171679f630805eb76e09daf823c2b9770eb716986d01f351ce1fb4
133 md step-05-wrapup bmm bmm/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md c2d4dbc17fed4f8ecbfcb09a2f9c77f0d4562bd27397270e2c27ba37cf4fe073
134 md step-06-design-system bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-06-design-system.md 1c71e452916c5b9ed000af4dd1b83954ae16887463c73776251e1e734e7d7641
135 md step-06-final-assessment bmm bmm/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md 1b9c8c2a21950f85e052e05309bfcb3bfe38157ba6fca402ccf37444e7f1cee3
136 md step-06-research-completion bmm bmm/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md 4f5d3158a8462656d6959d29544ed5c92cb925641e2205e47af05c124fcd7666
137 md step-06-research-synthesis bmm bmm/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md 805f2cb7005a4fb0c4da52ea38d45de863f3e840e0f2de1456c414831cdbdf59
138 md step-06-research-synthesis bmm bmm/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md 64d9303ef0c9ce839dc3b2d72e94e3e5b4673e82dc7fc9f1571bd0b03b3a43a7
139 md step-06-structure bmm bmm/3-solutioning/bmad-create-architecture/steps/step-06-structure.md f8333ca290b62849c1e2eb2f770b46705b09fe0322217b699b13be047efdd03e
140 md step-07-defining-experience bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-07-defining-experience.md 17f78d679a187cfb703c2cd30eea84d9dd683f3708d24885421239338eea4edd
141 md step-07-validation bmm bmm/3-solutioning/bmad-create-architecture/steps/step-07-validation.md dde4cd93a3015011da9a8a3237aafa2d15d0935205686231e58bf7064f81cc28
142 md step-08-complete bmm bmm/3-solutioning/bmad-create-architecture/steps/step-08-complete.md 6466e5196be63e9e21b81d6fed45f1ff547d6ac739271201d6180febc857473d
143 md step-08-visual-foundation bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-08-visual-foundation.md 985b4da65435114529056f33ff583ec4d1b29feb3550494ae741b6dbb89798a9
144 md step-09-design-directions bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-09-design-directions.md 07962c637e69a612a904efccf6188b7f08c9e484d4d7369c74cd0de7da0cb1e3
145 md step-10-user-journeys bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-10-user-journeys.md eabe15745e6b68df06833bca103c704d31094c8f070c84e35f1ee9b0c28d10bd
146 md step-11-component-strategy bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-11-component-strategy.md 52a1d0230160124496467ddbe26dd9cc4ae7d9afceaea987aad658e1bb195f59
147 md step-12-ux-patterns bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-12-ux-patterns.md 37215fe8ea33247e9a31b5f8b8fe3b36448d7f743c18803e4d5054c201348be8
148 md step-13-responsive-accessibility bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-13-responsive-accessibility.md cd4d4e2a307b4cbc805c6954860c93c14a11b74b1e206c45ff89f8b81ab03a62
149 md step-14-complete bmm bmm/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md 47880f82ada9f2edff8be144328f86fb683fd78aba64ec0ea9dfbff71732be00
150 md step-oneshot bmm bmm/4-implementation/bmad-quick-dev/step-oneshot.md d68648848bb8277f5b762c3528c2aa42122268037b97d252a8f4faf4edb2830e
151 md sync-sprint-status bmm bmm/4-implementation/bmad-quick-dev/sync-sprint-status.md c0f2fdb940984206a4b7aa2467fbcb7c4811f648634e43a488593974e9efff5e
152 md template bmm bmm/4-implementation/bmad-create-story/template.md 29ba697368d77e88e88d0e7ac78caf7a78785a7dcfc291082aa96a62948afb67
153 md ux-design-template bmm bmm/2-plan-workflows/bmad-create-ux-design/ux-design-template.md ffa4b89376cd9db6faab682710b7ce755990b1197a8b3e16b17748656d1fca6a
154 md validate bmm bmm/2-plan-workflows/bmad-prd/references/validate.md 689387606f3df71641a619e42d2841c815de1e05f8ceb87d2198383e3cb0f98b
155 md validate-doc bmm bmm/1-analysis/bmad-agent-tech-writer/validate-doc.md 3b8d25f60be191716266726393f2d44b77262301b785a801631083b610d6acc5
156 md verdict bmm bmm/1-analysis/bmad-prfaq/references/verdict.md 1a1cbc34090114d3a2b928456edec1563b73d84fea66c58e98780bf680f172ec
157 md web-researcher bmm bmm/1-analysis/bmad-prfaq/agents/web-researcher.md 6e9127bb9bd3e4b15c701e4ced9eef328769262cd34eadc221bebe954c1f3aef
158 md write-document bmm bmm/1-analysis/bmad-agent-tech-writer/write-document.md c0ddfd981f765b82cba0921dad331cd1fa32bacdeea1f02320edfd60a0ae7e6f
159 toml customize bmm bmm/1-analysis/bmad-agent-analyst/customize.toml 7191f4a60ada7dbabe083699b2461f35778af7688924dc6ce05911cf1ffc9054
160 toml customize bmm bmm/1-analysis/bmad-agent-tech-writer/customize.toml bb828f2d26a136870099226f07c61297ce88ddd335823b7549592932bbe14a2e
161 toml customize bmm bmm/1-analysis/bmad-document-project/customize.toml 3bbb5044fafdd865a5fe0863df116a479f30de7fb95c9cf4213f0b87ba1ba924
162 toml customize bmm bmm/1-analysis/bmad-prfaq/customize.toml 8994f925bab65bd310e5ab38781286b6137f3e0c9e89f3ab8a2f480eb05e0de5
163 toml customize bmm bmm/1-analysis/bmad-product-brief/customize.toml cdd7b417de5b280ab5bd3af65f69654b05980b5efb48077563827c16391f42d6
164 toml customize bmm bmm/1-analysis/research/bmad-domain-research/customize.toml 462ef14dc1e1f6d145fdfe25e315168fd1fa278ea5f87fe0fa86689234daec2f
165 toml customize bmm bmm/1-analysis/research/bmad-market-research/customize.toml f47af9158893dd3f7ab81ac03cb07d8c76342eead61ec7c9a8a463066b7db9af
166 toml customize bmm bmm/1-analysis/research/bmad-technical-research/customize.toml 7f567fddb04674f817398addbc06e6d1620883d89dcc7ac643c5a56eef5ac337
167 toml customize bmm bmm/2-plan-workflows/bmad-agent-pm/customize.toml afc2250de25241ac3577c9827e1ee62d8c7a1913de8eb23aadfffb0aadbadc37
168 toml customize bmm bmm/2-plan-workflows/bmad-agent-ux-designer/customize.toml 0d36c8ce31fe332770b2755a7e952dc102f184fb3275448bf6ac2ebc783ed03b
169 toml customize bmm bmm/2-plan-workflows/bmad-create-prd/customize.toml 0c990af4c0a755402abbef893850bd542744a8b6f36c0587c9f3fb6dab6bf5e7
170 toml customize bmm bmm/2-plan-workflows/bmad-create-ux-design/customize.toml e51d02120e5e79788b183f1fb377f823f7626bac7702a6aa4dc6ddd0a3b00000
171 toml customize bmm bmm/2-plan-workflows/bmad-edit-prd/customize.toml 35cc2dfb3eda73a0ae4dc987cac58867b25c43ce5c5a1bf552e215826ede6c22
172 toml customize bmm bmm/2-plan-workflows/bmad-prd/customize.toml c30f48939b558a0e7e4c0a4d4f6eb08e50bad4464ca64c652247d114a398d459
173 toml customize bmm bmm/2-plan-workflows/bmad-validate-prd/customize.toml bfb3c8af5b81b2cdb9bb84f844f1b0dafc40d8c283effa6ad48db966b9446cef
174 toml customize bmm bmm/3-solutioning/bmad-agent-architect/customize.toml 32e8074896a569ce59e4db62ab3b4af396798b8fa50c67449a5d90c2a7a7e074
175 toml customize bmm bmm/3-solutioning/bmad-check-implementation-readiness/customize.toml 571d81087b7a6d914cf7c2f21aa26354eac385bb2f5cfeabc591892cbb98d036
176 toml customize bmm bmm/3-solutioning/bmad-create-architecture/customize.toml 618be49b0286f18dfb4b6bac3d96bf1cf062994fa5278cd327be9898e651b43c
177 toml customize bmm bmm/3-solutioning/bmad-create-epics-and-stories/customize.toml da9e43605bc6aec83173050b5d37ede6aebfa9a17c5959a5029681d73e744ba3
178 toml customize bmm bmm/3-solutioning/bmad-generate-project-context/customize.toml 00b6e893b93a1be7c04408f1824dad72c2c43ee02ba29210725e34394bf8225e
179 toml customize bmm bmm/4-implementation/bmad-agent-dev/customize.toml b56c0af68e2234dc57d9ebe765049ebd9df9e24909d0d9b2b4b12ddefe5291a7
180 toml customize bmm bmm/4-implementation/bmad-checkpoint-preview/customize.toml 1a1e0c3a8914adda3cec925eed51287e067c7f84120de64a8df53348e7b77de5
181 toml customize bmm bmm/4-implementation/bmad-code-review/customize.toml 94ad662fdf3fda372c6e24b2cbda629d0b2aa851097d02afb5905c768b1a1cde
182 toml customize bmm bmm/4-implementation/bmad-correct-course/customize.toml 78095d25afb5b2d4844956f61e9ac640a62d0245174fab9406e9c289f33fd84b
183 toml customize bmm bmm/4-implementation/bmad-create-story/customize.toml 1204f6de064436922f2813069778eb4eaa5f1f945aa8ea5a5ffb87897a2ba8fe
184 toml customize bmm bmm/4-implementation/bmad-dev-story/customize.toml 592fa7948c4614609a59b152518f518d35e0178d6aa7c22e169d78157fd9f5aa
185 toml customize bmm bmm/4-implementation/bmad-investigate/customize.toml 7ec009a4a0a9b476de98de99938f58e319dc6f410b9176910c9ed725e80519e1
186 toml customize bmm bmm/4-implementation/bmad-qa-generate-e2e-tests/customize.toml 04a99926b2d79318b927434c7344dc666f143026ed99c7fcc0d6e3d68263ae66
187 toml customize bmm bmm/4-implementation/bmad-quick-dev/customize.toml 5ba463713de51762eb381be787cbd657932333b808be370d851fc3e40f139e2f
188 toml customize bmm bmm/4-implementation/bmad-retrospective/customize.toml 0b032c342129732820ca2db386d2d5e26d033d8ac296388fc9f2e78765fab9fb
189 toml customize bmm bmm/4-implementation/bmad-sprint-planning/customize.toml f7bde2f792e8604f26122ee792c5493e270710296044b07680db7d90e886caf3
190 toml customize bmm bmm/4-implementation/bmad-sprint-status/customize.toml 96261fd227befaa4685b0a5091d3e85299d4ae8e4404176b42f5ef2e2fb501bd
191 yaml config bmm bmm/config.yaml 06e1233c58826b94feede08e24d01a7044ca8d0d25f4d3c26e5656dc3b46e257
192 yaml sprint-status-template bmm bmm/4-implementation/bmad-sprint-planning/sprint-status-template.yaml deeec135d875b107618dd41278349689b5f3dcb5894d7509909417a570f46fd9
193 toml config config.toml config.toml a5289f86cfb065a86da2b66e6e7af3f4ecf97d9ebe77236d9f346ae30ed75d1c
194 toml config.user config.user.toml config.user.toml 3d482b5bce4aa49577c42895f9a4d79a6859a9b6df2740b8f7829dcce50f142c
195 csv brain-methods core core/bmad-brainstorming/brain-methods.csv 0ab5878b1dbc9e3fa98cb72abfc3920a586b9e2b42609211bb0516eefd542039
196 csv methods core core/bmad-advanced-elicitation/methods.csv e08b2e22fec700274982e37be608d6c3d1d4d0c04fa0bae05aa9dba2454e6141
197 csv module-help core core/module-help.csv 6677d8aaeaae3ed92185ea51094bfb08c5f2fb059faf88029da30d3313c36273
198 md compression-rules core core/bmad-distillator/resources/compression-rules.md 86e53d6a2072b379864766681d1cc4e1aad3d4428ecca8c46010f7364da32724
199 md distillate-compressor core core/bmad-distillator/agents/distillate-compressor.md c00da33b39a43207a224c4043d1aa4158e90e41ab421fff0ea7cc55beec81ef8
200 md distillate-format-reference core core/bmad-distillator/resources/distillate-format-reference.md 47b6734ab8c327215e3578f98c3b7ef3772332dcf6710d92f5599ffb4d03a085
201 md round-trip-reconstructor core core/bmad-distillator/agents/round-trip-reconstructor.md 47c83f4a37249ddac38460d8c95d162f6fc175a8919888e8090aed71bd9383bc
202 md SKILL core core/bmad-advanced-elicitation/SKILL.md 705cc827cab892855d07dcc926f5c9f749c3abab33e915d6ac1574aec174896b
203 md SKILL core core/bmad-brainstorming/SKILL.md f4a2c22b40ed34cdbd3282dd6161a3b869902f3bc75b58e181fc9faf78eedd9d
204 md SKILL core core/bmad-customize/SKILL.md 6461073f9322f5f2ec64477d6ae5e79c601e36e3160e492fef4bc2d928d4a9a8
205 md SKILL core core/bmad-distillator/SKILL.md 756ee0706ff6b8a3d5726b465e81ba244e4eaeba21b7de0d2390473acebb5ddc
206 md SKILL core core/bmad-editorial-review-prose/SKILL.md b3687fe80567378627bc2a0c5034ae8d65dfeedcf2b6c90da077f4feca462d0c
207 md SKILL core core/bmad-editorial-review-structure/SKILL.md 164444359d74f695a84faf7ea558d0eef39c75561e6b26669f97a165c6f75538
208 md SKILL core core/bmad-help/SKILL.md a766c6bd76bcfc4a49a683417440d39978a2e10bb5618dfd469fff03f96b4b4d
209 md SKILL core core/bmad-index-docs/SKILL.md a855d7060414e73ca4fe8e1a3e1cc4d0f2ce394846e52340bdf5a1317e0d234a
210 md SKILL core core/bmad-party-mode/SKILL.md bd13fd6cb0b0c61d215e4c394cf4b7cac3a465394c3918f4dc14910513ddb898
211 md SKILL core core/bmad-review-adversarial-general/SKILL.md 7bffc39e6dba4d9123648c5d4d79e17c3c5b1efbd927c3fe0026c2dbb8d99cff
212 md SKILL core core/bmad-review-edge-case-hunter/SKILL.md f49ed9976f46b4cefa1fc8b4f0a495f16089905e6a7bbf4ce73b8f05c9ae3ee6
213 md SKILL core core/bmad-shard-doc/SKILL.md 3a1538536514725fd4f31aded280ee56b9645fc61d114fd94aacb3ac52304e52
214 md splitting-strategy core core/bmad-distillator/resources/splitting-strategy.md 26d3ed05f912cf99ff9ebe2353f2d84d70e3e852e23a32b1215c13416ad708b5
215 md step-01-session-setup core core/bmad-brainstorming/steps/step-01-session-setup.md 7fd2aed9527ccdf35fc86bd4c9b27b4a530b5cfdfb90ae2b7385d3185bcd60bc
216 md step-01b-continue core core/bmad-brainstorming/steps/step-01b-continue.md 49f8d78290291f974432bc8e8fce340de58ed62aa946e9e3182858bf63829920
217 md step-02a-user-selected core core/bmad-brainstorming/steps/step-02a-user-selected.md 7ff3bca27286d17902ecea890494599796633e24a25ea6b31bbd6c3d2e54eba2
218 md step-02b-ai-recommended core core/bmad-brainstorming/steps/step-02b-ai-recommended.md cb77b810e0c98e080b4378999f0e250bacba4fb74c1bcb0a144cffe9989d2cbd
219 md step-02c-random-selection core core/bmad-brainstorming/steps/step-02c-random-selection.md 91c6e16213911a231a41b1a55be7c939e7bbcd1463bd49cb03b5b669a90c0868
220 md step-02d-progressive-flow core core/bmad-brainstorming/steps/step-02d-progressive-flow.md 6b6fbbd34bcf334d79f09e8c36ed3c9d55ddd3ebb8f8f77aa892643d1a4e3436
221 md step-03-technique-execution core core/bmad-brainstorming/steps/step-03-technique-execution.md b97afefd4ccc5234e554a3dfc5555337269ce171e730b250c756718235e9df60
222 md step-04-idea-organization core core/bmad-brainstorming/steps/step-04-idea-organization.md acb7eb6a54161213bb916cabf7d0d5084316704e792a880968fc340855cdcbbb
223 md template core core/bmad-brainstorming/template.md 5c99d76963eb5fc21db96c5a68f39711dca7c6ed30e4f7d22aedee9e8bb964f9
224 md workflow core core/bmad-brainstorming/workflow.md 74c87846a5cda7a4534ea592ea3125a8d8a1a88d19c94f5f4481fb28d0d16bf2
225 py analyze_sources core core/bmad-distillator/scripts/analyze_sources.py 31e2a8441c3c43c2536739c580cdef6abecb18ff20e7447f42dd868875783166
226 py list_customizable_skills core core/bmad-customize/scripts/list_customizable_skills.py 8787f542930b927789e7fdf12bc5a67ff08e19865903a5ad05ff2cc8fc426b66
227 py test_analyze_sources core core/bmad-distillator/scripts/tests/test_analyze_sources.py d90525311f8010aaf8d7d9212a370468a697866190bae78c35d0aae9b7f23fdf
228 py test_list_customizable_skills core core/bmad-customize/scripts/tests/test_list_customizable_skills.py b55fc2e454f245753874f359c18ade9f3ad04debd66176c6e6bf3e403ca9c812
229 yaml config core core/config.yaml 5fc4b2e796091877a445427b4e3a1f403daaa177193277b6fda26180a88f71ee
230 file .gitignore custom custom/.gitignore 973b03a33f142c22cf9b65be285bebadd85790b6b55be04637d2f8c716f58fab
231 py resolve_config scripts scripts/resolve_config.py 8e326149d9170477ecc21aa2aa2389d8fbaa5d1cd95db2de2ad33029ce8ae528
232 py resolve_customization scripts scripts/resolve_customization.py 6dbf36a2fea13392426fdbaf4f074b6d9b93488a964d2d1bff2a5c1c3a1d506e
+21
View File
@@ -0,0 +1,21 @@
installation:
version: 6.7.1
installDate: 2026-05-25T16:02:56.785Z
lastUpdated: 2026-05-25T16:02:56.785Z
modules:
- name: core
version: 6.7.1
installDate: 2026-05-25T16:02:56.768Z
lastUpdated: 2026-05-25T16:02:56.784Z
source: built-in
npmPackage: null
repoUrl: null
- name: bmm
version: 6.7.1
installDate: 2026-05-25T16:02:56.751Z
lastUpdated: 2026-05-25T16:02:56.785Z
source: built-in
npmPackage: null
repoUrl: null
ides:
- claude-code
+45
View File
@@ -0,0 +1,45 @@
canonicalId,name,description,module,path
"bmad-advanced-elicitation","bmad-advanced-elicitation","Push the LLM to reconsider, refine, and improve its recent output. Use when user asks for deeper critique or mentions a known deeper critique method, e.g. socratic, first principles, pre-mortem, red team.","core","_bmad/core/bmad-advanced-elicitation/SKILL.md"
"bmad-brainstorming","bmad-brainstorming","Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods. Use when the user says help me brainstorm or help me ideate.","core","_bmad/core/bmad-brainstorming/SKILL.md"
"bmad-customize","bmad-customize","Authors and updates customization overrides for installed BMad skills. Use when the user says 'customize bmad', 'override a skill', 'change agent behavior', or 'customize a workflow'.","core","_bmad/core/bmad-customize/SKILL.md"
"bmad-distillator","bmad-distillator","Lossless LLM-optimized compression of source documents. Use when the user requests to 'distill documents' or 'create a distillate'.","core","_bmad/core/bmad-distillator/SKILL.md"
"bmad-editorial-review-prose","bmad-editorial-review-prose","Clinical copy-editor that reviews text for communication issues. Use when user says review for prose or improve the prose","core","_bmad/core/bmad-editorial-review-prose/SKILL.md"
"bmad-editorial-review-structure","bmad-editorial-review-structure","Structural editor that proposes cuts, reorganization, and simplification while preserving comprehension. Use when user requests structural review or editorial review of structure","core","_bmad/core/bmad-editorial-review-structure/SKILL.md"
"bmad-help","bmad-help","Analyzes current state and user query to answer BMad questions or recommend the next skill(s) to use. Use when user asks for help, bmad help, what to do next, or what to start with in BMad.","core","_bmad/core/bmad-help/SKILL.md"
"bmad-index-docs","bmad-index-docs","Generates or updates an index.md to reference all docs in the folder. Use if user requests to create or update an index of all files in a specific folder","core","_bmad/core/bmad-index-docs/SKILL.md"
"bmad-party-mode","bmad-party-mode","Orchestrates group discussions between installed BMAD agents, enabling natural multi-agent conversations where each agent is a real subagent with independent thinking. Use when user requests party mode, wants multiple agent perspectives, group discussion, roundtable, or multi-agent conversation about their project.","core","_bmad/core/bmad-party-mode/SKILL.md"
"bmad-review-adversarial-general","bmad-review-adversarial-general","Perform a Cynical Review and produce a findings report. Use when the user requests a critical review of something","core","_bmad/core/bmad-review-adversarial-general/SKILL.md"
"bmad-review-edge-case-hunter","bmad-review-edge-case-hunter","Walk every branching path and boundary condition in content, report only unhandled edge cases. Orthogonal to adversarial review - method-driven not attitude-driven. Use when you need exhaustive edge-case analysis of code, specs, or diffs.","core","_bmad/core/bmad-review-edge-case-hunter/SKILL.md"
"bmad-shard-doc","bmad-shard-doc","Splits large markdown documents into smaller, organized files based on level 2 (default) sections. Use if the user says perform shard document","core","_bmad/core/bmad-shard-doc/SKILL.md"
"bmad-agent-analyst","bmad-agent-analyst","Strategic business analyst and requirements expert. Use when the user asks to talk to Mary or requests the business analyst.","bmm","_bmad/bmm/1-analysis/bmad-agent-analyst/SKILL.md"
"bmad-agent-tech-writer","bmad-agent-tech-writer","Technical documentation specialist and knowledge curator. Use when the user asks to talk to Paige or requests the tech writer.","bmm","_bmad/bmm/1-analysis/bmad-agent-tech-writer/SKILL.md"
"bmad-document-project","bmad-document-project","Document brownfield projects for AI context. Use when the user says ""document this project"" or ""generate project docs""","bmm","_bmad/bmm/1-analysis/bmad-document-project/SKILL.md"
"bmad-prfaq","bmad-prfaq","Working Backwards PRFAQ challenge to forge product concepts. Use when the user requests to 'create a PRFAQ', 'work backwards', or 'run the PRFAQ challenge'.","bmm","_bmad/bmm/1-analysis/bmad-prfaq/SKILL.md"
"bmad-product-brief","bmad-product-brief","Create, update, or validate a product brief. Use when the user wants help producing, editing, or validating a brief.","bmm","_bmad/bmm/1-analysis/bmad-product-brief/SKILL.md"
"bmad-domain-research","bmad-domain-research","Conduct domain and industry research. Use when the user says wants to do domain research for a topic or industry","bmm","_bmad/bmm/1-analysis/research/bmad-domain-research/SKILL.md"
"bmad-market-research","bmad-market-research","Conduct market research on competition and customers. Use when the user says they need market research","bmm","_bmad/bmm/1-analysis/research/bmad-market-research/SKILL.md"
"bmad-technical-research","bmad-technical-research","Conduct technical research on technologies and architecture. Use when the user says they would like to do or produce a technical research report","bmm","_bmad/bmm/1-analysis/research/bmad-technical-research/SKILL.md"
"bmad-agent-pm","bmad-agent-pm","Product manager for PRD creation and requirements discovery. Use when the user asks to talk to John or requests the product manager.","bmm","_bmad/bmm/2-plan-workflows/bmad-agent-pm/SKILL.md"
"bmad-agent-ux-designer","bmad-agent-ux-designer","UX designer and UI specialist. Use when the user asks to talk to Sally or requests the UX designer.","bmm","_bmad/bmm/2-plan-workflows/bmad-agent-ux-designer/SKILL.md"
"bmad-create-prd","bmad-create-prd","DEPRECATED — consolidated into bmad-prd create intent - this skill will be removed in v7 in favor of `bmad-prd`.","bmm","_bmad/bmm/2-plan-workflows/bmad-create-prd/SKILL.md"
"bmad-create-ux-design","bmad-create-ux-design","Plan UX patterns and design specifications. Use when the user says ""lets create UX design"" or ""create UX specifications"" or ""help me plan the UX""","bmm","_bmad/bmm/2-plan-workflows/bmad-create-ux-design/SKILL.md"
"bmad-edit-prd","bmad-edit-prd","DEPRECATED — consolidated into bmad-prd update intent - this skill will be removed in v7 in favor of `bmad-prd`.","bmm","_bmad/bmm/2-plan-workflows/bmad-edit-prd/SKILL.md"
"bmad-prd","bmad-prd","Create, update, or validate a PRD. Use when the user wants help producing, editing, or validating a PRD.","bmm","_bmad/bmm/2-plan-workflows/bmad-prd/SKILL.md"
"bmad-validate-prd","bmad-validate-prd","DEPRECATED — consolidated into bmad-prd validate intent - this skill will be removed in v7 in favor of `bmad-prd`.","bmm","_bmad/bmm/2-plan-workflows/bmad-validate-prd/SKILL.md"
"bmad-agent-architect","bmad-agent-architect","System architect and technical design leader. Use when the user asks to talk to Winston or requests the architect.","bmm","_bmad/bmm/3-solutioning/bmad-agent-architect/SKILL.md"
"bmad-check-implementation-readiness","bmad-check-implementation-readiness","Validate PRD, UX, Architecture and Epics specs are complete. Use when the user says ""check implementation readiness"".","bmm","_bmad/bmm/3-solutioning/bmad-check-implementation-readiness/SKILL.md"
"bmad-create-architecture","bmad-create-architecture","Create architecture solution design decisions for AI agent consistency. Use when the user says ""lets create architecture"" or ""create technical architecture"" or ""create a solution design""","bmm","_bmad/bmm/3-solutioning/bmad-create-architecture/SKILL.md"
"bmad-create-epics-and-stories","bmad-create-epics-and-stories","Break requirements into epics and user stories. Use when the user says ""create the epics and stories list""","bmm","_bmad/bmm/3-solutioning/bmad-create-epics-and-stories/SKILL.md"
"bmad-generate-project-context","bmad-generate-project-context","Create project-context.md with AI rules. Use when the user says ""generate project context"" or ""create project context""","bmm","_bmad/bmm/3-solutioning/bmad-generate-project-context/SKILL.md"
"bmad-agent-dev","bmad-agent-dev","Senior software engineer for story execution and code implementation. Use when the user asks to talk to Amelia or requests the developer agent.","bmm","_bmad/bmm/4-implementation/bmad-agent-dev/SKILL.md"
"bmad-checkpoint-preview","bmad-checkpoint-preview","LLM-assisted human-in-the-loop review. Make sense of a change, focus attention where it matters, test. Use when the user says ""checkpoint"", ""human review"", or ""walk me through this change"".","bmm","_bmad/bmm/4-implementation/bmad-checkpoint-preview/SKILL.md"
"bmad-code-review","bmad-code-review","Review code changes adversarially using parallel review layers (Blind Hunter, Edge Case Hunter, Acceptance Auditor) with structured triage into actionable categories. Use when the user says ""run code review"" or ""review this code""","bmm","_bmad/bmm/4-implementation/bmad-code-review/SKILL.md"
"bmad-correct-course","bmad-correct-course","Manage significant changes during sprint execution. Use when the user says ""correct course"" or ""propose sprint change""","bmm","_bmad/bmm/4-implementation/bmad-correct-course/SKILL.md"
"bmad-create-story","bmad-create-story","Creates a dedicated story file with all the context the agent will need to implement it later. Use when the user says ""create the next story"" or ""create story [story identifier]""","bmm","_bmad/bmm/4-implementation/bmad-create-story/SKILL.md"
"bmad-dev-story","bmad-dev-story","Execute story implementation following a context filled story spec file. Use when the user says ""dev this story [story file]"" or ""implement the next story in the sprint plan""","bmm","_bmad/bmm/4-implementation/bmad-dev-story/SKILL.md"
"bmad-investigate","bmad-investigate","Forensic case investigation with evidence-graded findings, calibrated to the input. Use when the user asks to investigate a bug, trace what caused an incident, walk through unfamiliar code, or build a mental model of a code area before working on it.","bmm","_bmad/bmm/4-implementation/bmad-investigate/SKILL.md"
"bmad-qa-generate-e2e-tests","bmad-qa-generate-e2e-tests","Generate end to end automated tests for existing features. Use when the user says ""create qa automated tests for [feature]""","bmm","_bmad/bmm/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md"
"bmad-quick-dev","bmad-quick-dev","Implements any user intent, requirement, story, bug fix or change request by producing clean working code artifacts that follow the project's existing architecture, patterns and conventions. Use when the user wants to build, fix, tweak, refactor, add or modify any code, component or feature.","bmm","_bmad/bmm/4-implementation/bmad-quick-dev/SKILL.md"
"bmad-retrospective","bmad-retrospective","Post-epic review to extract lessons and assess success. Use when the user says ""run a retrospective"" or ""lets retro the epic [epic]""","bmm","_bmad/bmm/4-implementation/bmad-retrospective/SKILL.md"
"bmad-sprint-planning","bmad-sprint-planning","Generate sprint status tracking from epics. Use when the user says ""run sprint planning"" or ""generate sprint plan""","bmm","_bmad/bmm/4-implementation/bmad-sprint-planning/SKILL.md"
"bmad-sprint-status","bmad-sprint-status","Summarize sprint status and surface risks. Use when the user says ""check sprint status"" or ""show sprint status""","bmm","_bmad/bmm/4-implementation/bmad-sprint-status/SKILL.md"
1 canonicalId name description module path
2 bmad-advanced-elicitation bmad-advanced-elicitation Push the LLM to reconsider, refine, and improve its recent output. Use when user asks for deeper critique or mentions a known deeper critique method, e.g. socratic, first principles, pre-mortem, red team. core _bmad/core/bmad-advanced-elicitation/SKILL.md
3 bmad-brainstorming bmad-brainstorming Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods. Use when the user says help me brainstorm or help me ideate. core _bmad/core/bmad-brainstorming/SKILL.md
4 bmad-customize bmad-customize Authors and updates customization overrides for installed BMad skills. Use when the user says 'customize bmad', 'override a skill', 'change agent behavior', or 'customize a workflow'. core _bmad/core/bmad-customize/SKILL.md
5 bmad-distillator bmad-distillator Lossless LLM-optimized compression of source documents. Use when the user requests to 'distill documents' or 'create a distillate'. core _bmad/core/bmad-distillator/SKILL.md
6 bmad-editorial-review-prose bmad-editorial-review-prose Clinical copy-editor that reviews text for communication issues. Use when user says review for prose or improve the prose core _bmad/core/bmad-editorial-review-prose/SKILL.md
7 bmad-editorial-review-structure bmad-editorial-review-structure Structural editor that proposes cuts, reorganization, and simplification while preserving comprehension. Use when user requests structural review or editorial review of structure core _bmad/core/bmad-editorial-review-structure/SKILL.md
8 bmad-help bmad-help Analyzes current state and user query to answer BMad questions or recommend the next skill(s) to use. Use when user asks for help, bmad help, what to do next, or what to start with in BMad. core _bmad/core/bmad-help/SKILL.md
9 bmad-index-docs bmad-index-docs Generates or updates an index.md to reference all docs in the folder. Use if user requests to create or update an index of all files in a specific folder core _bmad/core/bmad-index-docs/SKILL.md
10 bmad-party-mode bmad-party-mode Orchestrates group discussions between installed BMAD agents, enabling natural multi-agent conversations where each agent is a real subagent with independent thinking. Use when user requests party mode, wants multiple agent perspectives, group discussion, roundtable, or multi-agent conversation about their project. core _bmad/core/bmad-party-mode/SKILL.md
11 bmad-review-adversarial-general bmad-review-adversarial-general Perform a Cynical Review and produce a findings report. Use when the user requests a critical review of something core _bmad/core/bmad-review-adversarial-general/SKILL.md
12 bmad-review-edge-case-hunter bmad-review-edge-case-hunter Walk every branching path and boundary condition in content, report only unhandled edge cases. Orthogonal to adversarial review - method-driven not attitude-driven. Use when you need exhaustive edge-case analysis of code, specs, or diffs. core _bmad/core/bmad-review-edge-case-hunter/SKILL.md
13 bmad-shard-doc bmad-shard-doc Splits large markdown documents into smaller, organized files based on level 2 (default) sections. Use if the user says perform shard document core _bmad/core/bmad-shard-doc/SKILL.md
14 bmad-agent-analyst bmad-agent-analyst Strategic business analyst and requirements expert. Use when the user asks to talk to Mary or requests the business analyst. bmm _bmad/bmm/1-analysis/bmad-agent-analyst/SKILL.md
15 bmad-agent-tech-writer bmad-agent-tech-writer Technical documentation specialist and knowledge curator. Use when the user asks to talk to Paige or requests the tech writer. bmm _bmad/bmm/1-analysis/bmad-agent-tech-writer/SKILL.md
16 bmad-document-project bmad-document-project Document brownfield projects for AI context. Use when the user says "document this project" or "generate project docs" bmm _bmad/bmm/1-analysis/bmad-document-project/SKILL.md
17 bmad-prfaq bmad-prfaq Working Backwards PRFAQ challenge to forge product concepts. Use when the user requests to 'create a PRFAQ', 'work backwards', or 'run the PRFAQ challenge'. bmm _bmad/bmm/1-analysis/bmad-prfaq/SKILL.md
18 bmad-product-brief bmad-product-brief Create, update, or validate a product brief. Use when the user wants help producing, editing, or validating a brief. bmm _bmad/bmm/1-analysis/bmad-product-brief/SKILL.md
19 bmad-domain-research bmad-domain-research Conduct domain and industry research. Use when the user says wants to do domain research for a topic or industry bmm _bmad/bmm/1-analysis/research/bmad-domain-research/SKILL.md
20 bmad-market-research bmad-market-research Conduct market research on competition and customers. Use when the user says they need market research bmm _bmad/bmm/1-analysis/research/bmad-market-research/SKILL.md
21 bmad-technical-research bmad-technical-research Conduct technical research on technologies and architecture. Use when the user says they would like to do or produce a technical research report bmm _bmad/bmm/1-analysis/research/bmad-technical-research/SKILL.md
22 bmad-agent-pm bmad-agent-pm Product manager for PRD creation and requirements discovery. Use when the user asks to talk to John or requests the product manager. bmm _bmad/bmm/2-plan-workflows/bmad-agent-pm/SKILL.md
23 bmad-agent-ux-designer bmad-agent-ux-designer UX designer and UI specialist. Use when the user asks to talk to Sally or requests the UX designer. bmm _bmad/bmm/2-plan-workflows/bmad-agent-ux-designer/SKILL.md
24 bmad-create-prd bmad-create-prd DEPRECATED — consolidated into bmad-prd create intent - this skill will be removed in v7 in favor of `bmad-prd`. bmm _bmad/bmm/2-plan-workflows/bmad-create-prd/SKILL.md
25 bmad-create-ux-design bmad-create-ux-design Plan UX patterns and design specifications. Use when the user says "lets create UX design" or "create UX specifications" or "help me plan the UX" bmm _bmad/bmm/2-plan-workflows/bmad-create-ux-design/SKILL.md
26 bmad-edit-prd bmad-edit-prd DEPRECATED — consolidated into bmad-prd update intent - this skill will be removed in v7 in favor of `bmad-prd`. bmm _bmad/bmm/2-plan-workflows/bmad-edit-prd/SKILL.md
27 bmad-prd bmad-prd Create, update, or validate a PRD. Use when the user wants help producing, editing, or validating a PRD. bmm _bmad/bmm/2-plan-workflows/bmad-prd/SKILL.md
28 bmad-validate-prd bmad-validate-prd DEPRECATED — consolidated into bmad-prd validate intent - this skill will be removed in v7 in favor of `bmad-prd`. bmm _bmad/bmm/2-plan-workflows/bmad-validate-prd/SKILL.md
29 bmad-agent-architect bmad-agent-architect System architect and technical design leader. Use when the user asks to talk to Winston or requests the architect. bmm _bmad/bmm/3-solutioning/bmad-agent-architect/SKILL.md
30 bmad-check-implementation-readiness bmad-check-implementation-readiness Validate PRD, UX, Architecture and Epics specs are complete. Use when the user says "check implementation readiness". bmm _bmad/bmm/3-solutioning/bmad-check-implementation-readiness/SKILL.md
31 bmad-create-architecture bmad-create-architecture Create architecture solution design decisions for AI agent consistency. Use when the user says "lets create architecture" or "create technical architecture" or "create a solution design" bmm _bmad/bmm/3-solutioning/bmad-create-architecture/SKILL.md
32 bmad-create-epics-and-stories bmad-create-epics-and-stories Break requirements into epics and user stories. Use when the user says "create the epics and stories list" bmm _bmad/bmm/3-solutioning/bmad-create-epics-and-stories/SKILL.md
33 bmad-generate-project-context bmad-generate-project-context Create project-context.md with AI rules. Use when the user says "generate project context" or "create project context" bmm _bmad/bmm/3-solutioning/bmad-generate-project-context/SKILL.md
34 bmad-agent-dev bmad-agent-dev Senior software engineer for story execution and code implementation. Use when the user asks to talk to Amelia or requests the developer agent. bmm _bmad/bmm/4-implementation/bmad-agent-dev/SKILL.md
35 bmad-checkpoint-preview bmad-checkpoint-preview LLM-assisted human-in-the-loop review. Make sense of a change, focus attention where it matters, test. Use when the user says "checkpoint", "human review", or "walk me through this change". bmm _bmad/bmm/4-implementation/bmad-checkpoint-preview/SKILL.md
36 bmad-code-review bmad-code-review Review code changes adversarially using parallel review layers (Blind Hunter, Edge Case Hunter, Acceptance Auditor) with structured triage into actionable categories. Use when the user says "run code review" or "review this code" bmm _bmad/bmm/4-implementation/bmad-code-review/SKILL.md
37 bmad-correct-course bmad-correct-course Manage significant changes during sprint execution. Use when the user says "correct course" or "propose sprint change" bmm _bmad/bmm/4-implementation/bmad-correct-course/SKILL.md
38 bmad-create-story bmad-create-story Creates a dedicated story file with all the context the agent will need to implement it later. Use when the user says "create the next story" or "create story [story identifier]" bmm _bmad/bmm/4-implementation/bmad-create-story/SKILL.md
39 bmad-dev-story bmad-dev-story Execute story implementation following a context filled story spec file. Use when the user says "dev this story [story file]" or "implement the next story in the sprint plan" bmm _bmad/bmm/4-implementation/bmad-dev-story/SKILL.md
40 bmad-investigate bmad-investigate Forensic case investigation with evidence-graded findings, calibrated to the input. Use when the user asks to investigate a bug, trace what caused an incident, walk through unfamiliar code, or build a mental model of a code area before working on it. bmm _bmad/bmm/4-implementation/bmad-investigate/SKILL.md
41 bmad-qa-generate-e2e-tests bmad-qa-generate-e2e-tests Generate end to end automated tests for existing features. Use when the user says "create qa automated tests for [feature]" bmm _bmad/bmm/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md
42 bmad-quick-dev bmad-quick-dev Implements any user intent, requirement, story, bug fix or change request by producing clean working code artifacts that follow the project's existing architecture, patterns and conventions. Use when the user wants to build, fix, tweak, refactor, add or modify any code, component or feature. bmm _bmad/bmm/4-implementation/bmad-quick-dev/SKILL.md
43 bmad-retrospective bmad-retrospective Post-epic review to extract lessons and assess success. Use when the user says "run a retrospective" or "lets retro the epic [epic]" bmm _bmad/bmm/4-implementation/bmad-retrospective/SKILL.md
44 bmad-sprint-planning bmad-sprint-planning Generate sprint status tracking from epics. Use when the user says "run sprint planning" or "generate sprint plan" bmm _bmad/bmm/4-implementation/bmad-sprint-planning/SKILL.md
45 bmad-sprint-status bmad-sprint-status Summarize sprint status and surface risks. Use when the user says "check sprint status" or "show sprint status" bmm _bmad/bmm/4-implementation/bmad-sprint-status/SKILL.md
+16
View File
@@ -0,0 +1,16 @@
# BMM Module Configuration
# Generated by BMAD installer
# Version: 6.7.1
# Date: 2026-05-25T16:02:56.772Z
user_skill_level: intermediate
planning_artifacts: "{project-root}/_bmad-output/planning-artifacts"
implementation_artifacts: "{project-root}/_bmad-output/implementation-artifacts"
project_knowledge: "{project-root}/docs"
# Core Configuration Values
user_name: Dejancosic
project_name: projectmanagement
communication_language: german
document_output_language: german
output_folder: "{project-root}/_bmad-output"
+32
View File
@@ -0,0 +1,32 @@
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,,anytime,,,false,project-knowledge,*
BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,,anytime,,,false,output_folder,project context
BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,,anytime,,,false,implementation_artifacts,spec and project implementation
BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,,anytime,,,false,planning_artifacts,change proposal
BMad Method,bmad-agent-tech-writer,Write Document,WD,"Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review.",write,,anytime,,,false,project-knowledge,document
BMad Method,bmad-agent-tech-writer,Update Standards,US,Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.,update-standards,,anytime,,,false,_bmad/_memory/tech-writer-sidecar,standards
BMad Method,bmad-agent-tech-writer,Mermaid Generate,MG,Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.,mermaid,,anytime,,,false,planning_artifacts,mermaid diagram
BMad Method,bmad-agent-tech-writer,Validate Document,VD,Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.,validate,[path],anytime,,,false,planning_artifacts,validation report
BMad Method,bmad-agent-tech-writer,Explain Concept,EC,Create clear technical explanations with examples and diagrams for complex concepts.,explain,[topic],anytime,,,false,project_knowledge,explanation
BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,,1-analysis,,,false,planning_artifacts,brainstorming session
BMad Method,bmad-market-research,Market Research,MR,Market analysis competitive landscape customer needs and trends.,,,1-analysis,,,false,planning_artifacts|project-knowledge,research documents
BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
BMad Method,bmad-product-brief,Create Brief,CB,An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you.,,-A,1-analysis,,,false,planning_artifacts,product brief
BMad Method,bmad-prfaq,PRFAQ Challenge,WB,Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief.,,-H,1-analysis,,,false,planning_artifacts,prfaq document
BMad Method,bmad-prd,Create Edit and Review PRD,PRD,"Facilitated PRD workflow — create a new PRD via coached discovery, update an existing one against a change signal, or validate a finished PRD against a checklist with an HTML findings report.",,,2-planning,bmad-product-brief,,true,planning_artifacts,prd
BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,,2-planning,bmad-prd,,false,planning_artifacts,ux design
BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,,3-solutioning,,,true,planning_artifacts,architecture
BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report
BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,,4-implementation,,,true,implementation_artifacts,sprint status
BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,,4-implementation,bmad-sprint-planning,,false,,
BMad Method,bmad-create-story,Create Story,CS,Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.,create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story
BMad Method,bmad-create-story,Validate Story,VS,Validates story readiness and completeness before development work begins.,validate,,4-implementation,bmad-create-story:create,bmad-dev-story,false,implementation_artifacts,story validation report
BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,,4-implementation,bmad-create-story:validate,,true,,
BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,,4-implementation,bmad-dev-story,,false,,
BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,,4-implementation,,,false,,
BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite
BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective
BMad Method,bmad-investigate,Investigate,IN,Forensic case investigation calibrated to the input. Evidence-graded analysis with hypothesis tracking. Produces a structured case file.,,4-implementation,,,false,implementation_artifacts,investigation report
1 module skill display-name menu-code description action args phase preceded-by followed-by required output-location outputs
2 BMad Method _meta false https://docs.bmad-method.org/llms.txt
3 BMad Method bmad-document-project Document Project DP Analyze an existing project to produce useful documentation. anytime false project-knowledge *
4 BMad Method bmad-generate-project-context Generate Project Context GPC Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects. anytime false output_folder project context
5 BMad Method bmad-quick-dev Quick Dev QQ Unified intent-in code-out workflow: clarify plan implement review and present. anytime false implementation_artifacts spec and project implementation
6 BMad Method bmad-correct-course Correct Course CC Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories. anytime false planning_artifacts change proposal
7 BMad Method bmad-agent-tech-writer Write Document WD Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review. write anytime false project-knowledge document
8 BMad Method bmad-agent-tech-writer Update Standards US Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions. update-standards anytime false _bmad/_memory/tech-writer-sidecar standards
9 BMad Method bmad-agent-tech-writer Mermaid Generate MG Create a Mermaid diagram based on user description. Will suggest diagram types if not specified. mermaid anytime false planning_artifacts mermaid diagram
10 BMad Method bmad-agent-tech-writer Validate Document VD Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority. validate [path] anytime false planning_artifacts validation report
11 BMad Method bmad-agent-tech-writer Explain Concept EC Create clear technical explanations with examples and diagrams for complex concepts. explain [topic] anytime false project_knowledge explanation
12 BMad Method bmad-brainstorming Brainstorm Project BP Expert guided facilitation through a single or multiple techniques. 1-analysis false planning_artifacts brainstorming session
13 BMad Method bmad-market-research Market Research MR Market analysis competitive landscape customer needs and trends. 1-analysis false planning_artifacts|project-knowledge research documents
14 BMad Method bmad-domain-research Domain Research DR Industry domain deep dive subject matter expertise and terminology. 1-analysis false planning_artifacts|project_knowledge research documents
15 BMad Method bmad-technical-research Technical Research TR Technical feasibility architecture options and implementation approaches. 1-analysis false planning_artifacts|project_knowledge research documents
16 BMad Method bmad-product-brief Create Brief CB An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you. -A 1-analysis false planning_artifacts product brief
17 BMad Method bmad-prfaq PRFAQ Challenge WB Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief. -H 1-analysis false planning_artifacts prfaq document
18 BMad Method bmad-prd Create Edit and Review PRD PRD Facilitated PRD workflow — create a new PRD via coached discovery, update an existing one against a change signal, or validate a finished PRD against a checklist with an HTML findings report. 2-planning bmad-product-brief true planning_artifacts prd
19 BMad Method bmad-create-ux-design Create UX CU Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project. 2-planning bmad-prd false planning_artifacts ux design
20 BMad Method bmad-create-architecture Create Architecture CA Guided workflow to document technical decisions. 3-solutioning true planning_artifacts architecture
21 BMad Method bmad-create-epics-and-stories Create Epics and Stories CE 3-solutioning bmad-create-architecture true planning_artifacts epics and stories
22 BMad Method bmad-check-implementation-readiness Check Implementation Readiness IR Ensure PRD UX Architecture and Epics Stories are aligned. 3-solutioning bmad-create-epics-and-stories true planning_artifacts readiness report
23 BMad Method bmad-sprint-planning Sprint Planning SP Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story. 4-implementation true implementation_artifacts sprint status
24 BMad Method bmad-sprint-status Sprint Status SS Anytime: Summarize sprint status and route to next workflow. 4-implementation bmad-sprint-planning false
25 BMad Method bmad-create-story Create Story CS Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation. create 4-implementation bmad-sprint-planning bmad-create-story:validate true implementation_artifacts story
26 BMad Method bmad-create-story Validate Story VS Validates story readiness and completeness before development work begins. validate 4-implementation bmad-create-story:create bmad-dev-story false implementation_artifacts story validation report
27 BMad Method bmad-dev-story Dev Story DS Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed. 4-implementation bmad-create-story:validate true
28 BMad Method bmad-code-review Code Review CR Story cycle: If issues back to DS if approved then next CS or ER if epic complete. 4-implementation bmad-dev-story false
29 BMad Method bmad-checkpoint-preview Checkpoint CK Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs. 4-implementation false
30 BMad Method bmad-qa-generate-e2e-tests QA Automation Test QA Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that. 4-implementation bmad-dev-story false implementation_artifacts test suite
31 BMad Method bmad-retrospective Retrospective ER Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC. 4-implementation bmad-code-review false implementation_artifacts retrospective
32 BMad Method bmad-investigate Investigate IN Forensic case investigation calibrated to the input. Evidence-graded analysis with hypothesis tracking. Produces a structured case file. 4-implementation false implementation_artifacts investigation report
+69
View File
@@ -0,0 +1,69 @@
# ─────────────────────────────────────────────────────────────────
# Installer-managed. Regenerated on every install — treat as read-only.
#
# Direct edits to this file will be overwritten on the next install.
# To change an install answer durably, re-run the installer (your prior
# answers are remembered as defaults). To pin a value regardless of
# install answers, or to add custom agents / override descriptors, use:
# _bmad/custom/config.toml (team, committed)
# _bmad/custom/config.user.toml (personal, gitignored)
# Those files are never touched by the installer.
# ─────────────────────────────────────────────────────────────────
[core]
project_name = "projectmanagement"
document_output_language = "german"
output_folder = "{project-root}/_bmad-output"
[modules.bmm]
planning_artifacts = "{project-root}/_bmad-output/planning-artifacts"
implementation_artifacts = "{project-root}/_bmad-output/implementation-artifacts"
project_knowledge = "{project-root}/docs"
[agents.bmad-agent-analyst]
module = "bmm"
team = "software-development"
name = "Mary"
title = "Business Analyst"
icon = "📊"
description = "Channels Porter's strategic rigor and Minto's Pyramid Principle, grounds every finding in verifiable evidence, represents every stakeholder voice. Speaks like a treasure hunter narrating the find: thrilled by every clue, precise once the pattern emerges."
[agents.bmad-agent-tech-writer]
module = "bmm"
team = "software-development"
name = "Paige"
title = "Technical Writer"
icon = "📚"
description = "Master of CommonMark, DITA, and OpenAPI; turns complex concepts into accessible structured docs, favors diagrams over walls of text, every word earning its place. Speaks like the patient teacher you wish you'd had, using analogies that make complex things feel simple."
[agents.bmad-agent-pm]
module = "bmm"
team = "software-development"
name = "John"
title = "Product Manager"
icon = "📋"
description = "Drives Jobs-to-be-Done over template filling, user value first, technical feasibility is a constraint not the driver. Speaks like a detective interrogating a cold case: short questions, sharper follow-ups, every 'why?' tightening the net."
[agents.bmad-agent-ux-designer]
module = "bmm"
team = "software-development"
name = "Sally"
title = "UX Designer"
icon = "🎨"
description = "Balances empathy with edge-case rigor, starts simple and evolves through feedback, every decision serves a genuine user need. Speaks like a filmmaker pitching the scene before the code exists, painting user stories that make you feel the problem."
[agents.bmad-agent-architect]
module = "bmm"
team = "software-development"
name = "Winston"
title = "System Architect"
icon = "🏗️"
description = "Favors boring technology for stability, developer productivity as architecture, ties every decision to business value. Speaks like a seasoned engineer at the whiteboard: measured, always laying out trade-offs rather than verdicts."
[agents.bmad-agent-dev]
module = "bmm"
team = "software-development"
name = "Amelia"
title = "Senior Software Engineer"
icon = "💻"
description = "Test-first discipline (red, green, refactor), 100% pass before review, no fluff all precision. Speaks like a terminal prompt: exact file paths, AC IDs, and commit-message brevity — every statement citable."
+17
View File
@@ -0,0 +1,17 @@
# ─────────────────────────────────────────────────────────────────
# Installer-managed. Regenerated on every install — treat as read-only.
# Holds install answers scoped to YOU personally.
#
# Direct edits to this file will be overwritten on the next install.
# To change an answer durably, re-run the installer (your prior answers
# are remembered as defaults). For pinned overrides or custom sections
# the installer does not know about, use _bmad/custom/config.user.toml
# — it is never touched by the installer.
# ─────────────────────────────────────────────────────────────────
[core]
user_name = "Dejancosic"
communication_language = "german"
[modules.bmm]
user_skill_level = "intermediate"
+10
View File
@@ -0,0 +1,10 @@
# CORE Module Configuration
# Generated by BMAD installer
# Version: 6.7.1
# Date: 2026-05-25T16:02:56.772Z
user_name: Dejancosic
project_name: projectmanagement
communication_language: german
document_output_language: german
output_folder: "{project-root}/_bmad-output"
+13
View File
@@ -0,0 +1,13 @@
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,,
Core,bmad-help,BMad Help,BH,,,,anytime,,,false,,
Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,,anytime,,,false,,
Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,,[path],anytime,,,false,,
Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes
Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,,[path],anytime,,,false,report located with target document,
Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",,[path],anytime,,,false,,
Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,,
Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s)
Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files
1 module skill display-name menu-code description action args phase preceded-by followed-by required output-location outputs
2 Core _meta false https://docs.bmad-method.org/llms.txt
3 Core bmad-brainstorming Brainstorming BSP Use early in ideation or when stuck generating ideas. anytime false {output_folder}/brainstorming brainstorming session
4 Core bmad-party-mode Party Mode PM Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate. anytime false
5 Core bmad-help BMad Help BH anytime false
6 Core bmad-index-docs Index Docs ID Use when LLM needs to understand available docs without loading everything. anytime false
7 Core bmad-shard-doc Shard Document SD Use when doc becomes too large (>500 lines) to manage effectively. [path] anytime false
8 Core bmad-editorial-review-prose Editorial Review - Prose EP Use after drafting to polish written content. [path] anytime false report located with target document three-column markdown table with suggested fixes
9 Core bmad-editorial-review-structure Editorial Review - Structure ES Use when doc produced from multiple subprocesses or needs structural improvement. [path] anytime false report located with target document
10 Core bmad-review-adversarial-general Adversarial Review AR Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews. [path] anytime false
11 Core bmad-review-edge-case-hunter Edge Case Hunter Review ECH Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven. [path] anytime false
12 Core bmad-distillator Distillator DG Use when you need token-efficient distillates that preserve all information for downstream LLM consumption. [path] anytime false adjacent to source document or specified output_path distillate markdown file(s)
13 Core bmad-customize BMad Customize BC Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required. anytime false {project-root}/_bmad/custom TOML override files
+1
View File
@@ -0,0 +1 @@
*.user.toml
+7
View File
@@ -0,0 +1,7 @@
# Team / enterprise overrides for _bmad/config.toml.
# Committed to the repo — applies to every developer on the project.
# Tables deep-merge over base config; keyed entries merge by key.
# Example: override an agent descriptor, or add a new agent.
#
# [agents.bmad-agent-pm]
# description = "Prefers short, bulleted PRDs over narrative drafts."
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""
Resolve BMad's central config using four-layer TOML merge.
Reads from four layers (highest priority last):
1. {project-root}/_bmad/config.toml (installer-owned team)
2. {project-root}/_bmad/config.user.toml (installer-owned user)
3. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
4. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
Outputs merged JSON to stdout. Errors go to stderr.
Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
no virtualenv — plain `python3` is sufficient.
python3 resolve_config.py --project-root /abs/path/to/project
python3 resolve_config.py --project-root ... --key core
python3 resolve_config.py --project-root ... --key agents
Merge rules (same as resolve_customization.py):
- Scalars: override wins
- Tables: deep merge
- Arrays of tables where every item shares `code` or `id`: merge by that key
- All other arrays: append
"""
import argparse
import json
import sys
from pathlib import Path
try:
import tomllib
except ImportError:
sys.stderr.write(
"error: Python 3.11+ is required (stdlib `tomllib` not found).\n"
)
sys.exit(3)
_MISSING = object()
_KEYED_MERGE_FIELDS = ("code", "id")
def load_toml(file_path: Path, required: bool = False) -> dict:
if not file_path.exists():
if required:
sys.stderr.write(f"error: required config file not found: {file_path}\n")
sys.exit(1)
return {}
try:
with file_path.open("rb") as f:
parsed = tomllib.load(f)
if not isinstance(parsed, dict):
return {}
return parsed
except tomllib.TOMLDecodeError as error:
level = "error" if required else "warning"
sys.stderr.write(f"{level}: failed to parse {file_path}: {error}\n")
if required:
sys.exit(1)
return {}
except OSError as error:
level = "error" if required else "warning"
sys.stderr.write(f"{level}: failed to read {file_path}: {error}\n")
if required:
sys.exit(1)
return {}
def _detect_keyed_merge_field(items):
if not items or not all(isinstance(item, dict) for item in items):
return None
for candidate in _KEYED_MERGE_FIELDS:
if all(item.get(candidate) is not None for item in items):
return candidate
return None
def _merge_by_key(base, override, key_name):
result = []
index_by_key = {}
for item in base:
if not isinstance(item, dict):
continue
if item.get(key_name) is not None:
index_by_key[item[key_name]] = len(result)
result.append(dict(item))
for item in override:
if not isinstance(item, dict):
result.append(item)
continue
key = item.get(key_name)
if key is not None and key in index_by_key:
result[index_by_key[key]] = dict(item)
else:
if key is not None:
index_by_key[key] = len(result)
result.append(dict(item))
return result
def _merge_arrays(base, override):
base_arr = base if isinstance(base, list) else []
override_arr = override if isinstance(override, list) else []
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
if keyed_field:
return _merge_by_key(base_arr, override_arr, keyed_field)
return base_arr + override_arr
def deep_merge(base, override):
if isinstance(base, dict) and isinstance(override, dict):
result = dict(base)
for key, over_val in override.items():
if key in result:
result[key] = deep_merge(result[key], over_val)
else:
result[key] = over_val
return result
if isinstance(base, list) and isinstance(override, list):
return _merge_arrays(base, override)
return override
def extract_key(data, dotted_key: str):
parts = dotted_key.split(".")
current = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return _MISSING
return current
def main():
parser = argparse.ArgumentParser(
description="Resolve BMad central config using four-layer TOML merge.",
)
parser.add_argument(
"--project-root", "-p", required=True,
help="Absolute path to the project root (contains _bmad/)",
)
parser.add_argument(
"--key", "-k", action="append", default=[],
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
project_root = Path(args.project_root).resolve()
bmad_dir = project_root / "_bmad"
base_team = load_toml(bmad_dir / "config.toml", required=True)
base_user = load_toml(bmad_dir / "config.user.toml")
custom_team = load_toml(bmad_dir / "custom" / "config.toml")
custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
merged = deep_merge(base_team, base_user)
merged = deep_merge(merged, custom_team)
merged = deep_merge(merged, custom_user)
if args.key:
output = {}
for key in args.key:
value = extract_key(merged, key)
if value is not _MISSING:
output[key] = value
else:
output = merged
sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
if __name__ == "__main__":
main()
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""
Resolve customization for a BMad skill using three-layer TOML merge.
Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/custom/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/custom/{name}.toml (team/org, committed)
3. {skill-root}/customize.toml (skill defaults)
Skill name is derived from the basename of the skill directory.
Outputs merged JSON to stdout. Errors go to stderr.
Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
no virtualenv — plain `python3` is sufficient.
python3 resolve_customization.py --skill /abs/path/to/skill-dir
python3 resolve_customization.py --skill ... --key agent
python3 resolve_customization.py --skill ... --key agent.menu
Merge rules (purely structural — no field-name special-casing):
- Scalars (string, int, bool, float): override wins
- Tables: deep merge (recursively apply these rules)
- Arrays of tables where every item shares the *same* identifier
field (every item has `code`, or every item has `id`):
merge by that key (matching keys replace, new keys append)
- All other arrays — including arrays where only some items have
`code` or `id`, or where items mix the two keys:
append (base items followed by override items)
No removal mechanism — overrides cannot delete base items. To suppress
a default, fork the skill or override the item by code with a no-op
description/prompt.
"""
import argparse
import json
import sys
from pathlib import Path
try:
import tomllib
except ImportError:
sys.stderr.write(
"error: Python 3.11+ is required (stdlib `tomllib` not found).\n"
"Install a newer Python or run the resolution manually per the\n"
"fallback instructions in the skill's SKILL.md.\n"
)
sys.exit(3)
_MISSING = object()
_KEYED_MERGE_FIELDS = ("code", "id")
def find_project_root(start: Path):
current = start.resolve()
while True:
if (current / "_bmad").exists() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_toml(file_path: Path, required: bool = False) -> dict:
if not file_path.exists():
if required:
sys.stderr.write(f"error: required customization file not found: {file_path}\n")
sys.exit(1)
return {}
try:
with file_path.open("rb") as f:
parsed = tomllib.load(f)
if not isinstance(parsed, dict):
if required:
sys.stderr.write(f"error: {file_path} did not parse to a table\n")
sys.exit(1)
return {}
return parsed
except tomllib.TOMLDecodeError as error:
level = "error" if required else "warning"
sys.stderr.write(f"{level}: failed to parse {file_path}: {error}\n")
if required:
sys.exit(1)
return {}
except OSError as error:
level = "error" if required else "warning"
sys.stderr.write(f"{level}: failed to read {file_path}: {error}\n")
if required:
sys.exit(1)
return {}
def _detect_keyed_merge_field(items):
"""Return 'code' or 'id' if every table item carries that *same* field.
All items must share the same identifier (all `code`, or all `id`).
Mixed arrays — where some items use `code` and others use `id` —
return None and fall through to append semantics. This is intentional:
mixing identifier keys within one array is a schema smell, and
append-fallback is safer than guessing which key should merge.
"""
if not items or not all(isinstance(item, dict) for item in items):
return None
for candidate in _KEYED_MERGE_FIELDS:
if all(item.get(candidate) is not None for item in items):
return candidate
return None
def _merge_by_key(base, override, key_name):
result = []
index_by_key = {}
for item in base:
if not isinstance(item, dict):
continue
if item.get(key_name) is not None:
index_by_key[item[key_name]] = len(result)
result.append(dict(item))
for item in override:
if not isinstance(item, dict):
result.append(item)
continue
key = item.get(key_name)
if key is not None and key in index_by_key:
result[index_by_key[key]] = dict(item)
else:
if key is not None:
index_by_key[key] = len(result)
result.append(dict(item))
return result
def _merge_arrays(base, override):
"""Shape-aware array merge. Base + override combined tables may opt into
keyed merge if every item has `code` or `id`. Otherwise: append."""
base_arr = base if isinstance(base, list) else []
override_arr = override if isinstance(override, list) else []
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
if keyed_field:
return _merge_by_key(base_arr, override_arr, keyed_field)
return base_arr + override_arr
def deep_merge(base, override):
"""Recursively merge override into base using structural rules.
- Table + table: deep merge
- Array + array: shape-aware (keyed merge if all items have code/id, else append)
- Anything else: override wins
"""
if isinstance(base, dict) and isinstance(override, dict):
result = dict(base)
for key, over_val in override.items():
if key in result:
result[key] = deep_merge(result[key], over_val)
else:
result[key] = over_val
return result
if isinstance(base, list) and isinstance(override, list):
return _merge_arrays(base, override)
return override
def extract_key(data, dotted_key: str):
parts = dotted_key.split(".")
current = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return _MISSING
return current
def main():
parser = argparse.ArgumentParser(
description="Resolve customization for a BMad skill using three-layer TOML merge.",
add_help=True,
)
parser.add_argument(
"--skill", "-s", required=True,
help="Absolute path to the skill directory (must contain customize.toml)",
)
parser.add_argument(
"--key", "-k", action="append", default=[],
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
skill_dir = Path(args.skill).resolve()
skill_name = skill_dir.name
defaults_path = skill_dir / "customize.toml"
defaults = load_toml(defaults_path, required=True)
# Prefer the project that contains this skill. Only fall back to cwd if
# the skill isn't inside a recognizable project tree (unusual but possible
# for standalone skills invoked directly). Using cwd first is unsafe when
# an ancestor of cwd happens to have a stray _bmad/ from another project.
project_root = find_project_root(skill_dir) or find_project_root(Path.cwd())
team = {}
user = {}
if project_root:
custom_dir = project_root / "_bmad" / "custom"
team = load_toml(custom_dir / f"{skill_name}.toml")
user = load_toml(custom_dir / f"{skill_name}.user.toml")
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)
if args.key:
output = {}
for key in args.key:
value = extract_key(merged, key)
if value is not _MISSING:
output[key] = value
else:
output = merged
sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
if __name__ == "__main__":
main()
+37
View File
@@ -0,0 +1,37 @@
# --- builder ---
FROM node:22-alpine AS builder
WORKDIR /repo
RUN corepack enable && corepack prepare pnpm@10.28.0 --activate
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./
COPY apps/api/package.json ./apps/api/
COPY packages/db/package.json ./packages/db/
COPY packages/shared/package.json ./packages/shared/
RUN pnpm install --frozen-lockfile
COPY apps/api ./apps/api
COPY packages/db ./packages/db
COPY packages/shared ./packages/shared
RUN pnpm --filter api build
# --- runtime ---
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
RUN corepack enable && corepack prepare pnpm@10.28.0 --activate
COPY --from=builder /repo/pnpm-workspace.yaml /repo/package.json /repo/pnpm-lock.yaml ./
COPY --from=builder /repo/apps/api/package.json ./apps/api/
COPY --from=builder /repo/packages/db ./packages/db
COPY --from=builder /repo/packages/shared ./packages/shared
COPY --from=builder /repo/apps/api/dist ./apps/api/dist
RUN pnpm install --frozen-lockfile --prod
EXPOSE 3000
WORKDIR /app/apps/api
CMD ["node", "dist/server.js"]
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@cockpit/api",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./router": "./src/routers/_app.ts"
},
"scripts": {
"dev": "tsx watch --env-file=../../.env src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@cockpit/db": "workspace:*",
"@cockpit/shared": "workspace:*",
"@fastify/cookie": "^11.0.1",
"@trpc/server": "^11.0.0-rc.660",
"drizzle-orm": "^0.38.2",
"fastify": "^5.2.0",
"openid-client": "^6.1.7",
"pg": "^8.13.1",
"pino": "^9.5.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/pg": "^8.11.10",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
}
}
+25
View File
@@ -0,0 +1,25 @@
// Fastify-Decorator: macht `req.cockpitUser` verfügbar, wenn die Session gültig ist.
// Die Typaugmentation lebt in `../types.ts`, damit sie auch in tRPC-Imports
// gezogen wird (web → api Type-Only-Import).
import '../types.js';
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { loadEnv } from '../env.js';
import { getSessionWithUser } from './session.js';
export async function registerAuthMiddleware(app: FastifyInstance): Promise<void> {
const env = loadEnv();
app.decorateRequest('cockpitUser', null);
app.decorateRequest('cockpitSessionId', null);
app.addHook('onRequest', async (req: FastifyRequest) => {
const sessionId = req.cookies[env.SESSION_COOKIE_NAME];
if (!sessionId) return;
const found = await getSessionWithUser(sessionId);
if (found) {
req.cockpitUser = found.user;
req.cockpitSessionId = sessionId;
}
});
}
+92
View File
@@ -0,0 +1,92 @@
// Authentik OIDC via openid-client v6. Authorization Code + PKCE.
import * as oidc from 'openid-client';
import { loadEnv } from '../env.js';
import { logger } from '../lib/logger.js';
let cachedConfig: oidc.Configuration | undefined;
export async function getOidcConfig(): Promise<oidc.Configuration> {
if (cachedConfig) return cachedConfig;
const env = loadEnv();
cachedConfig = await oidc.discovery(
new URL(env.AUTHENTIK_ISSUER),
env.AUTHENTIK_CLIENT_ID,
env.AUTHENTIK_CLIENT_SECRET,
);
logger.info({ issuer: env.AUTHENTIK_ISSUER }, 'oidc discovery completed');
return cachedConfig;
}
export interface PendingFlow {
state: string;
nonce: string;
codeVerifier: string;
returnTo: string;
}
export async function buildAuthorizationUrl(returnTo: string): Promise<{
url: URL;
pending: PendingFlow;
}> {
const config = await getOidcConfig();
const env = loadEnv();
const codeVerifier = oidc.randomPKCECodeVerifier();
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
const state = oidc.randomState();
const nonce = oidc.randomNonce();
const url = oidc.buildAuthorizationUrl(config, {
redirect_uri: env.AUTHENTIK_REDIRECT_URI,
scope: 'openid profile email groups',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state,
nonce,
});
return {
url,
pending: { state, nonce, codeVerifier, returnTo },
};
}
export interface OidcUserInfo {
sub: string;
email: string;
name: string;
groups: string[];
}
export async function completeAuthorization(
callbackUrl: URL,
pending: PendingFlow,
): Promise<OidcUserInfo> {
const config = await getOidcConfig();
const tokens = await oidc.authorizationCodeGrant(config, callbackUrl, {
pkceCodeVerifier: pending.codeVerifier,
expectedState: pending.state,
expectedNonce: pending.nonce,
idTokenExpected: true,
});
const claims = tokens.claims();
if (!claims) {
throw new Error('OIDC-Token enthält keine Claims.');
}
const sub = String(claims.sub ?? '');
const email = String(claims.email ?? '');
const name = String(claims.name ?? claims.preferred_username ?? email);
const rawGroups = claims.groups;
const groups: string[] = Array.isArray(rawGroups)
? rawGroups.filter((g): g is string => typeof g === 'string')
: [];
if (!sub || !email) {
throw new Error('OIDC-Claims unvollständig (sub oder email fehlt).');
}
return { sub, email, name, groups };
}
+141
View File
@@ -0,0 +1,141 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { loadEnv } from '../env.js';
import { logger } from '../lib/logger.js';
import { buildAuthorizationUrl, completeAuthorization, type PendingFlow } from './oidc.js';
import { createSession, deleteSession } from './session.js';
import { upsertUserFromOidc } from './user.js';
const PENDING_COOKIE = 'cockpit_oidc_pending';
function setSessionCookie(reply: FastifyReply, sessionId: string): void {
const env = loadEnv();
const maxAge = env.SESSION_TTL_HOURS * 60 * 60;
reply.setCookie(env.SESSION_COOKIE_NAME, sessionId, {
path: '/',
httpOnly: true,
secure: env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge,
signed: false,
});
}
function clearSessionCookie(reply: FastifyReply): void {
const env = loadEnv();
reply.clearCookie(env.SESSION_COOKIE_NAME, { path: '/' });
}
function setPendingCookie(reply: FastifyReply, pending: PendingFlow): void {
reply.setCookie(PENDING_COOKIE, JSON.stringify(pending), {
path: '/',
httpOnly: true,
secure: loadEnv().NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 10 * 60, // 10 Minuten für den Round-Trip
signed: true,
});
}
function readPendingCookie(req: FastifyRequest): PendingFlow | null {
const raw = req.cookies[PENDING_COOKIE];
if (!raw) return null;
const unsigned = req.unsignCookie(raw);
if (!unsigned.valid || !unsigned.value) return null;
try {
const parsed = JSON.parse(unsigned.value) as PendingFlow;
return parsed;
} catch {
return null;
}
}
export async function registerAuthRoutes(app: FastifyInstance): Promise<void> {
const env = loadEnv();
// Startet den OIDC-Flow.
app.get('/api/auth/login', async (req, reply) => {
const returnTo =
typeof req.query === 'object' && req.query !== null && 'returnTo' in req.query
? String((req.query as Record<string, unknown>).returnTo ?? '/puls')
: '/puls';
const { url, pending } = await buildAuthorizationUrl(returnTo);
setPendingCookie(reply, pending);
return reply.redirect(url.toString(), 302);
});
// OIDC-Callback. Tauscht den Code gegen Tokens und legt eine Session an.
app.get('/api/auth/callback', async (req, reply) => {
const pending = readPendingCookie(req);
if (!pending) {
logger.warn('callback ohne pending-cookie');
return reply.redirect(`${env.PUBLIC_BASE_URL}/login?error=session_expired`, 302);
}
try {
const callbackUrl = new URL(env.AUTHENTIK_REDIRECT_URI);
for (const [k, v] of Object.entries(req.query as Record<string, string>)) {
callbackUrl.searchParams.set(k, v);
}
const oidcUser = await completeAuthorization(callbackUrl, pending);
const userRow = await upsertUserFromOidc(oidcUser);
const newSession = await createSession({
userId: userRow.id,
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
});
setSessionCookie(reply, newSession.id);
reply.clearCookie(PENDING_COOKIE, { path: '/' });
const target = pending.returnTo.startsWith('/') ? pending.returnTo : '/puls';
return reply.redirect(`${env.PUBLIC_BASE_URL}${target}`, 302);
} catch (err) {
logger.error({ err }, 'oidc callback failed');
return reply.redirect(`${env.PUBLIC_BASE_URL}/login?error=oidc_failed`, 302);
}
});
app.post('/api/auth/logout', async (req, reply) => {
const sessionId = req.cookies[env.SESSION_COOKIE_NAME];
if (sessionId) {
await deleteSession(sessionId);
}
clearSessionCookie(reply);
return { ok: true };
});
// Dev-Auto-Login: legt sofort eine Session für den Admin-Dejan an, sodass
// die App ohne Authentik durchnavigierbar ist. Authentik kommt in der späteren
// Roadmap. Nur aktiv bei NODE_ENV=development.
if (env.NODE_ENV === 'development') {
const handler = async (req: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply) => {
const { getDb } = await import('../db.js');
const { user } = await import('@cockpit/db');
const { eq } = await import('drizzle-orm');
const { db } = getDb();
const found = (await db.select().from(user).where(eq(user.email, 'dejan@tool-ctrl.de')).limit(1))[0];
const userRow = found
? found
: await upsertUserFromOidc({
sub: 'local-dejan',
email: 'dejan@tool-ctrl.de',
name: 'Dejan Cosic',
groups: ['entwicklung'],
});
const newSession = await createSession({
userId: userRow.id,
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
});
setSessionCookie(reply, newSession.id);
return reply.redirect(`${env.PUBLIC_BASE_URL}/puls`, 302);
};
app.get('/api/auth/dev-login', handler);
app.post('/api/auth/dev-login', handler);
}
}
+63
View File
@@ -0,0 +1,63 @@
// Session-Store in Postgres. Browser sieht nur `session_id` als HttpOnly-Cookie.
import { eq, lt } from 'drizzle-orm';
import { session, user, type Session, type User } from '@cockpit/db';
import { getDb } from '../db.js';
import { loadEnv } from '../env.js';
export interface CreateSessionInput {
userId: string;
ipAddress?: string;
userAgent?: string;
}
export async function createSession(input: CreateSessionInput): Promise<Session> {
const env = loadEnv();
const { db } = getDb();
const expiresAt = new Date(Date.now() + env.SESSION_TTL_HOURS * 60 * 60 * 1000);
const [created] = await db
.insert(session)
.values({
userId: input.userId,
expiresAt,
ipAddress: input.ipAddress ?? null,
userAgent: input.userAgent ?? null,
})
.returning();
if (!created) {
throw new Error('Session konnte nicht angelegt werden.');
}
return created;
}
export async function getSessionWithUser(
sessionId: string,
): Promise<{ session: Session; user: User } | null> {
const { db } = getDb();
const rows = await db
.select({ session, user })
.from(session)
.innerJoin(user, eq(session.userId, user.id))
.where(eq(session.id, sessionId))
.limit(1);
const row = rows[0];
if (!row) return null;
if (row.session.expiresAt.getTime() <= Date.now()) {
await db.delete(session).where(eq(session.id, sessionId));
return null;
}
return row;
}
export async function deleteSession(sessionId: string): Promise<void> {
const { db } = getDb();
await db.delete(session).where(eq(session.id, sessionId));
}
export async function pruneExpiredSessions(): Promise<number> {
const { db } = getDb();
const result = await db.delete(session).where(lt(session.expiresAt, new Date()));
return result.rowCount ?? 0;
}
+37
View File
@@ -0,0 +1,37 @@
import { eq } from 'drizzle-orm';
import { user, type User } from '@cockpit/db';
import { getDb } from '../db.js';
import type { OidcUserInfo } from './oidc.js';
// MVP: Group-Claim → default_area_id wird in Story 1.x mit echter Area-Tabelle
// gemappt. Aktuell speichern wir den User mit `default_area_id = null` und
// pflegen den Bereich später manuell. (OQ-1.)
export async function upsertUserFromOidc(info: OidcUserInfo): Promise<User> {
const { db } = getDb();
const existing = await db.select().from(user).where(eq(user.authentikSub, info.sub)).limit(1);
if (existing[0]) {
const [updated] = await db
.update(user)
.set({
name: info.name,
email: info.email,
updatedAt: new Date(),
})
.where(eq(user.id, existing[0].id))
.returning();
if (!updated) throw new Error('User-Update fehlgeschlagen.');
return updated;
}
const [created] = await db
.insert(user)
.values({
authentikSub: info.sub,
name: info.name,
email: info.email,
})
.returning();
if (!created) throw new Error('User-Anlage fehlgeschlagen.');
return created;
}
+77
View File
@@ -0,0 +1,77 @@
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { loadEnv } from '../env.js';
import { logger } from '../lib/logger.js';
import { getDb } from '../db.js';
import { publishYesterdaysEntries } from '../routers/day-entry.js';
import { previousWerktag, todayIso } from '../lib/werktag.js';
function authorize(req: FastifyRequest): void {
const env = loadEnv();
const header = req.headers.authorization ?? '';
const expected = `Bearer ${env.CRON_BEARER_TOKEN}`;
if (header !== expected) {
throw new Error('Unauthorized cron call.');
}
}
export async function registerCronRoutes(app: FastifyInstance): Promise<void> {
app.post('/internal/cron/publish-day-entries', async (req, reply) => {
try {
authorize(req);
} catch (err) {
reply.code(401);
logger.warn({ err }, 'unauthorized cron call');
return { error: 'unauthorized' };
}
const { db } = getDb();
const today = todayIso();
const yesterday = previousWerktag(today);
const updated = await publishYesterdaysEntries({ db }, yesterday);
logger.info({ updated, date: yesterday }, 'published yesterday entries');
return { ok: true, updated, date: yesterday };
});
app.post('/internal/cron/recompute-streaks', async (req, reply) => {
try {
authorize(req);
} catch {
reply.code(401);
return { error: 'unauthorized' };
}
// Streaks werden lazy in `user.me` berechnet. Dieser Endpoint ist Reserve
// für künftige denormalisierte Caches (NFR-1 Performance, sobald viele User).
return { ok: true, note: 'streaks are computed lazily' };
});
app.post('/internal/cron/recompute-pflicht-minimum-5', async (req, reply) => {
try {
authorize(req);
} catch {
reply.code(401);
return { error: 'unauthorized' };
}
const { db } = getDb();
const { recomputePflichtMinimumStatusActual } = await import('../lib/pflicht-minimum.js');
const res = await recomputePflichtMinimumStatusActual(db);
logger.info(res, 'pflicht-minimum status_aktuell recomputed');
return { ok: true, ...res };
});
app.post('/internal/cron/pg-dump-backup', async (req, reply) => {
try {
authorize(req);
} catch {
reply.code(401);
return { error: 'unauthorized' };
}
// pg_dump läuft als System-Cron *im Postgres-Container* (siehe
// docs/operations.md § Backups). Dieser Endpoint ist der Audit-Trigger:
// wenn er gerufen wird, ist der Backup-Cron lebendig und der Logeintrag
// landet im API-Log (zentrale Beobachtbarkeit).
logger.info('pg-dump-backup heartbeat received');
return {
ok: true,
note: 'pg_dump runs in postgres container; see docs/operations.md',
};
});
}
+12
View File
@@ -0,0 +1,12 @@
import { createDbClient, type Db } from '@cockpit/db';
import type pg from 'pg';
import { loadEnv } from './env.js';
let cached: { db: Db; pool: pg.Pool } | undefined;
export function getDb(): { db: Db; pool: pg.Pool } {
if (cached) return cached;
const env = loadEnv();
cached = createDbClient({ connectionString: env.DATABASE_URL });
return cached;
}
+39
View File
@@ -0,0 +1,39 @@
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
PORT: z.coerce.number().int().default(3000),
DATABASE_URL: z.string().url(),
AUTHENTIK_ISSUER: z.string().url(),
AUTHENTIK_CLIENT_ID: z.string().min(1),
AUTHENTIK_CLIENT_SECRET: z.string().min(1),
AUTHENTIK_REDIRECT_URI: z.string().url(),
SESSION_COOKIE_NAME: z.string().default('cockpit_session'),
SESSION_TTL_HOURS: z.coerce.number().int().min(1).default(24),
SESSION_COOKIE_SECRET: z.string().min(32),
CRON_BEARER_TOKEN: z.string().min(8),
PUBLIC_BASE_URL: z.string().url().default('http://localhost:5173'),
});
export type Env = z.infer<typeof envSchema>;
let cached: Env | undefined;
export function loadEnv(): Env {
if (cached) return cached;
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
const issues = parsed.error.issues
.map((i) => ` - ${i.path.join('.')}: ${i.message}`)
.join('\n');
throw new Error(`Ungültige Umgebung:\n${issues}`);
}
cached = parsed.data;
return cached;
}
+39
View File
@@ -0,0 +1,39 @@
// Ampel-Aggregations-Algorithmus (FR-1a).
export type Ampel = 'green' | 'amber' | 'red';
export type AreaAmpel = Ampel | 'grey';
export interface ProjectStatusLike {
statusCache: Ampel | null;
pflichtMinimumState: Record<string, boolean>;
archiviert: boolean;
}
export function aggregateAreaAmpel(projects: ProjectStatusLike[]): {
ampel: AreaAmpel;
counts: { green: number; amber: number; red: number; grey: number };
} {
const active = projects.filter((p) => !p.archiviert);
if (active.length === 0)
return { ampel: 'grey', counts: { green: 0, amber: 0, red: 0, grey: 0 } };
let red = 0;
let amber = 0;
let green = 0;
let grey = 0;
for (const p of active) {
// Pflicht-Minimum-Verstoss hebt mindestens auf gelb.
const hasMissingPflicht = Object.entries(p.pflichtMinimumState).some(([_, v]) => v === false);
let s: Ampel | 'grey' = p.statusCache ?? 'grey';
if (s === 'green' && hasMissingPflicht) s = 'amber';
if (s === 'red') red++;
else if (s === 'amber') amber++;
else if (s === 'green') green++;
else grey++;
}
if (red > 0) return { ampel: 'red', counts: { green, amber, red, grey } };
if (amber > 0) return { ampel: 'amber', counts: { green, amber, red, grey } };
if (green > 0) return { ampel: 'green', counts: { green, amber, red, grey } };
return { ampel: 'grey', counts: { green, amber, red, grey } };
}
+6
View File
@@ -0,0 +1,6 @@
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
base: { service: 'cockpit-api' },
});
+41
View File
@@ -0,0 +1,41 @@
// Pflicht-Minimum-Berechnung (Leitfaden §9).
// Punkte 1–4 + 6 werden manuell in der App gepflegt; Punkt 5 (Status-Ampel aktuell)
// wird hier zeitabhängig berechnet und im Cron auf alle Projekte aktualisiert.
import { eq, isNotNull, isNull, and, desc } from 'drizzle-orm';
import { project, dayEntry } from '@cockpit/db';
import { werktageBetween, todayIso } from './werktag.js';
const MAX_AGE_WERKTAGE = 7;
export async function recomputePflichtMinimumStatusActual(
db: import('@cockpit/db').Db,
): Promise<{ updated: number }> {
const projects = await db.select().from(project).where(eq(project.archiviert, false));
const today = todayIso();
let updated = 0;
for (const p of projects) {
// Letzte Reflexion mit Projektbezug? (Vereinfachung: letzter day_entry mit Ampel,
// dessen Update das Projekt-Status-Cache gesetzt hat = `status_cache` selbst.)
const lastEntry = (
await db
.select()
.from(dayEntry)
.where(and(isNotNull(dayEntry.reflectedAt)))
.orderBy(desc(dayEntry.reflectedAt))
.limit(1)
)[0];
const actual = lastEntry ? werktageBetween(lastEntry.date, today) <= MAX_AGE_WERKTAGE : false;
const newState = { ...p.pflichtMinimumState, status_aktuell: actual };
if (JSON.stringify(newState) !== JSON.stringify(p.pflichtMinimumState)) {
await db.update(project).set({ pflichtMinimumState: newState }).where(eq(project.id, p.id));
updated++;
}
}
return { updated };
}
// linter
void isNull;
+30
View File
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import { formatIsoDate, isWerktag, nextWerktag, previousWerktag, werktageBetween } from './werktag';
describe('werktag', () => {
it('formatIsoDate liefert YYYY-MM-DD in Berlin TZ', () => {
const d = new Date(Date.UTC(2026, 4, 25, 8, 30));
expect(formatIsoDate(d)).toBe('2026-05-25');
});
it('isWerktag erkennt Mo-Fr', () => {
expect(isWerktag('2026-05-25')).toBe(true); // Mo
expect(isWerktag('2026-05-29')).toBe(true); // Fr
expect(isWerktag('2026-05-30')).toBe(false); // Sa
expect(isWerktag('2026-05-31')).toBe(false); // So
});
it('nextWerktag springt über Wochenende', () => {
expect(nextWerktag('2026-05-29')).toBe('2026-06-01'); // Fr → Mo
expect(nextWerktag('2026-05-25')).toBe('2026-05-26'); // Mo → Di
});
it('previousWerktag springt über Wochenende', () => {
expect(previousWerktag('2026-06-01')).toBe('2026-05-29'); // Mo → Fr
});
it('werktageBetween zählt korrekt', () => {
// Mo 2026-05-25 bis Mo 2026-06-01 (exkl.): 5 Werktage
expect(werktageBetween('2026-05-25', '2026-06-01')).toBe(5);
});
});
+68
View File
@@ -0,0 +1,68 @@
// Werktag-Logik in Europe/Berlin. Single source of truth fürs Backend
// (Frontend bekommt fertige Felder).
const TIMEZONE = 'Europe/Berlin';
/**
* Gibt das aktuelle Datum (oder `now`) als `YYYY-MM-DD` in Europe/Berlin zurück.
*/
export function todayIso(now: Date = new Date()): string {
return formatIsoDate(now);
}
export function formatIsoDate(d: Date): string {
const fmt = new Intl.DateTimeFormat('en-CA', {
timeZone: TIMEZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
return fmt.format(d); // en-CA liefert ISO YYYY-MM-DD
}
export function parseIsoDate(s: string): Date {
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) {
throw new Error(`Ungültiges ISO-Datum: ${s}`);
}
const [y, m, d] = s.split('-').map(Number) as [number, number, number];
return new Date(Date.UTC(y, m - 1, d, 12, 0, 0)); // 12:00 UTC vermeidet TZ-Drift
}
/**
* Mo-Fr = Werktag. Feiertage Phase 2.
*/
export function isWerktag(s: string): boolean {
const d = parseIsoDate(s);
const wd = new Intl.DateTimeFormat('en-US', { timeZone: TIMEZONE, weekday: 'short' }).format(d);
return wd !== 'Sat' && wd !== 'Sun';
}
export function nextWerktag(s: string): string {
let cur = parseIsoDate(s);
do {
cur = new Date(cur.getTime() + 24 * 60 * 60 * 1000);
} while (!isWerktag(formatIsoDate(cur)));
return formatIsoDate(cur);
}
export function previousWerktag(s: string): string {
let cur = parseIsoDate(s);
do {
cur = new Date(cur.getTime() - 24 * 60 * 60 * 1000);
} while (!isWerktag(formatIsoDate(cur)));
return formatIsoDate(cur);
}
/**
* Zählt Werktage zwischen zwei ISO-Daten (inkl. start, exkl. end).
*/
export function werktageBetween(startIso: string, endIso: string): number {
let cur = parseIsoDate(startIso);
const end = parseIsoDate(endIso);
let count = 0;
while (cur.getTime() < end.getTime()) {
if (isWerktag(formatIsoDate(cur))) count++;
cur = new Date(cur.getTime() + 24 * 60 * 60 * 1000);
}
return count;
}
+29
View File
@@ -0,0 +1,29 @@
// Root tRPC-Router. Pro Story kommen weitere Sub-Router dazu.
import { router, publicProcedure } from '../trpc.js';
import { dayEntryRouter } from './day-entry.js';
import { userRouter } from './user.js';
import { pulsRouter } from './puls.js';
import { projectRouter } from './project.js';
import { dealRouter, gtmRouter } from './deal.js';
import { campaignRouter } from './campaign.js';
import { areaRouter } from './area.js';
import { blockerRouter } from './blocker.js';
export const appRouter = router({
health: publicProcedure.query(() => ({
status: 'ok',
timestamp: new Date().toISOString(),
})),
user: userRouter,
area: areaRouter,
project: projectRouter,
deal: dealRouter,
gtm: gtmRouter,
campaign: campaignRouter,
blocker: blockerRouter,
dayEntry: dayEntryRouter,
puls: pulsRouter,
});
export type AppRouter = typeof appRouter;
+8
View File
@@ -0,0 +1,8 @@
import { area } from '@cockpit/db';
import { router, protectedProcedure } from '../trpc.js';
export const areaRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
return ctx.db.select().from(area).orderBy(area.order);
}),
});
+97
View File
@@ -0,0 +1,97 @@
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { eq, isNull, desc } from 'drizzle-orm';
import { blocker, project, user } from '@cockpit/db';
import { router, protectedProcedure } from '../trpc.js';
export const blockerRouter = router({
listOpen: protectedProcedure.query(async ({ ctx }) => {
return ctx.db
.select({
blocker: blocker,
opener: user,
})
.from(blocker)
.leftJoin(user, eq(user.id, blocker.openedBy))
.where(isNull(blocker.resolvedAt))
.orderBy(desc(blocker.openedAt));
}),
byProject: protectedProcedure
.input(z.object({ projectId: z.string().uuid() }))
.query(async ({ ctx, input }) => {
return ctx.db
.select()
.from(blocker)
.where(eq(blocker.projectId, input.projectId))
.orderBy(desc(blocker.openedAt));
}),
create: protectedProcedure
.input(
z.object({
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
areaId: z.string().uuid().optional(),
projectId: z.string().uuid().optional(),
ownerUserId: z.string().uuid().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
const [created] = await ctx.db
.insert(blocker)
.values({
title: input.title,
description: input.description ?? null,
areaId: input.areaId ?? null,
projectId: input.projectId ?? null,
ownerUserId: input.ownerUserId ?? null,
openedBy: ctx.user.id,
})
.returning();
return created;
}),
resolve: protectedProcedure
.input(
z.object({
blockerId: z.string().uuid(),
resolutionText: z.string().min(1).max(500),
}),
)
.mutation(async ({ ctx, input }) => {
const b = (
await ctx.db.select().from(blocker).where(eq(blocker.id, input.blockerId)).limit(1)
)[0];
if (!b) throw new TRPCError({ code: 'NOT_FOUND', message: 'Blocker nicht gefunden.' });
// Authority-Check: bei projektgebundenen Blockern nur P1; sonst Ersteller oder Verantwortlicher.
let allowed = false;
if (b.projectId) {
const p = (
await ctx.db.select().from(project).where(eq(project.id, b.projectId)).limit(1)
)[0];
if (p?.verantwortlicheUserId === ctx.user.id) allowed = true;
} else {
if (b.openedBy === ctx.user.id || b.ownerUserId === ctx.user.id) allowed = true;
}
if (!allowed) {
throw new TRPCError({
code: 'FORBIDDEN',
message:
'Nur die/der Projektverantwortliche darf diesen Blocker auflösen. (Bei projektlosen Blockern: Ersteller oder Verantwortliche/r.)',
});
}
const [updated] = await ctx.db
.update(blocker)
.set({
resolvedBy: ctx.user.id,
resolvedAt: new Date(),
resolutionText: input.resolutionText,
})
.where(eq(blocker.id, input.blockerId))
.returning();
return updated;
}),
});
+67
View File
@@ -0,0 +1,67 @@
import { desc, eq } from 'drizzle-orm';
import { campaign, leadHandover, deal, area } from '@cockpit/db';
import { CampaignSchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
const MARKETING_AREA_SLUG = 'marketing';
export const campaignRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
return ctx.db.select().from(campaign).orderBy(desc(campaign.createdAt));
}),
create: protectedProcedure
.input(CampaignSchemas.createCampaignInput)
.mutation(async ({ ctx, input }) => {
const a = (
await ctx.db.select().from(area).where(eq(area.slug, MARKETING_AREA_SLUG)).limit(1)
)[0];
if (!a) throw new Error('Marketing-Area fehlt — bitte seed laufen lassen.');
const [created] = await ctx.db
.insert(campaign)
.values({
areaId: a.id,
name: input.name,
status: input.status,
reach: input.reach,
ownerUserId: input.ownerUserId ?? null,
})
.returning();
return created;
}),
funnel: protectedProcedure.query(async ({ ctx }) => {
// Lead-Funnel: simple Aggregation der bekannten Quellen.
const campaigns = await ctx.db.select().from(campaign);
const reichweite = campaigns.reduce((acc, c) => acc + c.reach, 0);
const handovers = await ctx.db.select().from(leadHandover);
const leads = handovers.length;
const deals = await ctx.db.select().from(deal);
const mql = deals.filter((d) => ['erstgespraech', 'demo', 'angebot'].includes(d.stage)).length;
const uebergabe = handovers.length;
// Frontend kann die Stage-Übergänge daraus rendern.
return {
reichweite,
website: Math.round(reichweite * 0.2), // Placeholder bis ein Tracking-Feed existiert.
leads,
mql,
uebergabe,
};
}),
crosslink: protectedProcedure.query(async ({ ctx }) => {
const handovers = await ctx.db.select().from(leadHandover);
const dealsFromHandovers = handovers.filter((h) => h.dealId !== null).length;
// Won-Volumen aus Handover-bezogenen Deals dieses Quartals.
const dealRows = await ctx.db.select().from(deal);
const wonValue = dealRows
.filter((d) => d.wonAt)
.reduce((acc, d) => acc + (d.wonValueEur ?? 0), 0);
return {
uebergebenSeitTagen: 30,
leadsUebergeben: handovers.length,
dealsErzeugt: dealsFromHandovers,
gewonneneEurQuartal: wonValue,
};
}),
});
+335
View File
@@ -0,0 +1,335 @@
import { TRPCError } from '@trpc/server';
import { and, eq, isNull, isNotNull, lt, asc, inArray } from 'drizzle-orm';
import { dayEntry, todo, blocker, type Todo } from '@cockpit/db';
import { DayEntrySchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
import { todayIso } from '../lib/werktag.js';
export const dayEntryRouter = router({
/**
* Heutige ToDos aller aktiven Mitarbeiter, gruppiert pro User.
* Pro PRD-Änderung 2026-05-25: Heutige ToDos sind teamweit sichtbar
* (ersetzt die ältere private-vor-Publikation-Regel).
*/
todayTeam: protectedProcedure.query(async ({ ctx }) => {
const date = todayIso();
const entries = await ctx.db
.select()
.from(dayEntry)
.where(eq(dayEntry.date, date));
if (entries.length === 0) return [] as Array<{ userId: string; entryStatus: string; todos: Todo[] }>;
const ids = entries.map((e) => e.id);
const todos = await ctx.db
.select()
.from(todo)
.where(inArray(todo.dayEntryId, ids))
.orderBy(asc(todo.order));
const todosByEntry = new Map<string, Todo[]>();
for (const t of todos) {
const arr = todosByEntry.get(t.dayEntryId) ?? [];
arr.push(t);
todosByEntry.set(t.dayEntryId, arr);
}
return entries.map((e) => ({
userId: e.userId,
entryStatus: e.status,
todos: todosByEntry.get(e.id) ?? [],
}));
}),
/**
* Offene ToDos der vergangenen Tage, die noch nicht übertragen wurden.
* Werden auf der Heute-Seite vor dem Einstempeln eingeblendet (siehe FR-22 Erweiterung).
*/
pendingCarryOver: protectedProcedure.query(async ({ ctx }) => {
return ctx.db
.select({
id: todo.id,
title: todo.title,
bezugKind: todo.bezugKind,
bezugId: todo.bezugId,
fromDate: todo.date,
})
.from(todo)
.where(
and(
eq(todo.userId, ctx.user.id),
eq(todo.status, 'open'),
eq(todo.carriedOver, false),
lt(todo.date, todayIso()),
),
)
.orderBy(asc(todo.date), asc(todo.order))
.limit(20);
}),
/**
* Heute-Status der eigenen Person.
*/
today: protectedProcedure.query(async ({ ctx }) => {
const date = todayIso();
const rows = await ctx.db
.select()
.from(dayEntry)
.where(and(eq(dayEntry.userId, ctx.user.id), eq(dayEntry.date, date)))
.limit(1);
const entry = rows[0] ?? null;
if (!entry) {
return { date, entry: null, todos: [] as Todo[] };
}
const todos = await ctx.db
.select()
.from(todo)
.where(eq(todo.dayEntryId, entry.id))
.orderBy(todo.order);
return { date, entry, todos };
}),
/**
* Morgen-Einstempeln + ToDo-Liste setzen.
*/
checkIn: protectedProcedure
.input(DayEntrySchemas.checkInInput)
.mutation(async ({ ctx, input }) => {
const date = input.date ?? todayIso();
// Verhindert Doppel-Einstempeln. Wenn bereits Einträge existieren, Fehler.
const existing = await ctx.db
.select()
.from(dayEntry)
.where(and(eq(dayEntry.userId, ctx.user.id), eq(dayEntry.date, date)))
.limit(1);
if (existing[0]) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Du bist heute schon locked in.',
});
}
const result = await ctx.db.transaction(async (tx) => {
const [created] = await tx
.insert(dayEntry)
.values({
userId: ctx.user.id,
date,
status: 'begun',
checkedInAt: new Date(),
})
.returning();
if (!created) throw new Error('DayEntry konnte nicht angelegt werden.');
const todoRows = await tx
.insert(todo)
.values(
input.todos.map((t, idx) => ({
dayEntryId: created.id,
userId: ctx.user.id,
date,
title: t.title,
order: idx,
bezugKind: t.bezugKind ?? null,
bezugId: t.bezugId ?? null,
})),
)
.returning();
// Alle alten offenen ToDos der Person als "übertragen" markieren.
// Egal, ob sie in der heutigen Liste landeten oder nicht — der User hat
// beim Einstempeln über sie entschieden.
const stale = await tx
.select({ id: todo.id })
.from(todo)
.where(
and(
eq(todo.userId, ctx.user.id),
eq(todo.status, 'open'),
eq(todo.carriedOver, false),
lt(todo.date, date),
),
);
if (stale.length > 0) {
await tx
.update(todo)
.set({ carriedOver: true })
.where(
inArray(
todo.id,
stale.map((s) => s.id),
),
);
}
return { entry: created, todos: todoRows, carriedOver: stale.length };
});
return result;
}),
/**
* ToDos im Verlauf des Tages ändern (vor Reflexion).
*/
updateTodos: protectedProcedure
.input(DayEntrySchemas.todoMutationInput)
.mutation(async ({ ctx, input }) => {
const entries = await ctx.db
.select()
.from(dayEntry)
.where(and(eq(dayEntry.id, input.dayEntryId), eq(dayEntry.userId, ctx.user.id)))
.limit(1);
const entry = entries[0];
if (!entry) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Tageseintrag nicht gefunden.' });
}
if (entry.status === 'full') {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Du hast schon reflektiert. ToDos sind eingefroren.',
});
}
const result = await ctx.db.transaction(async (tx) => {
await tx.delete(todo).where(eq(todo.dayEntryId, entry.id));
if (input.todos.length === 0) return { todos: [] as Todo[] };
const newTodos = await tx
.insert(todo)
.values(
input.todos.map((t, idx) => ({
dayEntryId: entry.id,
userId: ctx.user.id,
date: entry.date,
title: t.title,
order: idx,
bezugKind: t.bezugKind ?? null,
bezugId: t.bezugId ?? null,
})),
)
.returning();
return { todos: newTodos };
});
return result;
}),
/**
* Abend-Reflexion. Setzt Ampel + Trend, optional Blocker.
*/
submitReflection: protectedProcedure
.input(DayEntrySchemas.submitReflectionInput)
.mutation(async ({ ctx, input }) => {
const entries = await ctx.db
.select()
.from(dayEntry)
.where(and(eq(dayEntry.id, input.dayEntryId), eq(dayEntry.userId, ctx.user.id)))
.limit(1);
const entry = entries[0];
if (!entry) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Tageseintrag nicht gefunden.' });
}
if (entry.status === 'full' && entry.editLockedAt && entry.editLockedAt < new Date()) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Die Reflexion ist nicht mehr editierbar.',
});
}
// Pflicht-Trend bei Bezug=Projekt (Story 2.4)
const todoRows = await ctx.db.select().from(todo).where(eq(todo.dayEntryId, entry.id));
const hasProjectBezug = todoRows.some((t) => t.bezugKind === 'project');
if (hasProjectBezug && !input.trend) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Bei Projektbezug ist der Trend Pflicht.',
});
}
const result = await ctx.db.transaction(async (tx) => {
// ToDo-Statuswechsel.
for (const t of input.todos) {
await tx
.update(todo)
.set({
status: t.status,
dropReason: t.dropReason ?? null,
completedAt: t.status === 'done' ? new Date() : null,
droppedAt: t.status === 'dropped' ? new Date() : null,
})
.where(and(eq(todo.id, t.id), eq(todo.dayEntryId, entry.id)));
}
let blockerRecord: { id: string } | null = null;
if (input.blocker) {
const [b] = await tx
.insert(blocker)
.values({
title: input.blocker.title,
description: input.blocker.description ?? null,
openedBy: ctx.user.id,
})
.returning({ id: blocker.id });
blockerRecord = b ?? null;
}
const now = new Date();
const [updated] = await tx
.update(dayEntry)
.set({
status: 'full',
reflectedAt: now,
reflectionText: input.reflectionText,
ampel: input.ampel,
trend: input.trend ?? null,
url: input.url ?? null,
blockerId: blockerRecord?.id ?? entry.blockerId,
editLockedAt: new Date(now.getTime() + 30 * 60 * 1000),
updatedAt: now,
})
.where(eq(dayEntry.id, entry.id))
.returning();
if (!updated) throw new Error('Update fehlgeschlagen.');
return { entry: updated, blocker: blockerRecord };
});
return result;
}),
/**
* Konsolidierter Vortrags-Feed: alle bereits publizierten Einträge.
* Nutzbar in Story 3.4 (Puls-Feed).
*/
feed: protectedProcedure.query(async ({ ctx }) => {
const entries = await ctx.db
.select()
.from(dayEntry)
.where(isNotNull(dayEntry.publishedAt))
.orderBy(dayEntry.publishedAt)
.limit(50);
return entries;
}),
/**
* „Heute fehlt"-Liste (Anwesenheits-Übersicht).
*/
todayPresence: protectedProcedure.query(async ({ ctx }) => {
const date = todayIso();
const rows = await ctx.db
.select({
userId: dayEntry.userId,
status: dayEntry.status,
})
.from(dayEntry)
.where(eq(dayEntry.date, date));
return rows;
}),
});
// Helper für Cron-Endpoint
export async function publishYesterdaysEntries(
ctx: { db: import('@cockpit/db').Db },
yesterdayIso: string,
): Promise<number> {
const now = new Date();
const result = await ctx.db
.update(dayEntry)
.set({ publishedAt: now })
.where(and(eq(dayEntry.date, yesterdayIso), isNull(dayEntry.publishedAt)));
return result.rowCount ?? 0;
}
+136
View File
@@ -0,0 +1,136 @@
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { desc, eq, sql, sum } from 'drizzle-orm';
import { deal, gtmStep, area } from '@cockpit/db';
import { DealSchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
const SALES_AREA_SLUG = 'vertrieb';
async function getSalesAreaId(db: import('@cockpit/db').Db): Promise<string> {
const a = (await db.select().from(area).where(eq(area.slug, SALES_AREA_SLUG)).limit(1))[0];
if (!a) throw new Error('Sales-Area fehlt — bitte seed laufen lassen.');
return a.id;
}
export const dealRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
return ctx.db.select().from(deal).orderBy(desc(deal.enteredStageAt));
}),
kpis: protectedProcedure.query(async ({ ctx }) => {
const allDeals = await ctx.db.select().from(deal);
const open = allDeals.filter((d) => !d.wonAt);
const won = allDeals.filter((d) => d.wonAt);
const weightedPipeline = open.reduce((acc, d) => acc + (d.valueEur * d.probability) / 100, 0);
// „Laufendes Quartal" = aktuelles Kalenderquartal (Server-Zeit).
const now = new Date();
const qStart = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
const wonThisQuarter = won.filter((d) => d.wonAt && d.wonAt >= qStart);
const wonValueQuarter = wonThisQuarter.reduce((acc, d) => acc + (d.wonValueEur ?? 0), 0);
return {
weightedPipelineEur: Math.round(weightedPipeline),
openDealCount: open.length,
wonCountQuarter: wonThisQuarter.length,
wonValueQuarterEur: wonValueQuarter,
};
}),
create: protectedProcedure.input(DealSchemas.createDealInput).mutation(async ({ ctx, input }) => {
const areaId = await getSalesAreaId(ctx.db);
const [created] = await ctx.db
.insert(deal)
.values({
areaId,
name: input.name,
ownerUserId: input.ownerUserId ?? null,
valueEur: input.valueEur,
probability: input.probability,
stage: input.stage,
})
.returning();
return created;
}),
updateStage: protectedProcedure
.input(DealSchemas.updateDealStageInput)
.mutation(async ({ ctx, input }) => {
const [updated] = await ctx.db
.update(deal)
.set({
stage: input.stage,
enteredStageAt: new Date(),
wonAt: input.stage === 'abschluss' ? new Date() : null,
wonValueEur:
input.stage === 'abschluss'
? sql`COALESCE(${deal.valueEur} * ${deal.probability} / 100, 0)`
: null,
})
.where(eq(deal.id, input.dealId))
.returning();
if (!updated) throw new TRPCError({ code: 'NOT_FOUND', message: 'Deal nicht gefunden.' });
return updated;
}),
});
export const gtmRouter = router({
list: protectedProcedure
.input(z.object({ product: z.string().optional() }))
.query(async ({ ctx, input }) => {
if (input.product) {
return ctx.db
.select()
.from(gtmStep)
.where(eq(gtmStep.product, input.product))
.orderBy(gtmStep.order);
}
return ctx.db.select().from(gtmStep).orderBy(gtmStep.product, gtmStep.order);
}),
create: protectedProcedure
.input(
z.object({
product: z.string().min(1).max(100),
title: z.string().min(1).max(200),
ownerUserId: z.string().uuid().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db
.select()
.from(gtmStep)
.where(eq(gtmStep.product, input.product));
const [created] = await ctx.db
.insert(gtmStep)
.values({
product: input.product,
title: input.title,
ownerUserId: input.ownerUserId ?? null,
order: existing.length,
})
.returning();
return created;
}),
setStatus: protectedProcedure
.input(
z.object({
id: z.string().uuid(),
status: z.enum(['open', 'in_progress', 'done']),
}),
)
.mutation(async ({ ctx, input }) => {
const [updated] = await ctx.db
.update(gtmStep)
.set({ status: input.status })
.where(eq(gtmStep.id, input.id))
.returning();
return updated;
}),
});
// linter
void sum;
+148
View File
@@ -0,0 +1,148 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { and, eq, desc } from 'drizzle-orm';
import { project, milestone, area, user, type Project } from '@cockpit/db';
import { ProjectSchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
const byAreaInput = z.object({
slug: z.string().min(1),
includeArchived: z.boolean().optional(),
});
const byIdInput = z.object({ id: z.string().uuid() });
export const projectRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
const rows = await ctx.db
.select({
project: project,
area: area,
verantwortliche: user,
})
.from(project)
.innerJoin(area, eq(project.areaId, area.id))
.leftJoin(user, eq(user.id, project.verantwortlicheUserId))
.where(eq(project.archiviert, false));
return rows;
}),
byAreaSlug: protectedProcedure.input(byAreaInput).query(async ({ ctx, input }) => {
const a = (await ctx.db.select().from(area).where(eq(area.slug, input.slug)).limit(1))[0];
if (!a) return [];
const projects = await ctx.db
.select()
.from(project)
.where(
input.includeArchived
? eq(project.areaId, a.id)
: and(eq(project.areaId, a.id), eq(project.archiviert, false)),
)
.orderBy(desc(project.createdAt));
return projects;
}),
byId: protectedProcedure.input(byIdInput).query(async ({ ctx, input }) => {
const rows = await ctx.db
.select({ project: project, area: area })
.from(project)
.innerJoin(area, eq(area.id, project.areaId))
.where(eq(project.id, input.id))
.limit(1);
const row = rows[0];
if (!row) throw new TRPCError({ code: 'NOT_FOUND', message: 'Projekt nicht gefunden.' });
const ms = await ctx.db.select().from(milestone).where(eq(milestone.projectId, row.project.id));
return { project: row.project, area: row.area, milestones: ms };
}),
create: protectedProcedure
.input(ProjectSchemas.createProjectInput)
.mutation(async ({ ctx, input }) => {
const result = await ctx.db.transaction(async (tx) => {
const [created] = await tx
.insert(project)
.values({
areaId: input.areaId,
name: input.name,
kind: input.kind,
verantwortlicheUserId: input.verantwortlicheUserId ?? null,
arbeitsmodus: input.arbeitsmodus ?? null,
outlineSteckbriefUrl: input.outlineSteckbriefUrl,
planeProjectUrl: input.planeProjectUrl,
risikolisteUrl: input.risikolisteUrl ?? null,
dodText: input.dodText ?? null,
start: input.start ?? null,
deadline: input.deadline ?? null,
createdBy: ctx.user.id,
pflichtMinimumState: {
steckbrief_freigegeben: false,
verantwortlicher: !!input.verantwortlicheUserId,
plane_board: true,
meilensteine: input.milestones.length > 0,
status_aktuell: false,
retrospektive: false,
},
})
.returning();
if (!created) throw new Error('Projekt konnte nicht angelegt werden.');
if (input.milestones.length > 0) {
await tx.insert(milestone).values(
input.milestones.map((m, idx) => ({
projectId: created.id,
title: m.title,
due: m.due ?? null,
order: idx,
})),
);
}
return created;
});
return result;
}),
archive: protectedProcedure
.input(ProjectSchemas.archiveProjectInput)
.mutation(async ({ ctx, input }) => {
const p = (
await ctx.db.select().from(project).where(eq(project.id, input.projectId)).limit(1)
)[0];
if (!p) throw new TRPCError({ code: 'NOT_FOUND', message: 'Projekt nicht gefunden.' });
if (p.verantwortlicheUserId !== ctx.user.id) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Nur die/der Projektverantwortliche darf das Projekt abschließen.',
});
}
const [updated] = await ctx.db
.update(project)
.set({
archiviert: true,
archiviertAm: new Date(),
lessonsLearnedUrl: input.lessonsLearnedUrl,
pflichtMinimumState: { ...p.pflichtMinimumState, retrospektive: true },
})
.where(eq(project.id, input.projectId))
.returning();
return updated as Project;
}),
setMilestoneStatus: protectedProcedure
.input(ProjectSchemas.setMilestoneStatusInput)
.mutation(async ({ ctx, input }) => {
const [updated] = await ctx.db
.update(milestone)
.set({ status: input.status })
.where(eq(milestone.id, input.milestoneId))
.returning();
return updated;
}),
listArchived: protectedProcedure.query(async ({ ctx }) => {
return await ctx.db
.select()
.from(project)
.where(eq(project.archiviert, true))
.orderBy(desc(project.archiviertAm));
}),
});
+97
View File
@@ -0,0 +1,97 @@
import { and, eq, isNotNull, isNull, count } from 'drizzle-orm';
import { area, project, blocker, dayEntry, user, type Area, type Project } from '@cockpit/db';
import { router, protectedProcedure } from '../trpc.js';
import { todayIso, previousWerktag } from '../lib/werktag.js';
import { aggregateAreaAmpel } from '../lib/ampel.js';
export const pulsRouter = router({
overview: protectedProcedure.query(async ({ ctx }) => {
const todayDate = todayIso();
const [areas, allProjects, openBlockers, todayPresence, totalActiveUsers, feedRows] =
await Promise.all([
ctx.db.select().from(area).orderBy(area.order),
ctx.db.select().from(project).where(eq(project.archiviert, false)),
ctx.db.select().from(blocker).where(isNull(blocker.resolvedAt)),
ctx.db
.select({ userId: dayEntry.userId, status: dayEntry.status })
.from(dayEntry)
.where(eq(dayEntry.date, todayDate)),
ctx.db.select({ c: count() }).from(user).where(eq(user.active, true)),
ctx.db
.select({
id: dayEntry.id,
userId: dayEntry.userId,
userName: user.name,
date: dayEntry.date,
ampel: dayEntry.ampel,
trend: dayEntry.trend,
reflectionText: dayEntry.reflectionText,
publishedAt: dayEntry.publishedAt,
})
.from(dayEntry)
.innerJoin(user, eq(user.id, dayEntry.userId))
.where(and(isNotNull(dayEntry.publishedAt), eq(dayEntry.status, 'full')))
.orderBy(dayEntry.publishedAt)
.limit(50),
]);
const projectsByArea = new Map<string, Project[]>();
for (const p of allProjects) {
const list = projectsByArea.get(p.areaId) ?? [];
list.push(p);
projectsByArea.set(p.areaId, list);
}
const areaSummaries = areas.map((a: Area) => {
const projs = projectsByArea.get(a.id) ?? [];
const agg = aggregateAreaAmpel(projs);
return {
id: a.id,
slug: a.slug,
name: a.name,
kind: a.kind,
ampel: agg.ampel,
counts: agg.counts,
};
});
const presentToday = new Set(todayPresence.map((p) => p.userId));
const usersAll = await ctx.db
.select({ id: user.id, name: user.name })
.from(user)
.where(eq(user.active, true));
const fehlende = usersAll.filter((u) => !presentToday.has(u.id));
return {
date: todayDate,
areas: areaSummaries,
blockers: openBlockers.map((b) => ({
id: b.id,
title: b.title,
openedAt: b.openedAt,
areaId: b.areaId,
projectId: b.projectId,
})),
heuteQuote: {
gepingt: presentToday.size,
gesamt: totalActiveUsers[0]?.c ?? 0,
fehlende,
},
jourfixe: { nextDate: nextJourfixe(todayDate), label: 'Di 11:00' },
pflichtMinimumOffen: allProjects
.filter((p) => Object.values(p.pflichtMinimumState).some((v) => v === false))
.map((p) => ({ id: p.id, name: p.name, areaId: p.areaId })),
feed: feedRows,
gestern: previousWerktag(todayDate),
};
}),
});
function nextJourfixe(todayIsoDate: string): string {
// Nächster Dienstag (Default Jourfixe). Wenn heute Dienstag, dann heute.
const d = new Date(`${todayIsoDate}T00:00:00Z`);
const wd = d.getUTCDay(); // 0=So..6=Sa
const offset = (2 - wd + 7) % 7 || 0; // 2=Di
d.setUTCDate(d.getUTCDate() + offset);
return d.toISOString().slice(0, 10);
}
+111
View File
@@ -0,0 +1,111 @@
import { and, eq, gte, lte } from 'drizzle-orm';
import { TRPCError } from '@trpc/server';
import { user, dayEntry, absenceDay } from '@cockpit/db';
import { DayEntrySchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
import {
todayIso,
isWerktag,
previousWerktag,
parseIsoDate,
formatIsoDate,
} from '../lib/werktag.js';
async function computeStreak(db: import('@cockpit/db').Db, userId: string): Promise<number> {
const today = todayIso();
// Hole alle relevanten DayEntries und Abwesenheiten der letzten 90 Tage.
const endIso = today;
const start = parseIsoDate(today);
start.setUTCDate(start.getUTCDate() - 120);
const startIso = formatIsoDate(start);
const [entries, absences] = await Promise.all([
db
.select({ date: dayEntry.date, status: dayEntry.status })
.from(dayEntry)
.where(
and(eq(dayEntry.userId, userId), gte(dayEntry.date, startIso), lte(dayEntry.date, endIso)),
),
db
.select({ date: absenceDay.date })
.from(absenceDay)
.where(
and(
eq(absenceDay.userId, userId),
gte(absenceDay.date, startIso),
lte(absenceDay.date, endIso),
),
),
]);
const fullByDate = new Map<string, boolean>();
for (const e of entries) fullByDate.set(e.date, e.status === 'full');
const absSet = new Set(absences.map((a) => a.date));
// Zähle aufeinanderfolgende volle Werktage rückwärts ab heute.
let cursor = today;
let streak = 0;
// Wenn heute noch nicht voll: starten beim vorherigen Werktag.
if (!fullByDate.get(today)) {
cursor = previousWerktag(today);
}
while (true) {
if (!isWerktag(cursor)) break;
if (absSet.has(cursor)) {
cursor = previousWerktag(cursor);
continue;
}
if (fullByDate.get(cursor)) {
streak++;
cursor = previousWerktag(cursor);
continue;
}
break;
}
return streak;
}
export const userRouter = router({
me: protectedProcedure.query(async ({ ctx }) => {
const streak = await computeStreak(ctx.db, ctx.user.id);
return {
id: ctx.user.id,
name: ctx.user.name,
email: ctx.user.email,
streak,
};
}),
list: protectedProcedure.query(async ({ ctx }) => {
const rows = await ctx.db
.select({ id: user.id, name: user.name, email: user.email })
.from(user)
.where(eq(user.active, true));
return rows;
}),
markAbsent: protectedProcedure
.input(DayEntrySchemas.markAbsentInput)
.mutation(async ({ ctx, input }) => {
if (input.to < input.from) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Endedatum vor Anfangsdatum.' });
}
// Enumeriere Werktage zwischen from und to (inkl.) und füge sie als absence_day ein.
let cursor = input.from;
const toInsert: { userId: string; date: string; reason: 'urlaub' | 'krank' | 'sonstiges' }[] =
[];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (cursor <= input.to) {
if (isWerktag(cursor)) {
toInsert.push({ userId: ctx.user.id, date: cursor, reason: input.reason });
}
const next = parseIsoDate(cursor);
next.setUTCDate(next.getUTCDate() + 1);
cursor = formatIsoDate(next);
}
if (toInsert.length === 0) return { inserted: 0 };
await ctx.db.insert(absenceDay).values(toInsert).onConflictDoNothing();
return { inserted: toInsert.length };
}),
});
+91
View File
@@ -0,0 +1,91 @@
import Fastify from 'fastify';
import cookie from '@fastify/cookie';
import { fastifyTRPCPlugin, type FastifyTRPCPluginOptions } from '@trpc/server/adapters/fastify';
import { loadEnv } from './env.js';
import { logger } from './lib/logger.js';
import { registerAuthMiddleware } from './auth/middleware.js';
import { registerAuthRoutes } from './auth/routes.js';
import { registerCronRoutes } from './cron/routes.js';
import { getDb } from './db.js';
import { appRouter, type AppRouter } from './routers/_app.js';
import { createContext } from './trpc.js';
async function main() {
const env = loadEnv();
const app = Fastify({
logger: {
level: env.LOG_LEVEL,
base: { service: 'cockpit-api' },
},
});
await app.register(cookie, {
secret: env.SESSION_COOKIE_SECRET,
});
await registerAuthMiddleware(app);
await registerAuthRoutes(app);
await registerCronRoutes(app);
await app.register(fastifyTRPCPlugin, {
prefix: '/trpc',
trpcOptions: {
router: appRouter,
createContext,
onError({ path, error }) {
logger.error({ path, err: error }, 'trpc error');
},
} satisfies FastifyTRPCPluginOptions<AppRouter>['trpcOptions'],
});
app.get('/api/health', async () => {
const { pool } = getDb();
let dbOk = false;
try {
await pool.query('SELECT 1');
dbOk = true;
} catch (err) {
logger.warn({ err }, 'db ping failed');
}
let authentikOk = false;
try {
const res = await fetch(new URL('.well-known/openid-configuration', env.AUTHENTIK_ISSUER), {
signal: AbortSignal.timeout(3_000),
});
authentikOk = res.ok;
} catch (err) {
logger.warn({ err }, 'authentik ping failed');
}
const status = dbOk && authentikOk ? 'ok' : 'degraded';
return {
status,
db: dbOk ? 'ok' : 'down',
authentik: authentikOk ? 'ok' : 'down',
timestamp: new Date().toISOString(),
};
});
app.get('/api/me', async (req, reply) => {
if (!req.cockpitUser) {
reply.code(401);
return { error: 'Nicht angemeldet.' };
}
return {
id: req.cockpitUser.id,
name: req.cockpitUser.name,
email: req.cockpitUser.email,
};
});
try {
await app.listen({ port: env.PORT, host: '0.0.0.0' });
logger.info({ port: env.PORT }, 'api server started');
} catch (err) {
logger.error(err, 'failed to start api server');
process.exit(1);
}
}
main();
+69
View File
@@ -0,0 +1,69 @@
// tRPC v11 Core: Context, initTRPC, Procedure-Typen, Audit-Wrapper.
import './types.js';
import { initTRPC, TRPCError } from '@trpc/server';
import type { FastifyRequest, FastifyReply } from 'fastify';
import type { User } from '@cockpit/db';
import { getDb } from './db.js';
export interface Context {
user: User | null;
sessionId: string | null;
req: FastifyRequest;
reply: FastifyReply;
db: ReturnType<typeof getDb>['db'];
}
export async function createContext(opts: {
req: FastifyRequest;
res: FastifyReply;
}): Promise<Context> {
const { db } = getDb();
return {
user: opts.req.cockpitUser,
sessionId: opts.req.cockpitSessionId,
req: opts.req,
reply: opts.res,
db,
};
}
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const mergeRouters = t.mergeRouters;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Du bist nicht angemeldet.',
});
}
return next({ ctx: { ...ctx, user: ctx.user } });
});
// Audit-Wrapper: schreibt nach erfolgreicher Mutation einen audit_event.
// Genutzt als zusätzliche Middleware auf Mutations.
export function withAudit<TInput, TResult extends Record<string, unknown> | unknown>(
action: string,
) {
return async function audit(opts: {
ctx: Context;
input: TInput;
next: () => Promise<{ ok: true; data: TResult } | TResult>;
}) {
const result = await opts.next();
if (!opts.ctx.user) return result;
const { auditEvent } = await import('@cockpit/db');
await opts.ctx.db.insert(auditEvent).values({
actorUserId: opts.ctx.user.id,
action,
entityKind: action.split('.')[0] ?? 'unknown',
payload: { input: opts.input as unknown },
});
return result;
};
}
+13
View File
@@ -0,0 +1,13 @@
// Globale Fastify-Augmentations — wird von jeder Datei mitgezogen, die
// `apps/api/src/server.ts` oder via Re-Export-Pfad in Reach kommt.
import type { User } from '@cockpit/db';
declare module 'fastify' {
interface FastifyRequest {
cockpitUser: User | null;
cockpitSessionId: string | null;
}
}
export {};
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"types": ["node"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+24
View File
@@ -0,0 +1,24 @@
# --- builder ---
FROM node:22-alpine AS builder
WORKDIR /repo
RUN corepack enable && corepack prepare pnpm@10.28.0 --activate
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./
COPY apps/web/package.json ./apps/web/
COPY apps/api/package.json ./apps/api/
COPY packages/db/package.json ./packages/db/
COPY packages/shared/package.json ./packages/shared/
RUN pnpm install --frozen-lockfile
COPY apps/web ./apps/web
COPY apps/api ./apps/api
COPY packages/db ./packages/db
COPY packages/shared ./packages/shared
RUN pnpm --filter web build
# --- runtime (static via Caddy in production compose) ---
FROM caddy:2-alpine AS runtime
COPY --from=builder /repo/apps/web/dist /srv
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/toolctrl-emblem.svg" />
<link rel="alternate icon" type="image/png" href="/favicon.png" />
<title>everythingbutwork — ToolCTRL</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
{
"name": "web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview --port 5173",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@cockpit/api": "workspace:*",
"@cockpit/db": "workspace:*",
"@cockpit/shared": "workspace:*",
"@tanstack/react-query": "^5.62.7",
"@trpc/client": "^11.0.0-rc.660",
"@trpc/react-query": "^11.0.0-rc.660",
"@trpc/server": "^11.0.0-rc.660",
"@fontsource/inter": "^5.1.1",
"@fontsource/jetbrains-mono": "^5.1.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^7.1.1",
"sonner": "^1.7.1",
"zod": "^3.24.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^5.4.11",
"vitest": "^2.1.8"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 713.32 678.61">
<defs>
<style>
.cls-1 {
fill: url(#linear-gradient-5);
}
.cls-2 {
fill: url(#linear-gradient-6);
}
.cls-3 {
fill: url(#linear-gradient-4);
}
.cls-4 {
fill: url(#linear-gradient-3);
}
.cls-5 {
fill: url(#linear-gradient-2);
}
.cls-6 {
fill: url(#linear-gradient);
}
</style>
<linearGradient id="linear-gradient" x1="658.96" y1="31.96" x2="297.7" y2="458.6" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#000"/>
<stop offset="1" stop-color="#333"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="1684.3" y1="1142.75" x2="2206.09" y2="1553.51" gradientTransform="translate(-1833.74 -1108.96) rotate(5.06) skewX(14.25)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b2b2b2"/>
<stop offset=".78" stop-color="#fff"/>
</linearGradient>
<linearGradient id="linear-gradient-3" x1="1719.13" y1="1098.51" x2="2240.91" y2="1509.28" xlink:href="#linear-gradient-2"/>
<linearGradient id="linear-gradient-4" x1="1764.82" y1="1040.47" x2="2286.6" y2="1451.24" xlink:href="#linear-gradient-2"/>
<linearGradient id="linear-gradient-5" x1="1806.27" y1="987.82" x2="2328.05" y2="1398.59" xlink:href="#linear-gradient-2"/>
<linearGradient id="linear-gradient-6" x1="345.03" y1="-10.15" x2="1014.55" y2="516.91" gradientTransform="matrix(1,0,0,1,0,0)" xlink:href="#linear-gradient-2"/>
</defs>
<g>
<path d="M688.7,572.09l-160,98.95c-9.14,5.73-19.99,8.35-30.77,7.37l-380.68-34.36c-22.48-2.01-40.76-19.01-44.35-41.31L.64,159.31c-3.17-19.56,5.6-39.12,22.3-49.72L189.46,8.08c-13.95,9.75,23.77,31.55,26.88,49.95l91.3,361.13c6.21,36.68,36.37,64.58,73.42,67.93l278.64,42.9c8.65.79,21.87,45.68,28.94,42.03l.06.06Z"/>
<path class="cls-6" d="M688.64,572.03c-7.07,3.66-15.23,5.36-23.88,4.57l-349-31.5c-37.04-3.35-67.21-31.26-73.42-67.93L170.39,53.35c-3.11-18.4,5.12-35.52,19.07-45.27l1.95-1.16c7.98-5.06,17.61-7.62,27.91-6.76l358.56,30.34c29.49,2.5,53.56,24.62,58.61,53.8l76.16,440.39c3.53,20.29-7.19,38.99-24.01,47.34Z"/>
</g>
<g>
<g>
<path class="cls-5" d="M300.22,343.93l-15.01,4.26,15.73,96.05c4.23,25.83,21.88,42.59,47.21,44.83l20.42,1.81-2.48-15.14-20.42-1.81c-15.81-1.4-26.04-11.44-28.79-28.28l-8.24-50.34,44.93,3.97-2.45-14.94-44.93-3.97-5.97-36.43Z"/>
<path class="cls-4" d="M421.96,387.97c-31.88-2.82-51.26,18.1-46.08,49.73,5.18,31.64,32.08,56.65,63.97,59.47,31.88,2.82,51.26-18.1,46.08-49.73-5.18-31.64-32.08-56.65-63.97-59.47ZM437.36,482.03c-22.9-2.03-41.54-19.67-45.35-42.9-3.8-23.23,9.53-38.04,32.43-36.02,22.9,2.03,41.54,19.66,45.35,42.9,3.8,23.23-9.53,38.04-32.42,36.02Z"/>
<path class="cls-3" d="M541.37,398.53c-31.88-2.82-51.26,18.1-46.08,49.73,5.18,31.64,32.08,56.65,63.97,59.47,31.88,2.82,51.26-18.1,46.08-49.73-5.18-31.64-32.08-56.65-63.97-59.47ZM556.77,492.59c-22.9-2.03-41.54-19.67-45.35-42.9-3.8-23.23,9.53-38.04,32.43-36.02,22.9,2.03,41.54,19.66,45.35,42.9,3.8,23.23-9.53,38.04-32.42,36.02Z"/>
<polygon class="cls-1" points="607.67 368.41 631.1 511.46 647.04 512.87 622.68 364.15 607.67 368.41"/>
</g>
<path class="cls-2" d="M582.39,164.07l18.3,1.62-56.29-75.12c-3.15-4.2-7.93-6.86-13.16-7.33h0c-7.61-.67-14.86,3.41-18.22,10.27l-30.3,61.75,19.2,1.7,27.57-56.2c.99-2.01,3.75-2.27,5.1-.48l47.81,63.79Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+12
View File
@@ -0,0 +1,12 @@
import { Toaster } from 'sonner';
import { TrpcProvider } from './lib/trpc-provider';
import { Router } from './lib/router';
export function App() {
return (
<TrpcProvider>
<Router />
<Toaster position="bottom-right" richColors={false} />
</TrpcProvider>
);
}
@@ -0,0 +1,71 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { trpc } from '../../lib/trpc';
import { voice } from '../../lib/voice';
import { Button } from '../ui/Button';
export function BlockerDialog({ onClose }: { onClose: () => void }) {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const utils = trpc.useUtils();
const mutation = trpc.blocker.create.useMutation({
onSuccess: async () => {
toast(voice.toast.blockerOpened);
await utils.puls.overview.invalidate();
onClose();
},
});
function submit(e: React.FormEvent) {
e.preventDefault();
if (!title.trim()) return;
mutation.mutate({ title: title.trim(), description: description.trim() || undefined });
}
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
onClick={onClose}
>
<div
className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-semibold">Neuer Blocker</h2>
<form onSubmit={submit} className="mt-4 space-y-3">
<label className="block">
<span className="text-sm font-medium">Was klemmt?</span>
<input
required
ref={(el) => {
if (el && !title) el.focus();
}}
value={title}
onChange={(e) => setTitle(e.target.value)}
maxLength={200}
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
</label>
<label className="block">
<span className="text-sm font-medium">Mehr Kontext (optional)</span>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
maxLength={1000}
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
</label>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="ghost" onClick={onClose}>
Abbrechen
</Button>
<Button type="submit" disabled={mutation.isPending}>
Erstellen
</Button>
</div>
</form>
</div>
</div>
);
}
+113
View File
@@ -0,0 +1,113 @@
import { Outlet, useNavigate } from 'react-router';
import { useEffect, useState } from 'react';
import { TopNav } from './TopNav';
import { useCurrentUser } from '../../hooks/useCurrentUser';
import { BlockerDialog } from '../blocker/BlockerDialog';
const SHORTCUT_MAP: Record<string, string> = {
p: '/puls',
h: '/heute',
b: '/bereich',
e: '/bereich/entwicklung',
v: '/bereich/vertrieb',
m: '/bereich/marketing',
a: '/archiv',
};
export function AppShell() {
const me = useCurrentUser();
const navigate = useNavigate();
const [blockerOpen, setBlockerOpen] = useState(false);
// Dev-Mode: bei fehlender Session sofort in den Auto-Login. Auf prod hingegen
// zeigen wir die Login-Aufforderung mit dem Authentik-Link.
useEffect(() => {
if (import.meta.env.DEV && me === null) {
// Kleiner Delay, damit der useQuery sicher fertig ist und wir nicht
// mehrmals redirecten.
const t = setTimeout(() => {
if (window.location.pathname !== '/login') {
window.location.replace('/api/auth/dev-login');
}
}, 150);
return () => clearTimeout(t);
}
return undefined;
}, [me]);
// Keyboard-Shortcuts: `g <key>` → Navigation, `n` → Neuer Blocker.
useEffect(() => {
let waitingForSecond = false;
let timer: ReturnType<typeof setTimeout> | null = null;
function reset() {
waitingForSecond = false;
if (timer) clearTimeout(timer);
timer = null;
}
function onKey(e: KeyboardEvent) {
const target = e.target as HTMLElement | null;
if (
target &&
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
) {
return;
}
if (e.key === '?' && !waitingForSecond) {
// eslint-disable-next-line no-alert
alert(
'Shortcuts:\n g p — Puls\n g b — Bereich\n g e/v/m — Entwicklung/Vertrieb/Marketing\n g h — Heute\n g a — Archiv\n n — Neuer Blocker\n ? — diese Hilfe',
);
return;
}
if (e.key.toLowerCase() === 'n' && !waitingForSecond) {
setBlockerOpen(true);
return;
}
if (!waitingForSecond && e.key.toLowerCase() === 'g') {
waitingForSecond = true;
timer = setTimeout(reset, 1500);
return;
}
if (waitingForSecond) {
const target = SHORTCUT_MAP[e.key.toLowerCase()];
if (target) navigate(target);
reset();
}
}
window.addEventListener('keydown', onKey);
return () => {
window.removeEventListener('keydown', onKey);
reset();
};
}, [navigate]);
return (
<div className="min-h-screen bg-[var(--color-bg)]">
<a href="#main" className="skip-link">
Zum Hauptinhalt springen
</a>
<TopNav />
<main id="main" className="mx-auto max-w-[1280px] px-6 py-8">
{me ? <Outlet /> : <NotLoggedInHint />}
</main>
{blockerOpen && <BlockerDialog onClose={() => setBlockerOpen(false)} />}
</div>
);
}
function NotLoggedInHint() {
return (
<div className="mx-auto mt-16 max-w-md rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] p-8 text-center shadow-[var(--shadow-sm)]">
<p className="text-[var(--color-ink-muted)]">Du bist nicht angemeldet.</p>
<a
href="/api/auth/login"
className="mt-4 inline-flex h-9 items-center rounded-[var(--radius-md)] bg-[var(--color-brand)] px-4 text-sm font-medium text-[var(--color-brand-fg)] hover:bg-black/85"
>
Über Authentik anmelden
</a>
</div>
);
}
+99
View File
@@ -0,0 +1,99 @@
import { NavLink, useLocation } from 'react-router';
import { clsx } from 'clsx';
import { Flame } from 'lucide-react';
import { ToolCtrlLogo } from '../ui/ToolCtrlLogo';
import { voice } from '../../lib/voice';
import { useCurrentUser } from '../../hooks/useCurrentUser';
const TABS = [
{ to: '/puls', label: 'Puls', shortcut: 'g p', matchPrefix: '/puls' },
{ to: '/bereich', label: 'Bereich', shortcut: 'g b', matchPrefix: '/bereich' },
{ to: '/heute', label: 'Heute', shortcut: 'g h', matchPrefix: '/heute' },
{ to: '/archiv', label: 'Archiv', shortcut: 'g a', matchPrefix: '/archiv' },
] as const;
function getInitials(name: string): string {
return name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((p) => p[0]?.toUpperCase() ?? '')
.join('');
}
export function TopNav() {
const me = useCurrentUser();
const location = useLocation();
return (
<header
className="sticky top-0 z-30 bg-[var(--color-bg)] border-b border-[var(--color-border)]"
role="banner"
>
<nav
className="mx-auto flex h-14 max-w-[1280px] items-center gap-2 px-6"
aria-label="Hauptnavigation"
>
<NavLink to="/puls" className="flex items-center gap-1.5 font-bold text-[var(--color-ink)]">
<span>{voice.appName}</span>
<ToolCtrlLogo size={18} />
</NavLink>
<ul className="flex items-center gap-0.5 ml-6">
{TABS.map((tab) => {
const isActive = location.pathname.startsWith(tab.matchPrefix);
return (
<li key={tab.to}>
<NavLink
to={tab.to}
className={clsx(
'inline-flex h-9 items-center rounded-[var(--radius-md)] px-3 text-sm font-medium',
'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-black/5',
isActive &&
'text-[var(--color-ink)] border-b-2 border-[var(--color-ink)] rounded-b-none',
)}
aria-keyshortcuts={tab.shortcut}
>
{tab.label}
</NavLink>
</li>
);
})}
</ul>
<div className="ml-auto flex items-center gap-3">
{me ? (
<>
<span
className={clsx(
'inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium',
me.streak > 0
? 'bg-[var(--color-ampel-amber-bg)] text-[var(--color-ampel-amber-fg)]'
: 'bg-black/5 text-[var(--color-ink-muted)]',
)}
title={`Aktuelle Update-Serie: ${me.streak} ${me.streak === 1 ? 'Tag' : 'Tage'}`}
>
<Flame size={12} fill={me.streak > 0 ? 'currentColor' : 'none'} />
<span className="font-mono tabular-nums">{me.streak}</span>
</span>
<div
className="flex h-8 w-8 items-center justify-center rounded-full bg-[var(--color-brand)] text-xs font-semibold text-[var(--color-brand-fg)]"
aria-label={me.name}
title={me.name}
>
{getInitials(me.name)}
</div>
</>
) : (
<a
href="/api/auth/login"
className="text-sm font-medium text-[var(--color-ink)] hover:underline"
>
Login
</a>
)}
</div>
</nav>
</header>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { clsx } from 'clsx';
import { forwardRef, type ButtonHTMLAttributes } from 'react';
type Variant = 'primary' | 'secondary' | 'ghost' | 'destructive';
type Size = 'sm' | 'md' | 'lg';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
}
const base =
'inline-flex items-center justify-center gap-2 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
const variantStyles: Record<Variant, string> = {
primary: 'bg-[var(--color-brand)] text-[var(--color-brand-fg)] hover:bg-black/85',
secondary: 'border border-[var(--color-ink)] text-[var(--color-ink)] hover:bg-black/5',
ghost: 'text-[var(--color-ink)] hover:bg-black/5',
destructive: 'bg-[var(--color-ampel-red-fg)] text-white hover:bg-red-800',
};
const sizeStyles: Record<Size, string> = {
sm: 'h-7 px-3 text-xs rounded-[var(--radius-md)]',
md: 'h-9 px-4 text-sm rounded-[var(--radius-md)]',
lg: 'h-11 px-6 text-base rounded-[var(--radius-md)]',
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ variant = 'primary', size = 'md', className, ...rest },
ref,
) {
return (
<button
ref={ref}
className={clsx(base, variantStyles[variant], sizeStyles[size], className)}
{...rest}
/>
);
});
+33
View File
@@ -0,0 +1,33 @@
import { clsx } from 'clsx';
import type { HTMLAttributes } from 'react';
function Root({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
return (
<div
className={clsx(
'bg-[var(--color-surface)] border border-[var(--color-border)] rounded-[var(--radius-lg)] shadow-[var(--shadow-sm)]',
className,
)}
{...rest}
/>
);
}
function Header({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
return <div className={clsx('px-6 pt-6 pb-3', className)} {...rest} />;
}
function Body({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
return <div className={clsx('px-6 py-3', className)} {...rest} />;
}
function Footer({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
return (
<div
className={clsx('px-6 pt-3 pb-6 border-t border-[var(--color-border)]', className)}
{...rest}
/>
);
}
export const Card = { Root, Header, Body, Footer };
+28
View File
@@ -0,0 +1,28 @@
// macOS-Ctrl-Caret als Brand-Akzent. Skalierbar via currentColor.
export function KeyCaret({
size = 16,
className,
'aria-hidden': ariaHidden = true,
}: {
size?: number;
className?: string;
'aria-hidden'?: boolean;
}) {
return (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden={ariaHidden}
>
<polyline points="3 10 8 5 13 10" />
</svg>
);
}
@@ -0,0 +1,24 @@
import { clsx } from 'clsx';
export function SkeletonCard({ className, lines = 3 }: { className?: string; lines?: number }) {
return (
<div
className={clsx(
'bg-[var(--color-surface)] border border-[var(--color-border)] rounded-[var(--radius-lg)] p-6 animate-pulse',
className,
)}
aria-hidden
>
<div className="h-4 w-1/3 bg-[var(--color-border)] rounded" />
<div className="mt-3 space-y-2">
{Array.from({ length: lines }).map((_, i) => (
<div
key={i}
className="h-3 bg-[var(--color-border)] rounded"
style={{ width: `${100 - i * 12}%` }}
/>
))}
</div>
</div>
);
}
@@ -0,0 +1,27 @@
// Brand-Emblem (das Tool-Ctrl-Tastenkappensymbol). Ersetzt den früher
// verwendeten Caret als Marken-Anker im Logo-Lockup und in Streaks.
export function ToolCtrlLogo({
size = 16,
className,
'aria-hidden': ariaHidden = true,
alt = 'ToolCTRL',
}: {
size?: number;
className?: string;
'aria-hidden'?: boolean;
alt?: string;
}) {
return (
<img
src="/toolctrl-emblem.svg"
width={size}
height={(size * 678) / 713}
className={className}
aria-hidden={ariaHidden}
alt={ariaHidden ? '' : alt}
draggable={false}
style={{ display: 'inline-block' }}
/>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { trpc } from '../lib/trpc';
export function useCurrentUser() {
const q = trpc.user.me.useQuery(undefined, {
retry: false,
refetchOnWindowFocus: false,
});
return q.data ?? null;
}
+51
View File
@@ -0,0 +1,51 @@
import { createBrowserRouter, RouterProvider } from 'react-router';
import { lazy, Suspense } from 'react';
import { AppShell } from '../components/layout/AppShell';
import { SkeletonCard } from '../components/ui/SkeletonCard';
const Puls = lazy(() => import('../routes/puls').then((m) => ({ default: m.PulsRoute })));
const Heute = lazy(() =>
import('../routes/heute').then((m) => ({ default: m.HeuteRoute })),
);
const Reflexion = lazy(() =>
import('../routes/reflexion').then((m) => ({ default: m.ReflexionRoute })),
);
const Bereich = lazy(() => import('../routes/bereich').then((m) => ({ default: m.BereichRoute })));
const Projekt = lazy(() => import('../routes/projekt').then((m) => ({ default: m.ProjektRoute })));
const Archiv = lazy(() => import('../routes/archiv').then((m) => ({ default: m.ArchivRoute })));
const NotFound = lazy(() =>
import('../routes/not-found').then((m) => ({ default: m.NotFoundRoute })),
);
const Login = lazy(() => import('../routes/login').then((m) => ({ default: m.LoginRoute })));
function withSuspense(node: React.ReactNode) {
return (
<Suspense fallback={<SkeletonCard className="mx-auto mt-12 max-w-2xl" />}>{node}</Suspense>
);
}
const router = createBrowserRouter([
{
path: '/login',
element: withSuspense(<Login />),
},
{
path: '/',
element: <AppShell />,
children: [
{ index: true, element: withSuspense(<Puls />) },
{ path: 'puls', element: withSuspense(<Puls />) },
{ path: 'heute', element: withSuspense(<Heute />) },
{ path: 'reflexion', element: withSuspense(<Reflexion />) },
{ path: 'bereich', element: withSuspense(<Bereich />) },
{ path: 'bereich/:slug', element: withSuspense(<Bereich />) },
{ path: 'projekt/:id', element: withSuspense(<Projekt />) },
{ path: 'archiv', element: withSuspense(<Archiv />) },
{ path: '*', element: withSuspense(<NotFound />) },
],
},
]);
export function Router() {
return <RouterProvider router={router} />;
}
+51
View File
@@ -0,0 +1,51 @@
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { toast } from 'sonner';
import { trpc } from './trpc';
import { voice } from './voice';
export function TrpcProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: 1,
},
mutations: {
onError(err) {
const msg =
err &&
typeof err === 'object' &&
'message' in err &&
typeof err.message === 'string'
? err.message
: voice.error.generic;
toast(msg);
},
},
},
}),
);
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/trpc',
fetch(url, options) {
return fetch(url, { ...options, credentials: 'include' });
},
}),
],
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
);
}
+4
View File
@@ -0,0 +1,4 @@
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@cockpit/api/router';
export const trpc = createTRPCReact<AppRouter>();
+59
View File
@@ -0,0 +1,59 @@
// Zentrale UI-Mikrocopy — NFR-12/13 Voice & Tone.
// Locker, direkt, deutsch. Keine Anglizismen, keine Corporate-Floskeln.
export const voice = {
appName: 'everythingbutwork',
caret: '^',
toast: {
checkedIn: 'Locked in.',
reflected: 'Reflektiert.',
blockerOpened: 'Blocker offen.',
blockerResolved: 'Blocker erledigt.',
projectCreated: 'Projekt angelegt.',
projectArchived: 'Projekt archiviert.',
saved: 'Gespeichert.',
strong: 'Stark.',
},
cta: {
checkIn: 'Lock in',
reflect: 'Reflektieren',
newBlocker: 'Neuer Blocker',
newProject: 'Neues Projekt',
},
hint: {
notCheckedInToday: 'Du hast heute noch nicht gepingt.',
reflectionPending: 'Reflexion offen',
notReflectedYet: 'Du hast heute noch nicht reflektiert.',
howIsItGoing: 'Wie läuft es?',
},
empty: {
feed: 'Hier erscheinen die Updates des Tages, sobald jemand reflektiert hat.',
blockers: 'Keine offenen Blocker.',
projects: 'Noch keine Projekte. Leg eines an.',
archive: 'Noch nichts archiviert.',
},
error: {
generic: 'Da ist etwas schiefgelaufen. Bitte nochmal versuchen.',
notLoggedIn: 'Du bist nicht angemeldet.',
notAllowed: 'Dafür hast du keine Berechtigung.',
notFound: 'Nicht gefunden.',
},
label: {
bereich: 'Bereich',
bezug: 'Bezug',
ampel: {
green: 'Im Plan',
amber: 'Gefährdet',
red: 'Blockiert',
},
trend: {
better: 'Besser',
flat: 'Gleich',
worse: 'Schlechter',
},
projektart: {
kundenprojekt: 'Kundenprojekt',
internesProjekt: 'Internes Projekt',
},
},
} as const;
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import '@fontsource/inter/latin-300.css';
import '@fontsource/inter/latin-400.css';
import '@fontsource/inter/latin-500.css';
import '@fontsource/inter/latin-600.css';
import '@fontsource/inter/latin-700.css';
import '@fontsource/jetbrains-mono/latin-400.css';
import './styles/globals.css';
import { App } from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+38
View File
@@ -0,0 +1,38 @@
import { Link } from 'react-router';
import { trpc } from '../lib/trpc';
import { Card } from '../components/ui/Card';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { voice } from '../lib/voice';
export function ArchivRoute() {
const q = trpc.project.listArchived.useQuery();
if (q.isPending) return <SkeletonCard lines={4} />;
const items = q.data ?? [];
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Archiv</h1>
{items.length === 0 ? (
<Card.Root>
<Card.Body>
<p className="text-sm text-[var(--color-ink-muted)]">{voice.empty.archive}</p>
</Card.Body>
</Card.Root>
) : (
<div className="space-y-2">
{items.map((p) => (
<Card.Root key={p.id}>
<Card.Body className="flex items-center justify-between">
<Link to={`/projekt/${p.id}`} className="font-medium hover:underline">
{p.name}
</Link>
<span className="text-xs text-[var(--color-ink-muted)]">
archiviert: {p.archiviertAm ? p.archiviertAm.toString().slice(0, 10) : '—'}
</span>
</Card.Body>
</Card.Root>
))}
</div>
)}
</div>
);
}
+320
View File
@@ -0,0 +1,320 @@
import { Link, NavLink, useParams } from 'react-router';
import { useState } from 'react';
import { trpc } from '../lib/trpc';
import { voice } from '../lib/voice';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { toast } from 'sonner';
import { clsx } from 'clsx';
const BEREICHE: Array<{ slug: 'entwicklung' | 'vertrieb' | 'marketing'; label: string }> = [
{ slug: 'entwicklung', label: 'Entwicklung' },
{ slug: 'vertrieb', label: 'Vertrieb' },
{ slug: 'marketing', label: 'Marketing' },
];
function BereichTabs({ active }: { active: string }) {
return (
<nav aria-label="Bereich wählen" className="flex gap-1 border-b border-[var(--color-border)]">
{BEREICHE.map((b) => (
<NavLink
key={b.slug}
to={`/bereich/${b.slug}`}
className={clsx(
'inline-flex h-10 items-center rounded-t-[var(--radius-md)] px-4 text-sm font-medium',
active === b.slug
? 'border-b-2 border-[var(--color-ink)] text-[var(--color-ink)]'
: 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] hover:bg-black/5',
)}
>
{b.label}
</NavLink>
))}
</nav>
);
}
export function BereichRoute() {
const { slug } = useParams<{ slug?: string }>();
// Default: Entwicklung, wenn /bereich ohne Slug.
const effective = slug && BEREICHE.some((b) => b.slug === slug) ? slug : 'entwicklung';
return (
<div className="space-y-6">
<BereichTabs active={effective} />
{effective === 'entwicklung' && <EntwicklungDetail />}
{effective === 'vertrieb' && <VertriebDetail />}
{effective === 'marketing' && <MarketingDetail />}
</div>
);
}
function EntwicklungDetail() {
const q = trpc.project.byAreaSlug.useQuery({ slug: 'entwicklung' });
const areas = trpc.area.list.useQuery();
const [open, setOpen] = useState(false);
if (q.isPending) return <SkeletonCard lines={5} />;
const projects = q.data ?? [];
const kunden = projects.filter((p) => p.kind === 'kundenprojekt');
const intern = projects.filter((p) => p.kind === 'internes_projekt');
return (
<div className="space-y-6">
<div className="flex items-center justify-end">
<Button onClick={() => setOpen(true)}>{voice.cta.newProject}</Button>
</div>
<Section title="Kundenprojekte" projects={kunden} />
<Section title="Interne Projekte" projects={intern} />
{open && (
<ProjectCreateDialog
onClose={() => setOpen(false)}
areas={areas.data ?? []}
defaultSlug="entwicklung"
/>
)}
</div>
);
}
function Section({
title,
projects,
}: {
title: string;
projects: Array<{
id: string;
name: string;
statusCache: 'green' | 'amber' | 'red' | null;
deadline: string | null;
pflichtMinimumState: Record<string, boolean>;
}>;
}) {
return (
<section>
<h2 className="mb-2 text-lg font-semibold">{title}</h2>
{projects.length === 0 ? (
<Card.Root>
<Card.Body>
<p className="text-sm text-[var(--color-ink-muted)]">{voice.empty.projects}</p>
</Card.Body>
</Card.Root>
) : (
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
{projects.map((p) => (
<Link key={p.id} to={`/projekt/${p.id}`} className="contents">
<Card.Root className="cursor-pointer hover:shadow-[var(--shadow-md)]">
<Card.Body className="flex items-center justify-between">
<div>
<p className="font-medium">{p.name}</p>
<p className="text-xs text-[var(--color-ink-muted)]">
{p.deadline ? `bis ${p.deadline}` : 'kein Deadline'}
</p>
</div>
<PflichtMinimumIndicator state={p.pflichtMinimumState} />
</Card.Body>
</Card.Root>
</Link>
))}
</div>
)}
</section>
);
}
function PflichtMinimumIndicator({ state }: { state: Record<string, boolean> }) {
const points = [
state.steckbrief_freigegeben,
state.verantwortlicher,
state.plane_board,
state.meilensteine,
state.status_aktuell,
state.retrospektive,
];
const filled = points.filter(Boolean).length;
return (
<div className="flex items-center gap-2" title={`Pflicht-Minimum ${filled}/6`}>
<div className="flex gap-0.5">
{points.map((p, i) => (
<span
key={i}
className={clsx(
'h-2 w-2 rounded-sm',
p ? 'bg-[var(--color-ink)]' : 'bg-[var(--color-border)]',
)}
/>
))}
</div>
<span className="text-xs text-[var(--color-ink-muted)]">{filled}/6</span>
</div>
);
}
function ProjectCreateDialog({
onClose,
areas,
defaultSlug,
}: {
onClose: () => void;
areas: Array<{ id: string; slug: string; name: string }>;
defaultSlug: string;
}) {
const defaultArea = areas.find((a) => a.slug === defaultSlug);
const [name, setName] = useState('');
const [areaId, setAreaId] = useState(defaultArea?.id ?? '');
const [kind, setKind] = useState<'kundenprojekt' | 'internes_projekt'>('internes_projekt');
const [arbeitsmodus, setArbeitsmodus] = useState<'cycle' | 'kanban'>('kanban');
const [steckbrief, setSteckbrief] = useState('');
const [plane, setPlane] = useState('');
const utils = trpc.useUtils();
const create = trpc.project.create.useMutation({
onSuccess: async () => {
toast(voice.toast.projectCreated);
await utils.project.byAreaSlug.invalidate();
await utils.puls.overview.invalidate();
onClose();
},
});
function submit(e: React.FormEvent) {
e.preventDefault();
if (!areaId) return;
create.mutate({
areaId,
name,
kind,
arbeitsmodus,
outlineSteckbriefUrl: steckbrief,
planeProjectUrl: plane,
milestones: [],
});
}
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
onClick={onClose}
>
<div
className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-semibold">Neues Projekt</h2>
<form onSubmit={submit} className="mt-4 space-y-3">
<Field label="Name">
<input
required
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
</Field>
<Field label="Bereich">
<select
required
value={areaId}
onChange={(e) => setAreaId(e.target.value)}
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
>
{areas.map((a) => (
<option key={a.id} value={a.id}>
{a.name}
</option>
))}
</select>
</Field>
<Field label="Projektart">
<select
value={kind}
onChange={(e) => setKind(e.target.value as 'kundenprojekt' | 'internes_projekt')}
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
>
<option value="kundenprojekt">Kundenprojekt</option>
<option value="internes_projekt">Internes Projekt</option>
</select>
</Field>
<Field label="Arbeitsmodus">
<select
value={arbeitsmodus}
onChange={(e) => setArbeitsmodus(e.target.value as 'cycle' | 'kanban')}
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
>
<option value="kanban">Kanban</option>
<option value="cycle">Cycle</option>
</select>
</Field>
<Field label="Outline-Steckbrief-URL">
<input
required
type="url"
value={steckbrief}
onChange={(e) => setSteckbrief(e.target.value)}
placeholder="https://outline.tool-ctrl.de/..."
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
</Field>
<Field label="Plane-Board-URL">
<input
required
type="url"
value={plane}
onChange={(e) => setPlane(e.target.value)}
placeholder="https://plane.tool-ctrl.de/..."
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
</Field>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="ghost" onClick={onClose}>
Abbrechen
</Button>
<Button type="submit" disabled={create.isPending}>
Anlegen
</Button>
</div>
</form>
</div>
</div>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<label className="block">
<span className="text-sm font-medium">{label}</span>
<div className="mt-1">{children}</div>
</label>
);
}
function VertriebDetail() {
return <ProjektListeFuerBereich slug="vertrieb" />;
}
function MarketingDetail() {
return <ProjektListeFuerBereich slug="marketing" />;
}
function ProjektListeFuerBereich({ slug }: { slug: 'vertrieb' | 'marketing' }) {
const q = trpc.project.byAreaSlug.useQuery({ slug });
const areas = trpc.area.list.useQuery();
const [open, setOpen] = useState(false);
if (q.isPending) return <SkeletonCard lines={5} />;
const projects = q.data ?? [];
const kunden = projects.filter((p) => p.kind === 'kundenprojekt');
const intern = projects.filter((p) => p.kind === 'internes_projekt');
return (
<div className="space-y-6">
<div className="flex items-center justify-end">
<Button onClick={() => setOpen(true)}>{voice.cta.newProject}</Button>
</div>
<Section title="Kundenprojekte" projects={kunden} />
<Section title="Interne Projekte" projects={intern} />
{open && (
<ProjectCreateDialog
onClose={() => setOpen(false)}
areas={areas.data ?? []}
defaultSlug={slug}
/>
)}
</div>
);
}
+218
View File
@@ -0,0 +1,218 @@
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { trpc } from '../lib/trpc';
import { voice } from '../lib/voice';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { toast } from 'sonner';
import { X, Plus, RotateCw } from 'lucide-react';
export function HeuteRoute() {
const navigate = useNavigate();
const today = trpc.dayEntry.today.useQuery();
if (today.isPending) return <SkeletonCard lines={4} />;
if (today.data?.entry) {
return <EinstempeltView todos={today.data.todos} entry={today.data.entry} />;
}
return <CheckInForm onDone={() => navigate('/puls')} />;
}
function CheckInForm({ onDone }: { onDone: () => void }) {
const carryQuery = trpc.dayEntry.pendingCarryOver.useQuery();
const [todos, setTodos] = useState<{ title: string; carried?: boolean }[]>([{ title: '' }]);
const [prefilledFromCarry, setPrefilledFromCarry] = useState(false);
// Wenn Carryover-ToDos geladen sind und das Formular noch leer ist, vorbefüllen.
if (
!prefilledFromCarry &&
carryQuery.data &&
carryQuery.data.length > 0 &&
todos.length === 1 &&
!todos[0]!.title
) {
setPrefilledFromCarry(true);
setTodos([...carryQuery.data.map((c) => ({ title: c.title, carried: true })), { title: '' }]);
}
const utils = trpc.useUtils();
const mutation = trpc.dayEntry.checkIn.useMutation({
onSuccess: async (result) => {
const msg =
result.carriedOver > 0
? `Locked in. ${result.carriedOver} offene ToDos vom Vortag übernommen.`
: voice.toast.checkedIn;
toast(msg);
await utils.dayEntry.today.invalidate();
await utils.dayEntry.pendingCarryOver.invalidate();
onDone();
},
});
function updateAt(i: number, value: string) {
setTodos((arr) => arr.map((t, idx) => (idx === i ? { ...t, title: value } : t)));
}
function addRow() {
setTodos((arr) => [...arr, { title: '' }]);
}
function removeAt(i: number) {
setTodos((arr) => arr.filter((_, idx) => idx !== i));
}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>, i: number) {
if (e.key === 'Enter') {
e.preventDefault();
if (i === todos.length - 1 && todos[i]?.title.trim()) {
addRow();
// Fokus auf neue Zeile in nächstem Tick
setTimeout(() => {
const inputs = document.querySelectorAll<HTMLInputElement>('input[data-todo-input]');
inputs[i + 1]?.focus();
}, 0);
}
}
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const filtered = todos.filter((t) => t.title.trim().length > 0);
if (filtered.length === 0) {
toast('Mindestens eine ToDo brauchst du.');
return;
}
mutation.mutate({ todos: filtered.map((t) => ({ title: t.title.trim() })) });
}
return (
<div className="mx-auto max-w-2xl space-y-6">
<header>
<h1 className="text-3xl font-bold">Heute</h1>
<p className="mt-1 text-[var(--color-ink-muted)]">{voice.hint.howIsItGoing}</p>
</header>
<Card.Root>
<form onSubmit={handleSubmit}>
<Card.Body className="space-y-3">
<p className="text-sm font-medium">Was nimmst du dir heute vor?</p>
{carryQuery.data && carryQuery.data.length > 0 && (
<p className="text-xs text-[var(--color-ink-muted)]">
<RotateCw size={12} className="inline -mt-0.5 mr-1" />
{carryQuery.data.length} offene ToDos vom Vortag wurden für dich vorgemerkt.
Behalte, ändere oder lösche sie.
</p>
)}
<ul className="space-y-2">
{todos.map((t, i) => (
<li key={i} className="flex items-center gap-2">
{t.carried && (
<span
className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-black/5 text-[var(--color-ink-muted)]"
title="vom Vortag übernommen"
aria-label="vom Vortag übernommen"
>
<RotateCw size={12} />
</span>
)}
<input
type="text"
value={t.title}
onChange={(e) => updateAt(i, e.target.value)}
onKeyDown={(e) => handleKey(e, i)}
placeholder="ToDo …"
maxLength={200}
data-todo-input
ref={(el) => {
if (i === 0 && el && el.value === '') el.focus();
}}
className="flex-1 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm focus:border-[var(--color-ink)] focus:outline-none"
/>
{todos.length > 1 && (
<button
type="button"
onClick={() => removeAt(i)}
aria-label="ToDo entfernen"
className="rounded-[var(--radius-md)] p-2 text-[var(--color-ink-muted)] hover:bg-black/5"
>
<X size={16} />
</button>
)}
</li>
))}
</ul>
<button
type="button"
onClick={addRow}
className="inline-flex items-center gap-1 text-sm text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]"
>
<Plus size={14} /> Weitere
</button>
</Card.Body>
<Card.Footer className="flex justify-end gap-2">
<Button variant="ghost" type="button" onClick={() => history.back()}>
Abbrechen
</Button>
<Button type="submit" disabled={mutation.isPending}>
{voice.cta.checkIn}
</Button>
</Card.Footer>
</form>
</Card.Root>
</div>
);
}
function EinstempeltView({
todos,
entry,
}: {
todos: Array<{ id: string; title: string; status: 'open' | 'done' | 'dropped' }>;
entry: { status: 'begun' | 'full' | 'absent' };
}) {
return (
<div className="mx-auto max-w-2xl space-y-6">
<header>
<h1 className="text-3xl font-bold">Heute</h1>
<p className="mt-1 text-[var(--color-ink-muted)]">
{entry.status === 'full'
? 'Tag abgeschlossen.'
: 'Locked in. Heute Abend reflektieren.'}
</p>
</header>
<Card.Root>
<Card.Body>
<ul className="space-y-2 text-sm">
{todos.map((t) => (
<li key={t.id} className="flex items-center gap-2">
<span
className={
t.status === 'done'
? 'h-2 w-2 rounded-full bg-[var(--color-ampel-green-fg)]'
: t.status === 'dropped'
? 'h-2 w-2 rounded-full bg-[var(--color-ink-faint)]'
: 'h-2 w-2 rounded-full bg-[var(--color-border-strong)]'
}
/>
<span
className={
t.status === 'dropped' ? 'line-through text-[var(--color-ink-muted)]' : ''
}
>
{t.title}
</span>
</li>
))}
</ul>
</Card.Body>
{entry.status !== 'full' && (
<Card.Footer className="flex justify-end">
<Button onClick={() => (location.href = '/reflexion')}>{voice.cta.reflect}</Button>
</Card.Footer>
)}
</Card.Root>
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router';
import { ToolCtrlLogo } from '../components/ui/ToolCtrlLogo';
const ERROR_MESSAGES: Record<string, string> = {
session_expired: 'Deine Sitzung ist abgelaufen. Bitte erneut anmelden.',
oidc_failed: 'Anmeldung über Authentik ist fehlgeschlagen. Bitte erneut versuchen.',
};
export function LoginRoute() {
const [params] = useSearchParams();
const error = params.get('error');
// Dev: automatisch in den Dev-Login-Endpoint redirecten — kein Klick nötig.
// Authentik kommt später in der Roadmap (siehe docs/operations.md).
useEffect(() => {
if (import.meta.env.DEV && !error) {
window.location.replace('/api/auth/dev-login');
}
}, [error]);
return (
<div className="grid min-h-screen place-items-center bg-[var(--color-bg)] px-4">
<div className="w-full max-w-md text-center">
<h1 className="inline-flex items-center gap-2 text-4xl font-bold">
everythingbutwork <ToolCtrlLogo size={32} />
</h1>
<p className="mt-3 text-[var(--color-ink-muted)]">
Internes ToolCTRL-Projektmanagement.
</p>
{error && (
<p
className="mt-4 rounded-[var(--radius-md)] border border-[var(--color-ampel-red-bg)] bg-[var(--color-ampel-red-bg)] px-4 py-2 text-sm text-[var(--color-ampel-red-fg)]"
role="alert"
>
{ERROR_MESSAGES[error] ?? 'Anmeldung fehlgeschlagen.'}
</p>
)}
{!import.meta.env.DEV && (
<a
href="/api/auth/login"
className="mt-6 inline-flex h-11 items-center justify-center rounded-[var(--radius-md)] bg-[var(--color-brand)] px-6 text-base font-medium text-[var(--color-brand-fg)] hover:bg-black/85"
>
Über Authentik anmelden
</a>
)}
{import.meta.env.DEV && (
<p className="mt-4 text-xs text-[var(--color-ink-muted)]">
Dev-Modus: Auto-Login wird vorgenommen…
</p>
)}
</div>
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import { Link } from 'react-router';
export function NotFoundRoute() {
return (
<div className="mx-auto mt-16 max-w-md text-center">
<h1 className="text-3xl font-bold">Nicht gefunden.</h1>
<p className="mt-2 text-[var(--color-ink-muted)]">
Diese Seite gibt es nicht. Vielleicht meintest du den Puls?
</p>
<Link
to="/puls"
className="mt-6 inline-flex h-9 items-center rounded-[var(--radius-md)] bg-[var(--color-brand)] px-4 text-sm font-medium text-[var(--color-brand-fg)] hover:bg-black/85"
>
Zum Puls
</Link>
</div>
);
}
+231
View File
@@ -0,0 +1,231 @@
import { useState } from 'react';
import { useParams, Link, useNavigate } from 'react-router';
import { ArrowLeft } from 'lucide-react';
import { trpc } from '../lib/trpc';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { useCurrentUser } from '../hooks/useCurrentUser';
import { voice } from '../lib/voice';
import { toast } from 'sonner';
import { clsx } from 'clsx';
export function ProjektRoute() {
const { id = '' } = useParams<{ id: string }>();
const me = useCurrentUser();
const navigate = useNavigate();
const q = trpc.project.byId.useQuery({ id });
const [abschlussOpen, setAbschlussOpen] = useState(false);
if (q.isPending) return <SkeletonCard lines={6} />;
if (!q.data) return null;
const { project: p, area: a, milestones } = q.data;
const isP1 = me?.id === p.verantwortlicheUserId;
const pflichtKeys: Array<{ key: string; label: string }> = [
{ key: 'steckbrief_freigegeben', label: 'Steckbrief in Outline freigegeben' },
{ key: 'verantwortlicher', label: 'Projektverantwortliche/r gesetzt' },
{ key: 'plane_board', label: 'Plane-Board verlinkt' },
{ key: 'meilensteine', label: 'Meilensteine vorhanden' },
{ key: 'status_aktuell', label: 'Status-Ampel aktuell (≤ 7 Werktage)' },
{ key: 'retrospektive', label: 'Abschluss-Retrospektive (bei beendeten Projekten)' },
];
function goBack() {
// Wenn der History-Stack einen Vorgänger hat, dorthin; sonst auf den Bereich.
if (window.history.length > 1) navigate(-1);
else navigate(`/bereich/${a.slug}`);
}
return (
<div className="space-y-6">
<header>
<div className="flex items-center gap-3">
<button
type="button"
onClick={goBack}
aria-label="Zurück"
className="inline-flex h-8 w-8 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] text-[var(--color-ink-muted)] hover:border-[var(--color-ink)] hover:text-[var(--color-ink)]"
>
<ArrowLeft size={16} />
</button>
<p className="text-sm text-[var(--color-ink-muted)]">
<Link to="/puls" className="hover:underline">
Puls
</Link>
{' › '}
<Link to={`/bereich/${a.slug}`} className="hover:underline">
{a.name}
</Link>
</p>
</div>
<div className="mt-2 flex items-start justify-between gap-4">
<h1 className="text-3xl font-bold">{p.name}</h1>
{isP1 && !p.archiviert && (
<Button variant="secondary" onClick={() => setAbschlussOpen(true)}>
Projekt abschließen
</Button>
)}
{p.archiviert && (
<span className="rounded-full bg-black/5 px-2.5 py-1 text-xs">archiviert</span>
)}
</div>
<p className="text-[var(--color-ink-muted)]">
{p.kind === 'kundenprojekt' ? 'Kundenprojekt' : 'Internes Projekt'} ·{' '}
{p.arbeitsmodus ?? '—'} · {p.deadline ? `Deadline ${p.deadline}` : 'kein Deadline'}
</p>
</header>
{abschlussOpen && (
<AbschlussDialog projectId={p.id} onClose={() => setAbschlussOpen(false)} />
)}
<section className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card.Root>
<Card.Header>
<h2 className="text-lg font-semibold">Verlinkungen</h2>
</Card.Header>
<Card.Body className="space-y-2 text-sm">
<LinkRow label="Outline-Steckbrief" url={p.outlineSteckbriefUrl} />
<LinkRow label="Plane-Board" url={p.planeProjectUrl} />
<LinkRow label="Risikoliste" url={p.risikolisteUrl} />
<LinkRow label="Lessons Learned" url={p.lessonsLearnedUrl} />
</Card.Body>
</Card.Root>
<Card.Root>
<Card.Header>
<h2 className="text-lg font-semibold">Pflicht-Minimum</h2>
</Card.Header>
<Card.Body>
<ul className="space-y-2 text-sm">
{pflichtKeys.map(({ key, label }) => {
const ok = p.pflichtMinimumState[key] === true;
return (
<li key={key} className="flex items-center gap-2">
<span
className={clsx(
'inline-block h-2.5 w-2.5 rounded-full',
ok ? 'bg-[var(--color-ampel-green-fg)]' : 'bg-[var(--color-border-strong)]',
)}
/>
<span className={ok ? '' : 'text-[var(--color-ink-muted)]'}>{label}</span>
</li>
);
})}
</ul>
</Card.Body>
</Card.Root>
</section>
<section>
<h2 className="mb-2 text-lg font-semibold">Meilensteine</h2>
{milestones.length === 0 ? (
<Card.Root>
<Card.Body>
<p className="text-sm text-[var(--color-ink-muted)]">Noch keine Meilensteine.</p>
</Card.Body>
</Card.Root>
) : (
<Card.Root>
<Card.Body>
<ul className="space-y-2 text-sm">
{milestones.map((m) => (
<li key={m.id} className="flex items-center justify-between">
<span
className={clsx(
m.status === 'done' && 'line-through text-[var(--color-ink-muted)]',
)}
>
{m.title}
</span>
<span className="text-xs text-[var(--color-ink-muted)]">
{m.due ?? '—'} · {m.status}
</span>
</li>
))}
</ul>
</Card.Body>
</Card.Root>
)}
</section>
{p.dodText && (
<Card.Root>
<Card.Header>
<h2 className="text-lg font-semibold">Definition of Done</h2>
</Card.Header>
<Card.Body>
<p className="whitespace-pre-wrap text-sm">{p.dodText}</p>
</Card.Body>
</Card.Root>
)}
</div>
);
}
function AbschlussDialog({ projectId, onClose }: { projectId: string; onClose: () => void }) {
const [url, setUrl] = useState('');
const navigate = useNavigate();
const utils = trpc.useUtils();
const mutation = trpc.project.archive.useMutation({
onSuccess: async () => {
toast(voice.toast.projectArchived);
await utils.project.byId.invalidate({ id: projectId });
await utils.project.listArchived.invalidate();
await utils.puls.overview.invalidate();
navigate('/archiv');
},
});
function submit(e: React.FormEvent) {
e.preventDefault();
mutation.mutate({ projectId, lessonsLearnedUrl: url });
}
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
onClick={onClose}
>
<div
className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-semibold">Projekt abschließen</h2>
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
Lessons-Learned-URL ist Pflicht (Leitfaden §9 Punkt 6).
</p>
<form onSubmit={submit} className="mt-4 space-y-3">
<input
required
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://outline.tool-ctrl.de/lessons-learned/..."
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" onClick={onClose}>
Abbrechen
</Button>
<Button type="submit" disabled={mutation.isPending}>
Abschließen
</Button>
</div>
</form>
</div>
</div>
);
}
function LinkRow({ label, url }: { label: string; url: string | null }) {
return (
<p className="flex items-center justify-between">
<span className="text-[var(--color-ink-muted)]">{label}</span>
{url ? (
<a href={url} target="_blank" rel="noreferrer" className="truncate hover:underline">
{url}
</a>
) : (
<span className="text-[var(--color-ink-faint)]">—</span>
)}
</p>
);
}
+301
View File
@@ -0,0 +1,301 @@
import { Link } from 'react-router';
import { trpc } from '../lib/trpc';
import { voice } from '../lib/voice';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { clsx } from 'clsx';
type Ampel = 'green' | 'amber' | 'red' | 'grey';
export function PulsRoute() {
const q = trpc.puls.overview.useQuery();
const today = trpc.dayEntry.today.useQuery();
if (q.isPending) {
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
);
}
if (!q.data) return null;
const isCheckedIn =
today.data?.entry?.status !== undefined && today.data.entry.status !== 'absent';
return (
<div className="space-y-8">
<header className="flex items-end justify-between">
<div>
<h1 className="text-3xl font-bold">Unternehmens-Puls</h1>
<p className="text-[var(--color-ink-muted)]">
Stand: {formatDate(q.data.date)} · Jourfixe {q.data.jourfixe.label}
</p>
</div>
{!isCheckedIn && (
<Link
to="/heute"
className="inline-flex h-11 items-center rounded-[var(--radius-md)] bg-[var(--color-brand)] px-5 text-sm font-medium text-[var(--color-brand-fg)] hover:bg-black/85"
>
{voice.hint.notCheckedInToday} → {voice.cta.checkIn}
</Link>
)}
</header>
<section aria-labelledby="bereiche-heading">
<h2 id="bereiche-heading" className="sr-only">
Bereiche
</h2>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{q.data.areas.map((a) => (
<Link key={a.id} to={`/bereich/${a.slug}`} className="contents">
<Card.Root className="cursor-pointer hover:shadow-[var(--shadow-md)] transition-shadow">
<Card.Body className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">{a.name}</h3>
<AmpelDot ampel={a.ampel as Ampel} />
</div>
<div className="flex gap-3 text-xs text-[var(--color-ink-muted)]">
<span>{a.counts.green} grün</span>
<span>{a.counts.amber} gelb</span>
<span>{a.counts.red} rot</span>
{a.counts.grey > 0 && <span>{a.counts.grey} ohne Status</span>}
</div>
</Card.Body>
</Card.Root>
</Link>
))}
</div>
</section>
<section className="grid grid-cols-1 gap-6 md:grid-cols-3">
<Card.Root>
<Card.Header>
<h2 className="text-lg font-semibold">Heute</h2>
</Card.Header>
<Card.Body>
<p className="text-2xl font-bold">
{q.data.heuteQuote.gepingt} / {q.data.heuteQuote.gesamt}
</p>
<p className="text-sm text-[var(--color-ink-muted)]">Locked in</p>
{q.data.heuteQuote.fehlende.length > 0 && (
<details className="mt-3">
<summary className="cursor-pointer text-sm text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]">
{q.data.heuteQuote.fehlende.length} fehlt heute noch
</summary>
<ul className="mt-2 space-y-1 text-sm">
{q.data.heuteQuote.fehlende.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
</details>
)}
</Card.Body>
</Card.Root>
<Card.Root className="md:col-span-2">
<Card.Header>
<h2 className="text-lg font-semibold">Bereichsübergreifend offen</h2>
</Card.Header>
<Card.Body>
{q.data.blockers.length === 0 ? (
<p className="text-sm text-[var(--color-ink-muted)]">{voice.empty.blockers}</p>
) : (
<ul className="space-y-2">
{q.data.blockers.map((b) => (
<li
key={b.id}
className="flex items-center justify-between rounded-[var(--radius-md)] border border-[var(--color-border)] p-3"
>
<span className="text-sm font-medium">{b.title}</span>
<span className="text-xs text-[var(--color-ink-muted)]">
seit {formatDate(b.openedAt.toString())}
</span>
</li>
))}
</ul>
)}
</Card.Body>
</Card.Root>
</section>
{q.data.pflichtMinimumOffen.length > 0 && (
<Card.Root>
<Card.Header>
<h2 className="text-lg font-semibold">Pflicht-Minimum offen</h2>
</Card.Header>
<Card.Body>
<ul className="space-y-1 text-sm">
{q.data.pflichtMinimumOffen.map((p) => (
<li key={p.id}>
<Link to={`/projekt/${p.id}`} className="hover:underline">
{p.name}
</Link>
</li>
))}
</ul>
</Card.Body>
</Card.Root>
)}
<TeamHeuteSection />
<section aria-labelledby="feed-heading">
<h2 id="feed-heading" className="mb-3 text-lg font-semibold">
Gestern im Team
</h2>
{q.data.feed.length === 0 ? (
<Card.Root>
<Card.Body>
<p className="text-sm text-[var(--color-ink-muted)]">{voice.empty.feed}</p>
</Card.Body>
</Card.Root>
) : (
<div className="space-y-2">
{q.data.feed.map((entry) => (
<Card.Root key={entry.id}>
<Card.Body>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold">{entry.userName}</p>
<p className="text-xs text-[var(--color-ink-muted)]">{entry.date}</p>
</div>
{entry.ampel && <AmpelBadge ampel={entry.ampel} />}
</div>
{entry.reflectionText && <p className="mt-2 text-sm">{entry.reflectionText}</p>}
</Card.Body>
</Card.Root>
))}
</div>
)}
</section>
</div>
);
}
function AmpelDot({ ampel }: { ampel: Ampel }) {
const colorClass = {
green: 'bg-[var(--color-ampel-green-fg)]',
amber: 'bg-[var(--color-ampel-amber-fg)]',
red: 'bg-[var(--color-ampel-red-fg)]',
grey: 'bg-[var(--color-ink-faint)]',
}[ampel];
return <span className={clsx('inline-block h-3 w-3 rounded-full', colorClass)} aria-hidden />;
}
function AmpelBadge({ ampel }: { ampel: 'green' | 'amber' | 'red' }) {
const cls = {
green: 'bg-[var(--color-ampel-green-bg)] text-[var(--color-ampel-green-fg)]',
amber: 'bg-[var(--color-ampel-amber-bg)] text-[var(--color-ampel-amber-fg)]',
red: 'bg-[var(--color-ampel-red-bg)] text-[var(--color-ampel-red-fg)]',
}[ampel];
const label = { green: 'Im Plan', amber: 'Gefährdet', red: 'Blockiert' }[ampel];
return (
<span
className={clsx(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',
cls,
)}
>
<span className="h-1.5 w-1.5 rounded-full bg-current" />
{label}
</span>
);
}
function TeamHeuteSection() {
const team = trpc.dayEntry.todayTeam.useQuery();
const users = trpc.user.list.useQuery();
if (team.isPending) return null;
const data = team.data ?? [];
const userMap = new Map((users.data ?? []).map((u) => [u.id, u.name]));
if (data.length === 0) {
return (
<section>
<h2 className="mb-3 text-lg font-semibold">Heute im Team</h2>
<Card.Root>
<Card.Body>
<p className="text-sm text-[var(--color-ink-muted)]">
Heute hat sich noch niemand locked in.
</p>
</Card.Body>
</Card.Root>
</section>
);
}
return (
<section>
<h2 className="mb-3 text-lg font-semibold">Heute im Team</h2>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{data.map((u) => (
<Card.Root key={u.userId}>
<Card.Body>
<div className="flex items-center justify-between">
<p className="text-sm font-semibold">{userMap.get(u.userId) ?? '—'}</p>
<span
className={clsx(
'rounded-full px-2 py-0.5 text-xs',
u.entryStatus === 'full'
? 'bg-[var(--color-ampel-green-bg)] text-[var(--color-ampel-green-fg)]'
: u.entryStatus === 'begun'
? 'bg-black/5 text-[var(--color-ink-muted)]'
: 'bg-black/5 text-[var(--color-ink-muted)]',
)}
>
{u.entryStatus === 'full'
? 'reflektiert'
: u.entryStatus === 'begun'
? 'locked in'
: 'abwesend'}
</span>
</div>
{u.todos.length === 0 ? (
<p className="mt-2 text-xs text-[var(--color-ink-faint)]">Keine ToDos eingetragen.</p>
) : (
<ul className="mt-2 space-y-1.5 text-sm">
{u.todos.map((t) => (
<li key={t.id} className="flex items-start gap-2">
<span
className={clsx(
'mt-1 inline-block h-2 w-2 rounded-full shrink-0',
t.status === 'done'
? 'bg-[var(--color-ampel-green-fg)]'
: t.status === 'dropped'
? 'bg-[var(--color-ink-faint)]'
: 'bg-[var(--color-border-strong)]',
)}
/>
<span
className={clsx(
t.status === 'dropped' && 'line-through text-[var(--color-ink-muted)]',
)}
>
{t.title}
</span>
</li>
))}
</ul>
)}
</Card.Body>
</Card.Root>
))}
</div>
</section>
);
}
function formatDate(input: string | Date): string {
const dt = input instanceof Date ? input : new Date(input);
if (Number.isNaN(dt.getTime())) return '—';
return new Intl.DateTimeFormat('de-DE', {
weekday: 'short',
day: 'numeric',
month: 'long',
}).format(dt);
}
// Button als referenz behalten (Linter)
void Button;
+366
View File
@@ -0,0 +1,366 @@
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { trpc } from '../lib/trpc';
import { voice } from '../lib/voice';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { toast } from 'sonner';
import { clsx } from 'clsx';
import { Check, X, Minus, TrendingUp, TrendingDown, ArrowRight } from 'lucide-react';
type Ampel = 'green' | 'amber' | 'red';
type Trend = 'besser' | 'gleich' | 'schlechter';
type TodoStatus = 'open' | 'done' | 'dropped';
export function ReflexionRoute() {
const today = trpc.dayEntry.today.useQuery();
const navigate = useNavigate();
if (today.isPending) return <SkeletonCard lines={6} />;
if (!today.data?.entry) {
return (
<div className="mx-auto max-w-2xl text-center">
<p className="text-[var(--color-ink-muted)]">
Du bist heute noch nicht locked in. {voice.hint.notCheckedInToday}
</p>
<Button className="mt-4" onClick={() => navigate('/heute')}>
Zum Lock-in
</Button>
</div>
);
}
return (
<ReflectForm
entry={today.data.entry}
todos={today.data.todos}
onDone={() => navigate('/puls')}
/>
);
}
function ReflectForm({
entry,
todos,
onDone,
}: {
entry: { id: string };
todos: Array<{
id: string;
title: string;
status: TodoStatus;
bezugKind: 'project' | 'deal' | 'campaign' | 'allgemein' | null;
}>;
onDone: () => void;
}) {
const [todoStates, setTodoStates] = useState<
Record<string, { status: TodoStatus; reason?: string }>
>(Object.fromEntries(todos.map((t) => [t.id, { status: t.status }])));
const [reflectionText, setReflectionText] = useState('');
const [ampel, setAmpel] = useState<Ampel | null>(null);
const [trend, setTrend] = useState<Trend | null>(null);
const [blockerEnabled, setBlockerEnabled] = useState(false);
const [blockerTitle, setBlockerTitle] = useState('');
const [blockerDescription, setBlockerDescription] = useState('');
const hasProjectBezug = todos.some((t) => t.bezugKind === 'project');
const utils = trpc.useUtils();
const mutation = trpc.dayEntry.submitReflection.useMutation({
onSuccess: async () => {
toast(voice.toast.reflected);
await utils.dayEntry.today.invalidate();
onDone();
},
});
function setTodoStatus(id: string, status: TodoStatus) {
setTodoStates((prev) => ({ ...prev, [id]: { ...prev[id], status } }));
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!ampel) {
toast('Bitte Ampel setzen.');
return;
}
if (hasProjectBezug && !trend) {
toast('Bei Projektbezug ist der Trend Pflicht.');
return;
}
if (blockerEnabled && !blockerTitle.trim()) {
toast('Blocker braucht einen Titel.');
return;
}
mutation.mutate({
dayEntryId: entry.id,
todos: Object.entries(todoStates).map(([id, s]) => ({
id,
status: s.status,
dropReason: s.reason,
})),
reflectionText,
ampel,
trend: trend ?? null,
...(blockerEnabled
? { blocker: { title: blockerTitle, description: blockerDescription || undefined } }
: {}),
});
}
return (
<div className="mx-auto max-w-2xl space-y-6">
<header>
<h1 className="text-3xl font-bold">Reflexion</h1>
<p className="mt-1 text-[var(--color-ink-muted)]">{voice.hint.howIsItGoing}</p>
</header>
<form onSubmit={handleSubmit} className="space-y-4">
<Card.Root>
<Card.Header>
<h2 className="text-lg font-semibold">Was ist heute passiert?</h2>
</Card.Header>
<Card.Body className="space-y-3">
<p className="text-sm font-medium text-[var(--color-ink-muted)]">
Deine ToDos vom Morgen
</p>
<ul className="space-y-2">
{todos.map((t) => {
const status = todoStates[t.id]?.status ?? 'open';
return (
<li key={t.id} className="flex items-center gap-2">
<span className="flex-1 text-sm">{t.title}</span>
<div className="flex gap-1">
<StatusBtn
active={status === 'done'}
onClick={() => setTodoStatus(t.id, 'done')}
label="Erledigt"
>
<Check size={14} />
</StatusBtn>
<StatusBtn
active={status === 'open'}
onClick={() => setTodoStatus(t.id, 'open')}
label="Offen"
>
<Minus size={14} />
</StatusBtn>
<StatusBtn
active={status === 'dropped'}
onClick={() => setTodoStatus(t.id, 'dropped')}
label="Verworfen"
>
<X size={14} />
</StatusBtn>
</div>
</li>
);
})}
</ul>
</Card.Body>
</Card.Root>
<Card.Root>
<Card.Body className="space-y-3">
<label className="block text-sm font-medium" htmlFor="reflection">
Sonst noch was?
</label>
<textarea
id="reflection"
value={reflectionText}
onChange={(e) => setReflectionText(e.target.value)}
maxLength={1000}
rows={4}
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm focus:border-[var(--color-ink)] focus:outline-none"
/>
<p className="text-xs text-[var(--color-ink-faint)] text-right">
{reflectionText.length} / 1000
</p>
</Card.Body>
</Card.Root>
<Card.Root>
<Card.Body className="space-y-4">
<fieldset>
<legend className="text-sm font-medium">Ampel</legend>
<div className="mt-2 grid grid-cols-3 gap-2">
<AmpelBtn
value="green"
current={ampel}
onChange={setAmpel}
label={voice.label.ampel.green}
/>
<AmpelBtn
value="amber"
current={ampel}
onChange={setAmpel}
label={voice.label.ampel.amber}
/>
<AmpelBtn
value="red"
current={ampel}
onChange={setAmpel}
label={voice.label.ampel.red}
/>
</div>
</fieldset>
<fieldset>
<legend className="text-sm font-medium">
Trend {hasProjectBezug && <span aria-hidden>*</span>}
<span className="ml-1 text-xs text-[var(--color-ink-faint)]">
{hasProjectBezug ? '(Pflicht bei Projektbezug)' : '(optional)'}
</span>
</legend>
<div className="mt-2 grid grid-cols-3 gap-2">
<TrendBtn value="besser" current={trend} onChange={setTrend}>
<TrendingUp size={14} /> Besser
</TrendBtn>
<TrendBtn value="gleich" current={trend} onChange={setTrend}>
<ArrowRight size={14} /> Gleich
</TrendBtn>
<TrendBtn value="schlechter" current={trend} onChange={setTrend}>
<TrendingDown size={14} /> Schlechter
</TrendBtn>
</div>
</fieldset>
</Card.Body>
</Card.Root>
<Card.Root>
<Card.Body className="space-y-3">
<label className="flex items-center gap-2 text-sm font-medium">
<input
type="checkbox"
checked={blockerEnabled}
onChange={(e) => setBlockerEnabled(e.target.checked)}
/>
Blocker?
</label>
{blockerEnabled && (
<div className="space-y-2">
<input
type="text"
value={blockerTitle}
onChange={(e) => setBlockerTitle(e.target.value)}
placeholder="Was klemmt?"
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
<textarea
value={blockerDescription}
onChange={(e) => setBlockerDescription(e.target.value)}
placeholder="Mehr Kontext (optional)"
rows={2}
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
</div>
)}
</Card.Body>
</Card.Root>
<div className="flex justify-end gap-2">
<Button variant="ghost" type="button" onClick={() => history.back()}>
Abbrechen
</Button>
<Button type="submit" disabled={mutation.isPending}>
{voice.cta.reflect}
</Button>
</div>
</form>
</div>
);
}
function StatusBtn({
active,
onClick,
children,
label,
}: {
active: boolean;
onClick: () => void;
children: React.ReactNode;
label: string;
}) {
return (
<button
type="button"
onClick={onClick}
aria-label={label}
title={label}
className={clsx(
'inline-flex h-7 w-7 items-center justify-center rounded-[var(--radius-md)] border',
active
? 'border-[var(--color-ink)] bg-[var(--color-ink)] text-[var(--color-brand-fg)]'
: 'border-[var(--color-border)] text-[var(--color-ink-muted)] hover:border-[var(--color-border-strong)]',
)}
>
{children}
</button>
);
}
function AmpelBtn({
value,
current,
onChange,
label,
}: {
value: Ampel;
current: Ampel | null;
onChange: (a: Ampel) => void;
label: string;
}) {
const bgMap: Record<Ampel, string> = {
green: 'bg-[var(--color-ampel-green-bg)] text-[var(--color-ampel-green-fg)]',
amber: 'bg-[var(--color-ampel-amber-bg)] text-[var(--color-ampel-amber-fg)]',
red: 'bg-[var(--color-ampel-red-bg)] text-[var(--color-ampel-red-fg)]',
};
const dotMap: Record<Ampel, string> = {
green: 'bg-[var(--color-ampel-green-fg)]',
amber: 'bg-[var(--color-ampel-amber-fg)]',
red: 'bg-[var(--color-ampel-red-fg)]',
};
const active = current === value;
return (
<button
type="button"
onClick={() => onChange(value)}
className={clsx(
'flex h-12 items-center gap-2 rounded-[var(--radius-md)] px-3 text-sm font-medium border',
active
? bgMap[value] + ' border-[var(--color-ink)]'
: 'border-[var(--color-border)] hover:border-[var(--color-border-strong)]',
)}
>
<span className={clsx('h-2.5 w-2.5 rounded-full', dotMap[value])} />
{label}
</button>
);
}
function TrendBtn({
value,
current,
onChange,
children,
}: {
value: Trend;
current: Trend | null;
onChange: (t: Trend) => void;
children: React.ReactNode;
}) {
const active = current === value;
return (
<button
type="button"
onClick={() => onChange(value)}
className={clsx(
'flex h-10 items-center justify-center gap-1.5 rounded-[var(--radius-md)] border px-3 text-sm font-medium',
active
? 'border-[var(--color-ink)] bg-[var(--color-ink)] text-[var(--color-brand-fg)]'
: 'border-[var(--color-border)] text-[var(--color-ink-muted)] hover:border-[var(--color-border-strong)]',
)}
>
{children}
</button>
);
}
+90
View File
@@ -0,0 +1,90 @@
@import 'tailwindcss';
/* ------ Brand Tokens (UX-Spec § Visual Design Foundation) ------- */
@theme {
/* Neutrals */
--color-bg: #ffffff;
--color-surface: #fafafa;
--color-surface-elevated: #ffffff;
--color-border: #e5e5e5;
--color-border-strong: #d4d4d4;
--color-ink: #0a0a0a;
--color-ink-muted: #525252;
--color-ink-faint: #a3a3a3;
--color-brand: #0a0a0a;
--color-brand-fg: #ffffff;
/* Ampel */
--color-ampel-green-fg: #15803d;
--color-ampel-green-bg: #dcfce7;
--color-ampel-amber-fg: #a16207;
--color-ampel-amber-bg: #fef3c7;
--color-ampel-red-fg: #b91c1c;
--color-ampel-red-bg: #fee2e2;
/* Trend */
--color-trend-up: #15803d;
--color-trend-flat: #525252;
--color-trend-down: #b91c1c;
/* Typografie */
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', Menlo, monospace;
/* Radii */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-xl: 24px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(10, 10, 10, 0.04);
--shadow-md: 0 4px 12px rgba(10, 10, 10, 0.06);
--shadow-lg: 0 12px 32px rgba(10, 10, 10, 0.1);
}
/* ------ Base styles ------- */
html {
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body {
background: var(--color-bg);
color: var(--color-ink);
margin: 0;
}
*:focus-visible {
outline: 2px solid var(--color-ink);
outline-offset: 2px;
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0ms !important;
transition-duration: 0ms !important;
}
}
/* Reusable utility classes (NFR-9 — ≤ 3 Klicks Workflow). */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--color-ink);
color: var(--color-brand-fg);
padding: 8px 16px;
z-index: 100;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "vite.config.ts"]
}
+21
View File
@@ -0,0 +1,21 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import path from 'node:path';
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 7173,
strictPort: true,
proxy: {
'/api': 'http://localhost:7100',
'/trpc': 'http://localhost:7100',
},
},
});
+26
View File
@@ -0,0 +1,26 @@
# Development-only services. App und API laufen lokal via pnpm,
# nur Postgres wird containerisiert für eine konsistente Datenbasis.
services:
postgres:
image: postgres:16-alpine
container_name: cockpit-postgres-dev
restart: unless-stopped
environment:
POSTGRES_USER: cockpit
POSTGRES_PASSWORD: cockpit_dev
POSTGRES_DB: cockpit
TZ: Europe/Berlin
ports:
- '55432:5432'
volumes:
- cockpit_postgres_dev:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U cockpit -d cockpit']
interval: 5s
timeout: 5s
retries: 5
volumes:
cockpit_postgres_dev:
name: cockpit_postgres_dev
+85
View File
@@ -0,0 +1,85 @@
# Production Docker-Compose. Deployment auf ToolCTRL-interne Infrastruktur.
# Erforderliche Env-Datei: .env.production (per Docker-Secret-Mount oder
# Compose-Override).
services:
postgres:
image: postgres:16-alpine
container_name: cockpit-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-cockpit}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-cockpit}
TZ: Europe/Berlin
volumes:
- cockpit_postgres_data:/var/lib/postgresql/data
- cockpit_backups:/backups
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER:-cockpit} -d $${POSTGRES_DB:-cockpit}']
interval: 10s
timeout: 5s
retries: 5
api:
build:
context: .
dockerfile: apps/api/Dockerfile
container_name: cockpit-api
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
NODE_ENV: production
DATABASE_URL: postgres://${POSTGRES_USER:-cockpit}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-cockpit}
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER}
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
AUTHENTIK_REDIRECT_URI: ${AUTHENTIK_REDIRECT_URI}
SESSION_COOKIE_SECRET: ${SESSION_COOKIE_SECRET}
CRON_BEARER_TOKEN: ${CRON_BEARER_TOKEN}
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-https://cockpit.tool-ctrl.de}
LOG_LEVEL: ${LOG_LEVEL:-info}
PORT: 3000
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/api/health']
interval: 15s
timeout: 5s
retries: 3
web:
build:
context: .
dockerfile: apps/web/Dockerfile
container_name: cockpit-web
restart: unless-stopped
# Web ist statisches Asset-Image; Caddy mountet /srv aus diesem Container.
# In Compose teilen wir es via volume statt named-host.
volumes:
- cockpit_web_dist:/srv
caddy:
image: caddy:2-alpine
container_name: cockpit-caddy
restart: unless-stopped
depends_on:
- api
- web
ports:
- '80:80'
- '443:443'
environment:
COCKPIT_HOST: ${COCKPIT_HOST:-cockpit.tool-ctrl.de}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- cockpit_web_dist:/srv:ro
- cockpit_caddy_data:/data
- cockpit_caddy_config:/config
volumes:
cockpit_postgres_data:
cockpit_backups:
cockpit_web_dist:
cockpit_caddy_data:
cockpit_caddy_config:
+81
View File
@@ -0,0 +1,81 @@
# Operations Guide — ToolCTRL everythingbutwork
## Lokale Entwicklung
```bash
# Postgres (Docker)
docker compose -f docker-compose.dev.yml up -d postgres
# Migration anwenden
cp .env.example .env
pnpm --filter @cockpit/db migrate:up
# API und Web
pnpm --filter api dev # :3000
pnpm --filter web dev # :5173 (proxy /api+/trpc → :3000)
```
## Authentik-Setup (Story 1.3)
1. In Authentik (auth.tool-ctrl.de) als Admin einloggen.
2. **Providers → Create → OAuth2/OpenID Provider**
- Name: `cockpit`
- Authorization flow: `default-provider-authorization-explicit-consent`
- Client type: `Confidential`
- Client ID + Secret automatisch generieren lassen, in `.env` übertragen.
- Signing key: `authentik Self-signed Certificate`
- Redirect URIs: `https://cockpit.tool-ctrl.de/api/auth/callback` (Prod), `http://localhost:3000/api/auth/callback` (Dev)
3. **Applications → Create**
- Name: `everythingbutwork`
- Slug: `cockpit`
- Provider: `cockpit`
- Launch URL: `https://cockpit.tool-ctrl.de/`
4. `.env`-Variablen ausfüllen:
```
AUTHENTIK_ISSUER=https://auth.tool-ctrl.de/application/o/cockpit/
AUTHENTIK_CLIENT_ID=<aus Authentik>
AUTHENTIK_CLIENT_SECRET=<aus Authentik>
```
5. Gruppen-Mapping (OQ-1) wird im API-Code per `AUTHENTIK_GROUP_TO_AREA`-Variable umgesetzt (Erweiterung in späterer Story).
## Production-Deployment
```bash
# Auf der ToolCTRL-Infra:
git pull
docker compose pull
docker compose build
docker compose up -d
# Migration:
docker compose exec api node -e "import('@cockpit/db/client').then(async ({ createDbClient }) => { /* migrate via drizzle-kit in CI */ })"
# Alternativ direkt via drizzle-kit von außen mit Production-DATABASE_URL.
```
## Backups (Story 6.3)
Tägliches `pg_dump` via Cron-Endpoint `POST /internal/cron/pg-dump-backup` mit Bearer-Token.
14 Tage Retention. Restore-Drill quartalsweise.
### Restore-Anleitung
1. API stoppen: `docker compose stop api`
2. Backup aus `/backups` auswählen.
3. Restore in temporäre DB: `docker compose exec postgres pg_restore -U cockpit -d cockpit_restore /backups/<file>.sql.gz`
4. Smoke-Test: `curl http://localhost:3000/api/health`
5. API wieder starten: `docker compose start api`
## Healthcheck
`GET /api/health` liefert:
```json
{ "status": "ok|degraded", "db": "ok|down", "authentik": "ok|down", "timestamp": "..." }
```
Wird von Docker und (Phase 2) externer Uptime-Überwachung genutzt.
## OQ-5 (Archivierung)
Soft-Delete via `project.archiviert=true` + `archiviert_am`. Kein automatisches Löschen.
Manuelle Bereinigung bei Bedarf durch DB-Admin via Migration.
+55
View File
@@ -0,0 +1,55 @@
// Flat config (ESLint 9+). Root config; per-app configs extend this implicitly via parser/plugins.
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
export default [
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/coverage/**',
'_bmad/**',
'_bmad-output/**',
'ToolCTRL Brand Identity/**',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['apps/web/**/*.{ts,tsx}'],
plugins: {
react: reactPlugin,
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y,
},
languageOptions: {
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
settings: {
react: { version: 'detect' },
},
rules: {
...reactPlugin.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
...jsxA11y.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
},
},
{
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/consistent-type-imports': 'warn',
// Modal-Backdrops sind Click-Catcher; sie sind a11y-tauglich (Esc + sichtbarer
// Close-Button), die jsx-a11y-Regeln rufen aber Falsch-Positive.
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/no-static-element-interactions': 'off',
},
},
];
+31
View File
@@ -0,0 +1,31 @@
{
"name": "cockpit",
"private": true,
"version": "0.0.0",
"description": "ToolCTRL Unternehmens-Cockpit — internal project overview tool",
"packageManager": "pnpm@10.28.0",
"engines": {
"node": ">=22"
},
"scripts": {
"typecheck": "pnpm -r --parallel run typecheck",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "pnpm -r --parallel run test",
"build": "pnpm -r run build",
"dev:web": "pnpm --filter web dev",
"dev:api": "pnpm --filter api dev"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/node": "^22.10.0",
"eslint": "^9.17.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.0"
}
}
+12
View File
@@ -0,0 +1,12 @@
import type { Config } from 'drizzle-kit';
export default {
schema: './src/schema.ts',
out: './migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL ?? 'postgres://cockpit:cockpit_dev@localhost:55432/cockpit',
},
strict: true,
verbose: true,
} satisfies Config;
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more