From 101aaa6e32bcb95ee3e209a79eda50ebac618779 Mon Sep 17 00:00:00 2001 From: Christoph Gasser Date: Tue, 21 Apr 2026 14:59:17 +0200 Subject: [PATCH] Rework device type view to group by deviceType tag - /api/devices now groups hosts by deviceType tag instead of host group; hosts without the tag are skipped - /api/detail device lookup filters by deviceType tag instead of groupid - getTag() is now case-insensitive on the tag name - Removed selectHostGroups from fetchKFCData (no longer needed) - Frontend: hex id and Zabbix deep-link use deviceType instead of groupid - Smaller hex label (1.2rem) for device type view via hex-item--device class - Skip DOM re-render on auto-refresh when data is unchanged (lastDataKey diff) Co-Authored-By: Claude Sonnet 4.6 --- public/app.js | 18 +++++++++++++----- public/style.css | 5 +++++ server.js | 30 ++++++++++++++---------------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/public/app.js b/public/app.js index dc189de..b2c34ff 100644 --- a/public/app.js +++ b/public/app.js @@ -36,6 +36,7 @@ let refreshTimer = null; let zabbixUrl = ''; let customerTagValue = 'QWE'; let hasRendered = false; +let lastDataKey = null; const REFRESH_MS = 30_000; /* ── Boot ────────────────────────────────────────────────────────────────── */ @@ -58,6 +59,7 @@ function initControls() { btn.classList.add('active'); currentView = btn.dataset.view; hasRendered = false; + lastDataKey = null; loadData(); }); }); @@ -87,8 +89,12 @@ async function loadData() { apiFetch(endpoint), apiFetch('/api/stats').catch(() => ({})), ]); - renderCountryColumns(items, stats); - updateSummary(items); + const key = JSON.stringify({ items, stats }); + if (key !== lastDataKey) { + lastDataKey = key; + renderCountryColumns(items, stats); + updateSummary(items); + } setLastUpdated(new Date()); hasRendered = true; } catch (e) { @@ -195,13 +201,13 @@ function buildHex(item) { const sev = item.severity; const info = sevInfo(sev); const label = item.location || item.name || '—'; - const id = item.location || item.groupid || ''; + const id = item.location || item.deviceType || ''; const type = item.location ? 'restaurant' : 'device'; const issues = item.problemCount || 0; const hosts = item.hostCount || 0; const el = document.createElement('div'); - el.className = `hex-item sev-${sev}`; + el.className = `hex-item sev-${sev}${type === 'device' ? ' hex-item--device' : ''}`; el.style.background = info.color; el.title = `${label}\n${hosts} host(s) · ${issues} issue(s) · ${info.label}\n\nClick: details Double-click: open in Zabbix`; @@ -364,10 +370,12 @@ function buildZabbixProblemsUrl(type, id) { p.push(`tags[1][value]=${encodeURIComponent(id)}`); p.push(`tags[1][operator]=1`); } else { - p.push(`groupids[]=${encodeURIComponent(id)}`); p.push(`tags[0][tag]=customer`); p.push(`tags[0][value]=${encodeURIComponent(customerTagValue)}`); p.push(`tags[0][operator]=1`); + p.push(`tags[1][tag]=deviceType`); + p.push(`tags[1][value]=${encodeURIComponent(id)}`); + p.push(`tags[1][operator]=1`); } return `${zabbixUrl}/zabbix.php?${p.join('&')}`; diff --git a/public/style.css b/public/style.css index e242980..13fbb44 100644 --- a/public/style.css +++ b/public/style.css @@ -317,6 +317,11 @@ header { .hex-item:active { transform: scale(0.97); } +.hex-item--device .hex-label { + font-size: 1.2rem; + font-weight: 700; +} + .hex-label { font-size: 1.8rem; font-weight: 800; diff --git a/server.js b/server.js index c92c5f1..36b7635 100644 --- a/server.js +++ b/server.js @@ -29,7 +29,8 @@ async function zabbix(method, params) { // ── Helpers ─────────────────────────────────────────────────────────────────── function getTag(tags, name) { - const t = tags.find(t => t.tag === name); + const lower = name.toLowerCase(); + const t = tags.find(t => t.tag.toLowerCase() === lower); return t ? t.value : null; } @@ -48,10 +49,9 @@ function problemCount(triggers) { async function fetchKFCData() { const hosts = await zabbix('host.get', { - output: ['hostid', 'host', 'name'], - tags: [{ tag: 'customer', value: CUSTOMER_TAG_VALUE, operator: '1' }], - selectTags: 'extend', - selectHostGroups: ['groupid', 'name'], + output: ['hostid', 'host', 'name'], + tags: [{ tag: 'customer', value: CUSTOMER_TAG_VALUE, operator: '1' }], + selectTags: 'extend', }); if (hosts.length === 0) return { hosts: [], triggersByHost: {} }; @@ -158,22 +158,20 @@ app.get('/api/restaurants', async (req, res) => { app.get('/api/devices', async (req, res) => { try { - const country = req.query.country || ''; const { hosts, triggersByHost } = await fetchKFCData(); const map = {}; for (const host of hosts) { const hostCountry = getTag(host.tags, COUNTRY_TAG); - if (country && hostCountry && hostCountry.toLowerCase() !== country.toLowerCase()) continue; + const deviceType = getTag(host.tags, 'deviceType'); + if (!deviceType) continue; - for (const group of host.hostgroups) { - const key = `${group.groupid}__${hostCountry || ''}`; - if (!map[key]) { - map[key] = { groupid: group.groupid, name: group.name, country: hostCountry, hosts: [], triggers: [] }; - } - map[key].hosts.push(host); - map[key].triggers.push(...(triggersByHost[host.hostid] || [])); + const key = `${deviceType}__${hostCountry || ''}`; + if (!map[key]) { + map[key] = { deviceType, name: deviceType, country: hostCountry, hosts: [], triggers: [] }; } + map[key].hosts.push(host); + map[key].triggers.push(...(triggersByHost[host.hostid] || [])); } const result = Object.values(map) @@ -193,7 +191,7 @@ app.get('/api/devices', async (req, res) => { } problems.sort((a, b) => b.priority - a.priority || b.lastchange - a.lastchange); return { - groupid: g.groupid, + deviceType: g.deviceType, name: g.name, country: g.country, hostCount: g.hosts.length, @@ -220,7 +218,7 @@ app.get('/api/detail', async (req, res) => { const filtered = type === 'restaurant' ? hosts.filter(h => getTag(h.tags, 'location') === id) - : hosts.filter(h => h.hostgroups.some(g => g.groupid === id)); + : hosts.filter(h => getTag(h.tags, 'deviceType') === id); const items = filtered.map(host => ({ hostid: host.hostid,