feat(dashboard_api): CRQ tab routes (crq-dashboard/search/filter-options)
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 <noreply@anthropic.com>
This commit is contained in:
parent
458e0be101
commit
03c66fbd17
1 changed files with 124 additions and 0 deletions
|
|
@ -27,6 +27,12 @@ is the base URL (the `N8N_BASE` constant in each dashboard SPA):
|
||||||
→ trips payload (reporting.fn_trips_for_map)
|
→ trips payload (reporting.fn_trips_for_map)
|
||||||
GET /webhook/tickets?service_type=&status=&open_only=
|
GET /webhook/tickets?service_type=&status=&open_only=
|
||||||
→ { summary, geojson } (reporting.fn_tickets_for_map) — INC/CRQ map layer
|
→ { 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
|
GET /health
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
"""
|
"""
|
||||||
|
|
@ -439,6 +445,124 @@ def inc_filter_options():
|
||||||
return JSONResponse({"owners": [], "clusters": [], "open_ticket_ids": []})
|
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) ───────────────────────────────────────────────────────
|
# ── Fleet trips (#002) ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
_FILTER_OPTIONS_SQL = """
|
_FILTER_OPTIONS_SQL = """
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue