mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat(nodes): SSH bootstrap + housekeeping — install script, bundled binaries, auto-prune (#50)
* feat(nodes): SSH bootstrap + housekeeping — install script, bundled binaries, auto-prune
- `containr nodes add <user@host>` mints an enroll token, SSHes once,
runs the install script remotely; --print emits the one-liner
- Public /api/agents/install.sh + /api/agents/download/{platform};
agent binaries built into the image under /agents
- Agent-side `prune` (bounded docker system prune, volumes opt-in)
and `system_df` commands
- node_agents.auto_prune + health sweep enqueues daily prune;
offline/online transitions notify admins
- CLI nodes update/prune/commands + MCP tools (prune confirm-gated)
- Usage page: install one-liner, per-node Prune + auto toggle
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(nodes): node detail page — telemetry, containers, commands, controls
/nodes/:id (admin): cpu/mem sparklines from heartbeat history
(1h/24h/7d), containers on node, command history, inline rename,
auto-prune toggle, prune-now with volume opt-in, remove. Agent rows
on the Usage page link into it.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(nodes): mesh awareness + node detail page
- agent reports tailscale/netbird/zerotier/wireguard interface IPs at
register (authoritative — absent interfaces clear stored state)
- backend stores under node_agents.metadata.mesh, merged on re-register
- node detail page shows mesh addresses under the primary IP
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
7547375cc2
commit
3f066b3b1e
23 files changed
+1410
-40
No files matched your search
@@ -341,6 +341,8 @@ export type NodeAgentEntity = {
|
||||
version: string;
|
||||
capabilities: AgentCapabilities;
|
||||
resources: NodeResources;
|
||||
autoPrune: boolean;
|
||||
metadata: Record<string, unknown>;
|
||||
lastHeartbeat?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
@@ -1076,6 +1078,8 @@ function normalizeAgent(agent: RawNodeAgent): NodeAgentEntity | null {
|
||||
},
|
||||
},
|
||||
lastHeartbeat: agent.last_heartbeat,
|
||||
autoPrune: agent.auto_prune ?? false,
|
||||
metadata: (agent.metadata ?? {}) as Record<string, unknown>,
|
||||
createdAt: agent.created_at,
|
||||
updatedAt: agent.updated_at,
|
||||
};
|
||||
@@ -1114,6 +1118,74 @@ export async function listAgents(): Promise<NodeAgentEntity[]> {
|
||||
return normalizeAgentArray(payload.agents);
|
||||
}
|
||||
|
||||
export async function updateAgent(id: string, body: { name?: string; auto_prune?: boolean }): Promise<NodeAgentEntity | null> {
|
||||
const payload = await requestJson<{ agent?: RawNodeAgent }>(`/agents/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return payload.agent ? normalizeAgent(payload.agent) : null;
|
||||
}
|
||||
|
||||
export async function pruneAgent(id: string, body?: { until?: string; volumes?: boolean }): Promise<void> {
|
||||
await requestJson(`/agents/${id}/prune`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAgent(id: string): Promise<NodeAgentEntity | null> {
|
||||
const payload = await requestJson<{ agent?: RawNodeAgent }>(`/agents/${id}`);
|
||||
return payload.agent ? normalizeAgent(payload.agent) : null;
|
||||
}
|
||||
|
||||
export async function deleteAgent(id: string): Promise<void> {
|
||||
await requestJson(`/agents/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export type AgentMetricPoint = {
|
||||
timestamp: string;
|
||||
cpu: { usage: number; usage_percent: number; cores: number };
|
||||
memory: { usage: number; usage_percent: number; limit: number; available: number };
|
||||
system_load: { load_1m: number; load_5m: number; load_15m: number };
|
||||
container_count: number;
|
||||
};
|
||||
|
||||
export async function getAgentMetrics(id: string, timeRange = '24h'): Promise<AgentMetricPoint[]> {
|
||||
const payload = await requestJson<{ metrics?: AgentMetricPoint[] }>(
|
||||
`/agents/${id}/metrics?time_range=${encodeURIComponent(timeRange)}`,
|
||||
);
|
||||
return payload.metrics ?? [];
|
||||
}
|
||||
|
||||
export type AgentContainer = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
status?: string;
|
||||
service_id?: string;
|
||||
created_at?: string;
|
||||
};
|
||||
|
||||
export async function listAgentContainers(id: string): Promise<AgentContainer[]> {
|
||||
const payload = await requestJson<{ containers?: AgentContainer[] }>(`/agents/${id}/containers`);
|
||||
return payload.containers ?? [];
|
||||
}
|
||||
|
||||
export type AgentCommand = {
|
||||
id?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
result?: string;
|
||||
error?: string;
|
||||
created_at?: string;
|
||||
completed_at?: string;
|
||||
};
|
||||
|
||||
export async function listAgentCommands(id: string): Promise<AgentCommand[]> {
|
||||
const payload = await requestJson<{ commands?: AgentCommand[] }>(`/agents/${id}/commands`);
|
||||
return payload.commands ?? [];
|
||||
}
|
||||
|
||||
export type AgentAuthToken = components['schemas']['AgentAuthToken'];
|
||||
export type AgentAuthTokenCreated = components['schemas']['AgentAuthTokenCreated'];
|
||||
|
||||
|
||||
Reference in new issue
Block a user