# 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 ```bash 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) ```bash # Still in 16_fleettickets with DATABASE_URL exported python run_migrations.py ``` - Applies any unapplied `migrations/*.sql` in order; already-applied (01–08) 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: ```bash 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): ```python from fastapi import FastAPI, Request, Query ``` 2. Add the handler (place near the `tickets()` endpoint): ```python _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 ```bash 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 ```bash 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 374–390). - 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 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:** `Cluster` select · `Status` select · `Window` select (Today / This week / This month / Custom) + custom start/end dates · `Apply` · `↻`. - **`
` 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 ```bash 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`.