feat(dashboard): complete admin panel — MCP catalog, enable/disable toggles, hook creation, system stats (#36736)

* feat(dashboard): MCP catalog + enable/disable, webhook toggle, hook create/delete, system stats

Backend for the comprehensive admin pass:
- MCP: GET /api/mcp/catalog (browse Nous-approved optional-mcps), POST
  /api/mcp/catalog/install, PUT /api/mcp/servers/{name}/enabled
- Webhooks: PUT /api/webhooks/{name}/enabled; gateway rejects disabled routes
  with 403 (hot-reloaded, no restart)
- Hooks: POST/DELETE /api/ops/hooks — create (with consent approval) + remove;
  list now reports accurate allowlist status + valid events
- System: GET /api/system/stats — OS/arch/python/cpu + psutil memory/disk/
  uptime/process, stdlib fallback

All gated by dashboard auth; secrets never returned.

* feat(dashboard): MCP catalog UI, enable/disable toggles, hook create, system stats

- McpPage: catalog section (browse Nous-approved MCPs, one-click install with
  env prompts) + per-server enable/disable toggle with gateway-restart note
- WebhooksPage: per-subscription enable/disable toggle (muted + badge when off)
- SystemPage: new Host stats section (OS/arch/python/cpu/mem/disk/uptime/load),
  shell-hook create modal + delete, 'Create backup' label
- api.ts: client methods + types for catalog, toggles, hook CRUD, system stats

* test(dashboard): cover catalog, toggles, hook CRUD, system stats, webhook toggle

Adds tests for the comprehensive pass: MCP enable/disable + catalog list +
catalog-install-unknown, hook create/delete with consent, system stats shape,
and webhook enable/disable. 26 tests total, all green.

* docs(dashboard): document the comprehensive admin pass + fresh screenshots

Updates the MCP/Webhooks/Pairing/System sections for catalog browse+install,
enable/disable toggles, hook creation, and host system stats; adds the new
endpoints to the API table; replaces the screenshots with live captures of
the rebuilt pages (real data, no dummies) including the hook-create modal.

* feat(dashboard): curator, portal status, and prompt-size/dump/migrate ops

Closes the last in-scope CLI gaps from the coverage audit:
- Curator: GET /api/curator (status), PUT /api/curator/paused, POST
  /api/curator/run (background)
- Portal: GET /api/portal (Nous auth + Tool Gateway routing, read-only)
- Diagnostics: POST /api/ops/prompt-size, /api/ops/dump, /api/ops/config-migrate
  (backgrounded, tailed via action status)

Host-bound commands (secrets/proxy/lsp/acp/computer-use/desktop/completion/
postinstall/uninstall/claw) remain CLI-only by design.

* feat(dashboard): curator + portal + diagnostics UI, tests

- SystemPage: Nous Portal status section (auth + Tool Gateway routing),
  Skill curator card (status + pause/resume + run now), and three new
  Operations buttons (prompt size, support dump, migrate config)
- api.ts: client methods + CuratorStatus/PortalStatus types
- tests: curator pause/resume, portal shape, system-stats shape, + auth-gate
  coverage for the new GET endpoints (31 tests total)

* docs(dashboard): document curator, portal, and diagnostics + refresh System screenshots

Updates the System section for the Nous Portal status, Skill curator
controls, and the new prompt-size/dump/migrate operations; adds them to the
API table; refreshes the System screenshots (now showing Portal + Curator)
and adds a dedicated curator/gateway/memory capture.

* feat(dashboard): session stats/export/prune + skills hub search endpoints

Completes the existing tabs' backend depth (audit vs CLI):
- Sessions: GET /api/sessions/stats (store stats), GET /api/sessions/{id}/export,
  POST /api/sessions/prune. /stats is registered before /{session_id} so the
  literal path isn't captured by the parameterized route.
- Skills: GET /api/skills/hub/search — parallel multi-source hub search (threaded),
  returns installable identifiers
- (rename via PATCH and cron-edit via PUT already existed; now surfaced in UI)

* feat(dashboard): complete existing tabs — sessions mgmt, skills hub browse, cron edit

Audited every existing tab against its CLI command and filled the gaps:
- Sessions: store stats bar, per-row rename + export (JSON download), and a
  prune-old-sessions control (mirrors hermes sessions rename/export/prune/stats)
- Skills: new 'Browse hub' view — search the skill hub across all sources,
  install by identifier with a live install log, and 'Update all' (mirrors
  hermes skills search/install/update)
- Cron: per-job Edit modal (pre-filled) calling updateCronJob (hermes cron edit)
- api.ts: renameSession/getSessionStats/exportSessionUrl/pruneSessions,
  updateCronJob, searchSkillsHub + types

Models tab was already comprehensive (provider+model picker, dynamic per-provider
lists, main + all 11 aux-task assignments, reset) — verified, no change needed.

* test(dashboard): cover session stats/rename/export/prune + skills hub search

Adds the route-shadowing guard for /api/sessions/stats (must not be captured
by /api/sessions/{session_id}), rename/export/prune, and the empty-query
short-circuit for hub search. 36 tests total, all green.

* docs(dashboard): document enhanced Sessions, Skills hub, and Cron edit

Sessions: stats bar, rename, export, prune (+ screenshot). Skills: new Browse
hub view for search/install/update (+ screenshot). Cron: edit action. API
table updated with the new endpoints.
This commit is contained in:
Teknium
2026-06-02 00:16:11 -04:00
committed by GitHub
parent 40ae170647
commit bd8e2ec1a6
20 changed files with 2597 additions and 210 deletions
+202 -1
View File
@@ -238,6 +238,24 @@ export const api = {
fetchJSON<{ ok: boolean }>(`/api/sessions/${encodeURIComponent(id)}`, {
method: "DELETE",
}),
renameSession: (id: string, title: string) =>
fetchJSON<{ ok: boolean; title: string }>(
`/api/sessions/${encodeURIComponent(id)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
},
),
getSessionStats: () => fetchJSON<SessionStoreStats>("/api/sessions/stats"),
exportSessionUrl: (id: string) =>
`/api/sessions/${encodeURIComponent(id)}/export`,
pruneSessions: (older_than_days: number, source?: string) =>
fetchJSON<{ ok: boolean; removed: number }>("/api/sessions/prune", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ older_than_days, source }),
}),
getLogs: (params: { file?: string; lines?: number; level?: string; component?: string }) => {
const qs = new URLSearchParams();
if (params.file) qs.set("file", params.file);
@@ -311,6 +329,19 @@ export const api = {
}),
pauseCronJob: (id: string, profile = "default") =>
fetchJSON<CronJob>(`/api/cron/jobs/${encodeURIComponent(id)}/pause?profile=${encodeURIComponent(profile)}`, { method: "POST" }),
updateCronJob: (
id: string,
updates: { prompt?: string; schedule?: string; name?: string; deliver?: string },
profile = "default",
) =>
fetchJSON<CronJob>(
`/api/cron/jobs/${encodeURIComponent(id)}?profile=${encodeURIComponent(profile)}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ updates }),
},
),
resumeCronJob: (id: string, profile = "default") =>
fetchJSON<CronJob>(`/api/cron/jobs/${encodeURIComponent(id)}/resume?profile=${encodeURIComponent(profile)}`, { method: "POST" }),
triggerCronJob: (id: string, profile = "default") =>
@@ -522,6 +553,32 @@ export const api = {
`/api/mcp/servers/${encodeURIComponent(name)}/test`,
{ method: "POST" },
),
setMcpServerEnabled: (name: string, enabled: boolean) =>
fetchJSON<{ ok: boolean; name: string; enabled: boolean }>(
`/api/mcp/servers/${encodeURIComponent(name)}/enabled`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled }),
},
),
getMcpCatalog: () =>
fetchJSON<{ entries: McpCatalogEntry[]; diagnostics: McpCatalogDiagnostic[] }>(
"/api/mcp/catalog",
),
installMcpCatalogEntry: (
name: string,
env: Record<string, string> = {},
enable = true,
) =>
fetchJSON<{ ok: boolean; name: string; background: boolean; action?: string }>(
"/api/mcp/catalog/install",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, env, enable }),
},
),
// ── Admin: Pairing ──────────────────────────────────────────────────
getPairing: () => fetchJSON<PairingResponse>("/api/pairing"),
@@ -554,6 +611,15 @@ export const api = {
fetchJSON<{ ok: boolean }>(`/api/webhooks/${encodeURIComponent(name)}`, {
method: "DELETE",
}),
setWebhookEnabled: (name: string, enabled: boolean) =>
fetchJSON<{ ok: boolean; name: string; enabled: boolean }>(
`/api/webhooks/${encodeURIComponent(name)}/enabled`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled }),
},
),
// ── Admin: Credential pool ──────────────────────────────────────────
getCredentialPool: () =>
@@ -616,6 +682,45 @@ export const api = {
body: JSON.stringify({ archive }),
}),
getHooks: () => fetchJSON<HooksResponse>("/api/ops/hooks"),
createHook: (body: HookCreate) =>
fetchJSON<{ ok: boolean; event: string; command: string; approved: boolean }>(
"/api/ops/hooks",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
),
deleteHook: (event: string, command: string) =>
fetchJSON<{ ok: boolean }>("/api/ops/hooks", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event, command }),
}),
getSystemStats: () => fetchJSON<SystemStats>("/api/system/stats"),
// ── Admin: Curator ──────────────────────────────────────────────────
getCurator: () => fetchJSON<CuratorStatus>("/api/curator"),
setCuratorPaused: (paused: boolean) =>
fetchJSON<{ ok: boolean; paused: boolean }>("/api/curator/paused", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ paused }),
}),
runCurator: () =>
fetchJSON<ActionResponse>("/api/curator/run", { method: "POST" }),
// ── Admin: Portal ───────────────────────────────────────────────────
getPortal: () => fetchJSON<PortalStatus>("/api/portal"),
// ── Admin: Diagnostics (backgrounded) ───────────────────────────────
runPromptSize: () =>
fetchJSON<ActionResponse>("/api/ops/prompt-size", { method: "POST" }),
runDump: () => fetchJSON<ActionResponse>("/api/ops/dump", { method: "POST" }),
runConfigMigrate: () =>
fetchJSON<ActionResponse>("/api/ops/config-migrate", { method: "POST" }),
getCheckpoints: () => fetchJSON<CheckpointsResponse>("/api/ops/checkpoints"),
pruneCheckpoints: () =>
fetchJSON<ActionResponse>("/api/ops/checkpoints/prune", { method: "POST" }),
@@ -635,6 +740,10 @@ export const api = {
}),
updateSkillsFromHub: () =>
fetchJSON<ActionResponse>("/api/skills/hub/update", { method: "POST" }),
searchSkillsHub: (q: string, source = "all", limit = 20) =>
fetchJSON<{ results: SkillHubResult[] }>(
`/api/skills/hub/search?q=${encodeURIComponent(q)}&source=${encodeURIComponent(source)}&limit=${limit}`,
),
};
/** Identity payload returned by ``GET /api/auth/me`` (Phase 7).
@@ -663,6 +772,24 @@ export interface ActionResponse {
update_command?: string;
}
export interface SessionStoreStats {
total: number;
active_store: number;
archived: number;
messages: number;
by_source: Record<string, number>;
}
export interface SkillHubResult {
name: string;
description: string;
source: string;
identifier: string;
trust_level: string;
repo: string | null;
tags: string[];
}
// ── Admin types ───────────────────────────────────────────────────────
export interface McpServer {
@@ -677,6 +804,25 @@ export interface McpServer {
tools: string[] | null;
}
export interface McpCatalogEntry {
name: string;
description: string;
source: string;
transport: "http" | "stdio";
auth_type: "api_key" | "oauth" | "none";
required_env: Array<{ name: string; prompt: string; required: boolean }>;
needs_install: boolean;
installed: boolean;
enabled: boolean;
}
export interface McpCatalogDiagnostic {
name: string;
kind: string;
message: string;
}
export interface McpServerCreate {
name: string;
url?: string;
@@ -716,6 +862,7 @@ export interface WebhookRoute {
created_at: string | null;
url: string;
secret_set: boolean;
enabled: boolean;
}
export interface WebhooksResponse {
@@ -771,11 +918,65 @@ export interface HookEntry {
command: string | null;
timeout: number | null;
allowed: boolean;
approved_at?: string | null;
executable?: boolean;
}
export interface HooksResponse {
hooks: HookEntry[];
allowlist: string[];
valid_events: string[];
}
export interface HookCreate {
event: string;
command: string;
matcher?: string;
timeout?: number;
approve?: boolean;
}
export interface SystemStats {
os: string;
os_release: string;
os_version: string;
platform: string;
arch: string;
hostname: string;
python_version: string;
python_impl: string;
hermes_version: string;
cpu_count: number | null;
psutil: boolean;
cpu_percent?: number;
load_avg?: number[];
uptime_seconds?: number;
memory?: { total: number; available: number; used: number; percent: number };
disk?: { total: number; used: number; free: number; percent: number };
process?: { pid: number; rss: number; create_time: number; num_threads: number };
}
export interface CuratorStatus {
enabled: boolean;
paused: boolean;
interval_hours: number | null;
last_run_at: string | null;
min_idle_hours: number | null;
stale_after_days: number | null;
archive_after_days: number | null;
}
export interface PortalFeature {
label: string;
state: string;
}
export interface PortalStatus {
logged_in: boolean;
portal_url: string | null;
inference_url: string | null;
provider: string;
subscription_url: string;
features: PortalFeature[];
}
export interface CheckpointSession {