fleetops/docs/tickets-inc-implementation-guide.md

241 lines
11 KiB
Markdown
Raw Normal View 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
```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 (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:
```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 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
```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`.