fleettickets/docs/dashboard-api-contract.md
david kiania da6da9d26f docs: dashboard_api endpoint contract for fn_inc_dashboard (handoff)
GET /webhook/inc-dashboard wrapper spec: query params (cluster/status/window/from/to)
-> SQL passthrough, full response schema, field semantics (open=live vs closed=window,
mttr minutes, derived vs source SLA, map/metrics geocoding gap), examples, caching/auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:13:09 +03:00

157 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Handoff — `dashboard_api` endpoint contract: INC operations dashboard
For the **dashboard_api** repo (tracksolid stack). This endpoint is a thin wrapper
over `reporting.fn_inc_dashboard(...)` in `tracksolid_db` (built by `fleettickets`,
migration 09). It powers the FleetOps **live INC map** (open tickets + windowed
closed overlay + metric cards). Vehicle positions/routes come from **FleetNow** and
are overlaid by the SPA — **not** part of this endpoint.
## Endpoint
```
GET /webhook/inc-dashboard
```
Mirror the existing `GET /webhook/tickets` (→ `reporting.fn_tickets_for_map`) for
auth, connection (role `dashboard_ro`), and JSON passthrough conventions.
### Query parameters
| Param | Type | Required | Default | Maps to | Notes |
|---|---|---|---|---|---|
| `cluster` | string | no | — (all) | `p_cluster` | Exact match on `tickets.inc.cluster` (UPPERCASE, e.g. `MUIGAI INN`). Empty = no filter. |
| `status` | string | no | — (all) | `p_status` | Exact `normalized_status` (UPPERCASE, e.g. `ACCEPTED`, `CLOSED COMPLETE`). |
| `window` | enum | no | `today` | `p_window` | One of `today` \| `week` \| `month` \| `custom`. Calendar **EAT** (week = ISO Mon-start). |
| `from` | ISO-8601 timestamp | only if `window=custom` | — | `p_from` | Inclusive start. Send with offset/Z (absolute time). |
| `to` | ISO-8601 timestamp | only if `window=custom` | — | `p_to` | Exclusive end. |
**Resolution rules (match the SQL):**
- If **either** `from` or `to` is present, the function treats it as a **custom**
window (the `window` value is overridden to `custom`); a missing side becomes
±infinity. So for a bounded custom range, **send both** `from` and `to`.
- Otherwise the preset (`today`/`week`/`month`) is resolved to EAT calendar bounds
**inside the function** — the API does not compute dates.
**Validation (API side):**
- `window` ∉ {today,week,month,custom} → `400`.
- `window=custom` with neither `from` nor `to``400` (would mean "all time").
- `from`/`to` unpar, or `from >= to``400`.
- `cluster`/`status` are passed through verbatim (unknown values simply return empty
sets — not an error).
### Implementation (one call, passthrough)
```sql
SELECT reporting.fn_inc_dashboard(
p_cluster := $1, -- text | null
p_status := $2, -- text | null
p_window := $3, -- text (default 'today')
p_from := $4, -- timestamptz | null
p_to := $5 -- timestamptz | null
);
```
Return the resulting `jsonb` **as the HTTP body unchanged** (`Content-Type:
application/json`). No reshaping needed.
## Response (200) — shape
```jsonc
{
"window": { "from": "2026-06-16T00:00:00+03:00", "to": "...", "preset": "today" },
"open": { // ALL currently-open tickets (live; NOT time-filtered)
"type": "FeatureCollection",
"features": [
{ "type": "Feature",
"geometry": { "type": "Point", "coordinates": [lng, lat] },
"properties": {
"ticket_id": "WOT…", "normalized_status": "ACCEPTED",
"cluster": "MUIGAI INN", "region": "nairobi", "location_name": "…",
"assigned_team": "…", "owner": "…", "geo_source": "location|cluster|feed",
"sla_state": "breached|at_risk|ok|unknown", "hours_open": 107.7
} }
]
},
"closed": { // closed_at within the selected window
"type": "FeatureCollection",
"features": [
{ "type": "Feature",
"geometry": { "type": "Point", "coordinates": [lng, lat] },
"properties": {
"ticket_id": "WOT…", "normalized_status": "CLOSED COMPLETE",
"cluster": "…", "region": "…", "location_name": "…",
"assigned_team": "…", "owner": "…", "geo_source": "…",
"closed_at": "2026-06-15T13:04:52+00:00", "mttr": 1467, "sla_status": "Compliant|Breached"
} }
]
},
"metrics": {
"open_now": 30,
"closed_in_window": 2605,
"sla": {
"open": { "breached": 30, "at_risk": 0, "ok": 0, "unknown": 0 },
"closed": { "compliant": 1718, "breached": 887 }
},
"by_status": { "ACCEPTED": 20, "PENDING DISPATCH": 7, "CLOSED COMPLETE": 2400, … },
"by_cluster": { "MUIGAI INN": 82, "JUJA": …, "(none)": 1, … },
"closure_rate": { "per_day_avg": 86.83, "series": [ { "day": "2026-06-15", "count": 13 }, … ] },
"avg_mttr_min": 1467.1
},
"freshness": { "inc": { "export_type": "full", "exported_at": "…", "records_ingested": 21301, "ingested_at": "…" } }
}
```
### Field semantics the SPA must know
- **`open` is always the full open set** (matching cluster/status); the time window
does **not** filter it. **`closed`** is only the overlay for the window.
- **`mttr` is in MINUTES** (null until closed). `hours_open` is hours.
- **`sla_state`** (open) is **derived** (`now created_at_service`, falling back to
`first_seen_at`); `unknown` = no creation clock. **`sla_status`** (closed) is the
source field (`Compliant`/`Breached`).
- **Map vs metrics gap:** `features` include only geocoded rows (`geom` present);
metric **counts include all matching rows** (a few tickets have no `geom`, e.g.
null cluster). So `open.features.length` may be `< metrics.open_now`.
- **`geo_source`** indicates precision: `location` (street-level) > `cluster`
(centroid; many tickets share a point) > `feed` > none.
- **Coordinates** are GeoJSON `[lng, lat]` (x, y) order.
- **`window.from/to`** are absolute timestamps; presets are EAT calendar ranges.
## Examples
```bash
# today (default)
curl '.../webhook/inc-dashboard'
# this month, one cluster
curl '.../webhook/inc-dashboard?window=month&cluster=MUIGAI%20INN'
# open ACCEPTED tickets (today window irrelevant to the open layer)
curl '.../webhook/inc-dashboard?status=ACCEPTED'
# custom range (send BOTH from & to; absolute times)
curl '.../webhook/inc-dashboard?from=2026-06-09T00:00:00%2B03:00&to=2026-06-16T00:00:00%2B03:00'
```
## Operational notes
- **Caching:** the open layer + freshness change at most **hourly** (the ingest
cadence); closed/metrics change only when filters change. A short cache
(≈60 s) keyed on the full query string is safe; or `Cache-Control: no-store` if
you prefer always-fresh — the function is cheap (indexed).
- **Performance:** indexed (`ix_inc_closed_at`, `ix_inc_cluster_col`,
`ix_inc_norm_status_col`, `ix_inc_geom`); month-window payloads are a few thousand
closed features — fine for one request. If payloads get large, consider a
`metrics-only` flag later (not in scope now).
- **Auth:** same as `/webhook/tickets`. The function is granted to `dashboard_ro`.
- **Timezone:** all preset math is EAT inside the DB; the API passes `from`/`to`
through as absolute `timestamptz`.
## Out of scope (other work)
- FleetNow vehicle positions/routes overlay — SPA + FleetNow.
- Open-backlog-over-time / observed transitions — needs the `fleettickets` history
capture (not built yet); this endpoint reports current state + closure events.