# 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