B1+B2: requirements, architecture, design, tasks
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user