mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 09:16:25 +00:00
71a7550f48
Git servers (forge_hosts) become full credential owners: - an encrypted forge token (Fernet, HANDLER_SECRET_KEY) stored per server and never returned by the API (has_token flag only); used automatically by every project on that host and addressable as db:host:<hostname> — the reserved db: credential scheme is now live - a per-server ed25519 SSH deploy key: generated server-side, public half shown in the dashboard to paste into the forge, private half encrypted at rest and materialized 0600 only in the control container (GIT_SSH_COMMAND / core.sshCommand) Project registration gets a git-server mode: pick a registered server, type owner/name, and the API derives the remote (ssh when the server has a deploy key, https otherwise), computes root_dir under PROJECTS_ROOT, and enqueues a new 'sync' command the worker executes (clone, or ff-only pull). Spawn always pulls first, so runs start from the remote's latest state; POST /projects/:p/sync and 'handler sync' re-pull on demand. Schedules: recurring agent spawns (prefix, prompt, interval, role). The worker fires due schedules as ordinary queued spawn commands with timestamped agent names, so runs are fresh stateless agents and appear in the Activity audit trail; missed intervals collapse into one catch-up run. Dashboard: Git Servers pane shows the SSH public key (copy button) and takes a write-only token; Repositories gains the server-first add form and a Pull now button; new Schedules pane. Rebuilt static export. Also restores the missing frontend/lib (api client + format helpers) the components import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XY1tEhQZXHZ5wci7dLc7rM
106 lines
3.9 KiB
TypeScript
106 lines
3.9 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 { SchedulesSection } from "@/components/sections/SchedulesSection";
|
|
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: "schedules", label: "Schedules", count: (s) => s.schedules.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 === "schedules" && <SchedulesSection />}
|
|
{s.section === "approvals" && <ApprovalsSection />}
|
|
{s.section === "servers" && <GitServersSection />}
|
|
{s.section === "activity" && <ActivitySection />}
|
|
{s.section === "shared" && <SharedSection />}
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|