diff --git a/nodejs/config/inventory.sqlite b/nodejs/config/inventory.sqlite index dba0c22..53b93fe 100644 Binary files a/nodejs/config/inventory.sqlite and b/nodejs/config/inventory.sqlite differ 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;