tickets-inc-overhaul-plan.md. CRQ deferred.FleetOps SPA (15_fleetops/src/index.html)
│ GET /webhook/inc-dashboard?cluster=&status=&window=&from=&to=
▼
dashboard_api (tracksolid_timescale_grafana_prod/dashboard_api_rev.py)
│ SELECT reporting.fn_inc_dashboard(p_cluster,p_status,p_window,p_from,p_to)
▼
tracksolid_db → reporting.fn_inc_dashboard → tickets.inc / tickets.inc_open_sla
(vehicles overlaid separately: SPA → GET /webhook/live-positions → FleetNow)
https://fleetapi.fivetitude.com (read-only dashboard_ro role, reads the prod DB).tracksolid_db on twala.rahamafresh.com:5433 (direct psql/psycopg2 via the write DATABASE_URL).| Need | Status / how |
|---|---|
Write DATABASE_URL to tracksolid_db | Provided by user; export as DATABASE_URL (do not commit). |
| Python + psycopg2 | Use 16_fleettickets/.venv. |
| Deploy access to staging host | scp + ssh kianiadee@twala.rahamafresh.com (SSH config entry exists). |
| Source repos | 15_fleetops (SPA), tracksolid_timescale_grafana_prod (API), 16_fleettickets (migrations/docs). |
reporting.fn_inc_dashboard is deployedcd ~/Downloads/projects/16_fleettickets
source .venv/bin/activate
export DATABASE_URL='postgres://…@twala.rahamafresh.com:5433/tracksolid_db' # provided
python - <<'PY'
import os, psycopg2
c = psycopg2.connect(os.environ["DATABASE_URL"]); cur = c.cursor()
cur.execute("SELECT filename FROM tickets.schema_migrations ORDER BY filename")
print("applied migrations:", [r[0] for r in cur.fetchall()])
cur.execute("SELECT to_regprocedure('reporting.fn_inc_dashboard(text,text,text,timestamptz,timestamptz)')")
print("fn_inc_dashboard:", cur.fetchone()[0])
PY
None → run A2.python run_migrations.py
Applies unapplied migrations/*.sql in order; 01–08 are skipped.
Expected new: 09_inc_dashboard_fn.sql (and 10_inc_history_capture.sql if absent).
All migrations are CREATE OR REPLACE / IF NOT EXISTS. Sanity check:
python - <<'PY'
import os, json, psycopg2
c = psycopg2.connect(os.environ["DATABASE_URL"]); cur = c.cursor()
cur.execute("SELECT reporting.fn_inc_dashboard()")
d = cur.fetchone()[0]
print("keys:", list(d.keys()))
print("window:", d["window"])
print("open feats:", len(d["open"]["features"]), " closed feats:", len(d["closed"]["features"]))
print("metrics.open_now:", d["metrics"]["open_now"], " closed_in_window:", d["metrics"]["closed_in_window"])
PY
/webhook/inc-dashboard handlerFile: tracksolid_timescale_grafana_prod/dashboard_api_rev.py. Mirror the existing
tickets() handler (:275). Reuse get_conn, _clean, log.
Query to the FastAPI import (~line 46): from fastapi import FastAPI, Request, Querytickets():_INC_WINDOWS = {"today", "week", "month", "custom"}
@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"),
to: str | None = None,
):
# --- validation (contract) ---
if window not in _INC_WINDOWS:
return JSONResponse({"error": {"type": "bad_request",
"message": "window must be one of today|week|month|custom"}}, status_code=400)
f, t = _clean(from_), _clean(to)
if window == "custom" and not f and not t:
return JSONResponse({"error": {"type": "bad_request",
"message": "custom window requires from and/or to"}}, status_code=400)
def _parse(v):
if not v: return None
try: return datetime.fromisoformat(v)
except ValueError: return False
pf, pt = _parse(f), _parse(t)
if pf is False or pt is False:
return JSONResponse({"error": {"type": "bad_request",
"message": "from/to must be ISO-8601 with offset"}}, status_code=400)
if pf and pt and pf >= pt:
return JSONResponse({"error": {"type": "bad_request",
"message": "from must be < to"}}, status_code=400)
# --- one passthrough call ---
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, f, t),
)
payload = cur.fetchone()[0] or {}
return JSONResponse(payload) # JSON body unchanged
except Exception:
log.exception("inc-dashboard failed")
return JSONResponse({"error": {"type": "unknown",
"message": "INC dashboard is unavailable. Try again in a few seconds."}})
datetime is already imported. Leave the legacy /webhook/tickets handler untouched.cd ~/Downloads/projects/tracksolid_timescale_grafana_prod
scp dashboard_api_rev.py kianiadee@twala.rahamafresh.com:~/dashboard_api_staging_rev.py
scp deploy_dashboard_api_staging.sh kianiadee@twala.rahamafresh.com:~/
ssh kianiadee@twala.rahamafresh.com 'bash ~/deploy_dashboard_api_staging.sh'
The script stages the file and recreates the dashboard_api_staging container
(CORS already allows https://fleetops.fivetitude.com).
B=https://fleetapi.fivetitude.com
curl -s "$B/webhook/inc-dashboard" | head -c 400; echo # 200, today
curl -s "$B/webhook/inc-dashboard?window=month" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["metrics"])'
curl -s "$B/webhook/inc-dashboard?status=ACCEPTED" | python3 -c 'import sys,json;d=json.load(sys.stdin);print("open",d["metrics"]["open_now"])'
curl -s -o /dev/null -w "%{http_code}\n" "$B/webhook/inc-dashboard?window=bogus" # 400
curl -s -o /dev/null -w "%{http_code}\n" "$B/webhook/inc-dashboard?window=custom" # 400
Cross-check metrics.open_now against SELECT count(*) FROM tickets.inc WHERE is_actionable.
15_fleetops/src/index.html)#view-tickets map section (markup, ~lines 374–390).loadTickets() (/webhook/tickets), the CRQ circle layer, combined summary handling, TICKET_COLORS, ticketStats.crq, old showTicketPopup().Vehicle overlay machinery stays: loadLive() (/webhook/live-positions, 15s poll),
upsertVeh(), showVehPopup(), vehState(), ccColor(), pastel(),
plateTail(), BASEMAP, COST_CENTRE_COLORS, CC_PALETTE, escapeHtml,
updateVehScale(), the layers-panel builder. Reuse the Logistics/Fuel filterbar + .card/.span*
grid + num()/intg() + custom-range show/hide (:467).
#view-tickets markup (dashboard cards + map)<main> 12-col grid: metric cards (Open now, Closed in window, Open SLA breached/at-risk/ok/unknown, Closed SLA compliant/breached, Avg MTTR min→h, Closure rate per_day_avg + Chart.js sparkline from closure_rate.series); Map card .span12 with layer toggles + SLA legend; By status + By cluster tables .span6 each; Freshness line.incQs() → query string; loadInc() → fetch(${API_BASE}/webhook/inc-dashboard?…).metrics.by_cluster / metrics.by_status keys.sla_state (breached=--danger, at_risk=--warn, ok=--live, unknown=--parked); data = open.features.closed.features; toggle (default off).loadLive().geo_source==='cluster'); closed → add closed_at, mttr (min→h), sla_status.renderTicketKpis() → renderIncKpis(); switchTab('tickets') → initIncMap() + lazy loadInc(). Apply/↻/window-change → loadInc(); keep the 15s vehicle poll.*.features, drive cards/tables from metrics (open.features.length may be < metrics.open_now).cd ~/Downloads/projects/15_fleetops/src
python3 -m http.server 8080 # API_BASE defaults to https://fleetapi.fivetitude.com
# open http://localhost:8080 → Tickets tab
Confirm: cards + header KPIs populate; by-status/by-cluster match metrics; open INC SLA-colored
+ vehicles render; Closed INC toggle overlays the windowed set; Cluster/Status/Window + Apply refetches;
popups show documented fields; network tab shows only /webhook/inc-dashboard + /webhook/live-positions.
/webhook/tickets unchanged. DB migrations are forward-only but idempotent and unused by the old path.src/index.html.16_fleettickets history capture).geog.