Files
stackpilot/frontend/src/api/agents.ts
T
menzeljandClaude Fable 5 11effdc2ca 0.31.1: make dashboard metrics honest
- Container card now compares compose-only counts across hosts: agents
  report compose_running in /agent/system (pre-0.31.1 agents fall back to
  the all-containers number); card retitled, ResourceBar stat labelled
  'Containers (all)'.
- Uptime is sampled every 5 min (background loop + opportunistic on read)
  and charted as daily averages instead of a once-a-day snapshot; no
  sample is written when no compose containers exist (was: fake 100%).
  Legacy daily entries in uptime.jsonl still count; file pruned at startup.
- Funnel stage 'monitored' is now per-stack and real: stacks with an
  enabled local auto-update policy (was: global webhook-exists toggle).
  Frontend label renamed to 'Auto-managed'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:13:16 +00:00

118 lines
4.3 KiB
TypeScript

import api from "./client";
import type { Agent, StackDetail, StackStats, StackSummary } from "@/types";
export type RemoteStackSummary = StackSummary & { agent_id: number; agent_name: string };
export type RemoteStackDetail = StackDetail & { agent_id: number; agent_name: string };
export interface AgentSystem {
hostname: string;
docker_version: string;
host_os: string;
cpu_cores: number;
mem_total: number;
mem_used: number;
disk_total: number;
disk_used: number;
containers_running: number;
containers_total: number;
compose_running?: number; // agents < 0.31.1 don't report it
}
export const agentsApi = {
list: (refresh = true) =>
api.get<Agent[]>(`/api/agents?refresh=${refresh}`).then((r) => r.data),
create: (body: { name: string; url: string; token: string }) =>
api.post<Agent>("/api/agents", body).then((r) => r.data),
update: (id: number, body: { name?: string; url?: string; token?: string }) =>
api.put<Agent>(`/api/agents/${id}`, body).then((r) => r.data),
remove: (id: number) => api.delete(`/api/agents/${id}`).then((r) => r.data),
ping: (id: number) =>
api.post<{ status: string; hostname?: string }>(`/api/agents/${id}/ping`).then((r) => r.data),
system: (id: number) =>
api.get<AgentSystem>(`/api/agents/${id}/system`).then((r) => r.data),
stacks: (id: number) =>
api.get<RemoteStackSummary[]>(`/api/agents/${id}/stacks`).then((r) => r.data),
stackStats: (id: number) =>
api.get<Record<string, StackStats>>(`/api/agents/${id}/stacks/stats`).then((r) => r.data),
createStack: (id: number, body: { name: string; yaml: string; env?: string }) =>
api
.post<{ id: string; name: string }>(`/api/agents/${id}/stacks`, body)
.then((r) => r.data),
stack: (id: number, stackId: string) =>
api.get<RemoteStackDetail>(`/api/agents/${id}/stacks/${stackId}`).then((r) => r.data),
logs: (id: number, stackId: string, tail = 200) =>
api
.get<{ logs: string }>(`/api/agents/${id}/stacks/${stackId}/logs?tail=${tail}`)
.then((r) => r.data),
update_stack: (id: number, stackId: string, body: { yaml?: string; env?: string }) =>
api.put(`/api/agents/${id}/stacks/${stackId}`, body).then((r) => r.data),
action: (id: number, stackId: string, action: string) =>
api.post(`/api/agents/${id}/stacks/${stackId}/${action}`).then((r) => r.data),
backupDownload: async (
id: number,
stackId: string,
opts: { includeVolumes: boolean; stopFirst: boolean }
) => {
const res = await api.get(`/api/agents/${id}/stacks/${stackId}/backup`, {
params: { include_volumes: opts.includeVolumes, stop_first: opts.stopFirst },
responseType: "blob",
});
const cd = res.headers["content-disposition"] as string | undefined;
const name = cd?.match(/filename="?([^"]+)"?/)?.[1] ?? `backup-${stackId}.tar.gz`;
const url = URL.createObjectURL(res.data as Blob);
const a = document.createElement("a");
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
},
backupPush: (
id: number,
stackId: string,
body: { destination_id: number; include_volumes: boolean; stop_first: boolean }
) =>
api
.post<{ ok: boolean; destination: string; name: string }>(
`/api/agents/${id}/stacks/${stackId}/backup/push`,
body
)
.then((r) => r.data),
restoreUpload: (
id: number,
file: File,
opts: { targetId?: string; overwrite: boolean; restoreVolumes: boolean }
) => {
const form = new FormData();
form.append("file", file);
if (opts.targetId) form.append("target_id", opts.targetId);
form.append("overwrite", String(opts.overwrite));
form.append("restore_volumes", String(opts.restoreVolumes));
return api
.post<{ stack_id: string; name: string; volumes_restored: number }>(
`/api/agents/${id}/stacks/restore`,
form
)
.then((r) => r.data);
},
restoreFrom: (
id: number,
body: {
destination_id: number;
name: string;
target_id?: string;
overwrite: boolean;
restore_volumes: boolean;
}
) =>
api
.post<{ stack_id: string; name: string; volumes_restored: number }>(
`/api/agents/${id}/stacks/restore-from`,
body
)
.then((r) => r.data),
};