202 lines
8.0 KiB
Markdown
202 lines
8.0 KiB
Markdown
# 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
|