From 32c64f9c03db6f540ee11270c80cded3ed176211 Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Mon, 15 Jun 2026 15:21:18 +0000 Subject: [PATCH] B1+B2: requirements, architecture, design, tasks --- specs/current/architecture.md | 116 ++++++++++++++++++++ specs/current/design.md | 201 ++++++++++++++++++++++++++++++++++ specs/current/requirements.md | 121 ++++++++++++++++++++ specs/current/tasks.md | 121 ++++++++++++++++++++ 4 files changed, 559 insertions(+) create mode 100644 specs/current/architecture.md create mode 100644 specs/current/design.md create mode 100644 specs/current/requirements.md create mode 100644 specs/current/tasks.md diff --git a/specs/current/architecture.md b/specs/current/architecture.md new file mode 100644 index 0000000..66661ed --- /dev/null +++ b/specs/current/architecture.md @@ -0,0 +1,116 @@ +# leocrm — Architektur (v0.1) + +## 1. Überblick + +`leocrm` ist eine monolithische FastAPI-Webanwendung, die HTML-Views (Jinja2) **und** eine JSON-API parallel anbietet. Beide Schichten teilen sich dieselben Service-Layer-Funktionen, dadurch sind Headless-Tests ohne Browser möglich. + +``` +┌─────────────────────────────────────────────────┐ +│ Browser (HTML/Jinja2) │ +│ ↓ 302-Login-Redirect bei fehlender Session │ +├─────────────────────────────────────────────────┤ +│ FastAPI App (app/main.py) │ +│ ├── / /login /logout /companies/* │ +│ │ /contacts/* → HTML-Routes (Jinja2) │ +│ │ │ +│ └── /api/health /api/auth/* /api/companies/* │ +│ /api/contacts/* → JSON-Routes │ +├─────────────────────────────────────────────────┤ +│ Service Layer (app/services/) │ +│ ├── auth_service.py │ +│ ├── company_service.py │ +│ └── contact_service.py │ +├─────────────────────────────────────────────────┤ +│ Persistence (SQLAlchemy 2.0, app/db/) │ +│ ├── models.py (User, Company, Contact) │ +│ ├── session.py (engine, SessionLocal) │ +│ └── init_db.py (create_all + seed) │ +├─────────────────────────────────────────────────┤ +│ SQLite (/data/leocrm.db) │ +└─────────────────────────────────────────────────┘ +``` + +## 2. Tech-Stack + +- **Python 3.11** +- **FastAPI 0.115** — Web-Framework +- **Uvicorn 0.30** — ASGI-Server +- **SQLAlchemy 2.0** — ORM +- **Jinja2 3.1** — Templates +- **bcrypt 4.1** — Passwort-Hashing (über `passlib[bcrypt]`) +- **itsdangerous 2.2** — Session-Cookie-Signatur (über Starlette `SessionMiddleware`) +- **pytest 8.3** + **httpx 0.27** — Tests +- **SQLite 3** (in-Container, Volume `/data`) +- **Docker** (Multi-Stage optional; Single-Stage reicht für v0.1) + +## 3. Schichten + +### 3.1 Routes (app/routes/) + +- `html_routes.py` — HTML-Endpoints, rendert Jinja2 +- `api_routes.py` — JSON-Endpoints, Pydantic-Schemas + +### 3.2 Services (app/services/) + +- `auth_service.py` — `verify_password`, `authenticate_user`, `get_current_user_optional` +- `company_service.py` — `list_companies(q, limit, offset)`, `get_company(id)`, `create_company(data)`, `update_company(id, data)`, `delete_company(id)` +- `contact_service.py` — analog für Contacts, inkl. Cascade-Delete-Logik + +### 3.3 DB (app/db/) + +- `models.py` — SQLAlchemy-Modelle +- `session.py` — `engine = create_engine(...)`, `SessionLocal = sessionmaker(...)`, `get_db()` als FastAPI-Dependency +- `init_db.py` — `init_db()` erstellt Tabellen + Seed-Daten, idempotent + +### 3.4 Schemas (app/schemas/) + +- Pydantic-v2-Schemas für Input/Output (CompanyIn, CompanyOut, ContactIn, ContactOut, LoginIn) + +## 4. Auth-Flow + +1. Browser ruft `/login` → Formular +2. `POST /login` mit `username`+`password` → `auth_service.authenticate_user()` → bcrypt-Vergleich +3. Bei Erfolg: `request.session['user_id'] = user.id` (signed Cookie via Starlette-SessionMiddleware) +4. Redirect auf `/` +5. Alle anderen Routes prüfen `request.session.get('user_id')`; wenn None → 302 → `/login` +6. API macht es analog: `POST /api/auth/login` setzt Cookie, alle anderen API-Routes prüfen Session + +`SECRET_KEY` wird aus ENV `LEOCRM_SECRET_KEY` gelesen; Fallback-Default nur für lokales Dev, in Coolify **Pflicht-ENV**. + +## 5. Datenpersistenz + +- SQLite-Datei: `/data/leocrm.db` (in Coolify als Volume gemountet) +- Schema-Migration: `Base.metadata.create_all(engine)` beim App-Start; Alembic-Migrationen sind v0.2 +- Cascade: Contact.company_id → Company.id mit `ondelete='CASCADE'`; beim Company-Delete verschwinden alle Contacts + +## 6. Deployment + +``` +Container: leocrm-app + Image: python:3.11-slim + pip install + copy app + Port: 8000 + Volume: /data (persistent, SQLite) + ENV: LEOCRM_SECRET_KEY (required) + LEOCRM_DB_PATH (default /data/leocrm.db) + Health: GET /api/health +``` + +Coolify-Setup: +1. Forgejo-Webhook registriert auf `https://forgejo.media-on.de/Leopoldadmin/leocrm` Push auf `main` +2. Coolify-Service vom Typ "Docker Compose" mit `docker-compose.yml` aus dem Repo-Root +3. Domain: `leocrm.media-on.de` mit Let's-Encrypt-SSL + +## 7. Sicherheit (v0.1 Minimum) + +- bcrypt für Passwörter (kein Plain-Text) +- Session-Cookie signed, `httponly=True`, `samesite='lax'`, in Prod `secure=True` +- CSRF: für v0.1 ausgeschlossen (Login + GET-only-Pattern reicht für Single-User-Demo); v0.2 fügt CSRF-Tokens hinzu +- SQL-Injection: durch SQLAlchemy-ORM ausgeschlossen +- Input-Validierung: Pydantic-Schemas +- Default-User `admin/admin` ist dokumentiert und nur in Demo gedacht + +## 8. Skalierbarkeit & Grenzen + +- v0.1: < 100 Companies, < 1000 Contacts problemlos; darüber SQLite-Performance grenzwertig +- v0.2: Migration auf Postgres + Connection-Pool +- Stateless-App: horizontal skalierbar; SQLite-Volume wird zum Engpass → Postgres-Migration diff --git a/specs/current/design.md b/specs/current/design.md new file mode 100644 index 0000000..85ff1c1 --- /dev/null +++ b/specs/current/design.md @@ -0,0 +1,201 @@ +# leocrm — Design (v0.1, Konkrete Specs) + +## 1. Verzeichnisstruktur + +``` +leocrm/ +├── README.md +├── LICENSE +├── .gitignore +├── pyproject.toml # Projekt-Metadaten, dependencies +├── requirements.txt # Pinned-Versionen für Reproduzierbarkeit +├── Dockerfile # Single-Stage, python:3.11-slim +├── docker-compose.yml # Coolify-kompatibel +├── .dockerignore +├── .env.example # LEOCRM_SECRET_KEY, LEOCRM_DB_PATH +├── docs/ +│ ├── architecture.md # (siehe architecture.md) +│ ├── test_report.md # wird in B7 erzeugt +│ ├── runtime_report.md # wird in B7 erzeugt +│ └── api.md # Auto-generierte OpenAPI-Übersicht (manuell) +├── deploy/ +│ ├── env.md # ENV-Variablen-Doku +│ ├── healthcheck.md # Coolify-Healthcheck-Config +│ ├── runbook.md # Runbook (Start/Stop/Logs/Restart) +│ └── rollback.md # Rollback-Procedure +├── app/ +│ ├── __init__.py +│ ├── main.py # FastAPI-App-Factory, Middleware, Router-Mount +│ ├── config.py # Settings (pydantic-settings) +│ ├── db/ +│ │ ├── __init__.py +│ │ ├── models.py # User, Company, Contact +│ │ ├── session.py # engine, SessionLocal, get_db +│ │ └── init_db.py # create_all + seed +│ ├── schemas/ +│ │ ├── __init__.py +│ │ ├── auth.py # LoginIn +│ │ ├── company.py # CompanyIn, CompanyOut +│ │ └── contact.py # ContactIn, ContactOut +│ ├── services/ +│ │ ├── __init__.py +│ │ ├── auth_service.py +│ │ ├── company_service.py +│ │ └── contact_service.py +│ ├── routes/ +│ │ ├── __init__.py +│ │ ├── html_routes.py +│ │ └── api_routes.py +│ ├── templates/ +│ │ ├── base.html # Layout: Navbar + Container + Footer +│ │ ├── login.html +│ │ ├── dashboard.html +│ │ ├── companies/ +│ │ │ ├── list.html +│ │ │ ├── form.html # new + edit +│ │ │ └── detail.html +│ │ └── contacts/ +│ │ ├── form.html +│ │ └── detail.html +│ └── static/ +│ └── style.css # Minimal-CSS, kein Build-Step +└── tests/ + ├── __init__.py + ├── conftest.py # TestClient + DB-Fixture (sqlite:///:memory:) + ├── test_health.py + ├── test_auth.py + ├── test_companies_api.py + ├── test_contacts_api.py + └── test_html_smoke.py # Light HTML-Tests (Status, Redirects) +``` + +## 2. Konkrete Funktions-Signaturen + +### app/services/auth_service.py +```python +def hash_password(plain: str) -> str: ... +def verify_password(plain: str, hashed: str) -> bool: ... +def authenticate_user(db: Session, username: str, password: str) -> User | None: ... +``` + +### app/services/company_service.py +```python +def list_companies(db: Session, q: str | None = None, limit: int = 50, offset: int = 0) -> list[Company]: ... +def get_company(db: Session, company_id: int) -> Company | None: ... +def create_company(db: Session, data: CompanyIn) -> Company: ... +def update_company(db: Session, company_id: int, data: CompanyIn) -> Company | None: ... +def delete_company(db: Session, company_id: int) -> bool: ... # cascade +def count_companies(db: Session) -> int: ... +``` + +### app/services/contact_service.py +```python +def list_contacts(db: Session, company_id: int | None = None, limit: int = 50, offset: int = 0) -> list[Contact]: ... +def get_contact(db: Session, contact_id: int) -> Contact | None: ... +def create_contact(db: Session, data: ContactIn) -> Contact: ... # Validierung: company muss existieren +def update_contact(db: Session, contact_id: int, data: ContactIn) -> Contact | None: ... +def delete_contact(db: Session, contact_id: int) -> bool: ... +def count_contacts(db: Session) -> int: ... +``` + +### app/routes/api_routes.py (Auszug) +```python +@router.get("/api/health") +def health(db: Session = Depends(get_db)) -> dict: ... + +@router.post("/api/auth/login") +def login(payload: LoginIn, request: Request, response: Response, db: Session = Depends(get_db)): ... +@router.post("/api/auth/logout") +def logout(request: Request): ... + +@router.get("/api/companies") +def list_companies(q: str | None = None, limit: int = 50, offset: int = 0, db: Session = Depends(get_db)) -> list[CompanyOut]: ... +@router.post("/api/companies", status_code=201) +def create_company(payload: CompanyIn, db: Session = Depends(get_db)) -> CompanyOut: ... +@router.get("/api/companies/{cid}") +def get_company(cid: int, db: Session = Depends(get_db)) -> CompanyOut: ... +@router.patch("/api/companies/{cid}") +def update_company(cid: int, payload: CompanyIn, db: Session = Depends(get_db)) -> CompanyOut: ... +@router.delete("/api/companies/{cid}", status_code=204) +def delete_company(cid: int, db: Session = Depends(get_db)): ... +# Analog für /api/contacts +``` + +### app/routes/html_routes.py (Auszug) +```python +@router.get("/login") +def login_form(request: Request): ... # 200 wenn nicht eingeloggt, sonst redirect / +@router.post("/login") +def login_submit(payload: ..., request: Request, db: Session = Depends(get_db)): ... +@router.post("/logout") +def logout(request: Request): ... +@router.get("/") +def dashboard(request: Request, db: Session = Depends(get_db)): ... # Auth-Required +@router.get("/companies") +def companies_list(request: Request, q: str | None = None, db: Session = Depends(get_db)): ... +# ... analog /companies/new, /companies/{id}, /companies/{id}/edit, /companies/{id}/delete +# ... /contacts/new, /contacts/{id}, /contacts/{id}/edit, /contacts/{id}/delete +``` + +## 3. Middleware-Reihenfolge (app/main.py) + +1. `SessionMiddleware(secret_key=..., https_only=in_prod)` +2. CORS nur in Dev (in Prod nicht nötig, da Same-Origin) +3. Custom `AuthRequiredMiddleware` für HTML: prüft Session auf allen Pfaden außer `["/login", "/static/*", "/api/health", "/api/auth/login"]` +4. API-Auth prüft jede Route via Dependency `current_user_required` + +## 4. Templates (Jinja2) + +### base.html +- Bootstrap-5 via CDN (kein npm-Build, da Single-Page-Container) +- Navbar: Logo "leocrm" + Links "Dashboard", "Firmen", "Kontakte" + User-Name + Logout-Form +- Block `content` + +### login.html +- Form mit `username`, `password`, Submit "Anmelden" +- Fehlermeldung bei `?error=1` + +### dashboard.html +- 3 Cards: Anzahl Firmen, Anzahl Kontakte, letzte 5 Einträge +- Link "Neue Firma" / "Neuer Kontakt" + +### companies/list.html +- Tabelle mit Spalten Name, Stadt, Land, # Kontakte, Aktionen +- Suchform oben (Input + Submit) +- Pagination (limit/offset) +- Button "Neue Firma" + +### companies/form.html +- Felder: Name*, Straße, PLZ, Stadt, Land, E-Mail, Telefon, Web, Notizen +- Submit + Cancel + +### companies/detail.html +- Alle Felder als Definition-List +- Liste der Kontakte als Tabelle +- Button "Kontakt hinzufügen" (verlinkt auf `/contacts/new?company_id={id}`) +- Buttons "Bearbeiten", "Löschen" + +### contacts/form.html + detail.html +- Analog, mit Dropdown/Input für company_id (Pflicht) + +## 5. CSS (app/static/style.css) + +- Minimal, nur Layout-Feinschliff (Buttons-Padding, Card-Spacing, Tabellen-Hover) +- Keine Custom-Designs, Bootstrap-Defaults reichen + +## 6. ENV-Variablen + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `LEOCRM_SECRET_KEY` | (Dev-Fallback `"dev-insecure-key"`) | Session-Signing-Key; in Prod Pflicht | +| `LEOCRM_DB_PATH` | `/data/leocrm.db` | SQLite-Pfad | +| `LEOCRM_SEED_ON_START` | `true` | Wenn `true`, wird bei leerer DB geseedet | + +## 7. OpenAPI / Swagger + +FastAPI generiert automatisch `/docs` (Swagger UI) und `/openapi.json`. Beide bleiben aktiv, auch im Prod-Mode (kein Auth auf `/docs` für v0.1, dokumentiert als bekannte Einschränkung; v0.2 mit Auth). + +## 8. Logging + +- Uvicorn-Default-Logging (stdout) reicht für v0.1 +- Kein File-Logging, kein Sentry diff --git a/specs/current/requirements.md b/specs/current/requirements.md new file mode 100644 index 0000000..4313bd3 --- /dev/null +++ b/specs/current/requirements.md @@ -0,0 +1,121 @@ +# leocrm — Requirements (v0.1, Mini-CRM, Single-Tenant) + +## 1. Ziel + +Ein schlankes, in sich geschlossenes Mini-CRM, das folgende Kern-Workflows abdeckt: +- Anmeldung (Login/Logout) per Session-Cookie +- Verwaltung von **Firmen** (Anlegen, Anzeigen, Bearbeiten, Löschen, Liste mit Suche) +- Verwaltung von **Kontaktpersonen** (Anlegen, Anzeigen, Bearbeiten, Löschen, Zuordnung zu Firma) +- Übersichts-Dashboard mit Anzahl Firmen, Kontakten und letzten Änderungen + +Single-Tenant, eine Org, eine SQLite-Datei. Keine Rollen, keine Multi-User-Berechtigungen jenseits Login ja/nein. + +## 2. Personas + +- **Admin/Nutzer** (1 Person pro Org): meldet sich an, verwaltet Daten. +- **Entwickler**: betreibt die App lokal und in Coolify. + +## 3. Funktionale Anforderungen (Must-Have) + +| ID | Anforderung | Akzeptanzkriterium | +|----|-------------|---------------------| +| F-1 | Login-Formular | GET `/login` zeigt Formular; POST mit gültigen Credentials setzt Session-Cookie und leitet auf `/` weiter; ungültige Credentials → 401 + Fehlermeldung. | +| F-2 | Logout | POST `/logout` löscht Session und leitet auf `/login` weiter. | +| F-3 | Auth-Schutz | Alle Seiten außer `/login` und `/api/health` erfordern eine aktive Session; ohne Session → 302 → `/login`. | +| F-4 | Dashboard | GET `/` zeigt Anzahl Companies, Anzahl Contacts, letzte 5 Änderungen (Companies + Contacts). | +| F-5 | Companies-Liste | GET `/companies` zeigt Tabelle mit Name, Stadt, Land, Anzahl Contacts; Volltextsuche über Name/Stadt filtert. | +| F-6 | Company anlegen | GET `/companies/new` zeigt Formular; POST erstellt Datensatz, Redirect auf Detail. | +| F-7 | Company-Detail | GET `/companies/{id}` zeigt Stammdaten + Liste der zugeordneten Contacts + Link „Contact hinzufügen". | +| F-8 | Company bearbeiten | GET `/companies/{id}/edit` zeigt vorausgefülltes Formular; PATCH speichert, Redirect auf Detail. | +| F-9 | Company löschen | POST `/companies/{id}/delete` löscht inkl. zugeordneter Contacts (cascade). | +| F-10 | Contact anlegen | GET `/contacts/new?company_id={id}` zeigt Formular; POST erstellt Datensatz mit FK auf Company. | +| F-11 | Contact-Detail | GET `/contacts/{id}` zeigt alle Felder + Link zur Firma. | +| F-12 | Contact bearbeiten | GET `/contacts/{id}/edit` zeigt Formular; PATCH speichert. | +| F-13 | Contact löschen | POST `/contacts/{id}/delete` löscht. FK-Company bleibt erhalten. | +| F-14 | API parallel zu HTML | Für jede HTML-Aktion gibt es einen äquivalenten JSON-API-Endpoint (siehe API-Spec unten), damit Tests Headless durchlaufen können. | +| F-15 | Health-Endpoint | GET `/api/health` → 200 `{"status":"ok","db":"ok"}`. | +| F-16 | Demo-Seed | Beim ersten Start wird automatisch 1 Admin-User (`admin`/`admin`), 2 Beispiel-Firmen und 3 Beispiel-Kontakte angelegt, falls DB leer. | + +## 4. Datenmodell + +``` +User + id INTEGER PK + username TEXT UNIQUE NOT NULL + password_hash TEXT NOT NULL + created_at TIMESTAMP DEFAULT now + +Company + id INTEGER PK + name TEXT NOT NULL + street TEXT + zip TEXT + city TEXT + country TEXT DEFAULT 'DE' + email TEXT + phone TEXT + website TEXT + notes TEXT + created_at TIMESTAMP DEFAULT now + updated_at TIMESTAMP DEFAULT now + +Contact + id INTEGER PK + company_id INTEGER FK -> Company.id ON DELETE CASCADE + first_name TEXT NOT NULL + last_name TEXT NOT NULL + email TEXT + phone TEXT + position TEXT + notes TEXT + created_at TIMESTAMP DEFAULT now + updated_at TIMESTAMP DEFAULT now +``` + +## 5. API-Spec (für Headless-Tests) + +| Methode | Pfad | Body / Params | Erfolg | Fehler | +|--------|------|---------------|--------|--------| +| GET | `/api/health` | — | 200 | — | +| POST | `/api/auth/login` | `{username,password}` | 200 + Set-Cookie | 401 | +| POST | `/api/auth/logout` | — | 204 | — | +| GET | `/api/companies` | `?q=&limit=&offset=` | 200 List | — | +| POST | `/api/companies` | JSON | 201 | 400 | +| GET | `/api/companies/{id}` | — | 200 | 404 | +| PATCH | `/api/companies/{id}` | JSON | 200 | 404, 400 | +| DELETE | `/api/companies/{id}` | — | 204 | 404 | +| GET | `/api/contacts` | `?company_id=` | 200 | — | +| POST | `/api/contacts` | JSON | 201 | 400, 404 (Company) | +| GET | `/api/contacts/{id}` | — | 200 | 404 | +| PATCH | `/api/contacts/{id}` | JSON | 200 | 404 | +| DELETE | `/api/contacts/{id}` | — | 204 | 404 | + +## 6. Nicht-Ziele (Out of Scope für v0.1) + +- Multi-Tenant / Multi-Org +- Rollen-/Rechtesystem (jeder Login-User darf alles) +- E-Mail-Versand, Datei-Upload, Bilder +- Audit-Log, Soft-Delete, Papierkorb +- 2FA, OAuth, Passwort-Reset per Mail +- Internationalisierung (deutsche UI, hartkodiert) +- Mobile-Apps, Push-Notifications +- Reporting, Dashboards mit Charts (nur Counts) + +## 7. Annahmen + +- Coolify-Host `server.media-on.de` ist erreichbar; Coolify-Token liegt in der Plugin-Config +- Forgejo-Host `forgejo.media-on.de` ist erreichbar; Token liegt in der Plugin-Config +- SQLite reicht für v0.1 (< 100k Datensätze), Migration auf Postgres später möglich +- Deployment erfolgt als einzelner Coolify-Service (Docker Compose mit 1 Container) +- Default-Admin-Credentials `admin/admin` werden beim ersten Start geseeded und sind nur in v0.1; im v0.2 verpflichtender Passwort-Change + +## 8. Qualitätskriterien + +- **Type-Check**: `python -m mypy app/` ohne Fehler (oder bewusst als nicht-blockend dokumentiert) +- **Tests**: `pytest` mit mindestens **15 Tests**, alle grün (Health, Auth happy + fail, Company CRUD 5, Contact CRUD 5, Cascade-Delete 1, Search 1) +- **Server-Start**: `uvicorn app.main:app` startet ohne ImportError oder 500er +- **Health-Endpoint**: 200, nicht 500 +- **API-Endpoints**: alle 13 oben genannten Endpunkte liefern die spezifizierten Status-Codes +- **Build**: Docker-Image buildet ohne Fehler, Image-Größe < 300 MB +- **Deploy**: Coolify-Service ist `running:healthy` nach `POST /api/v1/deploy` +- **Git**: alle Änderungen committed, `git status` ist sauber diff --git a/specs/current/tasks.md b/specs/current/tasks.md new file mode 100644 index 0000000..6d7b8c9 --- /dev/null +++ b/specs/current/tasks.md @@ -0,0 +1,121 @@ +# leocrm — Task-Graph (v0.1) + +## Phasen & Reihenfolge + +``` +B0 Setup B1 Requirements B2 Architecture B3 Scaffold B4 Auth B5 Companies +───────── ───────────── ────────────── ────────── ────── ────────── +repo+git requirements.md architecture.md pyproject+req.txt user-model company-model +.env.example design.md (parallel) app/main.py auth_service company_service +.gitignore tasks.md Dockerfile /api/auth/* /api/companies/* +README docker-compose login-form companies-list +init-commit plan-approval commit+pushed logout form+detail + → B4 ↓ + B6 Contacts + ─────────── + contact-model + contact_service + /api/contacts/* + contacts-form+detail + ↓ + B7 Test+Runtime + ─────────────── + 15+ pytest tests + server-start + /api/health = 200 + test_report.md + runtime_report.md + ↓ + B8 Deploy-Prep + ────────────── + coolify project + compose verified + env.md, runbook.md + rollback.md + quality-reviewer + ↓ + B9 Deploy + ──────── + coolify service + deploy trigger + running:healthy + release_auditor + user-report +``` + +## Tasks (atomar, klein, testbar) + +| ID | Block | Task | Abhängigkeit | Geschätzter Aufwand | +|----|-------|------|--------------|---------------------| +| T-01 | B3 | pyproject.toml + requirements.txt mit allen Pinned-Deps | — | 5 min | +| T-02 | B3 | app/main.py Skeleton (FastAPI-App, leere Router, Health-Endpoint) | T-01 | 5 min | +| T-03 | B3 | Dockerfile + docker-compose.yml + .dockerignore + .env.example | T-01 | 5 min | +| T-04 | B3 | README.md + .gitignore + LICENSE | — | 3 min | +| T-05 | B3 | Init-Commit + Push zu Forgejo (branch=main) | T-01..T-04 | 2 min | +| T-06 | B4 | app/db/models.py (User, Company, Contact) | T-02 | 8 min | +| T-07 | B4 | app/db/session.py + app/db/init_db.py (mit seed) | T-06 | 5 min | +| T-08 | B4 | app/schemas/auth.py (LoginIn) + app/services/auth_service.py | T-06 | 5 min | +| T-09 | B4 | app/routes/api_routes.py: /api/health + /api/auth/login + /api/auth/logout | T-08 | 8 min | +| T-10 | B4 | app/routes/html_routes.py: /login + /logout + base.html + login.html | T-09 | 8 min | +| T-11 | B4 | Auth-Middleware + current_user-Dependency | T-09, T-10 | 5 min | +| T-12 | B4 | Commit B4 + Push + lokaler Server-Test | T-11 | 5 min | +| T-13 | B5 | app/schemas/company.py (CompanyIn, CompanyOut) | T-06 | 3 min | +| T-14 | B5 | app/services/company_service.py (list, get, create, update, delete, count) | T-13 | 8 min | +| T-15 | B5 | app/routes/api_routes.py: /api/companies/* (alle 5 Endpoints) | T-14 | 8 min | +| T-16 | B5 | app/routes/html_routes.py: /companies + /companies/new + /companies/{id} + /companies/{id}/edit + /companies/{id}/delete | T-15 | 12 min | +| T-17 | B5 | app/templates/companies/*.html + dashboard.html mit Companies-Count | T-16 | 10 min | +| T-18 | B5 | Commit B5 + Push | T-17 | 2 min | +| T-19 | B6 | app/schemas/contact.py | T-06 | 3 min | +| T-20 | B6 | app/services/contact_service.py | T-19 | 8 min | +| T-21 | B6 | app/routes/api_routes.py: /api/contacts/* | T-20 | 8 min | +| T-22 | B6 | app/routes/html_routes.py: /contacts/* + Cascade-Delete-View | T-21 | 10 min | +| T-23 | B6 | app/templates/contacts/*.html + dashboard.html mit Contacts-Count | T-22 | 8 min | +| T-24 | B6 | Commit B6 + Push | T-23 | 2 min | +| T-25 | B7 | tests/conftest.py (TestClient + in-memory-DB) | T-24 | 5 min | +| T-26 | B7 | tests/test_health.py (2 Tests) | T-25 | 3 min | +| T-27 | B7 | tests/test_auth.py (3 Tests: happy, wrong-pw, no-creds) | T-25 | 5 min | +| T-28 | B7 | tests/test_companies_api.py (5 Tests) | T-25 | 8 min | +| T-29 | B7 | tests/test_contacts_api.py (5 Tests + 1 Cascade-Test) | T-25 | 10 min | +| T-30 | B7 | tests/test_html_smoke.py (3 Tests: redirect-when-anon, login-form, dashboard-renders) | T-25 | 5 min | +| T-31 | B7 | pytest ausführen, alle 18 Tests grün, test_report.md schreiben | T-26..T-30 | 5 min | +| T-32 | B7 | uvicorn app.main:app starten, /api/health=200, /api/auth/login, 5 Endpoints testen, runtime_report.md schreiben | T-31 | 5 min | +| T-33 | B7 | Commit B7 + Push | T-32 | 2 min | +| T-34 | B8 | deploy/env.md (Variablen-Doku) | — | 3 min | +| T-35 | B8 | deploy/healthcheck.md (Coolify-Config) | — | 3 min | +| T-36 | B8 | deploy/runbook.md (Start/Stop/Logs/Restart/Update) | — | 5 min | +| T-37 | B8 | deploy/rollback.md (Schritte + Coolify-Rollback) | — | 5 min | +| T-38 | B8 | docker build lokal: Image < 300 MB, Container startet | T-03 | 5 min | +| T-39 | B8 | quality_reviewer über alle Artefakte, findings.md | T-33..T-38 | 10 min | +| T-40 | B8 | Coolify: Projekt anlegen via coolify_action=projects_create | — | 3 min | +| T-41 | B8 | Coolify: Service/Compose-Ressource erstellen via coolify_action=resources_create | T-40 | 5 min | +| T-42 | B8 | Commit B8 + Push | T-41 | 2 min | +| T-43 | B9 | Coolify: ENV LEOCRM_SECRET_KEY setzen via coolify_action=security_envs_update | T-41 | 2 min | +| T-44 | B9 | Coolify: Domain setzen (PATCH application) | T-41 | 2 min | +| T-45 | B9 | Coolify: Deploy auslösen POST /api/v1/deploy | T-44 | 3 min | +| T-46 | B9 | Polling: Service running:healthy | T-45 | 3 min | +| T-47 | B9 | Public-Smoke: GET https://leocrm.media-on.de/api/health → 200 | T-46 | 2 min | +| T-48 | B9 | release_auditor: final audit, next-steps.md | T-47 | 5 min | +| T-49 | B9 | Final-Commit + User-Report | T-48 | 3 min | + +## Quality-Gates (zwischen Blocks) + +| Gate | Trigger | Aktion | +|------|---------|--------| +| G1 | B0 → B1 | project_registry.register, orchestrator_state.phase=intake | +| G2 | B1 → B2 | quality_reviewer über requirements.md, design.md, tasks.md | +| G3 | B2 → B3 | plan_mode_guard set mode=implementation_allowed (nach User-Approval) | +| G4 | B3 → B4 | docker build OK, app startet, /api/health=200 | +| G5 | B4 → B5 | 3 Auth-Tests grün, Login-HTML rendert | +| G6 | B5 → B6 | 5 Company-API-Tests grün, List/Detail-HTML rendert | +| G7 | B6 → B7 | 5 Contact-API-Tests grün, Cascade-Delete-Test grün | +| G8 | B7 → B8 | 18 Tests grün, Runtime-Report geschrieben, plan_mode=runtime_verification_allowed | +| G9 | B8 → B9 | quality_reviewer OK, plan_mode=deployment_preparation_allowed → release_handoff_allowed | +| G10 | B9 → Done | release_auditor OK, /api/health public=200 | + +## Parallelisierungsmöglichkeiten + +- B4-T-09 (api) und T-10 (html) parallelisierbar +- B5-T-15 (api) und T-16 (html) parallelisierbar +- B6-T-21 (api) und T-22 (html) parallelisierbar +- B7: alle test_*.py parallel schreibbar +- B8: alle deploy/*.md parallel schreibbar