diff --git a/finance/app/main.py b/finance/app/main.py index 9f46b7d..af484f7 100644 --- a/finance/app/main.py +++ b/finance/app/main.py @@ -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") diff --git a/finance/app/models/views.py b/finance/app/models/views.py new file mode 100644 index 0000000..bff99e9 --- /dev/null +++ b/finance/app/models/views.py @@ -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}")) diff --git a/finance/grafana/dashboards/finanzen.json b/finance/grafana/dashboards/finanzen.json new file mode 100644 index 0000000..6ccc136 --- /dev/null +++ b/finance/grafana/dashboards/finanzen.json @@ -0,0 +1,171 @@ +{ + "uid": "finanzen", + "title": "Finanzen", + "schemaVersion": 39, + "version": 1, + "editable": true, + "timezone": "browser", + "tags": ["finanzen"], + "time": { + "from": "now-1y", + "to": "now" + }, + "refresh": "", + "panels": [ + { + "id": 1, + "type": "timeseries", + "title": "Kontostand-Verlauf", + "datasource": { + "type": "postgres", + "uid": "FinanzDB" + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineWidth": 1, + "fillOpacity": 0 + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "postgres", + "uid": "FinanzDB" + }, + "format": "time_series", + "rawSql": "SELECT day AS time, account AS metric, balance AS value FROM v_balance_history ORDER BY day", + "rawQuery": true + } + ] + }, + { + "id": 2, + "type": "timeseries", + "title": "Gesamt-Saldo", + "datasource": { + "type": "postgres", + "uid": "FinanzDB" + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 10 + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "single" } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "postgres", + "uid": "FinanzDB" + }, + "format": "time_series", + "rawSql": "SELECT day AS time, balance AS value FROM v_balance_total ORDER BY day", + "rawQuery": true + } + ] + }, + { + "id": 3, + "type": "barchart", + "title": "Monat nach Kategorie", + "datasource": { + "type": "postgres", + "uid": "FinanzDB" + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "fieldConfig": { + "defaults": { + "custom": { + "stacking": { "mode": "normal", "group": "A" } + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "stacking": "normal", + "xTickLabelRotation": 0 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "postgres", + "uid": "FinanzDB" + }, + "format": "time_series", + "rawSql": "SELECT month AS time, category AS metric, total AS value FROM v_monthly_by_category ORDER BY month", + "rawQuery": true + } + ] + }, + { + "id": 4, + "type": "timeseries", + "title": "Szenario-Vergleich", + "datasource": { + "type": "postgres", + "uid": "FinanzDB" + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineWidth": 1, + "fillOpacity": 0, + "thresholdsStyle": { "mode": "line" } + }, + "unit": "currencyEUR", + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": 0 }, + { "color": "orange", "value": 500 } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "postgres", + "uid": "FinanzDB" + }, + "format": "time_series", + "rawSql": "SELECT day AS time, scenario AS metric, balance AS value FROM v_projection ORDER BY day", + "rawQuery": true + } + ] + } + ] +} diff --git a/finance/grafana/provisioning/dashboards/provider.yaml b/finance/grafana/provisioning/dashboards/provider.yaml new file mode 100644 index 0000000..d207c7b --- /dev/null +++ b/finance/grafana/provisioning/dashboards/provider.yaml @@ -0,0 +1,6 @@ +apiVersion: 1 +providers: + - name: finanzen + type: file + options: + path: /var/lib/grafana/dashboards diff --git a/finance/grafana/provisioning/datasources/postgres.yaml b/finance/grafana/provisioning/datasources/postgres.yaml new file mode 100644 index 0000000..91012c0 --- /dev/null +++ b/finance/grafana/provisioning/datasources/postgres.yaml @@ -0,0 +1,12 @@ +apiVersion: 1 +datasources: + - name: FinanzDB + type: postgres + access: proxy + url: localhost:5432 + user: finance_read + jsonData: + database: finance + sslmode: disable + secureJsonData: + password: ${FINANCE_READ_PASSWORD}