// — one agent's resource caps on the detail panel: the CPU and // memory caps the swarm sets for it, and an editor for them. // // Reads `GET /api/agents/{name}/limits`, which answers the stored caps as // `{ cpu_quota?, memory_max? }` (`{}` when the swarm sets none). A cap // replaces what the hive says for that field, and a field the swarm leaves // out stays with the hive, which is why an empty box means "the hive decides". // The editor PUTs the filled fields, a PUT replacing the stored value whole, so // a field left out is cleared; the clear action DELETEs. The route checks each // value against systemd's own syntax, and its refusal shows as it arrives, next // to the fields. // // Its state belongs to one agent: callers key it by agent name, so switching // agents remounts it instead of showing the previous agent's caps. import { useEffect, useState } from "preact/hooks"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { Badge } from "@hive/shared/badge.js"; import { Button } from "../../ui/button/Button.js"; import { Dialog } from "../../ui/dialog/Dialog.js"; import { Panel } from "../../ui/panel/Panel.js"; import { TextField } from "../../ui/text-field/TextField.js"; import "./AgentLimits.css"; // The caps as the route carries them; both fields are optional. interface Caps { cpu_quota?: string; memory_max?: string; } const HIVE_DECIDES = "the hive decides"; function limitsUrl(agent: string): string { return `/api/agents/${encodeURIComponent(agent)}/limits`; } // The caps as typed, as the body the route takes: only the filled fields, so // an empty box leaves that field to the hive. export function capsBody(cpu: string, memory: string): Caps { const body: Caps = {}; if (cpu.trim() !== "") body.cpu_quota = cpu.trim(); if (memory.trim() !== "") body.memory_max = memory.trim(); return body; } // The two cap boxes with their hints, shared by the editor here and the // create-agent form. export function LimitsFields({ cpu, memory, onCpu, onMemory, idPrefix = "limits", }: { cpu: string; memory: string; onCpu: (v: string) => void; onMemory: (v: string) => void; // Ids must be unique on the page, and the agents page can hold the editor // and the create dialog at once. idPrefix?: string; }) { return ( <>

A percentage of one core: 100% is one core, 400% is four.

A size such as 512M or 8G, a percentage of the machine's memory such as 50%, or infinity.

); } export function AgentLimits({ agent }: { agent: string }) { const [caps, setCaps] = useState(null); const [error, setError] = useState(null); // Bumped after a save or a clear, so the caps are fetched again. const [reload, setReload] = useState(0); const [editing, setEditing] = useState(false); useEffect(() => { let cancelled = false; (async () => { const r = await fetch(limitsUrl(agent)); if (!r.ok) { if (!cancelled) setError(await readApiError(r)); return; } const body = (await r.json()) as Caps; if (cancelled) return; setCaps(body); setError(null); })().catch((e: unknown) => { if (!cancelled) setError({ detail: String(e) }); }); return () => { cancelled = true; }; }, [agent, reload]); if (error) { return ( ); } if (caps === null) return …; return (
setEditing(true)} /> setEditing(false)} label="edit resource caps" plain > {/* Remounted per open, so a cancelled edit leaves nothing behind. */} {editing ? ( setEditing(false)} onSaved={() => { setEditing(false); setReload((n) => n + 1); }} /> ) : null}
); } function LimitsForm({ agent, caps, onClose, onSaved, }: { agent: string; caps: Caps; onClose: () => void; onSaved: () => void; }) { const [cpu, setCpu] = useState(caps.cpu_quota ?? ""); const [memory, setMemory] = useState(caps.memory_max ?? ""); const [busy, setBusy] = useState(false); const [problem, setProblem] = useState(null); const empty = cpu.trim() === "" && memory.trim() === ""; const capped = caps.cpu_quota !== undefined || caps.memory_max !== undefined; async function send(init: RequestInit) { setBusy(true); setProblem(null); try { const r = await fetch(limitsUrl(agent), init); if (!r.ok) { // The form stays as typed, so the operator can fix it and save again. setProblem(await readApiError(r)); return; } onSaved(); } catch (e: unknown) { setProblem({ detail: String(e) }); } finally { setBusy(false); } } function save(e: Event) { e.preventDefault(); // Only the filled fields: a PUT replaces the stored value whole, so an // empty box is cleared and the hive decides that field again. void send({ method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(capsBody(cpu, memory)), }); } return (

Caps what {agent} may use, wherever it runs. A cap here replaces the hive's own answer for that field; an empty box leaves the field to the hive. The hive reads a change within about 30 seconds, and a running agent takes it at its next spawn or rebuild.

{problem ? ( ) : null}
); }