Files
handler/frontend/components/Dashboard.tsx
T
Wyatt 7399315185 Replace Alpine frontend with Next.js Claude Activity Dashboard (#7)
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>
2026-07-10 14:34:43 -04:00

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>
);
}