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.
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):
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_dashboard signature + metrics.tracksolid_timescale_grafana_prodFile:~/Downloads/projects/tracksolid_timescale_grafana_prod/dashboard_api_rev.py. Deployed by scp + ssh to the remote host; the 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>.
reporting.fn_inc_dashboard exists in the live DB. If absent, apply via
16_fleettickets/run_migrations.py (needs the write DATABASE_URL;
applies 09_inc_dashboard_fn.sql, and 08/10 if not already in
tickets.schema_migrations). Migrations are idempotent + ledgered, so re-running is safe.SELECT reporting.fn_inc_dashboard(); → valid JSON (open/closed
FeatureCollections, metrics, window.preset='today', freshness)./webhook/inc-dashboard handlerMirror 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."}})
from/to as ISO-8601 strings; PostgreSQL casts text →
timestamptz on the function call. Validate parseability API-side
(e.g. datetime.fromisoformat) to return clean 400s rather than a 500 from the DB./webhook/tickets handler in place (CRQ / fallback may use it).dashboard_api_rev.py → host, scp the staging deploy script,
ssh … bash ~/deploy_dashboard_api_staging.sh (recreates the container).https://fleetapi.fivetitude.com:
GET /webhook/inc-dashboard → 200, documented shape, open/closed FCs.?window=month, ?cluster=MUIGAI%20INN, ?status=ACCEPTED,
?from=…%2B03:00&to=…%2B03:00 → counts sane; open not time-filtered.?window=bogus → 400; ?window=custom (no from/to) → 400.15_fleetops/src/index.html (single file)Remove from src/index.html:
#view-tickets (lines ~374-390).loadTickets() (calls /webhook/tickets), the
CRQ circle layer, combined INC/CRQ summary handling, showTicketPopup()
(rebuild for INC), TICKET_COLORS, ticketStats.crq.#tk-* selectors only if the new markup changes ids.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,
custom-range show/hide at index.html:467-471, .card/.span* grid,
table renderers, num()/intg()).#view-tickets (dashboard cards + map)Cluster select, Status select,
Window select (Today / This week / This month / Custom) + custom start/end date inputs
(reuse the .ff.custom show/hide), Apply, refresh ↻.<main> 12-col grid:
per_day_avg + a small Chart.js sparkline from closure_rate.series)..span12, tall): MapLibre map with layer toggles + SLA legend..span6 each)
from metrics.by_status / metrics.by_cluster.incQs() builds query (cluster, status,
window, and from/to when custom). loadInc() →
fetch(${API_BASE}/webhook/inc-dashboard?…).Cluster / Status from the first
unfiltered response's metrics.by_cluster / metrics.by_status keys (no
dedicated filters endpoint exists); keep stable thereafter.sla_state
(breached=--danger, at_risk=--warn,
ok=--live, unknown=--parked); data = open.features.closed.features; toggleable (default off).loadLive().ticket_id, normalized_status,
cluster · region, assigned_team/owner, sla_state +
hours_open, geo_source (note "approx — cluster" when
geo_source==='cluster'). closed → add closed_at, mttr (min→h),
sla_status.renderTicketKpis() →
renderIncKpis() showing INC metrics (Open now, Breached, Closed in window, Avg MTTR).
Update switchTab() so the tickets case calls initIncMap() +
loadInc() (lazy, like Fuel).Apply/↻/window-change → loadInc().
Keep the 15s vehicle poll; loadInc() is on-demand (open layer changes at most hourly).open.features.length may be < metrics.open_now
(un-geocoded rows) — drive map from features, drive cards/tables from metrics.fleetapi.fivetitude.com —
shapes, filters, 400s. Compare metrics.open_now to
SELECT count(*) FROM tickets.inc WHERE is_actionable (and inc_open_sla SLA distribution).src/ locally
(python3 -m http.server in src/, or the Caddy Docker image) with
API_BASE=https://fleetapi.fivetitude.com. Open the Tickets tab and confirm:
metrics./webhook/tickets; only /webhook/inc-dashboard +
/webhook/live-positions.16_fleettickets history capture —
not built). Nearest-vehicle dispatch off geog.