Staging environment + FleetOps split #17
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)
|
||||
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 = """
|
||||
|
|
|
|||
Loading…
Reference in a new issue