Overhaul the Tickets tab into the documented INC operations dashboard,
backed by the new GET /webhook/inc-dashboard endpoint (reporting.fn_inc_dashboard):
- Filterbar (cluster / status / window: today|week|month|custom)
- Metric strip: open now, closed in window, open/closed SLA breakdown,
avg MTTR, closures/day + freshness
- Live map: open INC coloured by SLA state, dimmed closed overlay,
FleetNow vehicle markers, layer toggles + SLA legend
- By-status / by-cluster tables + daily closures chart
- Data load decoupled from the basemap so the dashboard renders even if
WebGL/map init is slow or fails
Removes the old combined INC/CRQ map and the /webhook/tickets call (CRQ deferred).
Adds docs/tickets-inc-{overhaul-plan,implementation-guide}.{md,html}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
FleetOps Tickets → INC Operations Dashboard — Implementation Guide
A step-by-step execution guide for replacing the combined INC/CRQ Tickets map in the FleetOps SPA with the documented INC operations dashboard (open layer + windowed closed overlay + SLA states + metric cards). CRQ is deferred.
Companion to the higher-level
tickets-inc-overhaul-plan.mdin this folder. This guide is the actionable runbook.
0. Architecture & data flow
FleetOps SPA (15_fleetops/src/index.html)
│ GET /webhook/inc-dashboard?cluster=&status=&window=&from=&to=
▼
dashboard_api (tracksolid_timescale_grafana_prod/dashboard_api_rev.py)
│ SELECT reporting.fn_inc_dashboard(p_cluster,p_status,p_window,p_from,p_to)
▼
tracksolid_db → reporting.fn_inc_dashboard → tickets.inc / tickets.inc_open_sla
(vehicles overlaid separately: SPA → GET /webhook/live-positions → FleetNow)
- Staging API:
https://fleetapi.fivetitude.com(read-onlydashboard_rorole, reads the same prod DB). - DB:
tracksolid_dbontwala.rahamafresh.com:5433(direct psql/psycopg2 connection available via the writeDATABASE_URL).
1. Prerequisites & access
| Need | Status / how |
|---|---|
Write DATABASE_URL to tracksolid_db |
Provided by user; export as DATABASE_URL (do not commit). |
| Python + psycopg2 | Use 16_fleettickets/.venv. |
| Deploy access to staging host | scp + ssh kianiadee@twala.rahamafresh.com (SSH config entry exists). |
| Source repos | 15_fleetops (SPA), tracksolid_timescale_grafana_prod (API), 16_fleettickets (migrations/docs). |
Phase A — API endpoint (do this first)
Step A1 — Check whether reporting.fn_inc_dashboard is already deployed
cd ~/Downloads/projects/16_fleettickets
source .venv/bin/activate
export DATABASE_URL='postgres://…@twala.rahamafresh.com:5433/tracksolid_db' # provided
python - <<'PY'
import os, psycopg2
c = psycopg2.connect(os.environ["DATABASE_URL"]); cur = c.cursor()
cur.execute("SELECT filename FROM tickets.schema_migrations ORDER BY filename")
print("applied migrations:", [r[0] for r in cur.fetchall()])
cur.execute("SELECT to_regprocedure('reporting.fn_inc_dashboard(text,text,text,timestamptz,timestamptz)')")
print("fn_inc_dashboard:", cur.fetchone()[0])
PY
- If
fn_inc_dashboardprints a signature → skip A2, go to A3. - If it prints
None→ run A2.
Step A2 — Apply migrations (idempotent, ledgered)
# Still in 16_fleettickets with DATABASE_URL exported
python run_migrations.py
- Applies any unapplied
migrations/*.sqlin order; already-applied (01–08) are skipped. Expected new:09_inc_dashboard_fn.sql(and10_inc_history_capture.sqlif not yet present). All migrations areCREATE OR REPLACE/IF NOT EXISTS. - Sanity check the function returns valid JSON:
python - <<'PY'
import os, json, psycopg2
c = psycopg2.connect(os.environ["DATABASE_URL"]); cur = c.cursor()
cur.execute("SELECT reporting.fn_inc_dashboard()")
d = cur.fetchone()[0]
print("keys:", list(d.keys()))
print("window:", d["window"])
print("open feats:", len(d["open"]["features"]), " closed feats:", len(d["closed"]["features"]))
print("metrics.open_now:", d["metrics"]["open_now"], " closed_in_window:", d["metrics"]["closed_in_window"])
PY
Step A3 — Add the /webhook/inc-dashboard handler
File: tracksolid_timescale_grafana_prod/dashboard_api_rev.py. Mirror the existing
tickets() handler (:275). Reuse get_conn, _clean, log.
- Add
Queryto the FastAPI import (line ~46):from fastapi import FastAPI, Request, Query - Add the handler (place near the
tickets()endpoint):_INC_WINDOWS = {"today", "week", "month", "custom"} @app.get("/webhook/inc-dashboard") def inc_dashboard( cluster: str | None = None, status: str | None = None, window: str = "today", from_: str | None = Query(None, alias="from"), to: str | None = None, ): # --- validation (contract) --- if window not in _INC_WINDOWS: return JSONResponse({"error": {"type": "bad_request", "message": "window must be one of today|week|month|custom"}}, status_code=400) f, t = _clean(from_), _clean(to) if window == "custom" and not f and not t: return JSONResponse({"error": {"type": "bad_request", "message": "custom window requires from and/or to"}}, status_code=400) 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 JSONResponse({"error": {"type": "bad_request", "message": "from/to must be ISO-8601 with offset"}}, status_code=400) if pf and pt and pf >= pt: return JSONResponse({"error": {"type": "bad_request", "message": "from must be < to"}}, status_code=400) # --- one passthrough call --- try: with get_conn() as conn, 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) # JSON body unchanged except Exception: log.exception("inc-dashboard failed") return JSONResponse({"error": {"type": "unknown", "message": "INC dashboard is unavailable. Try again in a few seconds."}})
datetimeis already imported (from datetime import …).- Leave the legacy
/webhook/ticketshandler untouched (CRQ / fallback).
Step A4 — Deploy to staging
cd ~/Downloads/projects/tracksolid_timescale_grafana_prod
scp dashboard_api_rev.py kianiadee@twala.rahamafresh.com:~/dashboard_api_staging_rev.py
scp deploy_dashboard_api_staging.sh kianiadee@twala.rahamafresh.com:~/
ssh kianiadee@twala.rahamafresh.com 'bash ~/deploy_dashboard_api_staging.sh'
The script stages the file into the mount and recreates the dashboard_api_staging
container (CORS already allows https://fleetops.fivetitude.com).
Step A5 — Verify the endpoint
B=https://fleetapi.fivetitude.com
curl -s "$B/webhook/inc-dashboard" | head -c 400; echo # 200, today
curl -s "$B/webhook/inc-dashboard?window=month" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["metrics"])'
curl -s "$B/webhook/inc-dashboard?status=ACCEPTED" | python3 -c 'import sys,json;d=json.load(sys.stdin);print("open",d["metrics"]["open_now"])'
curl -s -o /dev/null -w "%{http_code}\n" "$B/webhook/inc-dashboard?window=bogus" # 400
curl -s -o /dev/null -w "%{http_code}\n" "$B/webhook/inc-dashboard?window=custom" # 400
Cross-check metrics.open_now against SELECT count(*) FROM tickets.inc WHERE is_actionable.
Phase B — SPA overhaul (15_fleetops/src/index.html, single file)
Step B1 — Erase the existing INC/CRQ view
- Delete the full-bleed
#view-ticketsmap section (markup, ~lines 374–390). - Remove
loadTickets()(calls/webhook/tickets), the CRQ circle layer, the combined INC/CRQ summary handling,TICKET_COLORS,ticketStats.crq, and the oldshowTicketPopup()(rebuilt for INC below). - Keep the map/marker/popup CSS (~lines 182–252) and the warm-dark palette.
Step B2 — Reuse (do NOT reinvent)
Vehicle overlay machinery stays: loadLive() (/webhook/live-positions, 15s poll),
upsertVeh(), showVehPopup(), vehState(), ccColor(), pastel(), plateTail(),
BASEMAP, COST_CENTRE_COLORS, CC_PALETTE, escapeHtml, updateVehScale(), the
layers-panel builder. Reuse the Logistics/Fuel filterbar + .card/.span* grid +
num()/intg() + custom-range show/hide (:467).
Step B3 — New #view-tickets markup (dashboard cards + map)
- Filterbar:
Clusterselect ·Statusselect ·Windowselect (Today / This week / This month / Custom) + custom start/end dates ·Apply·↻. <main>12-col grid:- Metric cards: Open now, Closed in window, Open SLA
(breached/at-risk/ok/unknown), Closed SLA (compliant/breached), Avg MTTR
(min→h), Closure rate (
per_day_avg+ Chart.js sparkline fromclosure_rate.series). - Map card (
.span12, tall) with layer toggles + SLA legend. - By status + By cluster tables (
.span6each). - Freshness line (exported_at / records_ingested / ingested_at).
- Metric cards: Open now, Closed in window, Open SLA
(breached/at-risk/ok/unknown), Closed SLA (compliant/breached), Avg MTTR
(min→h), Closure rate (
Step B4 — New JS (INC data + map)
incQs()→ query string;loadInc()→fetch(${API_BASE}/webhook/inc-dashboard?…).- Populate Cluster/Status dropdowns from the first unfiltered response's
metrics.by_cluster/metrics.by_statuskeys (no dedicated filters endpoint). - Layers (GeoJSON sources):
- Open INC — circle colored by
sla_state: breached=--danger, at_risk=--warn, ok=--live, unknown=--parked; data =open.features. - Closed INC — dimmed/hollow grey; data =
closed.features; toggle (default off). - Vehicles — existing DOM markers via
loadLive().
- Open INC — circle colored by
- Popups: open → ticket_id, normalized_status, cluster·region, assigned_team/owner,
sla_state + hours_open, geo_source ("approx — cluster" when
geo_source==='cluster'). closed → add closed_at, mttr (min→h), sla_status. - Repurpose
renderTicketKpis()→renderIncKpis()(Open now / Breached / Closed in window / Avg MTTR).switchTab('tickets')→initIncMap()+ lazyloadInc(). Apply/↻/window-change →loadInc(); keep the 15s vehicle poll.- Caveat: drive the map from
*.features, drive cards/tables frommetrics(open.features.lengthmay be< metrics.open_nowfor un-geocoded rows).
Step B5 — Verify the SPA locally
cd ~/Downloads/projects/15_fleetops/src
python3 -m http.server 8080 # API_BASE defaults to https://fleetapi.fivetitude.com
# open http://localhost:8080 → Tickets tab
Confirm: cards + header KPIs populate; by-status/by-cluster match metrics; open INC
SLA-colored + vehicles render; Closed INC toggle overlays the windowed set; changing
Cluster/Status/Window + Apply refetches; popups show documented fields; the network tab
shows only /webhook/inc-dashboard + /webhook/live-positions (no /webhook/tickets).
Rollback
- API: the change is additive (new route). To revert, remove the handler and
redeploy; the legacy
/webhook/ticketsis unchanged. DB migrations are forward-only but idempotent and unused by the old path. - SPA: single file under git — revert
src/index.html.
Out of scope (future)
- CRQ rebuild (same pattern once a CRQ feed/function exists).
- Open-backlog-over-time / observed transitions (needs
16_fleetticketshistory capture). - Nearest-vehicle dispatch off
geog.