Files
certain/backend/app/main.py
T
GuidoandClaude Opus 4.8 8ed4bf5254 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>
2026-08-19 19:16:27 +02:00

115 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""FastAPI-Anwendung.
Ein einzelner Prozess liefert sowohl die JSON-API unter ``/api/*`` als auch das
gebaute Frontend (``web/dist``) aus – hinter dem Nginx Proxy Manager des Nutzers.
Beim Start werden die Alembic-Migrationen angewandt und der erste Admin angelegt.
"""
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import APIRouter, FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from .branding import APP_NAME, VERSION, git_sha
from .config import get_settings
from .database import SessionLocal
from .migrate import upgrade_to_head
from .routers import (
accounts,
auth,
budgets,
categories,
rates,
settings as settings_router,
transactions,
users,
)
from .seed import ensure_first_admin
from .services import ecb_rates
settings = get_settings()
# Pfad zum gebauten Frontend (im Dev von Vite selbst ausgeliefert -> fehlt hier).
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
DIST_DIR = REPO_ROOT / "web" / "dist"
@asynccontextmanager
async def lifespan(app: FastAPI):
# Schema auf den aktuellen Stand bringen (idempotent).
upgrade_to_head()
db = SessionLocal()
try:
ensure_first_admin(db)
# Best-effort: aktuelle EZB-Kurse nachladen (ohne Internet läuft es weiter).
ecb_rates.ensure_rates_on_startup(db, settings.ecb_fetch_on_start)
finally:
db.close()
yield
app = FastAPI(title=APP_NAME, version=VERSION, lifespan=lifespan)
# CORS nur, wenn ausdrücklich konfiguriert (Standard: gleiche Herkunft, aus).
if settings.cors_origins.strip():
origins = (
["*"]
if settings.cors_origins.strip() == "*"
else [o.strip() for o in settings.cors_origins.split(",") if o.strip()]
)
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- API -------------------------------------------------------------------
api = APIRouter(prefix="/api")
@api.get("/health", tags=["meta"])
def health() -> dict:
return {"status": "ok"}
@api.get("/version", tags=["meta"])
def version() -> dict:
return {"name": APP_NAME, "version": VERSION, "git_sha": git_sha()}
api.include_router(auth.router)
api.include_router(users.router)
api.include_router(settings_router.router)
api.include_router(accounts.router)
api.include_router(categories.router)
api.include_router(transactions.router)
api.include_router(budgets.router)
api.include_router(rates.router)
app.include_router(api)
# --- Frontend (SPA) --------------------------------------------------------
# Gebaute, gehashte Assets unter /assets aggressiv cachebar ausliefern; alle
# übrigen (nicht-/api) Pfade auf index.html abbilden, damit Client-seitige
# Routen wie /accounts direkt aufrufbar sind (verlinkbar).
if DIST_DIR.exists():
assets_dir = DIST_DIR / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
@app.get("/{full_path:path}", include_in_schema=False)
async def spa(full_path: str):
# /api-Pfade, die keine echte Route trafen, sind echte 404 (kein SPA).
if full_path.startswith("api"):
raise HTTPException(status_code=404, detail="Not Found")
candidate = DIST_DIR / full_path
if full_path and candidate.is_file():
return FileResponse(candidate)
return FileResponse(DIST_DIR / "index.html", headers={"Cache-Control": "no-cache"})