reap: skip synthetic tree nodes (fixes 422 on batch-delete)
dashboard-data.ts synthesizes a few org/company/team placeholders
("my-workspace", "ws-teams", "ungrouped-co", "ungrouped-team") so
an empty workspace still has something to render. Their ids
aren't UUIDs and don't exist in the DB.
Before the cascade-reap change these were unreachable because
selectLevel="claw" restricted the checkbox affordance to leaf
agents. With selectLevel="*" they became selectable, and
POST /api/claws/batch-delete returned 422 (serde couldn't parse
"ws-teams" as a UUID).
Fix: skip synthetic ids in onToggleSelect so they can never enter
selectedAgents in the first place, and filter the request body to
strict UUIDs at send-time as a defense. If the filter empties the
list the modal surfaces a friendly message instead of firing.
This commit is contained in:
@@ -53,6 +53,16 @@ const COMPUTER_WIDTH: Record<DeviceSize, string> = { phone: "448px", tablet: "67
|
|||||||
type Tier = "world" | "research" | "loops" | "claw" | "repos" | "infra";
|
type Tier = "world" | "research" | "loops" | "claw" | "repos" | "infra";
|
||||||
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
||||||
|
|
||||||
|
// dashboard-data.ts fabricates a few org/company/team nodes so an empty
|
||||||
|
// workspace still has something to render. These ids aren't UUIDs and
|
||||||
|
// don't exist in the DB — treat them as non-selectable for reap.
|
||||||
|
const SYNTHETIC_TREE_IDS = new Set([
|
||||||
|
"my-workspace",
|
||||||
|
"ws-teams",
|
||||||
|
"ungrouped-co",
|
||||||
|
"ungrouped-team",
|
||||||
|
]);
|
||||||
|
|
||||||
// Gradient palette for structure nodes that don't carry their own (companies,
|
// Gradient palette for structure nodes that don't carry their own (companies,
|
||||||
// teams). Agents bring their own grad/ink.
|
// teams). Agents bring their own grad/ink.
|
||||||
const NODE_GRADS: [string, string][] = [
|
const NODE_GRADS: [string, string][] = [
|
||||||
@@ -656,7 +666,13 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* The collapsible org → company → team → agent tree (World) or the flat agents list. */}
|
{/* The collapsible org → company → team → agent tree (World) or the flat agents list. */}
|
||||||
<StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={"*"} selectedIds={selectedAgents} onToggleSelect={(id) => setSelectedAgents((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); return next; } const lv = nodeLevel.get(id); const curLv = prev.size ? nodeLevel.get([...prev][0]) : lv; if (lv !== curLv) return new Set([id]); next.add(id); return next; })} />
|
<StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={"*"} selectedIds={selectedAgents} onToggleSelect={(id) => {
|
||||||
|
// Synthetic UI-only containers ("my-workspace", "ws-teams",
|
||||||
|
// "ungrouped-co", "ungrouped-team") aren't real DB rows — the
|
||||||
|
// backend would 422 on the non-UUID id. Silently ignore taps.
|
||||||
|
if (SYNTHETIC_TREE_IDS.has(id)) return;
|
||||||
|
setSelectedAgents((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); return next; } const lv = nodeLevel.get(id); const curLv = prev.size ? nodeLevel.get([...prev][0]) : lv; if (lv !== curLv) return new Set([id]); next.add(id); return next; });
|
||||||
|
}} />
|
||||||
{selectMode ? (
|
{selectMode ? (
|
||||||
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
|
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
<div style={{ fontFamily: mono, fontSize: 11, color: selectedItems.length ? "#ff8a7a" : "#6a6a72" }}>{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}</div>
|
<div style={{ fontFamily: mono, fontSize: 11, color: selectedItems.length ? "#ff8a7a" : "#6a6a72" }}>{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}</div>
|
||||||
|
|||||||
@@ -45,7 +45,16 @@ export function ReapProgressModal({ items, kind, onClose, onDone }: { items: Ite
|
|||||||
// One SSE stream regardless of kind. The backend expands
|
// One SSE stream regardless of kind. The backend expands
|
||||||
// teams/companies/orgs to the agents inside them, reaps each, then
|
// teams/companies/orgs to the agents inside them, reaps each, then
|
||||||
// deletes the group rows.
|
// deletes the group rows.
|
||||||
const body: Record<string, string[]> = { [BODY_KEY[kind]]: items.map((i) => i.id) };
|
// Filter to real UUIDs — the tree can surface synthetic placeholder
|
||||||
|
// ids ("ws-teams", "ungrouped-co", …) that would 422 on the server.
|
||||||
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
const validIds = items.map((i) => i.id).filter((id) => UUID.test(id));
|
||||||
|
if (validIds.length === 0) {
|
||||||
|
setError("Nothing reap-able selected (synthetic containers can't be deleted).");
|
||||||
|
setFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body: Record<string, string[]> = { [BODY_KEY[kind]]: validIds };
|
||||||
const res = await fetch("/api/claws/batch-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
const res = await fetch("/api/claws/batch-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
||||||
if (!res.ok || !res.body) { setError(`Request failed (${res.status})`); setFinished(true); return; }
|
if (!res.ok || !res.body) { setError(`Request failed (${res.status})`); setFinished(true); return; }
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
|
|||||||
Reference in New Issue
Block a user