#!/usr/bin/env python3 import logging import os import sys import time import uuid from typing import Any, Dict import psycopg from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from psycopg.rows import dict_row DB_URL = os.environ.get("DATABASE_URL") if not DB_URL: print("ERR: DATABASE_URL not set", file=sys.stderr) sys.exit(1) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates")) app = FastAPI(title="Money Agents Dashboard") # Simple JSON logger with request IDs logger = logging.getLogger("money_agents.dashboard") handler = logging.StreamHandler() handler.setFormatter(logging.Formatter("%(message)s")) logger.setLevel(logging.INFO) logger.addHandler(handler) @app.middleware("http") async def add_request_id_logging(request: Request, call_next): rid = request.headers.get("X-Request-ID", str(uuid.uuid4())) start = time.time() try: response = await call_next(request) return response finally: dur_ms = int((time.time() - start) * 1000) record = { "event": "http_request", "method": request.method, "path": request.url.path, "status": getattr(response, "status_code", 0), "duration_ms": dur_ms, "request_id": rid, } logger.info(__import__("json").dumps(record)) def db_fetch(sql: str, args: tuple = ()): with psycopg.connect(DB_URL) as conn: with conn.cursor(row_factory=dict_row) as cur: cur.execute(sql, args) return list(cur.fetchall()) @app.get("/", response_class=HTMLResponse) def index(request: Request): return templates.TemplateResponse( "index.html", { "request": request, }, ) @app.get("/healthz") def healthz(): return JSONResponse({"status": "ok"}) @app.get("/readyz") def readyz(): try: db_fetch("SELECT 1") return JSONResponse({"status": "ready"}) except Exception as e: return JSONResponse({"status": "not_ready", "error": str(e)}, status_code=503) @app.get("/panel/overview", response_class=HTMLResponse) def panel_overview(request: Request): revenue = db_fetch("SELECT revenue FROM revenue_v") revenue_val = revenue[0]["revenue"] if revenue else 0 active_workers = db_fetch(""" SELECT COUNT(*) AS n FROM ( SELECT worker_id, MAX(ts) AS last FROM logs WHERE ts > NOW() - interval '30 seconds' AND worker_id IS NOT NULL GROUP BY worker_id ) t """) active = active_workers[0]["n"] if active_workers else 0 jobs_pending = db_fetch("SELECT COUNT(*) AS n FROM jobs WHERE status='pending'") pending = jobs_pending[0]["n"] if jobs_pending else 0 return templates.TemplateResponse( "panel_overview.html", {"request": request, "revenue": revenue_val, "active": active, "pending": pending}, ) @app.get("/panel/jobs", response_class=HTMLResponse) def panel_jobs(request: Request): rows = db_fetch( "SELECT id, status, payload, started_at, finished_at FROM jobs ORDER BY id DESC LIMIT 100" ) return templates.TemplateResponse("panel_jobs.html", {"request": request, "rows": rows}) @app.get("/panel/workers", response_class=HTMLResponse) def panel_workers(request: Request): rows = db_fetch( """ SELECT worker_id, MAX(ts) AS last_heartbeat, SUM(CASE WHEN message='completed' THEN 1 ELSE 0 END) AS jobs_completed FROM logs WHERE worker_id IS NOT NULL GROUP BY worker_id ORDER BY last_heartbeat DESC LIMIT 100 """ ) return templates.TemplateResponse("panel_workers.html", {"request": request, "rows": rows}) @app.get("/panel/transactions", response_class=HTMLResponse) def panel_transactions(request: Request): rows = db_fetch( "SELECT id, job_id, amount, service, status, ts FROM transactions ORDER BY ts DESC LIMIT 100" ) return templates.TemplateResponse("panel_transactions.html", {"request": request, "rows": rows}) @app.get("/panel/logs", response_class=HTMLResponse) def panel_logs(request: Request, q: str = ""): if q: rows = db_fetch( "SELECT id, worker_id, job_id, level, message, ts FROM logs WHERE message ILIKE %s OR level ILIKE %s ORDER BY ts DESC LIMIT 100", (f"%{q}%", f"%{q}%"), ) else: rows = db_fetch( "SELECT id, worker_id, job_id, level, message, ts FROM logs ORDER BY ts DESC LIMIT 100" ) return templates.TemplateResponse("panel_logs.html", {"request": request, "rows": rows, "q": q})