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>
10 KiB
Overhaul FleetOps Tickets → INC operations dashboard
Context
The FleetOps SPA's Tickets tab is currently a full-bleed MapLibre map showing
combined INC (red) + CRQ (blue) ticket circles plus live FleetNow vehicles, fed
by the legacy GET /webhook/tickets (→ reporting.fn_tickets_for_map). Meanwhile,
the 16_fleettickets repo has designed and documented a richer INC operations
dashboard (Phase 2): an open-ticket layer + windowed closed overlay + derived SLA
states + ticket metric cards, served by a new reporting.fn_inc_dashboard(...)
function and exposed at GET /webhook/inc-dashboard.
We are overhauling the SPA to that documented design. Per the user: erase the existing INC + CRQ ticket view and rebuild INC first (CRQ deferred). INC is fully documented; CRQ reuses the same machinery later.
Key blocker found: GET /webhook/inc-dashboard currently 404s — the DB
function lives in 16_fleettickets/migrations/09_inc_dashboard_fn.sql but the HTTP
wrapper is not in the dashboard_api service. The legacy /webhook/tickets returns
200 with live INC+CRQ data (INC ingest is live: 21,301 records, freshness current).
Decisions (confirmed with user):
- Endpoint first, then SPA — build/verify the API endpoint (+ DB function) and confirm it returns real data, then overhaul the SPA against the live endpoint.
- Layout: dashboard cards + map (matches the existing Logistics/Fuel tabs) — top filterbar (cluster / status / window), a metric-cards row, a large map card, and by-status / by-cluster tables below.
Reference docs (source of truth)
16_fleettickets/docs/dashboard-api-contract.md— endpoint params, response shape, field semantics (mttr=minutes, sla_state derived, coords[lng,lat], map-vs-metrics gap).16_fleettickets/docs/phase-2-dashboard.md—fn_inc_dashboardsignature + metrics.
Phase A — API endpoint (separate repo: tracksolid_timescale_grafana_prod)
File:
~/Downloads/projects/tracksolid_timescale_grafana_prod/dashboard_api_rev.py. Deployed by scp + ssh to the remote host; staging instance (fleetapi.fivetitude.com) runs read-only asdashboard_ro. These steps touch a live server and may need the user to run the scp/ssh deploy via! <cmd>.
A1. Verify / apply the DB function
- Confirm
reporting.fn_inc_dashboardexists in the live DB. If absent, apply via16_fleettickets/run_migrations.py(needs the writeDATABASE_URL; applies09_inc_dashboard_fn.sql, and08/10if not already intickets.schema_migrations). Migrations are idempotent + ledgered, so re-running is safe. - Sanity check in psql:
SELECT reporting.fn_inc_dashboard();→ valid JSON (open/closed FeatureCollections, metrics,window.preset='today', freshness).
A2. Add the /webhook/inc-dashboard handler
Mirror the existing tickets() handler (dashboard_api_rev.py:275-304): one
passthrough SQL call, JSON body returned unchanged. Reuse get_conn, _clean.
from fastapi import FastAPI, Request, Query # add Query to existing import (line 46)
@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"), # 'from' is reserved
to: str | None = None,
):
# Validation per the contract:
# - window not in {today,week,month,custom} -> 400
# - window == 'custom' with neither from nor to -> 400
# - from/to unparseable, or from >= to -> 400
# If either from/to is present, the SQL treats it as custom (window overridden).
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,
_clean(from_), _clean(to)),
)
payload = cur.fetchone()[0] or {}
return JSONResponse(payload) # passthrough, unchanged
except Exception:
log.exception("inc-dashboard failed")
return JSONResponse({"error": {"type": "unknown",
"message": "INC dashboard is unavailable. Try again in a few seconds."}})
- Pass
from/toas ISO-8601 strings; PostgreSQL casts text →timestamptzon the function call. Validate parseability API-side (e.g.datetime.fromisoformat) to return clean400s rather than a 500 from the DB. - Leave the legacy
/webhook/ticketshandler in place (CRQ / fallback may use it).
A3. Deploy + verify
- Deploy to staging: scp
dashboard_api_rev.py→ host, scp the staging deploy script,ssh … bash ~/deploy_dashboard_api_staging.sh(recreates the container). - Verify against
https://fleetapi.fivetitude.com:GET /webhook/inc-dashboard→ 200, documented shape,open/closedFCs.?window=month,?cluster=MUIGAI%20INN,?status=ACCEPTED,?from=…%2B03:00&to=…%2B03:00→ counts sane;opennot time-filtered.?window=bogus→ 400;?window=custom(no from/to) → 400.
Phase B — SPA overhaul (15_fleetops/src/index.html, single file)
B1. Erase the existing INC/CRQ view
Remove from src/index.html:
- Markup: the full-bleed map section
#view-tickets(lines ~374-390). - JS — drop:
loadTickets()(calls/webhook/tickets), the CRQ circle layer, combined INC/CRQ summary handling,showTicketPopup()(rebuild for INC),TICKET_COLORS,ticketStats.crq. - CSS: keep the map/marker/popup blocks (lines ~182-252) — reused; rename
#tk-*selectors only if the new markup changes ids.
B2. Keep + reuse (do NOT reinvent)
The vehicle overlay machinery stays — the contract says the SPA overlays FleetNow:
loadLive()(/webhook/live-positions, 15s poll),upsertVeh(),showVehPopup(),vehState(),ccColor(),pastel(),plateTail(),BASEMAP,COST_CENTRE_COLORS,CC_PALETTE,escapeHtml,updateVehScale(),initTicketsMap()(rename →initIncMap()), the layers-panel builder, the MapLibre popup CSS, and the warm-dark palette.- Filterbar markup/behaviour pattern from the Logistics/Fuel tabs (
.filterbar, custom-range show/hide atindex.html:467-471,.card/.span*grid, table renderers,num()/intg()).
B3. New markup — #view-tickets (dashboard cards + map)
- Filterbar:
Clusterselect,Statusselect,Windowselect (Today / This week / This month / Custom) + custom start/end date inputs (reuse the.ff.customshow/hide),Apply, refresh↻. <main>12-col grid:- Metric cards row: Open now, Closed in window, Open SLA (breached /
at-risk / ok / unknown), Closed SLA (compliant / breached), Avg MTTR
(minutes → show as h), Closure rate (
per_day_avg+ a small Chart.js sparkline fromclosure_rate.series). - Map card (
.span12, tall): MapLibre map with layer toggles + SLA legend. - By status table + By cluster table (
.span6each) frommetrics.by_status/metrics.by_cluster. - Freshness line (exported_at / records_ingested / ingested_at) under the map.
- Metric cards row: Open now, Closed in window, Open SLA (breached /
at-risk / ok / unknown), Closed SLA (compliant / breached), Avg MTTR
(minutes → show as h), Closure rate (
B4. New JS — INC data + map
- State:
incQs()builds query (cluster,status,window, andfrom/towhen custom).loadInc()→fetch(${API_BASE}/webhook/inc-dashboard?…). - Dropdowns: populate
Cluster/Statusfrom the first unfiltered response'smetrics.by_cluster/metrics.by_statuskeys (no dedicated filters endpoint exists); keep stable thereafter. - Map layers on one or two GeoJSON sources:
- Open INC — circle layer colored by
sla_state(breached=--danger,at_risk=--warn,ok=--live,unknown=--parked); data =open.features. - Closed INC — distinct dimmed style (e.g. hollow grey), data =
closed.features; toggleable (default off). - Vehicles — existing DOM markers via
loadLive(). - Layer panel: Open INC / Closed INC / Vehicles toggles + SLA color legend.
- Open INC — circle layer colored by
- Popups: open →
ticket_id,normalized_status,cluster · region,assigned_team/owner,sla_state+hours_open,geo_source(note "approx — cluster" whengeo_source==='cluster'). closed → addclosed_at,mttr(min→h),sla_status. - Header KPI strip: repurpose
renderTicketKpis()→renderIncKpis()showing INC metrics (Open now, Breached, Closed in window, Avg MTTR). UpdateswitchTab()so theticketscase callsinitIncMap()+loadInc()(lazy, like Fuel). - Filters:
Apply/↻/window-change →loadInc(). Keep the 15s vehicle poll;loadInc()is on-demand (open layer changes at most hourly). - Caveat to honor:
open.features.lengthmay be< metrics.open_now(un-geocoded rows) — drive map fromfeatures, drive cards/tables frommetrics.
Verification (end-to-end)
- API (Phase A): curl matrix above against
fleetapi.fivetitude.com— shapes, filters, 400s. Comparemetrics.open_nowtoSELECT count(*) FROM tickets.inc WHERE is_actionable(andinc_open_slaSLA distribution). - SPA (Phase B): serve
src/locally (python3 -m http.serverinsrc/, or the Caddy Docker image) withAPI_BASE=https://fleetapi.fivetitude.com. Open the Tickets tab and confirm:- Metric cards + header KPIs populate; by-status / by-cluster tables match
metrics. - Map shows SLA-colored open INC + live vehicles; toggling Closed INC overlays the windowed closed set; SLA legend correct.
- Changing Cluster / Status / Window + Apply refetches and updates cards, tables, and both layers; custom range shows date inputs and bounds the closed overlay.
- Hover popups show the documented fields (open vs closed).
- No console calls to
/webhook/tickets; only/webhook/inc-dashboard+/webhook/live-positions.
- Metric cards + header KPIs populate; by-status / by-cluster tables match
Out of scope (future)
- CRQ rebuild (deferred; reuses the same pattern once a CRQ feed/function exists).
- Open-backlog-over-time / observed transitions (needs
16_fleetticketshistory capture — not built). Nearest-vehicle dispatch offgeog.