diff --git a/nodejs/routes/api_directory_admin.js b/nodejs/routes/api_directory_admin.js index 38005ac..4a2221a 100644 --- a/nodejs/routes/api_directory_admin.js +++ b/nodejs/routes/api_directory_admin.js @@ -784,7 +784,8 @@ router.post('/resources/:id/driver-action', async (req, res, next) => { if (!resource) return res.status(404).json({ status: 'error', message: 'resource not found' }); const { action, params } = req.body || {}; if (!action) return res.status(400).json({ status: 'error', message: 'action is required' }); - const result = await DriverRegistry.execAction(resource, action, params || {}); + const actionParams = params || req.body || {}; + const result = await DriverRegistry.execAction(resource, action, actionParams); res.json({ status: 'ok', resourceId: resource.id, result }); } catch (err) { next(err); } }); diff --git a/nodejs/utils/agent_manager.js b/nodejs/utils/agent_manager.js index 4f4d924..68aad28 100644 --- a/nodejs/utils/agent_manager.js +++ b/nodejs/utils/agent_manager.js @@ -120,11 +120,17 @@ class AgentManager { const discovery = { hostname: payload.hostname || '', ip_addresses: Array.isArray(payload.ip_addresses) ? payload.ip_addresses : [], + public_ip: payload.public_ip || '', os: payload.os || '', kernel: payload.kernel || '', cpu: payload.cpu || '', + cpu_details: payload.cpu_details || {}, ram_total_gb: payload.ram_total_gb || 0, + ram_details: payload.ram_details || {}, disk_total_gb: payload.disk_total_gb || 0, + disks: Array.isArray(payload.disks) ? payload.disks : [], + logged_users: Array.isArray(payload.logged_users) ? payload.logged_users : [], + host_details: payload.host_details || {}, location: payload.location || 'default', // The agent's enabled capabilities (from its local agent.yml). The agent // is the authoritative source for what it will actually do. @@ -134,6 +140,107 @@ class AgentManager { await this.applyDiscoveryToDirectory(agent, discovery); } + async handleTelemetry(agent, payload) { + await this.touch(agent, { + lastTelemetry: { + cpu_usage_percent: payload.cpu_usage_percent || 0, + cpu_details: payload.cpu_details || {}, + ram_usage_percent: payload.ram_usage_percent || 0, + ram_details: payload.ram_details || {}, + disk_usage_percent: payload.disk_usage_percent || 0, + disks: Array.isArray(payload.disks) ? payload.disks : [], + logged_users: Array.isArray(payload.logged_users) ? payload.logged_users : [], + host_details: payload.host_details || {}, + zfs_health: payload.zfs_health || 'N/A', + gpu_usage_percent: payload.gpu_usage_percent ?? -1, + timestamp: payload.timestamp || new Date().toISOString() + } + }); + } + + async handleHeartbeat(agent, payload, ws) { + await this.touch(agent); + try { + ws.send(JSON.stringify({ + type: 'heartbeat_ack', + payload: { timestamp: new Date().toISOString() } + })); + } catch (e) {} + } + + async handleResponse(agent, payload) { + const state = this.live.get(agent.id); + if (state) { + state.lastResponse = { + status: payload.status || 'ok', + message: payload.message || '', + output: payload.output || '', + timestamp: new Date().toISOString() + }; + } + await this.touch(agent); + } + + async sendCommand(agent, commandType, payload = {}, isHighRisk = false) { + const state = this.live.get(agent.id); + if (!state || !state.ws || state.ws.readyState !== 1) { + throw new Error(`Agent "${agent.name}" is not connected`); + } + + const finalPayload = { ...payload }; + if (isHighRisk) finalPayload.signature = await this.signPayload(finalPayload); + + const message = { type: commandType, payload: finalPayload }; + state.ws.send(JSON.stringify(message)); + return message; + } + + // Live view for one agent, for merging into its row. + liveState(agentId) { + const state = this.live.get(agentId); + if (!state) return { connected: false, lastResponse: null }; + return { + connected: !!(state.ws && state.ws.readyState === 1), + ipAddress: state.ipAddress, + connectedAt: state.connectedAt, + lastResponse: state.lastResponse || null + }; + } + + // 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(() => []); + 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)); + } + + // Every enrolled agent, connected or not. + async listAgents() { + const rows = await Agent.list(); + return rows.map(a => a.toPublic(this.liveState(a.id))); + } + // An agent runs ON the host it describes, which makes it the most // authoritative source the directory has -- more so than a hypervisor API or // a network scan. It previously updated nothing at all: the facts sat on an diff --git a/nodejs/views/directory.ejs b/nodejs/views/directory.ejs index 7a963d3..20f7e15 100644 --- a/nodejs/views/directory.ejs +++ b/nodejs/views/directory.ejs @@ -810,8 +810,8 @@ // CPU details const cpuDet = t.cpu_details || d.cpu_details || {}; const cpuModel = cpuDet.model || d.cpu || 'Unknown CPU'; - const cpuCores = cpuDet.cores || 'N/A'; - const cpuThreads = cpuDet.threads || 'N/A'; + const cpuCores = (cpuDet.cores != null && cpuDet.cores > 0) ? cpuDet.cores : (d.cpu_details && d.cpu_details.cores ? d.cpu_details.cores : 'N/A'); + const cpuThreads = (cpuDet.threads != null && cpuDet.threads > 0) ? cpuDet.threads : (d.cpu_details && d.cpu_details.threads ? d.cpu_details.threads : 'N/A'); const cpuSpeed = cpuDet.mhz ? (cpuDet.mhz >= 1000 ? (cpuDet.mhz / 1000).toFixed(2) + ' GHz' : cpuDet.mhz.toFixed(0) + ' MHz') : ''; // Disks @@ -1579,8 +1579,10 @@ app.messages.action(`Sending '${action}' desktop command to agent...`, $('#res-modal'), 'info'); const res = await app.api.post(`directory-admin/resources/${resourceId}/driver-action`, { action: 'desktop_control', - subAction: action, - user: username + params: { + subAction: action, + user: username + } }); app.messages.action(`Desktop action '${action}' completed successfully!`, $('#res-modal'), 'success'); } catch (err) {