feat: Phase 2 – Budget-Engine + Direktzugriff (http://IP:Port)

Budget:
- Modelle + Migration 0003: budget_template_lines, planned_income_lines,
  budget_periods, budget_period_lines.
- services/budget: Perioden-Grenzen (einstellbarer Starttag, Label nach Endmonat),
  Standard-Vorlage vs. Monats-Override (Snapshot-on-Customize), Soll/Ist inkl.
  budgetierter Sparen-Transfers, geplante Einnahmen (Soll/Ist).
- routers/budgets: Vorlage, geplante Einnahmen (CRUD), Monats-Soll/Ist,
  Monats-Override, Zurücksetzen. pytest: Perioden, Soll/Ist, Override/Reset, Einnahmen.
- Frontend: Budget-Seite (Monat + Standard-Budget), Fortschrittsbalken grün/rot,
  Monats-Navigation, editierbare Soll-Beträge; Sparen-Option im Transfer-Formular.

Deployment-Fix (Nutzer-Anforderung: http://IP:Port muss funktionieren):
- Dienst bindet an APP_BIND_HOST=0.0.0.0 (direkt im LAN erreichbar).
- APP_COOKIE_SECURE=false als Standard (sonst kein Login über reines HTTP);
  für reinen HTTPS-Betrieb umstellbar. Doku + install.sh entsprechend angepasst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-19 19:16:27 +02:00
co-authored by Claude Opus 4.8
parent d692c04cc1
commit 8ed4bf5254
21 changed files with 1352 additions and 22 deletions
+14 -4
View File
@@ -15,9 +15,17 @@ erzeugt `/etc/certain-finance/env` (mit Zufallsgeheimnissen), legt den systemd-
Dienst `certain-finance` an und startet ihn. Node wird **nur zum Bauen** gebraucht;
der laufende Dienst ist reines Python.
## Nginx Proxy Manager
## Direktzugriff (http://IP:Port)
Neuen **Proxy Host** anlegen:
Standardmäßig lauscht der Dienst auf `0.0.0.0:<APP_PORT>` (Standard-Port `8071`) und
ist damit direkt im LAN erreichbar: `http://<LXC-IP>:8071`. Dafür ist
`APP_COOKIE_SECURE=false` gesetzt (sonst speichert der Browser die Sitzung über
reines HTTP nicht). Möchtest du den Zugriff auf einen Reverse-Proxy **auf demselben
Host** beschränken, setze `APP_BIND_HOST=127.0.0.1` in `/etc/certain-finance/env`.
## Optional: HTTPS über Nginx Proxy Manager
Wenn du von außen per HTTPS zugreifen willst, einen **Proxy Host** anlegen:
- **Domain Names:** deine (Sub-)Domain
- **Scheme:** `http`
@@ -30,8 +38,10 @@ Neuen **Proxy Host** anlegen:
Für spätere CSV-/PDF-Uploads in den *Advanced*-Einstellungen des Hosts
`client_max_body_size 20m;` setzen.
Da HTTPS extern terminiert, sind die Sitzungs-Cookies `Secure`
(`APP_COOKIE_SECURE=true`). Der Dienst selbst lauscht nur auf `127.0.0.1`.
Bei ausschließlichem HTTPS-Zugriff kannst du `APP_COOKIE_SECURE=true` in
`/etc/certain-finance/env` setzen (danach `systemctl restart certain-finance`) –
dann werden die Sitzungs-Cookies nur über HTTPS gesendet. Für gemischten Zugriff
(HTTP im LAN **und** HTTPS von außen) belasse es bei `false`.
## Betrieb
+4 -3
View File
@@ -14,9 +14,10 @@ User=certain-finance
Group=certain-finance
WorkingDirectory=/opt/certain-finance/backend
EnvironmentFile=/etc/certain-finance/env
# Der Port kommt aus der EnvironmentFile (APP_PORT). Nur an 127.0.0.1 gebunden –
# von außen ist der Dienst ausschließlich über den Nginx Proxy Manager erreichbar.
ExecStart=/opt/certain-finance/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port ${APP_PORT}
# Adresse/Port kommen aus der EnvironmentFile. APP_BIND_HOST=0.0.0.0 macht den
# Dienst direkt über http://<LXC-IP>:<APP_PORT> erreichbar; 127.0.0.1 beschränkt
# ihn auf einen Reverse-Proxy auf demselben Host.
ExecStart=/opt/certain-finance/backend/.venv/bin/uvicorn app.main:app --host ${APP_BIND_HOST} --port ${APP_PORT}
Restart=on-failure
RestartSec=3
+10 -4
View File
@@ -5,15 +5,21 @@
# Ablageort für SQLite-Datenbank und Backups (muss dem Dienst-Benutzer gehören).
APP_DATA_DIR=/var/lib/certain-finance
# Interner Port, auf dem der Dienst lauscht. Der Nginx Proxy Manager leitet
# von außen (HTTPS) hierher weiter. Nur an 127.0.0.1 gebunden (siehe Unit).
# Adresse + Port, auf denen der Dienst lauscht.
# APP_BIND_HOST=0.0.0.0 macht die Web-UI direkt über http://<LXC-IP>:<APP_PORT>
# im LAN erreichbar. Willst du NUR über einen Reverse-Proxy auf DEMSELBEN Host
# zugreifen, setze 127.0.0.1.
APP_BIND_HOST=0.0.0.0
APP_PORT=8071
# Signiert/HMACt Sitzungen. UNBEDINGT zufällig setzen (install.sh macht das).
APP_SESSION_SECRET=change-me-please
# Cookies nur über HTTPS senden. Hinter dem Nginx Proxy Manager: true.
APP_COOKIE_SECURE=true
# Cookies nur über HTTPS senden. Bei direktem HTTP-Zugriff (http://IP:Port) MUSS
# das false sein, sonst speichert der Browser die Sitzung nicht (Login klappt dann
# scheinbar, springt aber sofort zurück). Nur wenn du ausschließlich über HTTPS
# zugreifst (z. B. Nginx Proxy Manager mit erzwungenem SSL), kannst du true setzen.
APP_COOKIE_SECURE=false
# Erster Admin – wird NUR beim allerersten Start angelegt (wenn noch kein Nutzer da ist).
APP_ADMIN_USERNAME=admin
+13 -8
View File
@@ -95,6 +95,8 @@ if [[ -f "$ENV_FILE" ]]; then
else
echo "APP_GIT_SHA=$GIT_SHA" >> "$ENV_FILE"
fi
# Direktzugriff (http://IP:Port) für ältere Konfigurationen nachrüsten.
grep -q '^APP_BIND_HOST=' "$ENV_FILE" || echo "APP_BIND_HOST=0.0.0.0" >> "$ENV_FILE"
ADMIN_USER="$(grep -E '^APP_ADMIN_USERNAME=' "$ENV_FILE" | cut -d= -f2-)"
GENERATED_PASS=""
else
@@ -110,9 +112,10 @@ else
info "Erzeuge Konfiguration: $ENV_FILE"
cat > "$ENV_FILE" <<EOF
APP_DATA_DIR=$DATA_DIR
APP_BIND_HOST=0.0.0.0
APP_PORT=$local_port
APP_SESSION_SECRET=$(rand 48)
APP_COOKIE_SECURE=true
APP_COOKIE_SECURE=false
APP_ADMIN_USERNAME=$admin_user
APP_ADMIN_PASSWORD=$admin_pass
APP_DEFAULT_BASE_CURRENCY=EUR
@@ -139,7 +142,7 @@ User=$SERVICE_USER
Group=$SERVICE_USER
WorkingDirectory=$REPO_DIR/backend
EnvironmentFile=$ENV_FILE
ExecStart=$REPO_DIR/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port \${APP_PORT}
ExecStart=$REPO_DIR/backend/.venv/bin/uvicorn app.main:app --host \${APP_BIND_HOST} --port \${APP_PORT}
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
@@ -167,24 +170,26 @@ for _ in $(seq 1 30); do
sleep 2
done
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
echo
if [[ "$ok" -eq 1 ]]; then
info "Fertig – Certain Finance läuft. 🎉"
else
warn "Backend antwortet noch nicht. Logs: journalctl -u $APP_SLUG -e"
fi
echo " Interner Dienst: http://127.0.0.1:$PORT (nur lokal)"
echo " Web-UI (direkt): http://${LAN_IP:-<LXC-IP>}:$PORT"
echo " Admin-Login: ${ADMIN_USER:-admin}"
if [[ -n "${GENERATED_PASS:-}" ]]; then
echo " Admin-Passwort: ${GENERATED_PASS}"
warn "Bitte dieses Passwort notieren – es wird nicht erneut angezeigt."
fi
echo
echo " Nginx Proxy Manager: neuen Proxy-Host anlegen ->"
echo " Forward Hostname/IP: <IP-dieses-LXC>"
echo " Forward Port: $PORT"
echo " SSL: Let's Encrypt aktivieren; 'Block Common Exploits' ok."
echo " (Websockets nicht nötig. Für spätere Uploads client_max_body_size >= 20M.)"
echo " Optional über HTTPS (Nginx Proxy Manager): neuen Proxy-Host ->"
echo " Forward Hostname/IP: ${LAN_IP:-<LXC-IP>}"
echo " Forward Port: $PORT (Scheme: http)"
echo " SSL: Let's Encrypt aktivieren. Dann in $ENV_FILE APP_COOKIE_SECURE=true"
echo " setzen und 'systemctl restart $APP_SLUG'. (Uploads: client_max_body_size >= 20M.)"
echo
echo " Logs: journalctl -u $APP_SLUG -f"
echo " Neustart: systemctl restart $APP_SLUG"