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:
+202
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user