mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 15:16:25 +00:00
7399315185
Rebuild the bundled web UI as a Next.js (React + TypeScript) static export implementing the Claude Activity Dashboard design: a left-nav "Control Center" hub over Runs, Repositories, Agents, Approvals, Git Servers, Activity, and Shared, styled with the Leeworks design-system tokens (flat, dark, border-led). The dashboard is a pure client of the existing API (same contract as curl): the browser prompts for the token once, stores it in localStorage, attaches it to every call, and renders all API values as React text so agent-authored strings stay inert. Control actions enqueue a command and poll it to a terminal state, matching the worker model. The build output is committed to src/handler/api/static/ so the wheel ships it and FastAPI serves it same-origin. app.py now mounts the export at "/" after the API routers (a non-shadowing fallback: unmatched paths 404, no SPA rewrite). UI-serving tests updated for the export; frontend source lives in frontend/. Claude-Session: https://claude.ai/code/session_01ATgVWRjFzG8nHEnwgZpJWD Co-authored-by: Claude <noreply@anthropic.com>
103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
/* The Control Center shell: a left nav (Runs / Repositories / Agents / Approvals / Git
|
|
* Servers / Activity / Shared) and the active section on the right, matching the design's
|
|
* hub layout. Command feedback and load errors surface as banners at the top of main. */
|
|
"use client";
|
|
|
|
import { useDashboard, type Section } from "@/components/store";
|
|
import { RunsSection } from "@/components/sections/RunsSection";
|
|
import { RepositoriesSection } from "@/components/sections/RepositoriesSection";
|
|
import { AgentsSection } from "@/components/sections/AgentsSection";
|
|
import { ApprovalsSection } from "@/components/sections/ApprovalsSection";
|
|
import { GitServersSection } from "@/components/sections/GitServersSection";
|
|
import { ActivitySection } from "@/components/sections/ActivitySection";
|
|
import { SharedSection } from "@/components/sections/SharedSection";
|
|
|
|
interface NavDef {
|
|
key: Section;
|
|
label: string;
|
|
count: (s: ReturnType<typeof useDashboard>) => number;
|
|
accent?: (s: ReturnType<typeof useDashboard>) => boolean;
|
|
}
|
|
|
|
const NAV: NavDef[] = [
|
|
{
|
|
key: "runs",
|
|
label: "Runs",
|
|
count: (s) => s.agents.length,
|
|
accent: (s) => s.agents.some((a) => a.status === "paused_for_input"),
|
|
},
|
|
{ key: "repositories", label: "Repositories", count: (s) => s.projects.length },
|
|
{ key: "agents", label: "Agents", count: (s) => s.agents.length },
|
|
{ key: "approvals", label: "Approvals", count: (s) => s.approvals.length },
|
|
{ key: "servers", label: "Git Servers", count: (s) => s.hosts.length },
|
|
{ key: "activity", label: "Activity", count: (s) => s.commands.length },
|
|
{ key: "shared", label: "Shared", count: (s) => s.shared.context.length },
|
|
];
|
|
|
|
export function Dashboard({ onSignOut }: { onSignOut: () => void }) {
|
|
const s = useDashboard();
|
|
|
|
return (
|
|
<div className="app">
|
|
<aside className="sidebar">
|
|
<div className="brand">
|
|
<span className="logo" />
|
|
Claude Monitor
|
|
</div>
|
|
{NAV.map((n) => {
|
|
const c = n.count(s);
|
|
const isAccent = n.accent?.(s) ?? false;
|
|
return (
|
|
<button
|
|
key={n.key}
|
|
className={`nav-item${s.section === n.key ? " active" : ""}`}
|
|
onClick={() => s.setSection(n.key)}
|
|
>
|
|
<span>{n.label}</span>
|
|
<span className="count" style={isAccent ? { color: "var(--lw-warning-fg)" } : undefined}>
|
|
{c || ""}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
<div className="sidebar-spacer" />
|
|
<div className="sidebar-foot">
|
|
<button className="nav-item" onClick={s.refresh} title="Refresh now">
|
|
<span>Refresh</span>
|
|
<span className="count">↻</span>
|
|
</button>
|
|
<button className="nav-item" onClick={onSignOut} title="Sign out / change token">
|
|
<span>Sign out</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<main className="main">
|
|
{s.cmd.text && (
|
|
<p className={`banner ${s.cmd.error ? "err" : "ok"}`} style={{ marginTop: 16 }}>
|
|
{s.cmd.text}
|
|
</p>
|
|
)}
|
|
{s.lastError && (
|
|
<p className="banner err" style={{ marginTop: 12 }}>
|
|
{s.lastError}
|
|
</p>
|
|
)}
|
|
|
|
{s.section === "runs" ? (
|
|
<RunsSection />
|
|
) : (
|
|
<div className="main-scroll">
|
|
{s.section === "repositories" && <RepositoriesSection />}
|
|
{s.section === "agents" && <AgentsSection />}
|
|
{s.section === "approvals" && <ApprovalsSection />}
|
|
{s.section === "servers" && <GitServersSection />}
|
|
{s.section === "activity" && <ActivitySection />}
|
|
{s.section === "shared" && <SharedSection />}
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|