- 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>
118 lines
4.3 KiB
TypeScript
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),
|
|
};
|