feat: M10 — agent metadata collection, Apache httpd + HAProxy target connectors

Agents now report OS, architecture, IP address, hostname, and version
via heartbeat using runtime.GOOS, runtime.GOARCH, and net.Dial. New
migration adds columns to agents table. Heartbeat handler, service,
and repository updated to accept and persist metadata. GUI shows
OS/Arch in agent list and full system info in agent detail page.

Apache httpd connector: separate cert/chain/key files, apachectl
configtest validation, graceful reload. HAProxy connector: combined
PEM file (cert+chain+key), optional config validation, reload.
Both wired into agent binary's target connector switch.

14 tests for new connectors. All existing tests updated for new
Heartbeat/UpdateHeartbeat signatures. Docs updated across README,
architecture, concepts, and connectors guides.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-20 02:19:28 -04:00
parent e06ea310a8
commit 07275bf92f
24 changed files with 1087 additions and 70 deletions
+4
View File
@@ -39,11 +39,15 @@ export interface Agent {
name: string;
hostname: string;
ip_address: string;
os: string;
architecture: string;
status: string;
version: string;
last_heartbeat: string;
last_heartbeat_at: string;
capabilities: string[];
tags: Record<string, string>;
registered_at: string;
created_at: string;
updated_at: string;
}
+10 -10
View File
@@ -93,11 +93,15 @@ export default function AgentDetailPage() {
<InfoRow label="Updated" value={formatDateTime(agent.updated_at)} />
</div>
{/* Capabilities */}
{/* System Info */}
<div className="card p-5">
<h3 className="text-sm font-semibold text-slate-300 mb-4">Capabilities & Tags</h3>
<h3 className="text-sm font-semibold text-slate-300 mb-4">System Information</h3>
<InfoRow label="Operating System" value={agent.os || '—'} />
<InfoRow label="Architecture" value={agent.architecture || '—'} />
<InfoRow label="IP Address" value={<span className="font-mono text-xs">{agent.ip_address || '—'}</span>} />
<InfoRow label="Agent Version" value={agent.version || '—'} />
{agent.capabilities?.length ? (
<div className="mb-4">
<div className="mt-4">
<p className="text-xs text-slate-400 mb-2">Capabilities</p>
<div className="flex flex-wrap gap-2">
{agent.capabilities.map((c) => (
@@ -105,11 +109,9 @@ export default function AgentDetailPage() {
))}
</div>
</div>
) : (
<p className="text-sm text-slate-500 mb-4">No capabilities reported</p>
)}
) : null}
{agent.tags && Object.keys(agent.tags).length > 0 ? (
<div>
<div className="mt-4">
<p className="text-xs text-slate-400 mb-2">Tags</p>
<div className="flex flex-wrap gap-2">
{Object.entries(agent.tags).map(([k, v]) => (
@@ -117,9 +119,7 @@ export default function AgentDetailPage() {
))}
</div>
</div>
) : (
<p className="text-sm text-slate-500">No tags</p>
)}
) : null}
</div>
</div>
+1
View File
@@ -42,6 +42,7 @@ export default function AgentsPage() {
render: (a) => <StatusBadge status={a.status || heartbeatStatus(a.last_heartbeat)} />,
},
{ key: 'hostname', label: 'Hostname', render: (a) => <span className="text-slate-300 font-mono text-xs">{a.hostname || '—'}</span> },
{ key: 'os', label: 'OS / Arch', render: (a) => <span className="text-slate-400 text-xs">{a.os && a.architecture ? `${a.os}/${a.architecture}` : a.os || '—'}</span> },
{ key: 'ip', label: 'IP Address', render: (a) => <span className="text-slate-400 font-mono text-xs">{a.ip_address || '—'}</span> },
{ key: 'version', label: 'Version', render: (a) => <span className="text-slate-400 text-xs">{a.version || '—'}</span> },
{