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

6.8 KiB
Raw Blame History

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 to400 (would mean "all time").
  • from/to unpar, or from >= to400.
  • cluster/status are passed through verbatim (unknown values simply return empty sets — not an error).

Implementation (one call, passthrough)

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

{
  "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

# 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.