-
By cluster
-
+
+
+
+
Clusters — Mombasa / Voi
+
@@ -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);