B1+B2: requirements, architecture, design, tasks

This commit is contained in:
2026-06-15 15:21:18 +00:00
committed by leocrm-bot
parent b5f7a220fe
commit 6cf8955ded
4 changed files with 559 additions and 1 deletions
+116
View File
@@ -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
+201
View File
@@ -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
+121 -1
View File
@@ -1 +1,121 @@
PLACEHOLDER
# 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
+121
View File
@@ -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