258 lines
17 KiB
Markdown
258 lines
17 KiB
Markdown
# Finance Dashboard
|
||
|
||
Privates, schlankes Dashboard für passives Einkommen auf pinguAurora. FastAPI, Jinja2 und SQLite; Python 3.13 im Docker-Image. Kein Node, keine externen Finanz-APIs. Docker-Port **8081 → 8080**, Daten-Volume **./data → /data**, RAM-Limit **128 MiB** bleiben bestehen.
|
||
|
||
## Funktionen
|
||
|
||
- Acht Kennzahlen, dynamischer Monats-/Jahresvergleich und drei Chart.js-Diagramme.
|
||
- Zahlungen anlegen, bearbeiten, mit Bestätigung löschen; Positionen ergänzen.
|
||
- Vollständige Historie unter `/income`, nach Jahr, Monat, Position und Kategorie filterbar, 100 Buchungen pro Seite.
|
||
- CSV-Export, wiederholbarer XLSX-Import und getrennte Erfassung erwarteter/erhaltener Zahlungen.
|
||
- Alle Summen basieren ausschließlich auf `received=1`. Der Jahresvergleich vergleicht das aktuelle Kalenderjahr mit dem **gesamten** Vorjahr, kein YTD-Vergleich. Bei Vorjahreswert 0 erscheint `–`.
|
||
- Jahres-Spalten entstehen aus den vorhandenen Buchungsjahren, dem aktuellen Jahr und dem nächsten Kalenderjahr als Ausblick; Lücken werden ergänzt. Es sind keine konkreten Jahreszahlen im Dashboard fest programmiert.
|
||
|
||
## Lokale Entwicklung
|
||
|
||
```bash
|
||
python3.13 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install -r requirements-dev.txt
|
||
export FINANCE_DB_PATH="$PWD/data/finance.db"
|
||
uvicorn main:app --app-dir app --reload --host 127.0.0.1 --port 8081
|
||
```
|
||
|
||
`requirements.txt` enthält nur die Web-Abhängigkeiten, einschließlich `python-multipart` für HTML-Formulare. SQLite ist Teil von Python. `openpyxl` wird nur beim Import benötigt; `httpx` nur für Tests. Templates und statische Dateien werden relativ zum Anwendungscode gefunden, im Container also unter `/app/templates` und `/app/static`.
|
||
|
||
```bash
|
||
.venv/bin/python -m unittest discover -s tests -v
|
||
docker compose config
|
||
```
|
||
|
||
Tests verwenden ausschließlich temporäre Datenbanken. Die Entwicklungsbibliotheken werden nicht im Produktionsimage installiert.
|
||
|
||
## Docker Build und Start
|
||
|
||
```bash
|
||
DOCKER_BUILDKIT=0 docker build \
|
||
-t finance-dashboard-finance-dashboard:latest \
|
||
.
|
||
docker compose up -d --no-build
|
||
curl --fail http://127.0.0.1:8081/health
|
||
```
|
||
|
||
Die App ist unter `http://localhost:8081` erreichbar. `/health` liefert `{"status":"ok"}`. `python:3.13-slim` wird ohne feste CPU-Plattform verwendet; auf dem Raspberry wird nativ für ARM64 gebaut.
|
||
|
||
## SQLite und Geldbeträge
|
||
|
||
Standardpfad: **/data/finance.db**, persistent auf dem Host als **./data/finance.db**. Für lokale Entwicklung/Import ist `FINANCE_DB_PATH` oder beim Import `--db` verfügbar. Tabellen und die neun Grundpositionen werden beim Start automatisch angelegt.
|
||
|
||
`income_entries.amount` enthält **ganze Cent (INTEGER)**, keine Euro-Floats. Python summiert Integer und berechnet Prozentwerte mit Decimal. Formulare akzeptieren `0,04`, `0.04` und `28,00`, ohne Tausendertrennzeichen. Mehr als zwei Nachkommastellen werden abgelehnt. Negative Beträge sind für Korrekturen erlaubt. Nur Chart.js verwendet für die grafische Anzeige JavaScript-Zahlen; das ändert keine Finanzwerte in SQLite.
|
||
|
||
SQLite nutzt Foreign Keys, WAL, kurze Transaktionen und fünf Sekunden Wartezeit bei Locks. Referenzierte Positionen können nicht physisch gelöscht werden; `active=0` deaktiviert sie für neue Buchungen, vorhandene Historie bleibt erhalten. Bei temporären Datenbankproblemen antwortet die App mit HTTP 503. Schema-Version 2 wird über `PRAGMA user_version` geführt.
|
||
|
||
`expected=1, received=0` bezeichnet eine offene/ausgefallene erwartete Zahlung. Der Betrag enthält dann die Erwartung, fließt aber **nicht** in tatsächliche Summen ein. Eine teilweise erhaltene Zahlung wird als erhaltene Buchung plus separate offene Restbuchung erfasst. Es gibt noch keine automatische Prognose oder Fälligkeitsverwaltung.
|
||
|
||
## Excel-Import
|
||
|
||
SQLite ist die primäre Datenquelle. Excel wird nach dem Import nicht im laufenden Dashboard gelesen. Die XLSX-Datei bleibt lokal im ignorierten Verzeichnis `import/`.
|
||
|
||
```bash
|
||
source .venv/bin/activate
|
||
pip install -r requirements-import.txt
|
||
python scripts/import_excel.py import/tr_verbessert_2027_vergleiche_enbridge.xlsx \
|
||
--db ./data/finance.db --dry-run
|
||
python scripts/import_excel.py import/tr_verbessert_2027_vergleiche_enbridge.xlsx \
|
||
--db ./data/finance.db
|
||
```
|
||
|
||
Ohne `--db` verwendet das Script `FINANCE_DB_PATH` bzw. `/data/finance.db`:
|
||
|
||
```bash
|
||
python scripts/import_excel.py /pfad/datei.xlsx
|
||
```
|
||
|
||
Es wird nur das **erste Tabellenblatt** gelesen. Der Import sucht den Buchungskopf `Datum`, `Art des Ertrags`/`Position`, `Betrag (€)` und `Kategorie`. Dashboard-Zellen davor und weitere Blätter werden ignoriert. Ungültige Buchungen oder Formeln innerhalb der Buchungsfelder brechen den gesamten Import mit Zeilenangabe ab; es werden keine Teilimporte gespeichert. `--dry-run` rollt Buchungsänderungen zurück, legt jedoch bei Bedarf Datenbank und Seed an.
|
||
|
||
Bekannte Aliase werden normalisiert: **MSC → Main Street Capital**, CSW/CSWC → Capital Southwest, PC → Prospect Capital, Stoxx → STOXX Global Select Dividend 100, Air Baltic → airBaltic. Präfixe wie „Dividende“ werden entfernt. Groß-/Kleinschreibung, Leerzeichen und Satzzeichen führen nicht zu neuen Positionen. Unbekannte Positionen werden angelegt; unsichere Namen werden nicht anhand von Ähnlichkeit zusammengelegt. Historische airBaltic-Buchungen mit Dividendenkategorie werden als Anleihezinsen übernommen und mit einer Notiz gekennzeichnet.
|
||
|
||
Dublettenidentität: Datum, normalisierte Position, Kategorie, Centbetrag, Erwartet-/Erhalten-Status und Vorkommensnummer. Dadurch bleiben mehrere identische echte Zahlungen in einer Datei erhalten, während wiederholte Imports und passende manuelle Buchungen wiederverwendet werden. `import_records` merkt sich den Import auch nach Bearbeiten/Löschen einer Buchung; ein erneuter Import stellt gelöschte Buchungen nicht wieder her. Änderungen an identitätsbildenden Excel-Feldern gelten als neue Buchungen: Korrekturen nach dem Erstimport deshalb im Dashboard vornehmen. Die Excel anschließend als Archiv behandeln.
|
||
|
||
Die vorliegende Excel besitzt im Ertragsbuch keine Erwartet-/Erhalten-Spalten. Diese historischen Buchungen gelten als erhalten. Aus widersprüchlichen Dashboard-Texten werden **keine zusätzlichen Zahlungen oder Ausfälle erfunden**. Falls eine historische Buchung tatsächlich ausgefallen ist, ihren Status nach fachlicher Prüfung im Dashboard korrigieren. Optionale Importspalten `Erwartet`/`Erhalten` unterstützen Ja/Nein, true/false und 1/0.
|
||
|
||
Das Script zeigt einen Plausibilitätscheck: September 2026 **3,71 €**, einschließlich Enbridge am **02.09.2026 mit 0,04 €**. Abweichungen werden gemeldet, nicht durch erfundene Buchungen ausgeglichen.
|
||
|
||
Falls `data/` bereits durch Docker angelegt wurde und für deinen lokalen Benutzer nicht beschreibbar ist, kann der Import im temporären Container laufen. Die Importbibliothek wird dabei nicht im Produktionsimage gespeichert:
|
||
|
||
```bash
|
||
docker compose run --rm --no-deps -v "$PWD:/workspace:ro" finance-dashboard sh -c \
|
||
'pip install --no-cache-dir --target /tmp/import-deps openpyxl && PYTHONPATH=/tmp/import-deps python /workspace/scripts/import_excel.py /workspace/import/tr_verbessert_2027_vergleiche_enbridge.xlsx'
|
||
```
|
||
|
||
## CSV-Export
|
||
|
||
`/export/income.csv` exportiert **alle** Buchungen einschließlich offener Zahlungen: Datum, Position, Kategorie, Betrag, Notiz, Erwartet, Erhalten. UTF-8 mit BOM, Semikolon, Dezimalkomma und CRLF für Excel. Gefährliche Formelpräfixe in Textfeldern erhalten ein schützendes Apostroph. Export ist eine Buchungsliste, kein vollständiges Datenbankbackup.
|
||
|
||
## Backup und Wiederherstellung
|
||
|
||
`data/`, SQLite-Dateien, Excel-Dateien und `.env` werden von Git ausgeschlossen. `.dockerignore` hält sie auch aus dem Build-Kontext fern. **Nie nur die laufende SQLite-Hauptdatei kopieren:** Im WAL-Modus können neuere Transaktionen noch in `-wal` liegen.
|
||
|
||
Konsistentes Backup während des Betriebs mit der SQLite-Backup-API:
|
||
|
||
```bash
|
||
mkdir -p backups
|
||
docker compose exec -T finance-dashboard python -c 'import sqlite3; src=sqlite3.connect("/data/finance.db"); dst=sqlite3.connect("/data/finance-backup.db"); src.backup(dst); dst.close(); src.close()'
|
||
cp data/finance-backup.db "backups/finance-$(date +%Y%m%d-%H%M%S).db"
|
||
```
|
||
|
||
Backup zusätzlich auf einem anderen Datenträger sichern. Wiederherstellung nur bei gestoppter App: vorhandenes `data/` vollständig beiseite sichern (inkl. WAL/SHM), ein frisches `data/` anlegen, die Backup-Datei dort als `finance.db` einsetzen und Dateirechte prüfen. Anschließend `docker compose up -d --no-build`. Das Deploy-Script führt keine Wiederherstellung durch und löscht keine Daten.
|
||
|
||
## Deployment auf pinguAurora
|
||
|
||
Wegen des aktuellen Buildx-Versionskonflikts **nicht `docker compose up -d --build` verwenden**. Im bestehenden Checkout auf dem Raspberry:
|
||
|
||
```bash
|
||
git pull
|
||
|
||
DOCKER_BUILDKIT=0 docker build \
|
||
-t finance-dashboard-finance-dashboard:latest \
|
||
.
|
||
|
||
docker compose up -d --no-build
|
||
```
|
||
|
||
Oder im Checkout einfach:
|
||
|
||
```bash
|
||
./deploy.sh
|
||
```
|
||
|
||
Das ausführbare Script wechselt in sein eigenes Projektverzeichnis, nutzt `git pull --ff-only`, führt genau den klassischen Build und den Start ohne Build aus, zeigt den Containerstatus und prüft `http://127.0.0.1:8081/health` mit Wiederholungen. Bei einem Fehler bricht es ab. Es verändert keine Datenbankdateien. Zugriff: `http://pinguAurora:8081`.
|
||
|
||
Die lokale Datenbank wird **nicht mit Git übertragen**. Für die Erstübernahme auf dem Raspberry entweder die XLSX-Datei separat übertragen und das Import-Script dort in einer Python-Umgebung mit `requirements-import.txt` gegen `./data/finance.db` ausführen, oder ein konsistentes SQLite-Backup vor dem ersten Start in das dortige `data/` übernehmen. Vor einem Import in einen vorhandenen Datenbestand ein Backup erstellen.
|
||
|
||
## Betrieb und Quellen
|
||
|
||
Nur für das private LAN, ohne Benutzerverwaltung. Validierung, SQL-Parameterbindung, Jinja-Autoescaping und Prüfung fremder Browser-Formularursprünge sind enthalten. Chart.js wird fest versioniert vom CDN geladen; ohne Internet funktionieren Buchungen, Kennzahlen und Tabellen weiter. Die Diagramme benötigen Zugang zum CDN.
|
||
|
||
Implementierungsreferenzen: [FastAPI Templates](https://fastapi.tiangolo.com/advanced/templates/) und [Chart.js Integration](https://www.chartjs.org/docs/latest/getting-started/integration.html).
|
||
|
||
## REST-API aktivieren
|
||
|
||
Die API läuft im bestehenden FastAPI-Prozess unter **`/api/v1`** und verwendet dieselben Services und dieselbe SQLite-Datenbank wie die Weboberfläche. Es gibt keine neue Datenhaltung und keine zusätzlichen Laufzeit-Abhängigkeiten.
|
||
|
||
1. Ein langes zufälliges Token erzeugen:
|
||
|
||
```bash
|
||
python -c 'import secrets; print(secrets.token_urlsafe(32))'
|
||
```
|
||
|
||
2. Im Projektverzeichnis eine `.env` anlegen bzw. die vorhandene Datei ergänzen:
|
||
|
||
```dotenv
|
||
FINANCE_API_TOKEN=<langes-zufälliges-token>
|
||
```
|
||
|
||
`.env.example` enthält nur den Platzhalter `change-me`. Diesen durch das erzeugte Token ersetzen; `change-me` aktiviert die API ausdrücklich nicht. `.env` bleibt in `.gitignore` und `.dockerignore`. Ein echtes Token gehört niemals ins Repository.
|
||
|
||
3. Den Container nach einer Tokenänderung neu erstellen:
|
||
|
||
```bash
|
||
docker compose up -d --no-build --force-recreate
|
||
```
|
||
|
||
Compose liest `.env` automatisch und reicht `FINANCE_API_TOKEN` weiter. Bei direktem Uvicorn-Start muss die Variable in der Prozessumgebung gesetzt sein. Ohne Token, bei leerem Token oder beim Beispiel-Platzhalter antwortet die API mit **503**. Weboberfläche und `/health` funktionieren weiterhin. Ein fehlender/falscher Bearer-Header bei aktivierter API führt zu **401** mit `WWW-Authenticate: Bearer`. Der Vergleich erfolgt mit `secrets.compare_digest`. Tokens werden nicht protokolliert. CORS bleibt deaktiviert.
|
||
|
||
Für die folgenden curl-Beispiele muss `FINANCE_API_TOKEN` auch in der aufrufenden Shell gesetzt sein. Im eigenen Projektverzeichnis kann die selbst angelegte `.env` geladen werden:
|
||
|
||
```bash
|
||
set -a
|
||
. ./.env
|
||
set +a
|
||
|
||
curl \
|
||
-H "Authorization: Bearer $FINANCE_API_TOKEN" \
|
||
http://pinguAurora:8081/api/v1/assets
|
||
```
|
||
|
||
### Endpunkte
|
||
|
||
Alle folgenden Pfade beginnen mit `/api/v1` und erfordern den Bearer-Header, auch `/meta`.
|
||
|
||
| Methode | Pfad | Verhalten |
|
||
| --- | --- | --- |
|
||
| GET | `/assets` | Alle Positionen, optional `active=true/false` |
|
||
| GET | `/assets/{id}` | Einzelne Position, sonst 404 |
|
||
| POST | `/assets` | Position erstellen, 201; normalisierte Duplikate 409 |
|
||
| PATCH | `/assets/{id}` | Nur übergebene Felder ändern |
|
||
| DELETE | `/assets/{id}` | Position deaktivieren (`active=false`), 204; Historie bleibt erhalten |
|
||
| GET | `/income` | Historie, neueste zuerst; Filter und Pagination |
|
||
| GET | `/income/{id}` | Einzelne Buchung, sonst 404 |
|
||
| POST | `/income` | Zahlung erstellen und zurückgeben, 201 |
|
||
| PATCH | `/income/{id}` | Teiländerung, aktualisierte Zahlung zurückgeben |
|
||
| DELETE | `/income/{id}` | Buchung löschen, 204 ohne Inhalt |
|
||
| GET | `/stats/summary` | Dieselben acht Kennzahlen wie im Dashboard |
|
||
| GET | `/stats/monthly` | Monatswerte je Jahr, optional `year` |
|
||
| GET | `/stats/by-asset` | Betrag und Anteil je Position, alle Jahre |
|
||
| GET | `/stats/by-category` | Betrag und Anteil für alle vier Kategorien |
|
||
| GET | `/meta` | Name, API-Version und Datenbankstatus ohne Systemdetails |
|
||
|
||
`GET /income` unterstützt `year`, `month`, `asset_id`, `category`, `received`, `expected`, `limit` (Standard 100, maximal 1000) und `offset` (Standard 0). Sortierung: Datum absteigend, bei gleichem Datum ID absteigend. Listen werden als JSON-Arrays geliefert. `asset` in Buchungen und Anteilen ist der normalisierte Positionsname; `asset_id` identifiziert die Position eindeutig.
|
||
|
||
Geldbeträge werden im JSON **ausschließlich als Strings in Euro** mit zwei Nachkommastellen ausgegeben, z. B. `"0.04"`. Auch Eingaben müssen Dezimalstrings sein (`"0.04"` oder `"0,04"`); JSON-Fließkommazahlen werden abgelehnt. Intern bleibt es bei Decimal und Integer-Cent. Prozentwerte sind ebenfalls Strings mit zwei Nachkommastellen; bei Nenner 0 ist der Wert `null`. Ein Anteil wird relativ zur tatsächlichen Gesamtsumme berechnet, inklusive negativer Korrekturen.
|
||
|
||
`/stats/monthly` liefert ein Array aus `{year, months: [{month, amount}], total}` mit zwölf Monaten pro Jahr. Ein explizit angefragtes Jahr ohne Buchungen liefert Nullbeträge. Alle Statistik-Endpunkte zählen ausschließlich `received=true`; `expected=true, received=false` erhöht keine tatsächliche Einnahme. Der Jahresvergleich bleibt aktuelles Jahr gegen gesamtes Vorjahr. `/stats/by-category` trennt `dividend` und `distribution`; das Dashboard-Diagramm fasst sie zur Anzeige zusammen.
|
||
|
||
Unbekannte IDs liefern 404, doppelte Positionsnamen 409 und ungültige Felder/Referenzen 422. `PATCH` lässt fehlende Felder unverändert; `note: null` bzw. `ticker: null` leert optionale Felder. Andere Felder dürfen nicht `null` sein. Historische Buchungen deaktivierter Positionen bleiben lesbar und bearbeitbar; neue Buchungen für inaktive Positionen werden abgelehnt. Reaktivierung erfolgt über `PATCH /assets/{id}` mit `{"active":true}`. Datenbank-Locks führen nach der bestehenden Wartezeit zu 503 mit Wiederholungshinweis; interne Fehler zu 500 ohne Stacktrace oder interne Details.
|
||
|
||
### Zahlung per API erstellen
|
||
|
||
Zuerst die passende `asset_id` über `/assets` ermitteln; die ID im folgenden Beispiel ersetzen:
|
||
|
||
```bash
|
||
curl -X POST \
|
||
-H "Authorization: Bearer $FINANCE_API_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"date":"2026-09-02",
|
||
"asset_id":1,
|
||
"category":"dividend",
|
||
"amount":"0.04",
|
||
"note":"Test",
|
||
"expected":false,
|
||
"received":true
|
||
}' \
|
||
http://pinguAurora:8081/api/v1/income
|
||
```
|
||
|
||
POST legt jeweils eine echte neue Zahlung an; die Dublettenerkennung des Excel-Imports gilt nicht für manuelle/API-Buchungen.
|
||
|
||
Swagger: **http://pinguAurora:8081/docs**. Unter **Authorize** nur das Token eingeben; Swagger ergänzt `Bearer`. OpenAPI: `/openapi.json`. Die Tags heißen Assets, Income und Stats. Die Dokumentation ist lesbar, die dokumentierten API-Aufrufe erfordern Authentifizierung.
|
||
|
||
### API-Deployment auf pinguAurora
|
||
|
||
Einmalig `.env` mit einem echten Token auf dem Raspberry anlegen. Dann weiterhin der bestehende Weg ohne Buildx:
|
||
|
||
```bash
|
||
git pull
|
||
DOCKER_BUILDKIT=0 docker build \
|
||
-t finance-dashboard-finance-dashboard:latest \
|
||
.
|
||
docker compose up -d --no-build
|
||
curl --fail http://127.0.0.1:8081/health
|
||
```
|
||
|
||
`./deploy.sh` bleibt unverändert nutzbar. Die vorhandene `/data/finance.db` wird weiterverwendet und weder gelöscht noch überschrieben. API-Tests setzen ein zufälliges Testtoken in der Testumgebung und verwenden ausschließlich temporäre Datenbanken.
|
||
|
||
Technische Referenzen: [FastAPI HTTPBearer](https://fastapi.tiangolo.com/reference/security/) und [Pydantic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/).
|
||
|
||
|
||
## Positionsänderung: Anleihezinsen und Notgroschen
|
||
|
||
Beim ersten Start dieser Version wird automatisch eine einmalige Datenmigration ausgeführt:
|
||
|
||
- **Anleihezinsen** (Anleihe) und **Zinsen NG** (Notgroschen/Zinskonto) werden angelegt bzw. aktiviert.
|
||
- **Zinsen** und **Steuerrückzahlung** werden deaktiviert und sind für neue Zahlungen nicht mehr auswählbar.
|
||
- Vorhandene Buchungen und ihre Zuordnung bleiben unverändert; sie bleiben in Historie und Auswertungen sichtbar.
|
||
|
||
Die Migration wird zusammen mit ihrer Ausführungsmarkierung in einer SQLite-Transaktion gespeichert. Spätere Starts überschreiben keine danach vorgenommenen Positionsänderungen. Auf pinguAurora reicht das normale Deployment mit `./deploy.sh`; die Datenbank wird nicht über Git übertragen.
|