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>
184 lines
10 KiB
Markdown
184 lines
10 KiB
Markdown
# 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`.
|