feat: SQL-Views und Grafana-Provisionierung

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 19:18:20 +02:00
parent df6615c8f7
commit 43e8e8e8f9
5 changed files with 233 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
import hmac
from contextlib import asynccontextmanager
from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
@@ -6,11 +7,20 @@ from fastapi.staticfiles import StaticFiles
from app import auth
from app.config import get_settings
from app.db import get_engine
from app.models.views import create_views
from app.routers import (accounts, categories, gui, imports, planning,
scenarios, transactions)
from app.routers.gui import templates
app = FastAPI(title="Finanzberatungs-Tool")
@asynccontextmanager
async def lifespan(app: FastAPI):
create_views(get_engine())
yield
app = FastAPI(title="Finanzberatungs-Tool", lifespan=lifespan)
app.mount("/static", StaticFiles(directory="app/static"), name="static")

View File

@@ -0,0 +1,33 @@
from sqlalchemy import text
from sqlalchemy.engine import Engine
VIEWS: dict[str, str] = {
"v_balance_history": """
SELECT t.booking_date AS day, a.name AS account,
SUM(SUM(t.amount)) OVER (PARTITION BY a.id
ORDER BY t.booking_date) AS balance
FROM transactions t JOIN accounts a ON a.id = t.account_id
WHERE t.status = 'confirmed'
GROUP BY a.id, a.name, t.booking_date""",
"v_balance_total": """
SELECT booking_date AS day,
SUM(SUM(amount)) OVER (ORDER BY booking_date) AS balance
FROM transactions WHERE status = 'confirmed' GROUP BY booking_date""",
"v_monthly_by_category": """
SELECT date_trunc('month', t.booking_date) AS month,
COALESCE(c.name, 'unkategorisiert') AS category,
SUM(t.amount) AS total
FROM transactions t LEFT JOIN categories c ON c.id = t.category_id
WHERE t.status = 'confirmed' GROUP BY 1, 2""",
"v_projection": """
SELECT s.name AS scenario, p.day, p.balance
FROM projection_points p JOIN scenarios s ON s.id = p.scenario_id""",
}
def create_views(engine: Engine) -> None:
if engine.dialect.name != "postgresql":
return
with engine.begin() as conn:
for name, body in VIEWS.items():
conn.execute(text(f"CREATE OR REPLACE VIEW {name} AS {body}"))