From 583b822e4ae87bdd30012e14f65a2de76d5639ba Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sat, 8 Aug 2026 19:38:42 -0400 Subject: [PATCH] feat(directory): add subtype templates picker, fuzzy merge search, auto-ignore openbao, inherit parent host agent, truncate resource names, key badge positioning (#177) --- nodejs/config/inventory.sqlite | Bin 118784 -> 118784 bytes nodejs/plugins/discovery/docker.js | 6 +- nodejs/utils/agent_manager.js | 23 +++++- nodejs/views/directory.ejs | 121 +++++++++++++++++++++++++---- 4 files changed, 133 insertions(+), 17 deletions(-) diff --git a/nodejs/config/inventory.sqlite b/nodejs/config/inventory.sqlite index dba0c22ccece0c06c96c9280dff3819cf2fe2525..53b93fe8729614d4092986fa33018277b4bd00f8 100644 GIT binary patch delta 3509 zcma)8NpBoQ6dq@@$K&a7B%*}GiS2ld0;r-|yQ(aa95{dvERo_9iA+^j6$uOhVP62D zP6XnDf&zWZ0R_Pk2@JuU|ag#o7stiAOw8i@3!E*~C!<|51~0tyLVdhsV1qBO+D`2~xyygadD4OA>FH3E3wnx>EyhvL8+y4<)e>Rm24f#0S@oEL|>$wRVhR z;twJdimP+2&=AL|}Llvd`w1+5-Wv=Qqv{&p)x+ zR}(x-j!*P3I-M8IY@-p~kG4B4cwg>(n!mReU;R2t&+K2Ds8ifi5UEoYg;&(wB(VKm zr%(=o;e)h}+7E)|H+2+y01Q8e#h#AWfrw${PcL7Ia zg1se55u11KjXJuhBZ<7dr;m4kj%7ukD$yG}>iZpYgE?Um`NTo@RoX*U8j6VIi4V#- z&BTu1qkKyH1ESFB$jjZB&B+=Nqzba$jt$JA3Y*;yK>*Ud<4=#TUl@)B+IrYnhQtA1 zMdEsSa`V|Z&L-WH-BClg40|=&Yxn8AAzV4F5TO=&w@9FOD;^_)xu*g10K33WPSQ~i zq@pSWCEA}j$-P%1Np|S=u%)nYP^>E?SE9XEpU%QC z0^M*VnTJJ#B@8J>0}2=?)W!*nSuXJq-JseEG%et0X`mGtJMG(z_HEbzE_Yhdk!ZX9Yv=pue&=CyD>~QV(Hrf*wiXvQ+mAMb8l$3<{nj! z7v|nBpB@e{Wih9O0);~X2uBD=Lm+^fGhr}BNE-WXIGBM{FoY#(oxQZO3e;6~-hlY} zjSz+mK;gb|#AXqnC1{3AY#%ksH5bRHA)JNK^E%%)2Pa#eH{iLaUNQaUwATiL1j8c2Y2ylWhhV-8>*8K$aG|4sBl}R1Ry9C z(M(5JYWux%G;6niq|(+7!mV~LE{qHXu^}inkpw2;As|pe5Xl4rK{S>urg5y9y+2X~ ze;L>!{-Rk~HF|g6nP8i|ySU9s;nxmMD0h@VnmD~wDtF3B;&=}gH(U^m5|&s#+J?Gv zZqS%$KZI_h)`BE$&bT(d-QjzS^ry|HTFHg)L`c3|v4CI>?_s z&g}QR(&y4f`@24{{JeB?s-s!Rb5^=NuP~a&-CYjtr^#W_y{_=<%{=Zay2=-MeAVd+ zujR2qhbvr2)vNzczCEsTDvvPl;Hy%0MBdLG$&!usq`x+qtsJXOWyyv1l_%}Vm)hi! z9hw7u?ut>XeWj{b?EB#PkNu$ZvF}6a#{6n=3i~hr8}$tv_w%H8-1;_nqI&+&!AMUf(!1v8Pn@iRa(- em;BfMH=j7uPWYXvY3)KIxP7Me=}fEe^73zNu6)G+ diff --git a/nodejs/plugins/discovery/docker.js b/nodejs/plugins/discovery/docker.js index c90cc24..9fd3eb0 100644 --- a/nodejs/plugins/discovery/docker.js +++ b/nodejs/plugins/discovery/docker.js @@ -78,6 +78,8 @@ module.exports = { const ports = (c.Ports || []).map(p => p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}` : `${p.PrivatePort}`).join(', '); const isOwnStack = !!(stackProject && composeProject === stackProject); + const isIgnored = name.includes('openbao') || name.includes('bao-renewer') || composeService.includes('openbao') || composeService.includes('bao-renewer'); + resources.push({ kind: 'container', name: composeService || name, @@ -87,13 +89,15 @@ module.exports = { state: c.State, status: c.Status, ports: ports, + subType: 'docker', composeProject: composeProject || undefined, composeService: composeService || undefined, containerName: name, sourceId: stableKey, + ignored: isIgnored ? true : undefined, // Part of the deployment we are running inside: already // accounted for, not something to promote. - managed: isOwnStack ? true : undefined + managed: (isOwnStack || isIgnored) ? true : undefined } }); diff --git a/nodejs/utils/agent_manager.js b/nodejs/utils/agent_manager.js index 9cd39c7..4f4d924 100644 --- a/nodejs/utils/agent_manager.js +++ b/nodejs/utils/agent_manager.js @@ -290,11 +290,30 @@ class AgentManager { }; } - // Find connected/enrolled agent bound to a resource ID. + // Find connected/enrolled agent bound to a resource ID (or inherited from parent Host). async getAgentForResource(resourceId) { if (!resourceId) return null; const rows = await Agent.list().catch(() => []); - const agent = rows.find(a => a.resourceId === resourceId); + let agent = rows.find(a => a.resourceId === resourceId); + if (!agent) { + try { + const { Resource } = require('../models/resource'); + const { ResourceEdge } = require('../models/resource'); + const res = await Resource.get(resourceId); + if (res && res.kind === 'service') { + const edges = await ResourceEdge.list({ where: { childId: resourceId } }); + for (const edge of edges) { + const parentRes = await Resource.get(edge.parentId); + if (parentRes && parentRes.kind === 'host') { + agent = rows.find(a => a.resourceId === parentRes.id || (parentRes.metadata && parentRes.metadata.agentId === a.id)); + if (agent) break; + } + } + } + } catch (err) { + console.error('[AgentManager] parent agent lookup error:', err.message); + } + } if (!agent) return null; return agent.toPublic(this.liveState(agent.id)); } diff --git a/nodejs/views/directory.ejs b/nodejs/views/directory.ejs index b18d849..a812af2 100644 --- a/nodejs/views/directory.ejs +++ b/nodejs/views/directory.ejs @@ -94,11 +94,11 @@ {{{indentHtml}}} {{{caretHtml}}} {{#isHost}}{{/isHost}} - - {{name}} + + {{displayName}} - {{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}} {{#hasSecret}}Secret{{/hasSecret}} + {{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}} {{#metadata.isProduction}}Prod{{/metadata.isProduction}} {{^metadata.isProduction}}Dev{{/metadata.isProduction}} @@ -304,8 +304,20 @@
+ + - +
@@ -804,19 +816,48 @@ let disksHtml = ''; if (disks.length > 0) { - disksHtml = `
` + + disksHtml = `
MountTypeFSUsageTotal
` + disks.map(dk => ` + - - + + `).join('') + `
DeviceMountTypeFSUsageUsed / Total
${esc(dk.device || dk.mountpoint)} ${esc(dk.mountpoint)} ${esc(dk.drivetype || 'Disk')} ${esc(dk.fstype || 'N/A')}${fmtNum(dk.usage_percent)}%${fmtSize(dk.total_bytes)} +
+ ${fmtNum(dk.usage_percent)}% +
+
+
${fmtSize(dk.used_bytes || (dk.total_bytes * (dk.usage_percent || 0) / 100))} / ${fmtSize(dk.total_bytes)}
`; } else { disksHtml = `
Disk Usage: ${fmtNum(t.disk_usage_percent ?? 0)}% ${bar(t.disk_usage_percent)}
`; } - const agentVer = d.version || t.version || agent.version || 'v1.7.0'; + const hd = t.host_details || d.host_details || agent.hostDetails || {}; + let hostDetailsHtml = ''; + if (hd.os || hd.kernel || hd.hardware_model || hd.static_hostname) { + hostDetailsHtml = ` +
+
+
System & Hardware Details
+
+
+
+
Static Hostname: ${esc(hd.static_hostname || d.hostname || 'N/A')}
+
Operating System: ${esc(hd.os || d.os || 'N/A')}
+
Kernel / Arch: ${esc(hd.kernel || d.kernel || 'N/A')} (${esc(hd.arch || 'x86_64')})
+
Hardware Model: ${esc(hd.hardware_vendor || '')} ${esc(hd.hardware_model || 'N/A')}
+
Chassis: ${esc(hd.chassis || 'N/A')}
+
Firmware: ${esc(hd.firmware_version || 'N/A')} ${hd.firmware_date ? '(' + esc(hd.firmware_date) + ')' : ''}
+
Machine ID: ${esc(hd.machine_id || 'N/A')}
+
Boot ID: ${esc(hd.boot_id || 'N/A')}
+
+
+
`; + } + + const agentVer = d.version || t.version || agent.version || 'v1.8.0'; const loggedUsers = t.logged_users || d.logged_users || agent.loggedUsers || []; let loggedUsersHtml = ''; @@ -842,6 +883,8 @@ Last seen ${lastSeenStr} + ${hostDetailsHtml} +
@@ -1150,6 +1193,7 @@ } n.indentHtml = indentHtml; n.depth = depth; + n.displayName = (n.name && n.name.length > 16) ? n.name.substring(0, 16) + '…' : (n.name || ''); // A leaf gets a spacer of the same width, so names stay aligned down // the column instead of jittering by whether a row has children. n.caretHtml = n.children.length @@ -1420,21 +1464,52 @@ toggleFormFields(); } + function applySubtypeTemplate() { + const tmpl = $('#res-subtype-template').val(); + if (!tmpl) return; + $('#res-subtype').val(tmpl); + const presets = { + docker: { icon: 'fa-brands fa-docker', tagline: 'Docker Engine Container Workload', port: '2375' }, + systemd: { icon: 'fa-solid fa-gears', tagline: 'Linux Systemd Daemon Service' }, + postgresql: { icon: 'fa-solid fa-database', tagline: 'PostgreSQL Relational Database', port: '5432' }, + redis: { icon: 'fa-solid fa-cubes', tagline: 'Redis In-Memory Key-Value Cache', port: '6379' }, + proxmox: { icon: 'fa-solid fa-server', tagline: 'Proxmox VE Hypervisor Host', port: '8006' }, + wireguard: { icon: 'fa-solid fa-shield-halved', tagline: 'WireGuard VPN Encrypted Tunnel', port: '51820' }, + unifi: { icon: 'fa-solid fa-wifi', tagline: 'UniFi Network Controller', port: '8443' }, + k8s: { icon: 'fa-solid fa-dharmachakra', tagline: 'Kubernetes Cluster Worker Node', port: '6443' } + }; + if (presets[tmpl]) { + const p = presets[tmpl]; + if (p.icon) $('#res-icon').val(p.icon); + if (p.tagline) $('#res-tagline').val(p.tagline); + if (p.port && !$('#res-port').val()) $('#res-port').val(p.port); + updateIconPreview(); + } + } + async function openMergeModal(discoveredId, discName) { const targets = rawResources.filter(r => r.kind === 'host' || r.kind === 'site' || r.kind === 'service'); - let optionsHtml = targets.map(r => ``).join(''); app.modal.open({ title: `Merge '${esc(discName)}' into Existing Resource`, size: 'md', bodyHtml: `
-

Select an existing Directory resource to merge IP addresses, network interfaces, and OS telemetry into:

+

Search and select an existing Directory resource to merge IP addresses, network interfaces, and OS telemetry into:

- - + + +
+ ${targets.map((r, i) => ` + + `).join('')} +
`, @@ -1447,6 +1522,24 @@ }); } + function filterMergeTargets() { + const q = ($('#merge-search-input').val() || '').toLowerCase(); + $('#merge-targets-list button').each(function() { + const text = $(this).text().toLowerCase(); + if (text.includes(q)) { + $(this).removeClass('d-none'); + } else { + $(this).addClass('d-none'); + } + }); + } + + function selectMergeTarget(btn) { + $('#merge-targets-list button').removeClass('active'); + $(btn).addClass('active'); + $('#merge-target-id').val($(btn).data('id')); + } + async function submitMergeResource(discoveredId) { const targetId = $('#merge-target-id').val(); if (!targetId) return;