diff --git a/hbd/server/templates/plugins.html b/hbd/server/templates/plugins.html
index 10896c8..0cb3eef 100644
--- a/hbd/server/templates/plugins.html
+++ b/hbd/server/templates/plugins.html
@@ -383,7 +383,7 @@
- {% 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 %}
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:
text = 'Loaded';
}
@@ -694,6 +707,7 @@
case 'memory_monitor': html = renderMemoryTable(cached.data); break;
case 'disk_monitor': html = renderDiskTables(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 'filesystem_info':html = renderFilesystemTable(cached.data); break;
default: html = renderGenericTable(cached.data); break;
@@ -1024,6 +1038,66 @@
return html;
}
+ function renderZfsTables(d) {
+ const pools = d.pools || {};
+ const names = Object.keys(pools);
+ if (names.length === 0) return '
No ZFS pools found
';
+
+ const healthCls = h => {
+ if (!h || h === 'ONLINE') return 'pct-ok';
+ if (h === 'DEGRADED') return 'pct-warn';
+ return 'pct-crit';
+ };
+
+ let pt = '
'
+ + '| Pool | Health | '
+ + 'Size | Used | '
+ + 'Free | Cap % | '
+ + 'Frag % | Dedup | '
+ + '
';
+ 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 += `
+ | ${escHtml(name)} |
+ ${escHtml(p.health || '—')} |
+ ${formatBytes(p.size || 0)} |
+ ${formatBytes(p.alloc || 0)} |
+ ${formatBytes(p.free || 0)} |
+ ${cap.toFixed(1)}% |
+ ${p.frag != null ? p.frag.toFixed(1) + '%' : '—'} |
+ ${p.dedup != null ? p.dedup.toFixed(2) + 'x' : '—'} |
+
`;
+ }
+ pt += '
';
+
+ const hasIo = names.some(n => pools[n].read_ops != null);
+ if (!hasIo) return pt;
+
+ let iot = '
'
+ + '| Pool | '
+ + 'Read ops | Write ops | '
+ + 'Read BW | Write BW | '
+ + '
';
+ for (const name of names) {
+ const p = pools[name];
+ iot += `
+ | ${escHtml(name)} |
+ ${p.read_ops != null ? p.read_ops.toLocaleString() : '—'} |
+ ${p.write_ops != null ? p.write_ops.toLocaleString() : '—'} |
+ ${p.read_bw != null ? formatBytes(p.read_bw) : '—'} |
+ ${p.write_bw != null ? formatBytes(p.write_bw) : '—'} |
+
`;
+ }
+ iot += '
';
+
+ return `
`;
+ }
+
function renderGenericTable(d) {
let html = '
| Field | Value |
';
for (const [k, v] of Object.entries(d)) {