/** * Metrics wall taxonomy — maps chart catalog entries into sections/groups. * Versioned independently so new collectors can land without UI rewrites. */ export const TAXONOMY_VERSION = 1 /** @typedef {{ id: string, title: string, order: number, match: (id: string, meta: object) => boolean }} SectionDef */ /** @type {SectionDef[]} */ export const SECTIONS = [ { id: 'system', title: 'System', order: 10, match: (id, meta) => starts(id, [ 'system.', 'cpu.', 'mem.', 'disk_', 'disk.', 'net.', 'ip.', 'ipv4.', 'ipv6.', 'processes.', ]) || ctxStarts(meta, ['system.', 'cpu.', 'mem.', 'disk.', 'net.', 'ip.']), }, { id: 'containers', title: 'Containers', order: 20, match: (id, meta) => starts(id, ['docker.', 'cgroup.', 'peardock.']) || ctxStarts(meta, ['docker.', 'cgroup.', 'peardock.']), }, { id: 'storage', title: 'Storage', order: 30, match: (id, meta) => starts(id, ['zfs.', 'bcache.', 'dmcache.', 'md.', 'mdstat.', 'smart.', 'fs.', 'ioping.']) || ctxStarts(meta, ['zfs.', 'bcache.', 'dmcache.', 'md.', 'smart.', 'fs.']), }, { id: 'hardware', title: 'Hardware', order: 40, match: (id, meta) => starts(id, ['sensors.', 'gpu.', 'nvidia.', 'ipmi.', 'system.power', 'power.']) || ctxStarts(meta, ['sensors.', 'gpu.', 'ipmi.', 'power.']), }, { id: 'applications', title: 'Applications', order: 50, match: (id, meta) => starts(id, [ 'nginx.', 'apache.', 'redis.', 'mysql.', 'postgres.', 'memcached.', 'mongodb.', 'rabbitmq.', 'kafka.', 'nats.', 'unbound.', ]) || ctxStarts(meta, [ 'nginx.', 'apache.', 'redis.', 'mysql.', 'postgres.', 'memcached.', 'mongodb.', 'rabbitmq.', 'kafka.', 'nats.', 'unbound.', ]), }, { id: 'network-svc', title: 'Network services', order: 60, match: (id, meta) => starts(id, ['sockets.', 'net.conntrack', 'system.softnet', 'ip.tcp', 'ip.udp']) || ctxStarts(meta, ['sockets.', 'net.conntrack']), }, { id: 'observability', title: 'Observability', order: 70, match: (id, meta) => starts(id, ['ebpf.', 'statsd.', 'prometheus.', 'peardata.', 'self.']) || ctxStarts(meta, ['ebpf.', 'statsd.', 'prometheus.', 'peardata.']), }, { id: 'fleet', title: 'Fleet', order: 80, match: (id, meta) => starts(id, ['fleet.']) || ctxStarts(meta, ['fleet.']), }, { id: 'other', title: 'Other', order: 900, match: () => true, }, ] /** * @param {string} id * @param {string[]} prefixes */ function starts(id, prefixes) { const s = String(id || '') return prefixes.some((p) => s === p || s.startsWith(p)) } /** * @param {object} meta * @param {string[]} prefixes */ function ctxStarts(meta, prefixes) { const c = String(meta?.context || meta?.name || '') return prefixes.some((p) => c === p || c.startsWith(p)) } /** * @param {string} id * @param {object} meta */ export function sectionForChart(id, meta = {}) { for (const sec of SECTIONS) { if (sec.id === 'other') continue if (sec.match(id, meta)) return sec } return SECTIONS[SECTIONS.length - 1] } /** * @param {Record} catalog * @param {string} [filter] * @returns {{ * sections: Array<{ * id: string, * title: string, * order: number, * count: number, * groups: Array<{ family: string, charts: Array<{ id: string, meta: object }> }> * }> * }} */ export function groupCatalog(catalog, filter = '') { const q = String(filter || '') .trim() .toLowerCase() /** @type {Map> }>} */ const bySection = new Map() for (const sec of SECTIONS) { bySection.set(sec.id, { id: sec.id, title: sec.title, order: sec.order, families: new Map(), }) } const ids = Object.keys(catalog || {}) for (const id of ids) { const meta = catalog[id] || {} if (q && !matchesFilter(id, meta, q)) continue const sec = sectionForChart(id, meta) const bucket = bySection.get(sec.id) if (!bucket) continue const family = String(meta.family || meta.context || '_default') if (!bucket.families.has(family)) bucket.families.set(family, []) bucket.families.get(family).push({ id, meta }) } const sections = [...bySection.values()] .map((sec) => { const groups = [...sec.families.entries()] .map(([family, charts]) => ({ family, charts: charts.sort(compareCharts), })) .sort((a, b) => a.family.localeCompare(b.family)) const count = groups.reduce((n, g) => n + g.charts.length, 0) return { id: sec.id, title: sec.title, order: sec.order, count, groups } }) .filter((s) => s.count > 0) .sort((a, b) => a.order - b.order) return { sections } } /** * @param {string} id * @param {object} meta * @param {string} q lowercase */ function matchesFilter(id, meta, q) { const hay = [ id, meta.title, meta.name, meta.context, meta.family, meta.plugin, meta.units, ] .filter(Boolean) .join(' ') .toLowerCase() return hay.includes(q) } /** * @param {{ id: string, meta: object }} a * @param {{ id: string, meta: object }} b */ function compareCharts(a, b) { const pa = Number(a.meta?.priority) || 99999 const pb = Number(b.meta?.priority) || 99999 if (pa !== pb) return pa - pb return a.id.localeCompare(b.id) } /** Time presets for the global metrics bar (seconds of history). */ export const TIME_PRESETS = [ { id: '1m', label: '1m', seconds: 60 }, { id: '5m', label: '5m', seconds: 300 }, { id: '15m', label: '15m', seconds: 900 }, { id: '1h', label: '1h', seconds: 3600 }, { id: '6h', label: '6h', seconds: 21600 }, ]