diff --git a/dashboard_api_rev.py b/dashboard_api_rev.py index ebf9262..c9d5d57 100644 --- a/dashboard_api_rev.py +++ b/dashboard_api_rev.py @@ -43,7 +43,7 @@ from urllib.parse import parse_qs import psycopg2 import psycopg2.extras -from fastapi import FastAPI, Request +from fastapi import FastAPI, Query, Request from fastapi.encoders import jsonable_encoder from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse @@ -304,6 +304,70 @@ def tickets( ) +# ── INC operations dashboard ────────────────────────────────────────────────── +# Thin passthrough over reporting.fn_inc_dashboard (fleettickets migration 09): +# returns { window, open: GeoJSON, closed: GeoJSON, metrics, freshness } for the +# FleetOps live INC map. open = all currently-open INC tickets (not time-filtered); +# closed = closures within the selected window; metrics react to the selection. +# Vehicle positions/routes are overlaid by the SPA (FleetNow), not this endpoint. +_INC_WINDOWS = {"today", "week", "month", "custom"} + + +def _bad_request(msg): + return JSONResponse({"error": {"type": "bad_request", "message": msg}}, status_code=400) + + +@app.get("/webhook/inc-dashboard") +def inc_dashboard( + cluster: str | None = None, # exact tickets.inc.cluster (UPPERCASE), blank = all + status: str | None = None, # exact normalized_status, blank = all + window: str = "today", # today | week | month | custom (calendar EAT) + from_: str | None = Query(None, alias="from"), # custom start (inclusive), ISO-8601 + to: str | None = None, # custom end (exclusive), ISO-8601 +): + # Validation (mirrors the contract). The SQL treats any from/to as a custom + # window; presets resolve to EAT calendar bounds inside the function. + if window not in _INC_WINDOWS: + return _bad_request("window must be one of today|week|month|custom") + f, t = _clean(from_), _clean(to) + if window == "custom" and not f and not t: + return _bad_request("custom window requires from and/or to") + + def _parse(v): + if not v: + return None + try: + return datetime.fromisoformat(v) + except ValueError: + return False + + pf, pt = _parse(f), _parse(t) + if pf is False or pt is False: + return _bad_request("from/to must be ISO-8601 timestamps with an offset/Z") + if pf and pt and pf >= pt: + return _bad_request("from must be earlier than to") + + try: + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT reporting.fn_inc_dashboard(%s, %s, %s, %s, %s)", + (_clean(cluster), _clean(status), window, f, t), + ) + payload = cur.fetchone()[0] or {} + return JSONResponse(payload) # jsonb body returned unchanged + except Exception: + log.exception("inc-dashboard failed") + return JSONResponse( + { + "error": { + "type": "unknown", + "message": "INC dashboard is unavailable. Try again in a few seconds.", + } + } + ) + + # ── Fleet trips (#002) ─────────────────────────────────────────────────────── _FILTER_OPTIONS_SQL = """