diff --git a/dashboard/js/grid.js b/dashboard/js/grid.js
new file mode 100644
index 0000000..649f11b
--- /dev/null
+++ b/dashboard/js/grid.js
@@ -0,0 +1,145 @@
+const Grid = (() => {
+ const _agents = new Map();
+
+ function statusClass(agent) {
+ if (agent.status === 'offline') return 't-off';
+ const m = _agents.get(agent.id)?.metrics;
+ if (!m) return '';
+ const cfg = App.serverConfig;
+ const errThreshold = cfg?.err_cpu ?? 85;
+ const warnThreshold = cfg?.warn_cpu ?? 70;
+ if ((m.cpu_percent ?? 0) >= errThreshold) return 't-err';
+ if ((m.cpu_percent ?? 0) >= warnThreshold || (m.hdd_used && m.hdd_total && (m.hdd_used / m.hdd_total * 100) >= (cfg?.warn_disk ?? 75))) return 't-warn';
+ return '';
+ }
+
+ function ledClass(status) {
+ return { online: 's-ok', warn: 's-warn', err: 's-err', offline: 's-off' }[status] ?? 's-off';
+ }
+
+ function fmt(bytes) {
+ if (!bytes) return '—';
+ if (bytes < 1024) return bytes + 'o';
+ if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + 'Ko';
+ if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + 'Mo';
+ return (bytes / 1024 ** 3).toFixed(1) + 'Go';
+ }
+
+ function fmtPct(val) {
+ return val != null ? val.toFixed(0) + '%' : '—';
+ }
+
+ function gFill(pct) {
+ const cfg = App.serverConfig;
+ if (pct >= (cfg?.err_cpu ?? 85)) return 'e';
+ if (pct >= (cfg?.warn_cpu ?? 70)) return 'w';
+ return '';
+ }
+
+ function renderTile(agent, metrics) {
+ const id = agent.id;
+ const sc = statusClass(agent);
+ const offline = agent.status === 'offline';
+
+ const cpu = metrics?.cpu_percent ?? null;
+ const memPct = (metrics?.memory_used && metrics?.memory_total)
+ ? metrics.memory_used / metrics.memory_total * 100 : null;
+ const diskPct = (metrics?.hdd_used && metrics?.hdd_total)
+ ? metrics.hdd_used / metrics.hdd_total * 100 : null;
+
+ const uptimeSec = metrics?.uptime;
+ let uptimeStr = '';
+ if (uptimeSec) {
+ const d = Math.floor(uptimeSec / 86400);
+ const h = Math.floor((uptimeSec % 86400) / 3600);
+ uptimeStr = d > 0 ? `${d}j ${h}h` : `${h}h`;
+ }
+
+ const iconContent = `
+
+ `;
+
+ return `