Wer nach dem Gehaltseingang plant, stellt unter Einstellungen den Tag ein, ab dem ein neuer Monat zählt. Der Zeitraum läuft dann vom Gehaltstag bis zum Vortag des nächsten und trägt den Namen des Monats, in dem er beginnt: Mit dem 25. umfasst „September 2026“ den 25.09. bis zum 24.10. Ein Starttag jenseits der Monatslänge rutscht auf den Monatsletzten, sodass 31 verlässlich den letzten Tag des Monats meint. Dashboard, Cashflow-Kalender, Budgets, Zwölf-Monats-Vorschau, die Kategorienauswertung, der Monatsexport und die Benachrichtigung über überschrittene Budgets rechnen mit diesem Zeitraum. Budgets bleiben je Monat gepflegt; der Bezeichner ist weiterhin der Monatserste, nur der Schnitt verschiebt sich. Bestandsinstallationen bleiben beim Ersten. Die Einstellung liegt in einer einzeiligen Tabelle hinter GET/PUT /api/settings; die Monatsauswertungen liefern zusätzlich period_start und period_end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fj7mB1PGA1aDHyfdSGHgzD
256 lines
11 KiB
Markdown
256 lines
11 KiB
Markdown
# moneyfy
|
||
|
||
Selbst gehostete Web-Anwendung zur Planung monatlicher Kosten und Einkünfte.
|
||
Single-User, deutschsprachige Oberfläche, ausgelegt für den Betrieb im Homelab
|
||
hinter einem Reverse Proxy.
|
||
|
||
> **Version 0.1.0.** Siehe [CHANGELOG.md](CHANGELOG.md).
|
||
|
||
## Screenshots
|
||
|
||
Die Bilder entstehen aus dem Demo-Datensatz (`make seed-demo`), damit sie ohne
|
||
echte Kontostände auskommen. Ablegen unter `docs/screenshots/`.
|
||
|
||
| | |
|
||
|---|---|
|
||
| <br>**Dashboard** – verfügbar nach Fixkosten, Kategorienring, Zwölf-Monats-Vorschau | <br>**Cashflow-Kalender** – Fälligkeiten je Tag mit Firmenlogo, darunter der Kontostandsverlauf |
|
||
| <br>**Wiederkehrend** – Abos, Verträge und Raten mit Klartext-Rhythmus | <br>**RRULE-Editor** – geführte Auswahl, Expertenmodus und Live-Vorschau der nächsten Termine |
|
||
| <br>**Firmen** – Kachelgrid mit Logo, Markenfarbe und Jahreskosten | <br>**Auswertungen** – laufende Kosten, Jahresvergleich und Export |
|
||
|
||
## Funktionsumfang
|
||
|
||
- Wiederkehrende Zahlungen und Einkünfte über vollständige RFC-5545-RRULEs,
|
||
inklusive Werktagsverschiebung nach NRW-Feiertagen.
|
||
- Preishistorie je Vertrag, sodass vergangene Monate betragstreu bleiben.
|
||
- Verträge mit Mindestlaufzeit, Kündigungsfrist und automatischer Verlängerung.
|
||
- Ratenzahlungen mit Restschuld- und Restratenberechnung.
|
||
- Rücklagenbildung für nicht-monatliche Posten.
|
||
- Firmenlogos und Markenfarben, lokal zwischengespeichert (siehe unten).
|
||
- Einstellbarer Monatsbeginn: Der Monat startet wahlweise am Ersten oder am
|
||
Gehaltstag; alle Monatsansichten folgen diesem Zeitraum (siehe unten).
|
||
- Auswertungen: Monatsübersicht, Cashflow-Kalender, 12-Monats-Forecast,
|
||
Kategorien, Abo-Übersicht, Jahresvergleich, Budgets, Sparziele.
|
||
- Benachrichtigungen per SMTP und Apprise.
|
||
|
||
## Technischer Stack
|
||
|
||
| Bereich | Technologie |
|
||
|---|---|
|
||
| Backend | Python 3.12, FastAPI, SQLAlchemy 2 (async), Alembic, Pydantic v2 |
|
||
| Datenbank | PostgreSQL 17 |
|
||
| Scheduler | APScheduler (in-process) |
|
||
| Frontend | React 18, TypeScript, Vite, TailwindCSS |
|
||
| Charts | Recharts |
|
||
| Tests | pytest + httpx, vitest + testing-library |
|
||
|
||
## Schnellstart (Betrieb)
|
||
|
||
Voraussetzungen: Docker mit Compose-Plugin und ein externes Netz `proxy`.
|
||
|
||
```bash
|
||
mkdir -p /opt/moneyfy && cd /opt/moneyfy
|
||
curl -fsSLO https://git.menzel.center/menzeljonas/moneyfy/raw/branch/main/docker-compose.yml
|
||
curl -fsSLO https://git.menzel.center/menzeljonas/moneyfy/raw/branch/main/.env.example
|
||
cp .env.example .env
|
||
|
||
# Mindestens setzen: POSTGRES_PASSWORD, SECRET_KEY, MONEYFY_ADMIN_PASSWORD
|
||
$EDITOR .env
|
||
|
||
docker login git.menzel.center
|
||
docker compose up -d
|
||
```
|
||
|
||
Das Backend wartet auf die Datenbank, spielt die Migrationen ein und legt beim
|
||
allerersten Start den Kategoriebaum an. Danach läuft die Oberfläche auf
|
||
`http://127.0.0.1:8087`. Die vollständige Einrichtung samt Reverse Proxy und
|
||
Sicherung steht in [docs/deployment.md](docs/deployment.md).
|
||
|
||
## Schnellstart (lokale Entwicklung)
|
||
|
||
Voraussetzungen: Python 3.12, [uv](https://docs.astral.sh/uv/), Node 20+,
|
||
eine erreichbare PostgreSQL-17-Instanz.
|
||
|
||
```bash
|
||
git clone https://git.menzel.center/menzeljonas/moneyfy.git
|
||
cd moneyfy
|
||
cp .env.example backend/.env # DATABASE_URL und SECRET_KEY anpassen
|
||
|
||
make install # Backend-Abhängigkeiten in backend/.venv
|
||
make migrate # Schema anlegen
|
||
make seed # Kategoriebaum und Standardregeln
|
||
make dev # http://localhost:8000/api/docs
|
||
```
|
||
|
||
In einem zweiten Terminal das Frontend, es leitet `/api` an das Backend weiter:
|
||
|
||
```bash
|
||
make fe-install
|
||
make fe-dev # http://localhost:5173
|
||
```
|
||
|
||
Für einen gefüllten Stand:
|
||
|
||
```bash
|
||
make seed-demo # realistischer Beispielhaushalt, Termine relativ zu heute
|
||
make seed-demo-reset # setzt Konten, Firmen, Posten und Buchungen vorher zurück
|
||
```
|
||
|
||
Der Demo-Seed legt drei Konten an, zehn Firmen samt Logos, zwölf wiederkehrende
|
||
Posten (Miete, Strom, Netflix, Spotify, jährliche Kfz-Versicherung, Handyvertrag
|
||
mit Mindestlaufzeit, Kredit über 36 Raten, Gehalt), bestätigt die vergangenen
|
||
Fälligkeiten, ergänzt Buchungen, Budgets und Sparziele.
|
||
|
||
Nützliche Ziele: `make check` (alle Prüfungen), `make test`, `make lint`,
|
||
`make format`, `make fe-lint`, `make fe-test`, `make fe-build`.
|
||
|
||
## Bedienung
|
||
|
||
| Kürzel | Wirkung |
|
||
|---|---|
|
||
| `n` | Neue Buchung |
|
||
| `r` | Neuer wiederkehrender Posten |
|
||
| `/` | Suchfeld der aktuellen Seite |
|
||
| `?` | Übersicht der Kürzel |
|
||
| `Esc` | Dialog schließen |
|
||
|
||
Kürzel greifen nur, solange kein Eingabefeld den Fokus hat.
|
||
|
||
### Monatsbeginn
|
||
|
||
Standardmäßig ist ein Monat der Kalendermonat. Wer nach dem Gehaltseingang
|
||
plant, stellt unter *Einstellungen › Monatsbeginn* den Tag ein, ab dem ein neuer
|
||
Monat zählt. Der Zeitraum läuft dann vom Gehaltstag bis zum Vortag des nächsten
|
||
und heißt nach dem Monat, in dem er beginnt: Mit dem 25. als Beginn umfasst
|
||
„September 2026“ die Tage vom 25.09. bis zum 24.10.
|
||
|
||
Ein Starttag jenseits der Monatslänge rutscht auf den Monatsletzten – die 31
|
||
bedeutet also verlässlich „letzter Tag des Monats“, auch im Februar.
|
||
|
||
Dashboard, Cashflow-Kalender, Budgets, Zwölf-Monats-Vorschau, die
|
||
Kategorienauswertung, der Monatsexport und die Benachrichtigung über
|
||
überschrittene Budgets rechnen mit diesem Zeitraum. Budgets bleiben dabei je
|
||
Monat gepflegt; der Bezeichner ist weiterhin der Monatserste, nur der Schnitt
|
||
verschiebt sich. Die Einstellung gilt für die gesamte Installation und wirkt
|
||
sofort, ohne die Daten anzufassen.
|
||
|
||
## Barrierefreiheit
|
||
|
||
Die Farbtoken sind gegen die WCAG-Kontraste geprüft – in beiden Modi erreicht
|
||
jeder Text mindestens 4,5:1, jedes Bedienelement 3:1. Zwei Werte weichen
|
||
deshalb bewusst von den naheliegenden Tailwind-Stufen ab: `slate-400` als
|
||
Hilfstext käme auf hellem Grund nur auf 2,56:1, und Weiß auf `green-600` nur
|
||
auf 3,30:1.
|
||
|
||
Die Serienfarben der Diagramme sind zusätzlich auf Farbfehlsichtigkeit geprüft
|
||
(siehe [Diagramme](#diagramme)). Jeder Zustand – Budget-Ampel, Versandstatus,
|
||
Vertragswarnung – trägt neben der Farbe ein Symbol und einen Wortlaut.
|
||
|
||
## Konfiguration
|
||
|
||
Alle Einstellungen kommen aus Umgebungsvariablen; `.env.example` enthält die
|
||
vollständige, kommentierte Liste. Die wichtigsten:
|
||
|
||
| Variable | Standard | Bedeutung |
|
||
|---|---|---|
|
||
| `DATABASE_URL` | – | PostgreSQL-DSN, `postgresql://` wird auf asyncpg umgestellt |
|
||
| `SECRET_KEY` | – | Signaturschlüssel für JWTs, mind. 32 Zeichen (`openssl rand -hex 32`) |
|
||
| `MONEYFY_ADMIN_USER` / `MONEYFY_ADMIN_PASSWORD` | `admin` / – | Beim Erststart angelegter Benutzer. Es gibt **kein** eingebautes Standardpasswort; ohne diesen Wert wird niemand angelegt. Die Anlage greift nur, solange die Benutzertabelle leer ist – danach hilft `app.scripts.reset_password`. |
|
||
| `TIMEZONE` | `Europe/Berlin` | Zeitzone der gesamten Anwendung |
|
||
| `HOLIDAY_REGION` | `DE-NW` | Feiertagsregion für Werktagsverschiebungen |
|
||
| `LOGO_STORAGE_DIR` | `/data/logos` | Verzeichnis des Logo-Caches |
|
||
| `LOGODEV_API_KEY` / `BRANDFETCH_API_KEY` | leer | Optionale Logo-Provider, ohne Schlüssel übersprungen |
|
||
| `SMTP_*` | leer | Mailversand für Benachrichtigungen |
|
||
| `APPRISE_URLS` | leer | Komma-separierte Apprise-Ziele |
|
||
| `COOKIE_SECURE` | `true` | Hinter HTTPS `true`, für lokales HTTP `false` |
|
||
| `SCHEDULER_ENABLED` | `true` | Täglicher Benachrichtigungslauf um 07:00 |
|
||
|
||
## Benachrichtigungen
|
||
|
||
Ein Job läuft täglich um 07:00 Uhr (`Europe/Berlin`) im Anwendungsprozess – ohne
|
||
Redis, ohne Celery. Er prüft vier Anlässe: bald fällige Posten, Kündigungsfristen
|
||
30, 14 und 7 Tage vor dem Termin, überschrittene Budgets und Vertragsverlängerungen
|
||
im kommenden Monat.
|
||
|
||
Der Duplikatsschutz hängt am **Zieltag des Ereignisses**, nicht am Versandtag:
|
||
Eine Fälligkeit am 15.03. wird genau einmal gemeldet, unabhängig davon, an
|
||
welchem Tag des Vorlaufs der Job läuft. Ein fehlgeschlagener Versand gilt als
|
||
offen und wird beim nächsten Lauf erneut versucht.
|
||
|
||
Als Kanäle stehen SMTP (HTML-Mail mit eingebetteten Firmenlogos) und Apprise zur
|
||
Verfügung. Über *Einstellungen → Benachrichtigungen* lassen sich Regeln pflegen,
|
||
eine Testnachricht verschicken und das Versandprotokoll einsehen.
|
||
|
||
## Diagramme
|
||
|
||
Die Serienfarben stammen aus einer Palette, die gegen die hellen und dunklen
|
||
Flächen der Anwendung auf Kontrast und Farbfehlsichtigkeit geprüft wurde; ihre
|
||
Reihenfolge ist Teil dieser Absicherung. Grün und Rot bleiben den Vorzeichen im
|
||
Text vorbehalten – als Serienpaar wären sie bei Deuteranopie nicht zu
|
||
unterscheiden. Jedes Diagramm mit mehr als einer Serie führt eine Legende, und
|
||
zu den Balkendiagrammen lässt sich die Wertetabelle aufklappen.
|
||
|
||
Einnahmen, Ausgaben und der kumulierte Kontostand stehen bewusst in zwei
|
||
getrennten Diagrammen untereinander: eine gemeinsame Größenachse würde bei so
|
||
verschiedenen Größenordnungen einen Zusammenhang vortäuschen.
|
||
|
||
## Logos und Markenfarben
|
||
|
||
Jede Firma bekommt ein Bild. Die Provider-Kette arbeitet der Reihe nach:
|
||
|
||
1. **simple-icons** – rund 3.500 Marken, im Repository unter
|
||
`backend/app/assets/simple_icons.json.gz` mitgeliefert. Kein Schlüssel, kein
|
||
Netzzugriff. Neu erzeugen mit `make vendor-icons`.
|
||
2. **logo.dev** – nur mit `LOGODEV_API_KEY`.
|
||
3. **Brandfetch** – nur mit `BRANDFETCH_API_KEY` und hinterlegter Domain.
|
||
4. **Favicon** – über den Google-Dienst, wenn eine Domain bekannt oder aus dem
|
||
Namen ableitbar ist.
|
||
5. **Generierter Avatar** – Buchstaben-Monogramm mit fester Farbe aus dem
|
||
Namens-Hash. Schlägt nie fehl.
|
||
|
||
Findet die Kette einen eindeutigen Treffer im lokalen Katalog, unterbleiben die
|
||
Anfragen nach außen vollständig – die Firmenliste verlässt den Server nicht. Im
|
||
Auswahldialog (`POST /api/merchants/{id}/logo/search`) läuft dagegen bewusst die
|
||
ganze Kette, damit Alternativen zur Wahl stehen.
|
||
|
||
Ausgewählte Logos landen unter `LOGO_STORAGE_DIR`, benannt nach ihrem
|
||
SHA-256-Hash, und werden nur über `GET /api/logos/{id}` ausgeliefert. Beim
|
||
Seitenaufruf entsteht dadurch kein Zugriff auf fremde Dienste. Ein Upload oder
|
||
eine bewusste Auswahl setzt den Status auf `manual` und wird von der Automatik
|
||
nie überschrieben.
|
||
|
||
## Projektstruktur
|
||
|
||
```
|
||
backend/ FastAPI-Anwendung, Modelle, Services, Alembic-Migrationen, Tests
|
||
frontend/ React-Oberfläche (Vite)
|
||
docs/ Betriebs- und Runner-Dokumentation
|
||
.gitea/ Gitea-Actions-Workflows
|
||
```
|
||
|
||
Alternativ lässt sich alles aus dem Quellcode bauen:
|
||
|
||
```bash
|
||
cp docker-compose.override.yml.example docker-compose.override.yml
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Die Override-Datei baut beide Images lokal, bindet den Quellcode ein und öffnet
|
||
die Ports für Backend (8000) und Vite (5173).
|
||
|
||
## Bauen und Ausrollen
|
||
|
||
Zwei Gitea-Workflows unter `.gitea/workflows/`:
|
||
|
||
- **`ci.yml`** läuft bei jedem Push und Pull Request: `ruff`, `pytest`,
|
||
`tsc --noEmit`, `eslint`, `vitest` und der Bundle-Bau.
|
||
- **`build.yml`** läuft auf `main` und bei Tags `v*` und lädt beide Images für
|
||
`linux/amd64` in die Gitea-Registry. Push auf `main` erzeugt `:main` und
|
||
`:sha-<kurz>`, ein Tag `v1.2.3` zusätzlich `:1.2.3`, `:1.2` und `:latest`.
|
||
|
||
Ausgerollt wird **nicht** automatisch. Die Einrichtung des Runners beschreibt
|
||
[docs/runner.md](docs/runner.md), das Ausrollen [docs/deployment.md](docs/deployment.md).
|
||
|
||
## Lizenz
|
||
|
||
[MIT](LICENSE)
|