fleettickets/docs/phase-2-dashboard.md
david kiania f2408f113e docs: add docs/ — phase-1/phase-2 PRDs + implementation record
- docs/phase-1-ingestion.md  — Phase 1 PRD (INC hourly CSV ingestion; deployed)
- docs/phase-2-dashboard.md  — Phase 2 PRD (inc_dashboard read-API for FleetOps map)
- docs/implementation.md     — as-built record (pipeline, migrations 01-08, deploy, DQ)

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

6.7 KiB
Raw Permalink Blame History

PRD (Phase 2) — INC operations dashboard: read-API layer

Phase 1 (hourly INC CSV ingestion → tickets.inc, geocoding, typed generated columns, inc_open_sla view) is complete and deployed (migrations 0108, Coolify hourly 15 7-19 * * * EAT). See docs/phase-1-ingestion.md / docs/implementation.md. This document is Phase 2.

Context

FleetOps needs a live INC operations map (modelled on FleetNow):

  • A map showing all currently-open INC tickets alongside live vehicle positions from FleetNow.
  • A bottom timeline bar that overlays closed tickets (alongside FleetNow vehicle routes) for a selected period.
  • Bottom filters: cluster, ticket status, and time = today / this week / this month / custom date.
  • Top metric cards that react to the selected filters — ticket metrics (not vehicle metrics).

Scope of THIS repo (confirmed): the data / read-API layer only. fleettickets exposes parameterized SQL in tracksolid_db that dashboard_api serves to the FleetOps SPA. The map UI, timeline bar, filter controls, metric cards, and the FleetNow vehicle positions/routes are other repos/systems. There is no vehicle id in the INC feed, so we serve tickets only; the SPA overlays FleetNow vehicles/routes.

Confirmed behaviour

  • Open layer (live): all is_actionable = true INC tickets matching the cluster/status filter — not time-filtered (open = needs action now).
  • Closed overlay (windowed): closed tickets whose closed_at falls in the selected window, matching cluster/status.
  • Metric cards (windowed): computed for the current selection.
  • Filters combine with AND, each optional. Windows are calendar EAT (today / ISO-week / month) or an explicit custom [from, to).
  • Delivery: one parameterized function returning a single JSON payload { open: GeoJSON, closed: GeoJSON, metrics: {…}, window, freshness }, mirroring the existing reporting.fn_tickets_for_map style.

Deliverable — migrations/09_inc_dashboard_fn.sql

A new read function (and supporting index if needed); additive, idempotent (CREATE OR REPLACE), no change to existing objects.

reporting.fn_inc_dashboard(...)

reporting.fn_inc_dashboard(
  p_cluster text        DEFAULT NULL,     -- exact cluster (matches tickets.inc.cluster)
  p_status  text        DEFAULT NULL,     -- normalized_status
  p_window  text        DEFAULT 'today',  -- 'today' | 'week' | 'month' | 'custom'
  p_from    timestamptz DEFAULT NULL,     -- custom window start (inclusive)
  p_to      timestamptz DEFAULT NULL      -- custom window end (exclusive)
) RETURNS jsonb
  • Window resolution: if p_from/p_to given → use them (custom). Else compute EAT calendar bounds from p_window: today = [date_trunc('day', now_eat), +1 day), week = date_trunc('week', …), month = date_trunc('month', …) — converted back to timestamptz via … AT TIME ZONE 'Africa/Nairobi'.
  • Returned JSON:
    {
      "window": { "from": "...", "to": "...", "preset": "today" },
      "open":   { "type":"FeatureCollection", "features":[ … ] },   // all open, filtered by cluster/status
      "closed": { "type":"FeatureCollection", "features":[ … ] },   // closed_at in window, filtered
      "metrics": {
        "open_now": int,
        "closed_in_window": int,
        "sla": {
          "open":   { "breached": int, "at_risk": int, "ok": int, "unknown": int },
          "closed": { "compliant": int, "breached": int }
        },
        "by_status":  { "<status>": int, … },
        "by_cluster": { "<cluster>": int, … },
        "closure_rate": { "per_day_avg": num, "series": [ { "day":"YYYY-MM-DD", "count":int }, … ] },
        "avg_mttr_min": num
      },
      "freshness": { … }                           // from tickets.import_meta
    }
    
  • Feature properties (both layers): ticket_id, normalized_status, cluster, region, location_name, assigned_team, owner, geo_source. Open adds sla_state, hours_open; closed adds closed_at, mttr, sla_status. Geometry from geom (ST_AsGeoJSON). Only geom IS NOT NULL rows become features; metrics count the full filtered set (note the small geocoding gap).

Reuse (don't reinvent)

  • tickets.inc_open_sla (migration 08) — sla_state / hours_open for the open layer + open-SLA metrics.
  • Typed generated columns (migrations 0307): cluster, normalized_status, closed_at, mttr (minutes), assigned_team, geom, geo_source.
  • reporting.fn_tickets_for_map (migrations 0102) — GeoJSON jsonb_build_object/ST_AsGeoJSON + summary.freshness patterns.
  • Derived SLA logicnow() COALESCE(created_at_service, first_seen_at) vs 48h/36h.

Indexes

In place: ix_inc_closed_at, ix_inc_cluster_col, ix_inc_norm_status_col, ix_inc_actionable_col, ix_inc_geom, ix_inc_geog. Add composite (closed_at, cluster) only if EXPLAIN shows it's needed.

Grants

GRANT EXECUTE ON FUNCTION reporting.fn_inc_dashboard(...) TO dashboard_ro (guarded).

Dependencies (other repos)

  • dashboard_api — endpoint e.g. GET /webhook/inc-dashboard?cluster=&status=&window=&from=&to= calling fn_inc_dashboard. (Contract here; impl there.)
  • FleetOps SPA (fleetops) — map, timeline bar, filter UI, metric cards; overlays FleetNow vehicles/routes.
  • FleetNow — live vehicle positions + historical routes.

Data-quality caveats (affect metrics, not delivery)

  • Source sla_status only meaningful for closed; open SLA is derived.
  • created_at_service null on ~30% → some open are SLA unknown (fallback flagged).
  • mttr is minutes, null until closed; closure/MTTR metrics filter accordingly.
  • Content lag ~2 days → recent days under-count.
  • A few tickets lack geom → counted in metrics, absent from map features.

Verification

  1. SELECT reporting.fn_inc_dashboard(); → valid JSON (open/closed FCs, metrics, window=today, freshness).
  2. Filters: p_cluster, p_status, p_window := 'month', and a custom p_from/p_to — counts match ad-hoc SELECTs on tickets.inc / tickets.inc_open_sla.
  3. Window math: today/week/month are correct EAT calendar ranges.
  4. SLA metrics match the inc_open_sla distribution / source sla_status in window.
  5. EXPLAIN ANALYZE on the windowed closed query uses ix_inc_closed_at.
  6. Apply via run_migrations.py; ledgered in tickets.schema_migrations.

Out of scope (future)

  • Open-backlog-over-time / observed open→closed transitions need the append-only history capture (tickets.closure_events + daily snapshot) — separate plan.
  • Dispatch surface (nearest-vehicle off geog) — after analytics.