require('dotenv').config(); const express = require('express'); const axios = require('axios'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; const ZABBIX_URL = (process.env.ZABBIX_URL || 'https://monitor.stranto.com').replace(/\/$/, ''); const ZABBIX_TOKEN = process.env.ZABBIX_TOKEN || ''; const COUNTRY_TAG = process.env.COUNTRY_TAG || 'country'; const CUSTOMER_TAG_VALUE = process.env.CUSTOMER_TAG_VALUE || 'QWE'; app.use(express.static(path.join(__dirname, 'public'))); // ── Zabbix JSON-RPC helper ──────────────────────────────────────────────────── async function zabbix(method, params) { const { data } = await axios.post( `${ZABBIX_URL}/api_jsonrpc.php`, { jsonrpc: '2.0', method, params, id: 1 }, { timeout: 20000, headers: { Authorization: `Bearer ${ZABBIX_TOKEN}` } } ); if (data.error) { throw new Error(data.error.data || data.error.message || 'Zabbix API error'); } return data.result; } // ── Helpers ─────────────────────────────────────────────────────────────────── function getTag(tags, name) { const t = tags.find(t => t.tag === name); return t ? t.value : null; } function worstSeverity(triggers) { // Severity 0 (Not classified) and 1 (Information) are treated as OK const relevant = (triggers || []).filter(t => parseInt(t.priority, 10) >= 2); if (relevant.length === 0) return -1; return Math.max(...relevant.map(t => parseInt(t.priority, 10))); } function problemCount(triggers) { return (triggers || []).filter(t => parseInt(t.priority, 10) >= 2).length; } // ── Core data fetch ─────────────────────────────────────────────────────────── 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'], }); if (hosts.length === 0) return { hosts: [], triggersByHost: {} }; const hostIds = hosts.map(h => h.hostid); const triggers = await zabbix('trigger.get', { output: ['triggerid', 'description', 'priority', 'lastchange'], hostids: hostIds, monitored: true, filter: { value: 1 }, withLastEventUnacknowledged: true, expandDescription: true, selectHosts: ['hostid', 'name'], sortfield: 'priority', sortorder: 'DESC', }); const triggersByHost = {}; for (const trigger of triggers) { for (const host of trigger.hosts) { if (!triggersByHost[host.hostid]) triggersByHost[host.hostid] = []; triggersByHost[host.hostid].push(trigger); } } return { hosts, triggersByHost }; } // ── Routes ──────────────────────────────────────────────────────────────────── app.get('/api/countries', async (req, res) => { try { const hosts = await zabbix('host.get', { output: ['hostid'], tags: [{ tag: 'customer', value: CUSTOMER_TAG_VALUE, operator: '1' }], selectTags: 'extend', }); const countries = [...new Set( hosts.map(h => getTag(h.tags, COUNTRY_TAG)).filter(Boolean) )].sort(); res.json(countries); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/restaurants', async (req, res) => { try { const country = req.query.country || ''; const { hosts, triggersByHost } = await fetchKFCData(); const map = {}; for (const host of hosts) { const location = getTag(host.tags, 'location'); const hostCountry = getTag(host.tags, COUNTRY_TAG); if (!location) continue; if (country && hostCountry && hostCountry.toLowerCase() !== country.toLowerCase()) continue; if (!map[location]) { map[location] = { location, country: hostCountry, hosts: [], triggers: [] }; } map[location].hosts.push(host); map[location].triggers.push(...(triggersByHost[host.hostid] || [])); } const result = Object.values(map) .map(loc => { const problems = []; for (const host of loc.hosts) { for (const t of (triggersByHost[host.hostid] || [])) { if (parseInt(t.priority, 10) >= 2) { problems.push({ description: t.description, priority: parseInt(t.priority, 10), lastchange: parseInt(t.lastchange, 10), hostName: host.name, }); } } } problems.sort((a, b) => b.priority - a.priority || b.lastchange - a.lastchange); return { location: loc.location, country: loc.country, hostCount: loc.hosts.length, problemCount: problems.length, severity: worstSeverity(loc.triggers), problems, }; }) .sort((a, b) => { if ((a.country || '') < (b.country || '')) return -1; if ((a.country || '') > (b.country || '')) return 1; return a.location.localeCompare(b.location); }); res.json(result); } catch (e) { console.error(e); res.status(500).json({ error: e.message }); } }); 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; 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 result = Object.values(map) .map(g => { const problems = []; for (const host of g.hosts) { for (const t of (triggersByHost[host.hostid] || [])) { if (parseInt(t.priority, 10) >= 2) { problems.push({ description: t.description, priority: parseInt(t.priority, 10), lastchange: parseInt(t.lastchange, 10), hostName: host.name, }); } } } problems.sort((a, b) => b.priority - a.priority || b.lastchange - a.lastchange); return { groupid: g.groupid, name: g.name, country: g.country, hostCount: g.hosts.length, problemCount: problems.length, severity: worstSeverity(g.triggers), problems, }; }) .sort((a, b) => a.name.localeCompare(b.name)); res.json(result); } catch (e) { console.error(e); res.status(500).json({ error: e.message }); } }); app.get('/api/detail', async (req, res) => { try { const { type, id } = req.query; if (!type || !id) return res.status(400).json({ error: 'type and id required' }); const { hosts, triggersByHost } = await fetchKFCData(); const filtered = type === 'restaurant' ? hosts.filter(h => getTag(h.tags, 'location') === id) : hosts.filter(h => h.hostgroups.some(g => g.groupid === id)); const items = filtered.map(host => ({ hostid: host.hostid, name: host.name, host: host.host, tags: host.tags, severity: worstSeverity(triggersByHost[host.hostid] || []), problems: (triggersByHost[host.hostid] || []) .filter(t => parseInt(t.priority, 10) >= 2) .map(t => ({ description: t.description, priority: parseInt(t.priority, 10), lastchange: new Date(parseInt(t.lastchange, 10) * 1000).toLocaleString(), })), })); res.json(items); } catch (e) { console.error(e); res.status(500).json({ error: e.message }); } }); app.get('/api/config', (req, res) => { res.json({ zabbixUrl: ZABBIX_URL, customerTagValue: CUSTOMER_TAG_VALUE }); }); app.get('/api/stats', async (req, res) => { try { const hosts = await zabbix('host.get', { output: ['hostid'], tags: [{ tag: 'customer', value: CUSTOMER_TAG_VALUE, operator: '1' }], selectTags: 'extend', }); // Group host IDs by country const byCountry = {}; for (const host of hosts) { const country = getTag(host.tags, COUNTRY_TAG) || ''; if (!byCountry[country]) byCountry[country] = []; byCountry[country].push(host.hostid); } // Fetch item + trigger counts per country in parallel const stats = {}; await Promise.all(Object.entries(byCountry).map(async ([country, hostIds]) => { const [itemCount, triggerCount] = await Promise.all([ zabbix('item.get', { countOutput: true, hostids: hostIds, monitored: true }), zabbix('trigger.get', { countOutput: true, hostids: hostIds, monitored: true }), ]); stats[country] = { hostCount: hostIds.length, itemCount: parseInt(itemCount, 10), triggerCount: parseInt(triggerCount, 10), }; })); res.json(stats); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/health', async (req, res) => { try { const version = await zabbix('apiinfo.version', {}); res.json({ ok: true, zabbixVersion: version }); } catch (e) { res.status(503).json({ ok: false, error: e.message }); } }); // ── Start ───────────────────────────────────────────────────────────────────── app.listen(PORT, () => { console.log(`Zabbix KFC Dashboard → http://localhost:${PORT}`); if (!ZABBIX_TOKEN) console.warn('Warning: ZABBIX_TOKEN is not set'); });