fleetops/docs/tickets-inc-overhaul-plan.md

185 lines
10 KiB
Markdown
Raw Permalink Normal View History

# 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.md``fn_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`.
```python
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 `400`s 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`.