Dienst, der in konfigurierbaren Intervallen Salden, Depotübersicht und Kontoumsätze aus
der comdirect REST API abruft und historisiert in einer extern bereitgestellten MariaDB
ablegt. Das vollständige Konzept (Architektur, Datenmodell, Auswertungen, offene Punkte)
steht in docs/konzept.md.
- .NET 10 SDK (für lokale Entwicklung) bzw. Docker
- Eine extern erreichbare MariaDB-Instanz (wird nicht von diesem Projekt bereitgestellt)
- comdirect-Zugangsdaten: Client-ID/Secret (API-Zugang) sowie Zugangsnummer/PIN
Alle Einstellungen werden über Umgebungsvariablen übergeben, siehe .env.example.
Für lokale Docker-Läufe: .env.example nach .env kopieren und Werte eintragen.
Seit 0.12.0 (docs/konzept.md Abschnitt 10) werden Client-ID/Client-Secret und Zugangsnummer/PIN
nicht mehr nur als Klartext in .env gehalten – siehe dort für das vollständige Konzept
(Bedrohungsmodell, Begründung). Einmaliges Setup vor dem ersten docker compose up:
# 1. Client-ID/Client-Secret als dateibasierte Secrets anlegen (ersetzt die entsprechenden
# Zeilen in .env; secrets/ ist gitignored):
mkdir -p secrets
printf '%s' 'DEINE_CLIENT_ID' > secrets/Comdirect__ClientId
printf '%s' 'DEIN_CLIENT_SECRET' > secrets/Comdirect__ClientSecret
chmod 600 secrets/Comdirect__ClientId secrets/Comdirect__ClientSecret
# 2. Dedizierten Schlüssel für die Zugangsnummer/PIN-Verschlüsselung erzeugen - bewusst
# außerhalb dieses Projektverzeichnisses, damit er nicht dieselbe Exposition wie .env hat:
./scripts/comdirectctl.sh generate-bootstrap-key
# Standardpfad /etc/comdirect-fetch/credential.key (überschreibbar per Argument), chmod 400.
# Bricht bewusst ab, falls dort schon ein Schlüssel liegt.
# 3. Seit 1.0.0 läuft der Container nicht mehr als root (docs/konzept.md Abschnitt 14) - alle
# drei Dateien müssen dem Container-Nutzer gehören, sonst startet der Dienst nicht
# (chown-Ziel = UID/GID des "app"-Nutzers im aspnet:10.0-Basisimage; prüfen mit
# `docker run --rm mcr.microsoft.com/dotnet/aspnet:10.0 sh -c 'id app'`, aktuell 1654):
chown 1654:1654 secrets/Comdirect__ClientId secrets/Comdirect__ClientSecret
sudo chown 1654:1654 /etc/comdirect-fetch/credential.keyAuch der Schlüssel für die Session-Token-Persistierung (Comdirect__TokenEncryptionKeyBase64,
siehe unten) lässt sich so erzeugen: ./scripts/comdirectctl.sh generate-token-key gibt einen
zufälligen Base64-Schlüssel aus, der manuell in .env einzutragen ist.
Danach Container (neu) starten und Zugangsnummer/PIN einmalig per Bootstrap-Schritt verschlüsselt ablegen:
./scripts/comdirectctl.sh set-credentials
# fragt Zugangsnummer und PIN interaktiv ab (PIN nicht sichtbar, landet nicht in der
# Shell-History) und legt sie verschlüsselt in credential_store abNach erfolgreicher Bestätigung Comdirect__Username/Comdirect__Password aus .env entfernen
– der Dienst liest sie danach ausschließlich verschlüsselt aus der DB. Ohne diesen Bootstrap-
Schritt (bzw. ohne die Schlüsseldatei) bleibt das bisherige Verhalten unverändert: Zugangsnummer/
PIN werden dann weiterhin aus .env gelesen, komplett opt-in.
dotnet build
dotnet testdocker compose -f docker/docker-compose.yml up --buildBeim Start wendet der Dienst automatisch alle ausstehenden Datenbank-Migrationen aus
db/migrations/ an.
.github/workflows/ci.yml baut und testet automatisch bei jedem
Push auf einen Branch sowie bei jedem Pull Request gegen main – unabhängig vom Release-Workflow
unten, der nur bei Versions-Tags läuft. Kein Docker-Image, keine Veröffentlichung, reines
Build+Test-Gate.
Bei jedem gepushten Versions-Tag (vX.Y.Z) baut und veröffentlicht
.github/workflows/docker-release.yml automatisch
ein Docker-Image nach GitHub Container Registry, getaggt sowohl mit der Versionsnummer als
auch mit latest:
docker pull ghcr.io/vulture20/comdirect-fetch:latest
docker pull ghcr.io/vulture20/comdirect-fetch:v0.7.0Vorher läuft dotnet build/dotnet test als Gate – schlägt das fehl, wird nichts
veröffentlicht. Manueller Testlauf (ohne latest zu überschreiben) über
„Run workflow" im Actions-Tab bzw. gh workflow run docker-release.yml.
comdirect verlangt beim Aufbau einer neuen Session eine TAN-Bestätigung, die der Container
nicht automatisch erledigen kann (siehe docs/konzept.md, Abschnitt 3). Seit 1.0.0 erfordern
/auth/* und /debug/* Admin__Password (HTTP Basic Auth, Breaking Change - siehe Abschnitt
14 des Konzepts; ohne gesetztes Passwort liefern diese Endpunkte HTTP 503 statt zu funktionieren;
nur /health bleibt offen). Einfachster Weg über
scripts/comdirectctl.sh (Voraussetzung: curl, jq) - das Skript
ermittelt das Passwort automatisch (Umgebungsvariable COMDIRECT_FETCH_ADMIN_PASSWORD, sonst aus
der .env-Datei im Repo-Root, sonst ein im Skript direkt eintragbarer Fallback):
./scripts/comdirectctl.sh auth start
# → löst z. B. eine PushTAN-Benachrichtigung in der comdirect-App aus
# ACHTUNG: nicht wiederholt aufrufen, siehe Warnung unten zur TAN-Sperre
./scripts/comdirectctl.sh auth confirm
# tanCode als Argument nur nötig, wenn der TAN-Typ eine manuelle Eingabe verlangt
# (z. B. photoTAN/mobileTAN): ./scripts/comdirectctl.sh auth confirm <TAN_CODE>
./scripts/comdirectctl.sh status
# menschenlesbarer Status: Auth-Status, Zeilen je Tabelle, letzte Abrufe.
# Für Skripte/Monitoring: --json; Exit-Code 0 = authentifiziert, 1 = Freigabe nötig/Fehler,
# 2 = Dienst nicht erreichbar (siehe comdirectctl.sh help für alle Codes).Äquivalent direkt per curl gegen die HTTP-API (Basis-URL per COMDIRECT_FETCH_URL
überschreibbar, Standard http://localhost:8750; -u admin:... mit dem Admin__Password
aus .env für /auth/*//debug/* - Benutzername ist beliebig, es gibt nur ein
gemeinsames Passwort):
curl -u "admin:$Admin__Password" -X POST http://localhost:8750/auth/start
curl -u "admin:$Admin__Password" -X POST http://localhost:8750/auth/confirm -H "Content-Type: application/json" -d '{"tanCode": null}'
curl http://localhost:8750/health # /health bleibt ohne Auth erreichbar(Port 8750 statt des ursprünglich geplanten 8080, da 8080 auf diesem Host bereits belegt war – siehe docker/docker-compose.yml.)
Solange der Dienst danach durchgehend läuft, hält ein interner Hintergrundprozess die Session per Token-Refresh am Leben; eine erneute Freigabe ist erst nach einem Neustart oder einer längeren Downtime wieder nötig.
Test-/Betriebshilfen (berühren keine Session/TAN, gefahrlos wiederholbar):
./scripts/comdirectctl.sh fetch-nowbzw.POST /debug/fetch-now– stößt Salden-, Depotübersicht- und Umsatzabruf sofort an, statt auf die konfigurierten Intervalle zu warten../scripts/comdirectctl.sh statusbzw.GET /debug/summary– Zeilenanzahl je Tabelle plus die letzten 10sync_log-Einträge, zur schnellen Verifikation ohne direkten DB-Zugriff../scripts/comdirectctl.sh consolidatebzw.POST /debug/consolidate– stößt den Konsolidierungs-/Aufräumlauf sofort an (KONZEPT.md Abschnitt 11). Komplett opt-in: ohne gesetzteRetention__*-Zeiträume (siehe.env.example) ein no-op../scripts/comdirectctl.sh notify-testbzw.POST /debug/notify-test– löst eine Testbenachrichtigung über alle aktivierten Kanäle aus (siehe unten), ohne eine echte Session-Störung abwarten zu müssen.
Bricht die Session-Refresh-Kette ab (Neustart, Downtime), wechselt der Status auf "Freigabe
erforderlich" – standardmäßig nur passiv über comdirectctl.sh status/sync_log sichtbar.
Optional, komplett opt-in und gleichzeitig nutzbar, zwei aktive Benachrichtigungskanäle (siehe
.env.example für alle Variablen):
- E-Mail –
Notification__EmailSmtpHost,-Port,-User,-Password,-UseStartTls,-From,-To(kommagetrennt bei mehreren Empfängern). - Webhook –
Notification__WebhookUrl, POST mit generischem JSON-Body ({"event", "subject", "body", "occurredAt", "appVersion"}), funktioniert z. B. mit ntfy.sh, Home Assistant oder n8n/Node-RED.
Beide Kanäle lassen sich gleichzeitig konfigurieren; ist keiner gesetzt, ändert sich nichts am
bisherigen, rein passiven Verhalten. Konfiguration mit ./scripts/comdirectctl.sh notify-test
prüfen, bevor man sich darauf verlässt.
Kleine, vom Dienst selbst ausgelieferte Web-Oberfläche unter /admin/rules/ (z. B.
http://localhost:8750/admin/rules/) – Tabellen-Editor für categories/categorization_rules,
inkl. Testen gegen echte Umsätze vor dem Übernehmen (Einzel-Regel-Vorschau und volle Simulation
mit Diff). Regeln können gegen Buchungstext, Umsatztyp oder – seit 0.17.0 – gegen den
strukturierten Empfänger-/Auftraggeber-Namen (CounterpartyName) geprüft werden, wichtig für
echte Überweisungen, deren Buchungstext nur den Verwendungszweck enthält. Ein Button „Nicht
kategorisiert" (seit 0.18.0) listet alle Umsätze ohne echte Kategorisierung (keine Kategorie
oder nur der Vorzeichen-Fallback) – gute Kandidaten für neue Regeln, ohne manuell in der DB
nachsehen zu müssen. Einzelne Umsätze lassen sich dort direkt einer Kategorie zuordnen (seit
0.21.0), ohne dass dafür eine neue Regel nötig ist – sinnvoll für Einzelposten, für die sich
keine allgemeine Regel lohnt. Jede Regel kann außerdem einen optionalen Freitext-Kommentar tragen (seit
0.19.0) – etwa warum sie existiert oder welcher Buchungstext sie ausgelöst hat –, direkt in der
Regeltabelle editierbar. Über den „Bearbeiten"-Button pro Regel (seit 0.20.0) lassen sich auch
Muster, Feld, Kategorie und Priorität einer bestehenden Regel nachträglich ändern, statt sie
löschen und neu anlegen zu müssen. Eine zweite Seite, „Alle Umsätze"
(/admin/rules/transactions/, seit 0.22.0, von beiden Seiten aus verlinkt), zeigt sämtliche
Umsätze mit Datum, Buchungstext, Empfänger, Betrag, Konto und Kategorie – mit Pagination sowie
Suche/Filter nach Freitext, Kategorie, Konto, Zeitraum und Betrag, und derselben
Kategorie-Zuordnung pro Zeile wie in der „Nicht kategorisiert"-Liste. Setzt Admin__Password
in .env voraus (HTTP Basic Auth, Benutzername beliebig) –
ohne gesetztes Passwort liefert /admin/rules/* durchgängig HTTP 503 statt ungeschützt erreichbar
zu sein. Bewusst strenger geschützt als die übrigen /debug/*//auth/*-Endpunkte, da hier
dauerhafte Konfiguration geändert wird statt nur eine Aktion angestoßen. Die drei besonderen
Kategorien (Intern/Neutral, Sonstige Einnahme, Sonstige Ausgabe) lassen sich weder löschen
noch umbenennen.
comdirect sperrt nach drei falschen TAN-Eingaben oder fünf TAN-Challenges ohne
zwischenzeitliche Einlösung einer korrekten TAN den gesamten Online-Banking-Zugang
(nicht nur den API-Zugriff). POST /auth/start daher nicht wiederholt/automatisiert
aufrufen – der Dienst liefert bei bereits ausstehender Freigabe die bestehende Challenge
zurück statt eine neue anzufordern, aber das schützt nicht vor externen Skripten/Retries.
Die in src/ComdirectFetch.Api verwendeten Endpunkt-Pfade und JSON-Felder wurden gegen die
offizielle comdirect REST API Dokumentation abgeglichen und zusätzlich mit echten
Zugangsdaten end-to-end live getestet: Login/Session/TAN-Flow, Salden (3 Konten),
Depotübersicht (inkl. Positionen mit ISIN/Name) und Kontoumsätze (inkl. Pagination über
mehrere Seiten) funktionieren nachweislich (siehe CHANGELOG.md 0.2.0–0.7.0). Dabei wurden
mehrere reale Abweichungen von der Doku gefunden und behoben (u. a. bookingDate als
einfacher String statt verschachteltem Objekt, paging-first erfordert
transactionState=BOOKED, comdirects Rate-Limit bei vielen Anfragen).
Rate-Limiting (HTTP 429) wird seit 0.6.0 mit echtem Retry/Backoff behandelt (ComdirectResilience,
Polly), zusätzlich zu proaktiven kurzen Pausen zwischen Requests – siehe CHANGELOG.md.
Bekannte Restrisiken: Verhalten bei sehr großen Depots/vielen Konten im Dauerbetrieb ist
nur mit den aktuellen Testdaten verifiziert, nicht an echten Großvolumina. Die früher teils
falsch codiert dargestellten Umlaute in geloggten Fehlertexten (Issue #8) sind behoben – siehe
CHANGELOG.md.
Dieses Projekt betreibt kein eigenes Grafana – die Auswertungen werden in eine bereits vorhandene Grafana-Instanz eingebunden (KONZEPT.md Abschnitt 2/6: „Grafana ist optional" heißt hier konkret: extern und schon da, nicht Teil dieses Deployments). Vier Dashboards, alle mit echten Daten verifiziert:
grafana/dashboards/salden.json– „Salden & Vermögen" (Phase 1): Saldo-Verlauf je Konto plus Gesamtsumme, sowie Vermögensentwicklung (Konten + Depots kombiniert) mit Stat-Panels für die jeweils aktuellen Werte.grafana/dashboards/depot.json– „Depot" (Phase 2): Asset-Allokation als Kreisdiagramm sowohl je Einzelposition als auch nach Anlageklasse gruppiert (Aktie/ETF/ Fonds/Zertifikat/… – Issue #4), Positionstabelle, sowie Kurswert- und Gewinn/Verlust-Entwicklung je Einzelposition über die Zeit.grafana/dashboards/cashflow.json– „Cashflow & Kosten" (Phase 3): Einnahmen/ Ausgaben/Netto je Monat, Ausgaben nach Kategorie, Gebührenübersicht (Kontoführungs-/ Ordergebühren). Interne Umbuchungen zwischen eigenen Konten sind ausgeschlossen. Enthält seit 0.18.0 einen Dashboard-Link „Kategorien/Regeln bearbeiten →", der/admin/rules/direkt öffnet.grafana/dashboards/depot-performance.json– „Depot-Performance" (Phase 4): zwei Sichten. Oben die ursprüngliche, vereinfachte Variante (Depotwert vs. Kapitaleinsatz laut comdirect-Anschaffungswert, unrealisierter Gewinn/Verlust) – deckt die volle Kaufhistorie ab, aber keine bereits realisierten Gewinne aus verkauften Positionen und kein Guthaben. Unten seit 0.23.0 (Issue #12, Modell überarbeitet in 1.1.0) Gesamtwert (Positionen + Guthaben des Verrechnungskontos) gegen Kapital, Gewinn/Verlust, Rendite und tagesverkettete Time-Weighted Return – bereinigt um externe Ein-/Auszahlungen (Käufe/Verkäufe über das Verrechnungskonto sind intern) und ausgehend vom Depotwert zum Trackingbeginn; gelten also für den Zeitraum seit Trackingbeginn, nicht für die gesamte Depothistorie (siehedocs/konzept.mdAbschnitt 13).
Alle JSON-Dateien sind die Quelle der Wahrheit und werden per
Grafana-HTTP-API
in die vorhandene Instanz importiert (POST /api/dashboards/db, overwrite: true – dieselbe
Datei erneut posten überschreibt die vorhandene Version). Eine MySQL/MariaDB-Datenquelle mit
uid comdirect-mariadb muss dort angelegt sein (Host/Port/DB/User/Passwort aus der .env);
dafür sind in dieser bestehenden Grafana-Instanz Admin-Rechte nötig – ein Service-Account mit
nur Editor-Rolle darf keine Datenquellen anlegen.
Beides (Datenquelle + alle vier Dashboards) automatisiert per
scripts/grafana-setup.sh statt manueller Ad-hoc-API-Calls –
nützlich bei Erstinbetriebnahme oder nach einem Grafana-Rebuild auf einem neuen Host:
GRAFANA_TOKEN=<Service-Account-Token mit Admin-Rolle> ./scripts/grafana-setup.sh
# GRAFANA_URL überschreibbar (Standard: http://localhost:3000)
# Nur die Datenquelle: ./scripts/grafana-setup.sh datasource
# Nur die Dashboards: ./scripts/grafana-setup.sh dashboardsIdempotent – ein erneuter Lauf aktualisiert eine bereits vorhandene Datenquelle/Dashboards
(gleiche uid), statt sie zu duplizieren. Datenbank-Verbindungsdaten für die Datenquelle liest
das Skript aus der lokalen .env.
Siehe docs/konzept.md Abschnitt 8 und CLAUDE.md: Anwendungsversion (SemVer, zentral in
ComdirectFetch.Domain.AppVersion) und Datenbank-Schema-Version (db/migrations/) werden
bei jeder relevanten Änderung automatisch angepasst.
GNU Affero General Public License v3.0 (or later) – Copyright (C) 2026 Thorsten Schröpel. Die AGPL verlangt (anders als z. B. die MIT/Apache-Lizenz) insbesondere: wer eine veränderte Version dieses Dienstes über ein Netzwerk zugänglich macht (nicht nur bei Weitergabe des Programms selbst), muss den Quellcode dieser Version ebenfalls unter der AGPL zur Verfügung stellen.