From 03c66fbd17be4754a4333610a9fb52c356eed3ea Mon Sep 17 00:00:00 2001 From: david kiania Date: Fri, 26 Jun 2026 00:20:10 +0300 Subject: [PATCH] feat(dashboard_api): CRQ tab routes (crq-dashboard/search/filter-options) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parallel to the INC routes, three new endpoints back the FleetOps Tickets tab's CRQ sub-tab: GET /webhook/crq-dashboard, /webhook/crq-search, /webhook/crq-filter-options — thin passthroughs over reporting.fn_crq_* (fleettickets migration 16, over tickets.crq). The INC routes are unchanged. Header route list updated. Not yet redeployed to the staging/prod bridges (pending go-ahead). Co-Authored-By: Claude Opus 4.8 --- dashboard_api_rev.py | 124 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/dashboard_api_rev.py b/dashboard_api_rev.py index 713455f..31c33d3 100644 --- a/dashboard_api_rev.py +++ b/dashboard_api_rev.py @@ -27,6 +27,12 @@ is the base URL (the `N8N_BASE` constant in each dashboard SPA): → trips payload (reporting.fn_trips_for_map) GET /webhook/tickets?service_type=&status=&open_only= → { summary, geojson } (reporting.fn_tickets_for_map) — INC/CRQ map layer + GET /webhook/{inc,crq}-dashboard?cluster=&status=&window=&from=&to= + → { window, open, closed, metrics, freshness } (reporting.fn_{inc,crq}_dashboard) + GET /webhook/{inc,crq}-search?ticket_id=&owner=&cluster=&status=&state=&from=&to= + → { count, truncated, limit, state, rows } (reporting.fn_{inc,crq}_search) + GET /webhook/{inc,crq}-filter-options + → { owners, clusters, open_ticket_ids } (reporting.fn_{inc,crq}_filter_options) GET /health ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """ @@ -439,6 +445,124 @@ def inc_filter_options(): return JSONResponse({"owners": [], "clusters": [], "open_ticket_ids": []}) +# ── CRQ operations dashboard (new-installation tickets) ─────────────────────── +# CRQ mirrors INC: identical payload shape, served by reporting.fn_crq_dashboard / +# fn_crq_search / fn_crq_filter_options over tickets.crq (fleettickets migration 16). +# Powers the FleetOps Tickets tab's CRQ sub-tab. The INC routes above are unchanged; +# these are parallel so the two datasets stay independently observable. +@app.get("/webhook/crq-dashboard") +def crq_dashboard( + cluster: str | None = None, # exact tickets.crq.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 +): + 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_crq_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("crq-dashboard failed") + return JSONResponse( + { + "error": { + "type": "unknown", + "message": "CRQ dashboard is unavailable. Try again in a few seconds.", + } + } + ) + + +@app.get("/webhook/crq-search") +def crq_search( + ticket_id: str | None = None, # substring match on ticket_id + owner: str | None = None, # engineer — case-insensitive substring on owner + cluster: str | None = None, # exact tickets.crq.cluster + status: str | None = None, # exact normalized_status + state: str = "closed", # closed | open | all + from_: str | None = Query(None, alias="from"), # closed-at range start (ISO-8601) + to: str | None = None, # closed-at range end (exclusive, ISO-8601) +): + state = (state or "closed").lower() + if state not in _INC_STATES: + return _bad_request("state must be one of open|closed|all") + f, t = _clean(from_), _clean(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_crq_search(%s, %s, %s, %s, %s, %s, %s)", + (_clean(ticket_id), _clean(owner), _clean(cluster), _clean(status), state, f, t), + ) + payload = cur.fetchone()[0] or {} + return JSONResponse(payload) # jsonb body returned unchanged + except Exception: + log.exception("crq-search failed") + return JSONResponse( + { + "error": { + "type": "unknown", + "message": "Ticket search is unavailable. Try again in a few seconds.", + } + } + ) + + +@app.get("/webhook/crq-filter-options") +def crq_filter_options(): + # Dropdown options for the CRQ ticket explorer (engineers, clusters, open ticket ids). + try: + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute("SELECT reporting.fn_crq_filter_options()") + payload = cur.fetchone()[0] or {} + return JSONResponse(payload) + except Exception: + log.exception("crq-filter-options failed") + return JSONResponse({"owners": [], "clusters": [], "open_ticket_ids": []}) + + # ── Fleet trips (#002) ─────────────────────────────────────────────────────── _FILTER_OPTIONS_SQL = """