From a3b3c598abf69f3a2ccfe27c67ddb08af3103aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9B=A2=E5=AD=90?= Date: Wed, 22 Apr 2026 16:13:51 +0000 Subject: [PATCH] feat: admin agent switcher - view any agent's config from WebUI - Add agent selector dropdown in header (admin only) - Return role in GET /config response - Collect agents from both auth: and agent_tokens: KV prefixes - Show 'viewing ' badge when viewing another agent - Non-admin users see only their own config (unchanged) --- packages/webui/src/components/Dashboard.tsx | 51 ++++++++++++++++++--- packages/worker/src/index.ts | 21 +++++++-- packages/worker/src/ui.ts | 2 +- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/packages/webui/src/components/Dashboard.tsx b/packages/webui/src/components/Dashboard.tsx index 90de239..bcf9643 100644 --- a/packages/webui/src/components/Dashboard.tsx +++ b/packages/webui/src/components/Dashboard.tsx @@ -18,18 +18,38 @@ export default function Dashboard({ onLogout, addToast }: Props) { const [modal, setModal] = useState<{ key?: string; entry?: ConfigEntry } | null>(null); const [tab, setTab] = useState<"config" | "admin">("config"); const [loading, setLoading] = useState(true); + const [agentList, setAgentList] = useState([]); + const [selectedAgent, setSelectedAgent] = useState(""); + const [ownAgentId, setOwnAgentId] = useState(""); - const load = useCallback(async () => { + const load = useCallback(async (agentId?: string) => { try { - const d = await api.getConfig(); - setData(d); + if (agentId && agentId !== ownAgentId) { + const d = await api.getAgent(agentId); + setData(d); + } else { + const d = await api.getConfig(); + setData(d); + if (!ownAgentId && d.agent_id) { + setOwnAgentId(d.agent_id); + setSelectedAgent(d.agent_id); + } + // Load agent list for admins + if (d.role === "admin" && agentList.length === 0) { + try { + const agents = await api.getAgents(); + const list = Array.isArray(agents) ? agents : agents.agents || []; + setAgentList(list); + } catch {} + } + } } catch (e: any) { addToast("Failed to load: " + e.message, "error"); if (e.message.includes("401") || e.message.includes("403")) onLogout(); } finally { setLoading(false); } - }, []); + }, [ownAgentId, agentList.length]); useEffect(() => { load(); }, [load]); @@ -45,7 +65,7 @@ export default function Dashboard({ onLogout, addToast }: Props) { await api.putKey(key, scope, { value, env, secret }); addToast(`Key "${key}" saved`, "success"); setModal(null); - load(); + load(selectedAgent); } catch (e: any) { addToast("Failed: " + e.message, "error"); } @@ -56,13 +76,20 @@ export default function Dashboard({ onLogout, addToast }: Props) { try { await api.deleteKey(key, scope); addToast(`Key "${key}" deleted`, "success"); - load(); + load(selectedAgent); } catch (e: any) { addToast("Failed: " + e.message, "error"); } }; + const handleAgentSwitch = (agentId: string) => { + setSelectedAgent(agentId); + setLoading(true); + load(agentId); + }; + const isAdmin = data?.role === "admin"; + const isViewingOther = selectedAgent && selectedAgent !== ownAgentId; if (loading) { return ( @@ -88,6 +115,18 @@ export default function Dashboard({ onLogout, addToast }: Props) {
{data?.agent_id} + {isAdmin && agentList.length > 0 && ( + + )} + {isViewingOther && viewing {selectedAgent}} {isAdmin && admin}
diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 0b157e3..8f64a91 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -195,7 +195,7 @@ async function handleSync(auth: AuthInfo, kv: KVNamespace): Promise { } } - return json({ agent_id: auth.agent_id, secrets: resolved }); + return json({ agent_id: auth.agent_id, role: auth.role, secrets: resolved }); } // ── Admin endpoints ──────────────────────────────────────────────────── @@ -232,9 +232,22 @@ async function handleAdminRevokeTokens(auth: AuthInfo, kv: KVNamespace, agentId: async function handleAdminListAgents(auth: AuthInfo, kv: KVNamespace): Promise { if (auth.role !== "admin") return err("admin required", 403); - const list = await kv.list({ prefix: "agent_tokens:" }); - const agents = list.keys.map((k) => k.name.replace("agent_tokens:", "")); - return json({ agents }); + // Collect agents from agent_tokens: prefix + const tokenList = await kv.list({ prefix: "agent_tokens:" }); + const fromTokens = tokenList.keys.map((k) => k.name.replace("agent_tokens:", "")); + // Also collect agents from auth: entries + const authList = await kv.list({ prefix: "auth:" }); + const agentIds = new Set(fromTokens); + for (const key of authList.keys) { + const val = await kv.get(key.name); + if (val) { + try { + const parsed = JSON.parse(val); + if (parsed.agent_id) agentIds.add(parsed.agent_id); + } catch {} + } + } + return json({ agents: [...agentIds].sort() }); } async function handleAdminInspect(auth: AuthInfo, kv: KVNamespace, agentId: string): Promise { diff --git a/packages/worker/src/ui.ts b/packages/worker/src/ui.ts index 0b00eeb..4f7fad3 100644 --- a/packages/worker/src/ui.ts +++ b/packages/worker/src/ui.ts @@ -1,3 +1,3 @@ export function renderUI(): string { - return "\n\n \n \n \n Config Service\n \n \n \n \n
\n \n\n"; + return "\n\n \n \n \n Config Service\n \n \n \n \n
\n \n\n"; }