import { stream } from "hono/streaming"; import type { DashboardPublicHttpsEntrypoint } from "../dashboard.ts"; import type { DashboardApp, DashboardSseClient } from "./shared.ts"; import type { Store } from "../store/store.ts"; import { parseBoundedInt } from "./shared.ts"; import { getSwarmDbPath } from "../agents/swarmDbConnection.ts"; import { getSwarmServerStatus } from "../agents/swarmServerStatus.ts"; import { getSwarmMcpSkillStatus } from "../agents/swarmMcpSkillStatus.ts"; import { installSwarmMcpSkill, type SkillInstallScope } from "../agents/swarmMcpSkillInstall.ts"; import { readDashboardBody } from "./shared.ts";
interface MetricsRouteDeps { store: Store; publicHttpsEntrypoint: DashboardPublicHttpsEntrypoint | null; getStatsPayload: (guildId?: string | null) => unknown; activitySseClients: Set; writeSseEvent: (client: DashboardSseClient, eventName: string, payload: unknown) => Promise; }
export function attachMetricsRoutes(app: DashboardApp, deps: MetricsRouteDeps) { const { store, publicHttpsEntrypoint, getStatsPayload, activitySseClients, writeSseEvent } = deps;
app.get("/api/swarm-server-status", async (c) => { const dbPath = getSwarmDbPath(store.getSettings()); const status = await getSwarmServerStatus(dbPath); return c.json(status); });
app.get("/api/swarm-mcp-skill-status", (c) => { const settings = store.getSettings(); const workspaceRoots = settings.permissions?.devTasks?.allowedWorkspaceRoots || []; const status = getSwarmMcpSkillStatus(workspaceRoots); return c.json(status); });
app.post("/api/swarm-mcp-skill-install", async (c) => { const body = await readDashboardBody(c); const scope = String(body.scope || "").trim() as SkillInstallScope; if (scope !== "user" && scope !== "workspace") { return c.json({ ok: false, reason: "scope must be 'user' or 'workspace'" }, 400); } const workspaceRoot = scope === "workspace" ? String(body.workspaceRoot || "").trim() : undefined; const settings = store.getSettings(); const allowedWorkspaceRoots = settings.permissions?.devTasks?.allowedWorkspaceRoots || []; try { const result = installSwarmMcpSkill({ scope, workspaceRoot }, allowedWorkspaceRoots); store.logAction({ kind: "dashboard", level: result.ok ? "info" : "warn", content: result.ok ? "swarm_mcp_skill_install_ok" : "swarm_mcp_skill_install_failed", metadata: { scope, workspaceRoot, created: result.created, skipped: result.skipped, reason: result.reason } }); if (!result.ok) return c.json(result, 400); return c.json(result); } catch (error) { const reason = error instanceof Error ? error.message : String(error); store.logAction({ kind: "dashboard", level: "error", content: "swarm_mcp_skill_install_error", metadata: { scope, workspaceRoot, error: reason } }); return c.json({ ok: false, reason }, 500); } });
app.get("/api/actions", (c) => { const limit = parseBoundedInt(c.req.query("limit"), 200, 1, 1000); const guildId = String(c.req.query("guildId") || "").trim() || null; const kinds = String(c.req.query("kinds") || "") .split(",") .map((value) => value.trim()) .filter(Boolean); const sinceHoursRaw = Number(c.req.query("sinceHours")); const sinceIso = Number.isFinite(sinceHoursRaw) && sinceHoursRaw > 0 ? new Date(Date.now() - sinceHoursRaw * 60 * 60 * 1000).toISOString() : null;
return c.json(store.getRecentActions(limit, { kinds, sinceIso, guildId }));
});
app.get("/api/agents/browser-sessions", (c) => { const sinceHoursRaw = Number(c.req.query("sinceHours")); const sinceHours = Number.isFinite(sinceHoursRaw) && sinceHoursRaw > 0 ? sinceHoursRaw : 24; const sinceIso = new Date(Date.now() - sinceHours * 60 * 60 * 1000).toISOString(); const limit = parseBoundedInt(c.req.query("limit"), 50, 1, 200); const guildId = String(c.req.query("guildId") || "").trim() || null; const sessions = store.getRecentBrowserSessions(limit, { sinceIso, guildId }); return c.json({ guildId, sessions }); });
app.get("/api/stats", (c) => { const guildId = String(c.req.query("guildId") || "").trim() || null; return c.json(getStatsPayload(guildId)); });
app.get("/api/public-https", (c) => { return c.json(publicHttpsEntrypoint?.getState?.() || null); });
app.get("/api/activity/events", (c) => { c.header("Content-Type", "text/event-stream"); c.header("Cache-Control", "no-cache"); c.header("Connection", "keep-alive"); c.header("X-Accel-Buffering", "no");
return stream(c, async (streaming) => {
const client: DashboardSseClient = {
write: async (chunk) => {
await streaming.write(chunk);
},
close: async () => {
await streaming.close();
},
onAbort(listener) {
streaming.onAbort(listener);
}
};
activitySseClients.add(client);
let statsInterval: ReturnType<typeof setInterval> | null = null;
let heartbeat: ReturnType<typeof setInterval> | null = null;
let closed = false;
const cleanup = () => {
if (closed) return;
closed = true;
if (statsInterval) clearInterval(statsInterval);
if (heartbeat) clearInterval(heartbeat);
activitySseClients.delete(client);
};
client.onAbort(cleanup);
try {
await writeSseEvent(client, "activity_snapshot", {
actions: store.getRecentActions(220),
stats: getStatsPayload(null)
});
} catch {
cleanup();
await streaming.close();
return;
}
statsInterval = setInterval(() => {
void writeSseEvent(client, "stats_update", getStatsPayload(null)).catch(() => {
cleanup();
});
}, 3_000);
heartbeat = setInterval(() => {
void client.write(": heartbeat
").catch(() => { cleanup(); }); }, 15_000);
await new Promise<void>((resolve) => {
client.onAbort(() => {
cleanup();
resolve();
});
});
});
});
app.get("/api/automations", (c) => { const guildId = String(c.req.query("guildId") || "").trim(); const channelId = String(c.req.query("channelId") || "").trim() || null; const statusParam = String(c.req.query("status") || "active,paused").trim(); const query = String(c.req.query("q") || "").trim(); const limit = parseBoundedInt(c.req.query("limit"), 30, 1, 120);
if (!guildId) {
return c.json({ error: "guildId is required" }, 400);
}
const statuses = statusParam
.split(",")
.map((item) => item.trim().toLowerCase())
.filter(Boolean);
const rows = store.listAutomations({
guildId,
channelId,
statuses,
query,
limit
});
return c.json({
guildId,
channelId,
statuses,
query,
limit,
rows
});
});
app.get("/api/automations/runs", (c) => { const guildId = String(c.req.query("guildId") || "").trim(); const automationId = Number(c.req.query("automationId")); const limit = parseBoundedInt(c.req.query("limit"), 30, 1, 120);
if (!guildId || !Number.isInteger(automationId) || automationId <= 0) {
return c.json({ error: "guildId and automationId are required" }, 400);
}
const rows = store.getAutomationRuns({
guildId,
automationId,
limit
});
return c.json({
guildId,
automationId,
limit,
rows
});
}); }
