fleetops/docs/tickets-inc-overhaul-plan.md
david kiania e32ec92cbf feat(tickets): replace INC/CRQ map with INC operations dashboard
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>
2026-06-16 11:42:23 +03:00

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.mdfn_inc_dashboard signature + 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 as dashboard_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_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.
  • 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/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.
  • Leave the legacy /webhook/tickets handler 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/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.

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 at index.html:467-471, .card/.span* grid, table renderers, num()/intg()).

B3. New markup — #view-tickets (dashboard cards + map)

  • Filterbar: 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:
    • 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 from closure_rate.series).
    • Map card (.span12, tall): MapLibre map with layer toggles + SLA legend.
    • By status table + By cluster table (.span6 each) from metrics.by_status / metrics.by_cluster.
    • Freshness line (exported_at / records_ingested / ingested_at) under the map.

B4. New JS — INC data + map

  • State: incQs() builds query (cluster, status, window, and from/to when custom). loadInc()fetch(${API_BASE}/webhook/inc-dashboard?…).
  • Dropdowns: populate Cluster / Status from the first unfiltered response's metrics.by_cluster / metrics.by_status keys (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.
  • Popups: open → 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.
  • Header KPI strip: repurpose 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).
  • 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.length may be < metrics.open_now (un-geocoded rows) — drive map from features, drive cards/tables from metrics.

Verification (end-to-end)

  1. API (Phase A): curl matrix above against 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).
  2. SPA (Phase B): serve 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:
    • 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.

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_fleettickets history capture — not built). Nearest-vehicle dispatch off geog.