# 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 1 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= ``` `.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/).