diff --git a/src/index.html b/src/index.html index 5bdf733..c72dfe8 100644 --- a/src/index.html +++ b/src/index.html @@ -138,7 +138,11 @@ .card.span8 { grid-column: span 8; } .card.span6 { grid-column: span 6; } .card.span4 { grid-column: span 4; } - @media (max-width: 1100px) { .card.span8, .card.span6, .card.span4 { grid-column: span 12; } } + .card.span3 { grid-column: span 3; } + @media (max-width: 1100px) { .card.span8, .card.span6, .card.span4 { grid-column: span 12; } .card.span3 { grid-column: span 6; } } + @media (max-width: 680px) { .card.span3 { grid-column: span 12; } } + /* Tickets grid: size cards to their content (no stretched empty space). */ + #tk-main { align-items: start; } .card h2 { margin: 0 0 12px; font-size: 12px; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); font-weight: 700; display: flex; align-items: center; gap: 8px; @@ -180,7 +184,7 @@ .loading { opacity: .45; pointer-events: none; } /* ── Tickets / INC dashboard map ─────────────────────────────────────── */ - .map-wrap { position: relative; height: 540px; } + .map-wrap { position: relative; height: 62vh; min-height: 520px; } #tk-map { position: absolute; inset: 0; border-radius: 8px; overflow: hidden; } .map-ctl { position: absolute; z-index: 5; font: 600 11px system-ui; color: #fff; user-select: none; } #tk-layers { right: 10px; top: 10px; } @@ -421,19 +425,24 @@ -
+

Closures — daily

-
+

By status

Loading…
-
-

By cluster

-
Loading…
+
+

Clusters — Nairobi

+
Loading…
+
+ +
+

Clusters — Mombasa / Voi

+
Loading…
@@ -846,14 +855,15 @@ $('fuf-refresh').addEventListener('click', loadFuel); // The header KPI strip is shared, so we cache the last logistics totals and // re-render them when switching back from Tickets. let lastTotals = null, lastFuelL = 0; -// INC ticket KPIs + metrics are rendered by the INC dashboard section below -// (renderIncKpis / renderIncMetrics), driven by GET /webhook/inc-dashboard. +// INC ticket metrics render into the INC overview card (renderIncMetrics) in the +// dashboard section below — driven by GET /webhook/inc-dashboard. The shared header +// KPI strip is intentionally left empty on the Tickets tab. function switchTab(name) { document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab === name)); document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === `view-${name}`)); if (name === 'tickets') { - if (incData) renderIncKpis(incData.metrics); + $('kpis').innerHTML = ''; // INC metrics live in the INC overview card, not the header loadInc(); // (re)load INC data first — independent of the basemap initIncMap(); // then build the map (lazy) / just resize if built } else if (name === 'fuel') { @@ -885,6 +895,12 @@ const EMPTY_FC = { type: 'FeatureCollection', features: [] }; const SLA_COLORS = { breached: '#ef5b5b', at_risk: '#f0a93b', ok: '#2dd4a7', unknown: '#6b7280' }; const SLA_LABELS = { breached: 'Breached', at_risk: 'At risk', ok: 'OK', unknown: 'Unknown' }; const CLOSED_COLOR = '#94a3b8'; // muted slate — closed tickets (status irrelevant) +// Coast (Mombasa / Voi) cluster classifier — splits the cluster breakdown by name +// (the feed's region field is noisy; cluster names classify cleanly). +const COAST_HINTS = ['coast', 'mombasa', 'voi', 'nyali', 'mtwapa', 'kiembeni', 'vipingo', + 'bombolulu', 'kizingo', 'kwale', 'shanzu', 'likoni', 'mariakani', 'bamburi', 'changamwe', + 'diani', 'ukunda', 'malindi', 'kilifi', 'mtongwe']; +const clusterIsCoast = (name) => { const s = (name || '').toLowerCase(); return COAST_HINTS.some(k => s.includes(k)); }; const COST_CENTRE_COLORS = { 'isp': '#3b82f6', 'osp': '#E8954A', 'osp patrol': '#f97316', 'fds': '#22c55e', @@ -1040,20 +1056,7 @@ function updateVehScale() { document.getElementById('tk-map').style.setProperty('--veh-scale', (0.42 + t * 0.78).toFixed(3)); } -// ── render: header KPIs + metric strip + tables + closure chart ───────────── -function renderIncKpis(m) { - if (activeTab() !== 'tickets') return; - m = m || {}; const so = (m.sla && m.sla.open) || {}; - const k = [ - ['accent', intg(m.open_now), 'INC open'], - ['warn', intg(so.breached), 'Breached'], - ['', intg(m.closed_in_window), 'Closed (win)'], - ['live', mttrFmt(m.avg_mttr_min), 'Avg MTTR'], - ]; - $('kpis').innerHTML = k.map(([c, v, l]) => - `
${v}${l}
`).join(''); -} - +// ── render: metric strip + tables + closure chart ─────────────────────────── function renderIncMetrics(m, freshness) { m = m || {}; const so = (m.sla && m.sla.open) || {}, sc = (m.sla && m.sla.closed) || {}, cr = m.closure_rate || {}; @@ -1079,9 +1082,14 @@ function incTable(obj) { } function renderIncTables(m) { m = m || {}; - const s = incTable(m.by_status), c = incTable(m.by_cluster); - $('tk-status-wrap').innerHTML = s.html; $('tk-status-count').textContent = s.n ? `(${s.n})` : ''; - $('tk-cluster-wrap').innerHTML = c.html; $('tk-cluster-count').textContent = c.n ? `(${c.n})` : ''; + const s = incTable(m.by_status); + $('tk-status-wrap').innerHTML = s.html; $('tk-status-count').textContent = s.n ? `(${s.n})` : ''; + // Split the cluster breakdown into Nairobi vs Mombasa/Voi (Coast) by cluster name. + const nbo = {}, coast = {}; + for (const [name, n] of Object.entries(m.by_cluster || {})) (clusterIsCoast(name) ? coast : nbo)[name] = n; + const cn = incTable(nbo), cc = incTable(coast); + $('tk-nbo-wrap').innerHTML = cn.html; $('tk-nbo-count').textContent = cn.n ? `(${cn.n})` : ''; + $('tk-coast-wrap').innerHTML = cc.html; $('tk-coast-count').textContent = cc.n ? `(${cc.n})` : ''; } function renderClosureChart(cr) { @@ -1122,7 +1130,6 @@ async function loadInc() { const j = await api(`/webhook/inc-dashboard?${incQs()}`); incData = j; if (!incDropdownsInit && j.metrics) { initIncDropdowns(j.metrics); incDropdownsInit = true; } - renderIncKpis(j.metrics); renderIncMetrics(j.metrics, j.freshness); renderIncTables(j.metrics); renderClosureChart(j.metrics && j.metrics.closure_rate);