feat: add ZFS pool renderer to Host Overview

Add renderZfsTables() to plugins.html with health/capacity/frag/dedup
table and cumulative I/O table; colour-code health and capacity thresholds;
add zfs_monitor to plugin_order and summary/render dispatch.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-05-02 13:21:28 -04:00
parent 46f8c32c0b
commit 72fc82b91f
+75 -1
View File
@@ -383,7 +383,7 @@
</div> </div>
<div class="host-body"> <div class="host-body">
{% set plugin_order = ['os_info','cpu_monitor','memory_monitor','disk_monitor','network_monitor','nagios_runner','filesystem_info'] %} {% set plugin_order = ['os_info','cpu_monitor','memory_monitor','disk_monitor','network_monitor','zfs_monitor','nagios_runner','filesystem_info'] %}
{% for plugin in plugin_order if plugin in host.plugins %} {% for plugin in plugin_order if plugin in host.plugins %}
<div class="plugin-accordion collapsed" <div class="plugin-accordion collapsed"
data-hostname="{{ host.name }}" data-hostname="{{ host.name }}"
@@ -673,6 +673,19 @@
text = `${count} filesystem${count !== 1 ? 's' : ''}`; text = `${count} filesystem${count !== 1 ? 's' : ''}`;
break; break;
} }
case 'zfs_monitor': {
const pools = d.pools || {};
const names = Object.keys(pools);
if (names.length === 0) { text = 'No pools'; break; }
const degraded = names.filter(n => pools[n].health && pools[n].health !== 'ONLINE');
text = names.map(n => {
const p = pools[n];
const cap = p.capacity != null ? ` ${p.capacity.toFixed(0)}%` : '';
return `${n}${cap}`;
}).join(' · ');
if (degraded.length) text += `${degraded.map(n => pools[n].health).join(',')}`;
break;
}
default: default:
text = 'Loaded'; text = 'Loaded';
} }
@@ -694,6 +707,7 @@
case 'memory_monitor': html = renderMemoryTable(cached.data); break; case 'memory_monitor': html = renderMemoryTable(cached.data); break;
case 'disk_monitor': html = renderDiskTables(cached.data); break; case 'disk_monitor': html = renderDiskTables(cached.data); break;
case 'network_monitor':html = renderNetworkTables(cached.data); break; case 'network_monitor':html = renderNetworkTables(cached.data); break;
case 'zfs_monitor': html = renderZfsTables(cached.data); break;
case 'nagios_runner': html = renderNagiosTable(cached.data); break; case 'nagios_runner': html = renderNagiosTable(cached.data); break;
case 'filesystem_info':html = renderFilesystemTable(cached.data); break; case 'filesystem_info':html = renderFilesystemTable(cached.data); break;
default: html = renderGenericTable(cached.data); break; default: html = renderGenericTable(cached.data); break;
@@ -1024,6 +1038,66 @@
return html; return html;
} }
function renderZfsTables(d) {
const pools = d.pools || {};
const names = Object.keys(pools);
if (names.length === 0) return '<div class="no-data">No ZFS pools found</div>';
const healthCls = h => {
if (!h || h === 'ONLINE') return 'pct-ok';
if (h === 'DEGRADED') return 'pct-warn';
return 'pct-crit';
};
let pt = '<table class="data-table"><thead><tr>'
+ '<th>Pool</th><th>Health</th>'
+ '<th class="num">Size</th><th class="num">Used</th>'
+ '<th class="num">Free</th><th class="num">Cap %</th>'
+ '<th class="num">Frag %</th><th class="num">Dedup</th>'
+ '</tr></thead><tbody>';
for (const name of names) {
const p = pools[name];
const cap = p.capacity != null ? p.capacity : 0;
const capCls = cap > 90 ? 'pct-crit' : cap > 75 ? 'pct-warn' : 'pct-ok';
pt += `<tr>
<td class="iface-name">${escHtml(name)}</td>
<td class="${healthCls(p.health)}">${escHtml(p.health || '—')}</td>
<td class="num">${formatBytes(p.size || 0)}</td>
<td class="num">${formatBytes(p.alloc || 0)}</td>
<td class="num">${formatBytes(p.free || 0)}</td>
<td class="num ${capCls}">${cap.toFixed(1)}%</td>
<td class="num">${p.frag != null ? p.frag.toFixed(1) + '%' : '—'}</td>
<td class="num">${p.dedup != null ? p.dedup.toFixed(2) + 'x' : '—'}</td>
</tr>`;
}
pt += '</tbody></table>';
const hasIo = names.some(n => pools[n].read_ops != null);
if (!hasIo) return pt;
let iot = '<table class="data-table"><thead><tr>'
+ '<th>Pool</th>'
+ '<th class="num">Read ops</th><th class="num">Write ops</th>'
+ '<th class="num">Read BW</th><th class="num">Write BW</th>'
+ '</tr></thead><tbody>';
for (const name of names) {
const p = pools[name];
iot += `<tr>
<td class="iface-name">${escHtml(name)}</td>
<td class="num">${p.read_ops != null ? p.read_ops.toLocaleString() : '—'}</td>
<td class="num">${p.write_ops != null ? p.write_ops.toLocaleString() : '—'}</td>
<td class="num">${p.read_bw != null ? formatBytes(p.read_bw) : '—'}</td>
<td class="num">${p.write_bw != null ? formatBytes(p.write_bw) : '—'}</td>
</tr>`;
}
iot += '</tbody></table>';
return `<div class="flex-tables">
<div><div class="table-section-label">Pools</div>${pt}</div>
<div><div class="table-section-label">I/O (cumulative)</div>${iot}</div>
</div>`;
}
function renderGenericTable(d) { function renderGenericTable(d) {
let html = '<table class="data-table"><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody>'; let html = '<table class="data-table"><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody>';
for (const [k, v] of Object.entries(d)) { for (const [k, v] of Object.entries(d)) {