docs: Architektur (Mermaid) und CLAUDE.md fuer kuenftige Sessions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
154
CLAUDE.md
Normal file
154
CLAUDE.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# CLAUDE.md — Repo `~/bin`
|
||||
|
||||
> Diese Datei richtet sich primär an künftige Claude-Coding-Sessions in
|
||||
> diesem Repo, sekundär an Menschen. Architektur/Modul-Übersicht (Mermaid):
|
||||
> **`docs/ARCHITEKTUR.md`**. Vor Code-Änderungen dort das Komponenten- und
|
||||
> Sequenzdiagramm lesen — jeder dort genannte Name existiert real im Code.
|
||||
|
||||
## Projektüberblick
|
||||
|
||||
Finanzberatungs-Tool (Verzeichnis `finance/`): FastAPI-App (API + Web-GUI),
|
||||
Postgres, Grafana-Dashboards. Importiert Kontoauszüge (PDF: VR/HVB/DKB;
|
||||
CSV: VR/HVB/DKB), verwaltet Buchungen/Kategorien/Fixposten, rechnet
|
||||
Kredit-Tilgungspläne und Liquiditäts-Szenarien durch. Läuft als Podman-Pod
|
||||
`finance_pod` (3 Container) unter systemd `--user`, siehe
|
||||
`docs/ARCHITEKTUR.md` Abschnitt 3 (Deployment). Aktuelle Version:
|
||||
`finance/VERSION` (Single Source — siehe dort).
|
||||
|
||||
Zugehöriges Repo `/home/wlfb/fb` ist die **Beratungsumgebung** (Skills,
|
||||
Gesprächsleitfäden für die Nutzung des Tools in der Finanzberatung selbst,
|
||||
eigenes `CLAUDE.md`) — nicht der Code dieses Tools. Dieses Repo (`~/bin`)
|
||||
enthält Quellcode, Deployment- und Betriebsskripte.
|
||||
|
||||
## Betriebskommandos
|
||||
|
||||
**Test-Suite** (Basis-Erwartung: alle grün, dreistellige Anzahl):
|
||||
```bash
|
||||
cd finance && .venv/bin/python -m pytest -q
|
||||
```
|
||||
|
||||
**Deploy / Redeploy** (baut das Image neu, erstellt Pod+Container neu,
|
||||
generiert systemd-Units, aktiviert den Service, wartet auf Bereitschaft):
|
||||
```bash
|
||||
./create_pod_finance.sh
|
||||
```
|
||||
Idempotent — erneutes Ausführen rotiert **keine** Secrets in einer
|
||||
bestehenden `.env` (Ausnahme: `FB_GUI_PASSWORD_HASH` wird bei jedem Lauf mit
|
||||
`FB_PASSWORD` synchron gehalten, siehe Passwort-Prozedur unten). Legacy-Pfad
|
||||
`~/bin/finance/.env` wird beim ersten Lauf automatisch nach
|
||||
`~/.local/share/finance_pod/.env` migriert.
|
||||
|
||||
**Backup** (kalt, kurze Downtime, Zielverzeichnis optional):
|
||||
```bash
|
||||
./backup_finance_pod.sh [ZIELDIR] # Default: ~/backups
|
||||
```
|
||||
Stoppt `pod-finance_pod.service`, sichert `~/.local/share/finance_pod`
|
||||
(inkl. `.env`) via `tar czf` **im `podman unshare`-Namespace** (Postgres-Daten
|
||||
gehören auf dem Host der subuid-verschobenen Container-UID und sind sonst
|
||||
unlesbar), startet den Service wieder, prüft Erreichbarkeit
|
||||
(`GET /login` → 200) und Archiv-Integrität (`.env`, `postgres-data`,
|
||||
`grafana-data`, `data` müssen enthalten sein), setzt `chmod 600` (Archiv
|
||||
enthält Secrets). Bei jedem Fehler läuft der Service danach trotzdem wieder
|
||||
(EXIT-Trap).
|
||||
|
||||
**Passwort ändern** (GUI + Grafana teilen sich `FB_PASSWORD`):
|
||||
- **Vorzugsweg (Admin-Seite):** `/admin` in der GUI aufrufen — ändert altes
|
||||
Passwort gegen Grafana (`services/admin.py::change_password`), schreibt
|
||||
danach die `.env` **in-place** neu. Bestehende Sitzungen bleiben gültig
|
||||
(Session-Secret ändert sich nicht).
|
||||
- **Handverfahren:** `FB_PASSWORD` in
|
||||
`~/.local/share/finance_pod/.env` von Hand editieren (Wert
|
||||
**single-quoted** lassen!) und `./create_pod_finance.sh` erneut ausführen
|
||||
— das Skript leitet `FB_GUI_PASSWORD_HASH` automatisch neu ab und
|
||||
synchronisiert Grafana per `grafana cli admin reset-admin-password`.
|
||||
|
||||
**Parser-Werkzeuge** (nur lokal, siehe DATENSCHUTZ unten):
|
||||
```bash
|
||||
cd finance
|
||||
PYTHONPATH=. .venv/bin/python scripts/parser_audit.py tests/fixtures/dkb_beispiel.pdf
|
||||
PYTHONPATH=. .venv/bin/python scripts/parser_vs_csv.py <pdf> <csv> [<csv>...]
|
||||
```
|
||||
`parser_audit.py` zeigt geparste Transaktionen + PDF-Rohtext einer Fixture.
|
||||
`parser_vs_csv.py` vergleicht ein PDF-Parser-Ergebnis gegen CSV-Ground-Truth
|
||||
(Betrag/Datum-Match, Gegenpartei/Verwendungszweck-Ähnlichkeit via
|
||||
`difflib`). Beide geben echte Kontodaten auf der Konsole aus.
|
||||
|
||||
## WICHTIGE REGELN für künftige Sessions
|
||||
|
||||
**`tests/fixtures/*` = ECHTE Kontodaten — NIE committen oder zitieren.**
|
||||
`finance/tests/fixtures/*.pdf`, `*.csv` und `expected_*.json` sind reale
|
||||
Kontoauszüge (via `finance/.gitignore` vom Repo ausgeschlossen). In Commits,
|
||||
PR-Beschreibungen, Reports, Ledger-Einträgen oder Chat-Antworten dürfen
|
||||
weder Dateiinhalte noch daraus abgeleitete Beträge/Namen/IBANs auftauchen.
|
||||
Ausgaben von `parser_audit.py`/`parser_vs_csv.py` sind ausschließlich zur
|
||||
lokalen Fehlersuche bestimmt, niemals zum Weiterreichen.
|
||||
|
||||
**Decimal, nicht float.** Geldbeträge sind überall `decimal.Decimal`
|
||||
(DB-Spaltentyp `Numeric(12,2)`, siehe `models/tables.py::MONEY`). Keine
|
||||
`float`-Arithmetik für Beträge einführen (Rundungsfehler).
|
||||
|
||||
**Deutsch.** GUI-Texte, Fehlermeldungen, Templates, Commit-Messages und
|
||||
Beratungsinhalte sind auf Deutsch. Datumsformat TT.MM.JJJJ.
|
||||
|
||||
**UX-Regel: Bedienelemente sichtbar, nicht versteckt.** Formulare/Buttons
|
||||
bleiben immer sichtbar und bedienbar; Sperrzustände werden über `disabled`
|
||||
ausgedrückt, nicht durch Entfernen des Elements aus dem DOM. Fehler werden
|
||||
inline auf derselben Seite gemeldet (Beispiel: `routers/admin.py` gibt bei
|
||||
Validierungsfehlern dasselbe Template mit `error`-Kontext zurück statt
|
||||
umzuleiten oder das Formular verschwinden zu lassen).
|
||||
|
||||
**Fable-Testagent-Gate (Nutzer-Vorgabe).** Jeder Task in einem
|
||||
Superpowers-Plan (`docs/superpowers/plans/*.md`) braucht vor dem Commit eine
|
||||
Abnahme durch den Fable-Testagenten (Faktencheck/Live-Test je nach Task).
|
||||
Kein Task gilt als abgeschlossen, solange dieses Gate nicht durchlaufen ist
|
||||
— auch nicht bei scheinbar trivialen Doku-/Config-Änderungen.
|
||||
|
||||
**Superpowers-Workflow.** Pläne liegen unter `docs/superpowers/plans/`
|
||||
(ein Plan pro Ausbaustufe, z.B. `2026-07-20-ausbaustufe-4.md`), gegliedert
|
||||
in Tasks mit Checkbox-Tracking. Fortschritt/Entscheidungen/offene Punkte
|
||||
werden fortlaufend im Ledger `.superpowers/sdd/progress.md` festgehalten
|
||||
(ein Eintrag pro abgeschlossenem Task, inkl. Commit-Range und Fable-Befund).
|
||||
Vor Arbeitsbeginn an einem Task: Plan-Datei UND Ledger lesen — der Ledger
|
||||
enthält oft bindende Nutzerentscheidungen und Warnungen aus früheren Tasks
|
||||
(z.B. Migrations-Hinweise), die nicht im Plan selbst stehen.
|
||||
|
||||
**Versionierung via `finance/VERSION`.** Einzige Versionsquelle (siehe
|
||||
`docs/ARCHITEKTUR.md` Abschnitt 4). Speist Image-Tag
|
||||
(`create_pod_finance.sh`), GUI-Footer und `GET /api/version`
|
||||
(`app/version.py::get_version`). Bei einem Release: `VERSION` hochzählen,
|
||||
bevor `create_pod_finance.sh` läuft — sonst baut das Skript ein Image mit
|
||||
altem Tag.
|
||||
|
||||
**`.env` unter `~/.local/share/finance_pod/`, nicht im Repo-Checkout.**
|
||||
Secrets liegen unter `$BIND_DIR/.env` (`chmod 600`), niemals unter
|
||||
`finance/.env` (Legacy-Pfad, wird migriert). Alle Werte darin sind
|
||||
**single-quoted** zu schreiben (`KEY='wert'`) — `create_pod_finance.sh`
|
||||
sourced die Datei per `set -a; . "$ENV_FILE"; set +a`, und unquoted Werte
|
||||
mit `$` (z.B. der GUI-Passwort-Hash `salt$digest`) würden dabei fälschlich
|
||||
shell-expandiert. Der API-Container mountet dieselbe Datei zusätzlich als
|
||||
**Einzeldatei-Bind-Mount** nach `/data/.env` — das folgt dem Host-Inode, die
|
||||
App muss sie **in-place** überschreiben (`open` im Modus `r+`, `flock`,
|
||||
`truncate` — siehe `services/admin.py::_rewrite_env_file`), niemals über
|
||||
Temp-Datei+`rename` (neuer Inode, vom laufenden Mount nicht mehr gesehen).
|
||||
Host-seitige `sed`-Edits in `create_pod_finance.sh` selbst sind unkritisch,
|
||||
weil das Skript die Container ohnehin bei jedem Lauf neu erstellt.
|
||||
|
||||
**Disaster Recovery = `BIND_DIR`-Backup + Repo + Skript.** Für vollständige
|
||||
Wiederherstellung werden **beide** gebraucht: ein Backup von
|
||||
`~/.local/share/finance_pod` (Secrets + Postgres-/Grafana-Daten, via
|
||||
`backup_finance_pod.sh`) UND der Repo-Checkout `~/bin` (Code für den
|
||||
Image-Build, `create_pod_finance.sh`, `finance/grafana/` für die
|
||||
Provisioning-Mounts). Wiederherstellung: Backup nach
|
||||
`~/.local/share/finance_pod` entpacken, dann `./create_pod_finance.sh`
|
||||
erneut ausführen (baut Image, erstellt Pod/Container/systemd-Units neu,
|
||||
übernimmt vorhandene `.env` unverändert). Verifiziert in A2-Task 7
|
||||
(Live-DR-Probe, siehe `.superpowers/sdd/progress.md`).
|
||||
|
||||
## Weiterführend
|
||||
|
||||
- Architektur (Module, Funktionen, drei Mermaid-Diagramme):
|
||||
`docs/ARCHITEKTUR.md`
|
||||
- Pläne: `docs/superpowers/plans/`
|
||||
- Fortschritts-Ledger: `.superpowers/sdd/progress.md`
|
||||
- Beratungsumgebung (Nutzung des Tools, Skills): `/home/wlfb/fb`
|
||||
(eigenes `CLAUDE.md` dort)
|
||||
314
docs/ARCHITEKTUR.md
Normal file
314
docs/ARCHITEKTUR.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# Architektur — Finanzberatungs-Tool (`finance/`)
|
||||
|
||||
> Diese Doku richtet sich **primär an künftige Claude-Coding-Sessions** in
|
||||
> diesem Repo, sekundär an Menschen. Sie beschreibt den Stand nach
|
||||
> Ausbaustufe 4 (v0.5.0). Jeder hier genannte Modul-/Funktionsname existiert
|
||||
> real im Code unter `finance/app/` — bei Unsicherheit den Pfad öffnen statt
|
||||
> zu raten. Betriebs-/Konventionsregeln stehen in `/home/wlfb/bin/CLAUDE.md`.
|
||||
|
||||
## 1. Komponenten-Diagramm
|
||||
|
||||
Kernachse: `routers/* → services/* → engine/* + parsers/* → models/tables.py
|
||||
→ db.py`. `routers/gui.py` ist ein Sonderfall — es importiert
|
||||
Handler-Funktionen direkt aus anderen Routern (`imports.py`, `planning.py`,
|
||||
`scenarios.py`, `transactions.py`), um serverseitige Logik nicht zu
|
||||
duplizieren. `routers/admin.py` importiert `gui_session` und `templates`
|
||||
direkt aus `routers/gui.py`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph TPL["templates (Jinja2, app/templates/)"]
|
||||
T_pages["base.html, login.html, index.html<br/>transactions.html, salden.html<br/>import.html, planning.html<br/>admin.html, hilfe.html"]
|
||||
end
|
||||
|
||||
subgraph ROUTERS["routers/"]
|
||||
R_gui["gui.py<br/>gui_session, index, buchungen,<br/>salden_page, import_page, planung_page"]
|
||||
R_accounts["accounts.py<br/>list_accounts, create_account,<br/>patch_account"]
|
||||
R_transactions["transactions.py<br/>list_transactions, create_transaction,<br/>patch_transaction, count_transactions"]
|
||||
R_categories["categories.py<br/>list_categories, create_category_rule"]
|
||||
R_imports["imports.py<br/>upload, scan_inbox, preview,<br/>confirm, rollback"]
|
||||
R_planning["planning.py<br/>list_recurring, list_planned,<br/>list_loans, recurring_suggestions"]
|
||||
R_scenarios["scenarios.py<br/>create_scenario, add_modifier,<br/>project_scenario"]
|
||||
R_admin["admin.py<br/>admin_page, admin_change_password,<br/>apply_category_rules"]
|
||||
end
|
||||
|
||||
subgraph SERVICES["services/"]
|
||||
S_importer["importer.py<br/>process_file, process_pdf, process_csv"]
|
||||
S_balances["balances.py<br/>account_balance(at), total_balance"]
|
||||
S_categorize["categorize.py<br/>apply_rules"]
|
||||
S_suggestions["suggestions.py<br/>suggest_recurring"]
|
||||
S_projection["projection_service.py<br/>run_projection"]
|
||||
S_admin["admin.py<br/>change_password, apply_rules_retroactively"]
|
||||
end
|
||||
|
||||
subgraph ENGINE["engine/ (rein, kein DB-Zugriff)"]
|
||||
E_loans["loans.py<br/>loan_schedule, annuity_payment, add_months"]
|
||||
E_recurrence["recurrence.py<br/>occurrences"]
|
||||
E_projection["projection.py<br/>project"]
|
||||
E_scenario["scenario.py<br/>build_cashflows (PlainRecurring/<br/>PlainPlanned/PlainModifier)"]
|
||||
end
|
||||
|
||||
subgraph PARSERS["parsers/"]
|
||||
P_base["base.py<br/>parse_german_amount, parse_german_date"]
|
||||
P_detect["detect.py<br/>detect_bank"]
|
||||
P_validate["validate.py<br/>balance_difference, dedup_hash"]
|
||||
P_vr["vr.py — parse"]
|
||||
P_hvb["hvb.py — parse"]
|
||||
P_dkb["dkb.py — parse"]
|
||||
P_csv["csv_formats.py<br/>detect_csv_format, parse_csv"]
|
||||
P_registry["registry.py — parse_pdf"]
|
||||
end
|
||||
|
||||
subgraph MODELS["models/ + alembic/"]
|
||||
M_tables["tables.py<br/>Account, Statement, Transaction,<br/>RecurringItem, PlannedItem, Loan,<br/>Scenario, ProjectionResult, ..."]
|
||||
M_views["views.py — create_views()<br/>v_balance_history, v_balance_total,<br/>v_monthly_by_category, v_projection"]
|
||||
M_alembic["alembic/versions<br/>b2b1f5a18a74_initial_schema<br/>1ef6a356f028_konto_anker"]
|
||||
end
|
||||
|
||||
R_gui --> T_pages
|
||||
R_admin --> T_pages
|
||||
|
||||
R_gui --> R_imports
|
||||
R_gui --> R_planning
|
||||
R_gui --> R_scenarios
|
||||
R_gui --> R_transactions
|
||||
R_gui --> E_loans
|
||||
R_gui --> E_recurrence
|
||||
R_gui --> S_balances
|
||||
R_admin --> R_gui
|
||||
|
||||
R_accounts --> S_balances
|
||||
R_transactions --> P_validate
|
||||
R_transactions --> S_categorize
|
||||
R_imports --> S_importer
|
||||
R_planning --> E_loans
|
||||
R_planning --> S_suggestions
|
||||
R_scenarios --> S_projection
|
||||
R_admin --> S_admin
|
||||
|
||||
ROUTERS -.-> M_tables
|
||||
|
||||
S_importer --> P_csv
|
||||
S_importer --> P_registry
|
||||
S_importer --> P_validate
|
||||
S_importer --> S_categorize
|
||||
S_projection --> E_loans
|
||||
S_projection --> E_projection
|
||||
S_projection --> E_scenario
|
||||
S_projection --> S_balances
|
||||
S_admin --> S_categorize
|
||||
|
||||
E_scenario --> E_loans
|
||||
E_scenario --> E_recurrence
|
||||
|
||||
P_registry --> P_vr
|
||||
P_registry --> P_hvb
|
||||
P_registry --> P_dkb
|
||||
P_registry --> P_detect
|
||||
P_vr --> P_base
|
||||
P_hvb --> P_base
|
||||
P_dkb --> P_base
|
||||
P_csv --> P_base
|
||||
P_validate --> P_base
|
||||
|
||||
SERVICES -.-> M_tables
|
||||
M_alembic -.->|erzeugt Schema fuer| M_tables
|
||||
M_tables -.->|Basis fuer| M_views
|
||||
```
|
||||
|
||||
Nicht im Diagramm (Querschnitt, siehe Prosa unten): `app/auth.py`
|
||||
(`require_auth`, `current_password_hash`, `session_valid` — an fast allen
|
||||
Routern als FastAPI-Dependency), `app/config.py` (`get_settings`),
|
||||
`app/db.py` (`get_session`, `get_engine`), `app/main.py` (bindet alle Router
|
||||
+ `lifespan` → `create_views`), `app/version.py` (`get_version`).
|
||||
|
||||
## 2. Sequenzdiagramm — Import-Fluss
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Browser/GUI
|
||||
participant RI as routers/imports.py
|
||||
participant IM as services/importer.py
|
||||
participant PA as parsers (registry.py / csv_formats.py)
|
||||
participant VA as parsers/validate.py
|
||||
participant CA as services/categorize.py
|
||||
participant DB as Postgres
|
||||
|
||||
Note over B,RI: Upload ODER Inbox-Scan
|
||||
alt Datei-Upload
|
||||
B->>RI: POST /api/imports/upload (PDF/CSV)
|
||||
else Inbox-Scan
|
||||
B->>RI: POST /api/imports/scan-inbox
|
||||
Note right of RI: iteriert *.pdf und *.csv in FB_INBOX_DIR
|
||||
end
|
||||
|
||||
RI->>IM: process_file(session, path)
|
||||
alt Endung .csv
|
||||
IM->>PA: parse_csv(path) [csv_formats.detect_csv_format]
|
||||
else Endung .pdf (Default)
|
||||
IM->>PA: parse_pdf(path) [registry.detect_bank -> vr/hvb/dkb.parse]
|
||||
end
|
||||
PA-->>IM: ParsedStatement / ParsedCsv
|
||||
|
||||
IM->>IM: _find_or_create_account(bank, iban)
|
||||
IM->>DB: INSERT Statement(status="draft")
|
||||
|
||||
opt Saldo pruefbar (PDF immer; CSV nur VR: balance_checkable=True)
|
||||
IM->>VA: balance_difference(parsed)
|
||||
Note right of IM: Differenz != 0 -> Statement.status="error", Abbruch
|
||||
end
|
||||
|
||||
loop je Buchungszeile
|
||||
IM->>VA: dedup_hash(account_id, booking_date, amount, purpose)
|
||||
IM->>DB: INSERT Transaction(status="draft", is_duplicate=?)
|
||||
end
|
||||
IM->>CA: apply_rules(session, drafts)
|
||||
IM->>IM: _apply_anchor_autofill(account, anchor)
|
||||
Note right of IM: nur wenn CSV-Anker-Datum >= Account.anchor_date (oder kein Anker)
|
||||
IM->>DB: Datei inbox -> uploads verschieben, COMMIT
|
||||
IM-->>RI: Statement (draft)
|
||||
RI-->>B: 201 StatementOut
|
||||
|
||||
B->>RI: GET /api/imports/{id}/preview
|
||||
RI->>DB: SELECT Transactions WHERE statement_id=id
|
||||
RI->>RI: balance_ok = true/false/None (dreiwertig:<br/>None bei fehlenden Salden, z.B. HVB/DKB-CSV)
|
||||
RI-->>B: PreviewOut(balance_ok, duplicates)
|
||||
|
||||
alt Bestaetigen
|
||||
B->>RI: POST /api/imports/{id}/confirm
|
||||
loop je Draft-Transaktion (nicht is_duplicate)
|
||||
RI->>DB: Dedup-Recheck: dedup_hash bereits confirmed<br/>in ANDEREM Statement?
|
||||
alt Kollision gefunden
|
||||
RI->>DB: tx.is_duplicate = true (bleibt draft)
|
||||
else keine Kollision
|
||||
RI->>DB: tx.status = "confirmed"
|
||||
end
|
||||
end
|
||||
RI->>DB: Statement.status = "confirmed"
|
||||
RI-->>B: 200 StatementOut
|
||||
else Verwerfen (Rollback)
|
||||
B->>RI: POST /api/imports/{id}/rollback
|
||||
Note right of RI: nur wenn Statement.status == "confirmed"
|
||||
RI->>DB: DELETE Transactions + Statement
|
||||
RI-->>B: 200 {deleted_transactions, statement_id}
|
||||
end
|
||||
```
|
||||
|
||||
## 3. Deployment-Diagramm
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph HOST["Host wlfb (rootless Podman)"]
|
||||
Boot["create_pod_finance.sh<br/>Bootstrap: FB_PASSWORD erzeugen<br/>-> PBKDF2-Hash (FB_GUI_PASSWORD_HASH)<br/>-> Grafana-Sync (grafana cli<br/>admin reset-admin-password)"]
|
||||
Backup["backup_finance_pod.sh [ZIELDIR]<br/>KALT: systemctl stop -> tar czf<br/>via 'podman unshare' (subuid-Daten)<br/>-> systemctl start -> Readiness-Curl<br/>-> tar -tzf Integritaet -> chmod 600"]
|
||||
|
||||
subgraph SYSTEMD["systemd --user (Unit-Dateien in ~/.config/systemd/user, je chmod 600)"]
|
||||
U1["pod-finance_pod.service"]
|
||||
U2["container-finance-db_ctr.service"]
|
||||
U3["container-finance-api_ctr.service"]
|
||||
U4["container-finance-grafana_ctr.service"]
|
||||
end
|
||||
|
||||
subgraph POD["Pod finance_pod (podman pod)"]
|
||||
API["finance-api_ctr<br/>127.0.0.1:8096 -> 8000<br/>uvicorn app.main:app<br/>(entrypoint.sh: alembic upgrade head)"]
|
||||
DBC["finance-db_ctr<br/>Postgres 17.10<br/>NUR pod-intern: localhost:5432<br/>(kein Host-Port)"]
|
||||
GRA["finance-grafana_ctr<br/>127.0.0.1:8097 -> 3000<br/>grafana-oss 12.1.0"]
|
||||
end
|
||||
|
||||
subgraph BINDDIR["Bind-Mounts ~/.local/share/finance_pod (BIND_DIR, chmod 700)"]
|
||||
DATA["data/ (-v DATA_DIR:/data:Z)<br/>inbox/, uploads/"]
|
||||
ENVF[".env (-v ENV_FILE:/data/.env:Z)<br/>EINZELDATEI-Mount folgt dem INODE!<br/>App schreibt IN-PLACE (open r+,<br/>flock, truncate) - NIE Temp+rename<br/>(services/admin.py _rewrite_env_file)"]
|
||||
PGD["postgres-data/"]
|
||||
GRD["grafana-data/ (setgid, gid 0)"]
|
||||
end
|
||||
|
||||
subgraph REPO["Repo-Checkout ~/bin/finance/grafana (read-only Mounts)"]
|
||||
PROV["grafana/provisioning<br/>-> /etc/grafana/provisioning:Z,ro"]
|
||||
DASH["grafana/dashboards/finanzen.json<br/>-> /var/lib/grafana/dashboards:Z,ro"]
|
||||
end
|
||||
end
|
||||
|
||||
Boot -->|erzeugt/aktualisiert| ENVF
|
||||
Boot -->|podman generate systemd --new| SYSTEMD
|
||||
Boot -->|podman run| API
|
||||
Boot -->|podman run| DBC
|
||||
Boot -->|podman run| GRA
|
||||
Boot -.->|exec grafana cli admin reset-admin-password| GRA
|
||||
|
||||
SYSTEMD -->|verwaltet Start/Stop| POD
|
||||
|
||||
API -->|liest/schreibt in-place| ENVF
|
||||
API -->|Daten| DATA
|
||||
API -.->|SQL localhost:5432| DBC
|
||||
DBC --> PGD
|
||||
GRA --> GRD
|
||||
GRA --> PROV
|
||||
GRA --> DASH
|
||||
GRA -.->|SQL read-only Rolle finance_read| DBC
|
||||
API -.->|HTTP PUT .../password, siehe change_password| GRA
|
||||
|
||||
Backup -->|stoppt/startet| U1
|
||||
Backup -->|tar czf BIND_DIR| BINDDIR
|
||||
```
|
||||
|
||||
## 4. Zentrale Konzepte (kompakt)
|
||||
|
||||
**Saldo-Anker (per Tagesende).** `Account.anchor_date` /
|
||||
`Account.anchor_balance` (Migration `1ef6a356f028_konto_anker`) ersetzen die
|
||||
frühere Statement-closing-Logik als Basis der Saldenrechnung.
|
||||
`services/balances.py::account_balance(session, account, at)` addiert bzw.
|
||||
subtrahiert bestätigte Buchungen zwischen Anker und `at`; der Anker gilt
|
||||
**per Tagesende** des Ankerdatums (enthält bereits alle Buchungen bis
|
||||
einschließlich diesem Tag). CSV-Importe füllen einen fehlenden oder
|
||||
veralteten Anker automatisch nach (`_apply_anchor_autofill` in
|
||||
`services/importer.py`) — ein manuell per PATCH gesetzter, neuerer Anker
|
||||
bleibt dabei unangetastet. Die Postgres-Views `v_balance_history` /
|
||||
`v_balance_total` (`models/views.py`) bilden dieselbe Anker-Semantik als
|
||||
lückenlose Tagesreihe (Carry-Forward via `generate_series` + Fenster-`SUM`)
|
||||
für Grafana ab.
|
||||
|
||||
**`dedup_hash`.** `parsers/validate.py::dedup_hash(account_id, booking_date,
|
||||
amount, purpose)` erzeugt einen deterministischen Hash je Buchung. Er wird
|
||||
zweimal genutzt: (1) beim Import markiert `services/importer.py` Entwürfe
|
||||
als `is_duplicate`, wenn der Hash bereits unter einer *bestätigten*
|
||||
Transaktion existiert; (2) beim Bestätigen (`routers/imports.py::confirm`)
|
||||
erfolgt ein **Dedup-Recheck** gegen zwischenzeitlich bestätigte Buchungen
|
||||
*anderer* Statements — zwei identische Buchungen *innerhalb* desselben
|
||||
Statements teilen sich zwar denselben Hash, dürfen sich aber nicht
|
||||
gegenseitig ausschließen, da die in der Preview geprüfte Bilanz von beiden
|
||||
abhängt.
|
||||
|
||||
**Statement-Lebenszyklus.** `Statement.status` (`models/tables.py`) durchläuft
|
||||
`draft` → `confirmed`, oder `draft` → `error` (Bank/Format nicht erkannt,
|
||||
Saldo-Differenz, kaputte Datei). Transaktionen haben denselben
|
||||
Status-Wortschatz (`draft`/`confirmed`) pro Zeile. Bestätigte Statements sind
|
||||
nur noch über `rollback` (löscht Statement + Transaktionen komplett) zu
|
||||
entfernen, nicht mehr über `DELETE /api/imports/{id}` (nur für Entwürfe und Fehl-Importe).
|
||||
|
||||
**Szenario-Engine ist rein.** `app/engine/*` (`loans.py`, `recurrence.py`,
|
||||
`projection.py`, `scenario.py`) hat **keinen** Datenbank- oder
|
||||
Request-Zugriff — nur `scenario.py` importiert innerhalb der Engine von
|
||||
`loans.py`/`recurrence.py`, sonst keine Abhängigkeiten auf `app.*`. Ein-/
|
||||
Ausgaben sind einfache Dataclasses (`Installment`, `PlainRecurring`,
|
||||
`PlainPlanned`, `PlainModifier`, `Projection`). `services/projection_service.py`
|
||||
ist die einzige Brücke: es lädt ORM-Objekte (`Loan`, `RecurringItem`,
|
||||
`PlannedItem`, `Scenario`, `ScenarioModifier`), baut daraus die Plain-Objekte
|
||||
und ruft `engine.scenario.build_cashflows` → `engine.projection.project`.
|
||||
|
||||
**`VERSION`-Datei als Single Source.** `finance/VERSION` (ein Einzeiler, z.B.
|
||||
`0.4.0`) ist die einzige Versionsquelle: `app/version.py::get_version()`
|
||||
liest sie (Fallback `"0.0.0-dev"`), gespeist in den Jinja2-Footer
|
||||
(`templates.env.globals["app_version"]` in `routers/gui.py`) und
|
||||
`GET /api/version` (`main.py`). `Containerfile` kopiert `VERSION` ins Image;
|
||||
`create_pod_finance.sh` liest dieselbe Datei für den Image-Tag
|
||||
(`API_IMAGE="localhost/finance-api:$(cat "$FINANCE_DIR/VERSION")"`). Ein
|
||||
Release ist somit: `VERSION` hochzählen → Image bauen → Redeploy.
|
||||
|
||||
**DATENSCHUTZ: Fixtures enthalten echte Kontodaten.** `finance/tests/fixtures/`
|
||||
enthält reale PDF-/CSV-Kontoauszüge (`*.pdf`, `*.csv`, `expected_*.json`) —
|
||||
siehe `finance/tests/fixtures/README.md` und `finance/.gitignore` (alle drei
|
||||
Muster ausgeschlossen). `scripts/parser_audit.py` und
|
||||
`scripts/parser_vs_csv.py` geben Klartext-Kontodaten auf der Konsole aus und
|
||||
sind ausdrücklich mit "NUR lokal verwenden" markiert — ihre Ausgabe darf
|
||||
nicht in Commits, Reports, Tickets oder Chat-Antworten landen. Details und
|
||||
verbindliche Regeln: siehe `CLAUDE.md`.
|
||||
Reference in New Issue
Block a user