fleetops/docs/tickets-inc-implementation-guide.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

11 KiB
Raw Blame History

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.md in 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-only dashboard_ro role, reads the same prod DB).
  • DB: tracksolid_db on twala.rahamafresh.com:5433 (direct psql/psycopg2 connection available via the write DATABASE_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_dashboard prints 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/*.sql in order; already-applied (0108) are skipped. Expected new: 09_inc_dashboard_fn.sql (and 10_inc_history_capture.sql if not yet present). All migrations are CREATE 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.

  1. Add Query to the FastAPI import (line ~46):
    from fastapi import FastAPI, Request, Query
    
  2. 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."}})
    
  • datetime is already imported (from datetime import …).
  • Leave the legacy /webhook/tickets handler 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-tickets map section (markup, ~lines 374390).
  • Remove loadTickets() (calls /webhook/tickets), the CRQ circle layer, the combined INC/CRQ summary handling, TICKET_COLORS, ticketStats.crq, and the old showTicketPopup() (rebuilt for INC below).
  • Keep the map/marker/popup CSS (~lines 182252) 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: Cluster select · Status select · Window select (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 from closure_rate.series).
    • Map card (.span12, tall) with layer toggles + SLA legend.
    • By status + By cluster tables (.span6 each).
    • Freshness line (exported_at / records_ingested / ingested_at).

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_status keys (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().
  • 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() + lazy loadInc().
  • Apply//window-change → loadInc(); keep the 15s vehicle poll.
  • Caveat: drive the map from *.features, drive cards/tables from metrics (open.features.length may be < metrics.open_now for 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/tickets is 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_fleettickets history capture).
  • Nearest-vehicle dispatch off geog.