mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 08:36:23 +00:00
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>
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
/* Dashboard state: one store owns the API client, the loaded data for every section,
|
||||
* the 5s polling loop, and all mutating actions. Control actions enqueue a command and
|
||||
* poll it to done/failed, surfacing the outcome in the command banner (`cmd`). */
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
AuthError,
|
||||
createClient,
|
||||
type Agent,
|
||||
type ApiError,
|
||||
type Approval,
|
||||
type Checkmark,
|
||||
type Command,
|
||||
type Host,
|
||||
type LogEntry,
|
||||
type Project,
|
||||
type SharedContext,
|
||||
} from "@/lib/api";
|
||||
|
||||
export type Section =
|
||||
| "runs"
|
||||
| "repositories"
|
||||
| "agents"
|
||||
| "approvals"
|
||||
| "servers"
|
||||
| "activity"
|
||||
| "shared";
|
||||
|
||||
export interface RunAgent extends Agent {}
|
||||
|
||||
export interface CmdState {
|
||||
text: string;
|
||||
error: boolean;
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
const LOG_LIMIT = 100;
|
||||
const POLL_MS = 5000;
|
||||
|
||||
interface StoreValue {
|
||||
section: Section;
|
||||
setSection: (s: Section) => void;
|
||||
|
||||
projects: Project[];
|
||||
agents: RunAgent[]; // every agent across every project
|
||||
selectedProjectId: string;
|
||||
selectProject: (id: string) => void;
|
||||
|
||||
// Runs inbox
|
||||
selectedRun: { projectId: string; name: string } | null;
|
||||
selectRun: (projectId: string, name: string) => void;
|
||||
checkmark: Checkmark | null;
|
||||
checkmarkMissing: boolean;
|
||||
log: LogEntry[];
|
||||
logOffset: number;
|
||||
pageLog: (dir: 1 | -1) => void;
|
||||
|
||||
approvals: Approval[];
|
||||
hosts: Host[];
|
||||
commands: Command[];
|
||||
shared: { log: LogEntry[]; context: SharedContext[] };
|
||||
|
||||
cmd: CmdState;
|
||||
lastError: string;
|
||||
loading: boolean;
|
||||
|
||||
refresh: () => void;
|
||||
|
||||
// actions
|
||||
spawnAgent: (body: SpawnBody) => Promise<boolean>;
|
||||
killAgent: (projectId: string, name: string) => Promise<void>;
|
||||
deleteAgent: (projectId: string, name: string) => Promise<void>;
|
||||
submitAnswer: (answer: string, resume: boolean) => Promise<boolean>;
|
||||
createProject: (b: ProjectBody) => Promise<boolean>;
|
||||
updateProject: (id: string, b: Omit<ProjectBody, "id">) => Promise<boolean>;
|
||||
deleteProject: (id: string) => Promise<void>;
|
||||
submitApproval: (b: ApprovalBody) => Promise<void>;
|
||||
createHost: (b: HostBody) => Promise<boolean>;
|
||||
updateHost: (hostname: string, b: Omit<HostBody, "hostname">) => Promise<boolean>;
|
||||
deleteHost: (hostname: string) => Promise<void>;
|
||||
pollCi: () => Promise<void>;
|
||||
setSharedKey: (key: string, value: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface SpawnBody {
|
||||
name: string;
|
||||
role: string;
|
||||
placement: "worktree" | "subdir";
|
||||
worktree: string;
|
||||
subdir: string;
|
||||
task: string;
|
||||
}
|
||||
export interface ProjectBody {
|
||||
id: string;
|
||||
root_dir: string;
|
||||
git_remote: string;
|
||||
credential_ref: string;
|
||||
}
|
||||
export interface ApprovalBody {
|
||||
branch: string;
|
||||
status: string;
|
||||
agent_name: string;
|
||||
sha: string;
|
||||
note: string;
|
||||
}
|
||||
export interface HostBody {
|
||||
hostname: string;
|
||||
forge_type: string;
|
||||
token_env_var: string;
|
||||
base_url: string;
|
||||
}
|
||||
|
||||
const Ctx = createContext<StoreValue | null>(null);
|
||||
|
||||
export function useDashboard(): StoreValue {
|
||||
const v = useContext(Ctx);
|
||||
if (!v) throw new Error("useDashboard outside provider");
|
||||
return v;
|
||||
}
|
||||
|
||||
export function DashboardProvider({
|
||||
token,
|
||||
onUnauthorized,
|
||||
children,
|
||||
}: {
|
||||
token: string;
|
||||
onUnauthorized: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const client = useMemo(() => createClient(token, onUnauthorized), [token, onUnauthorized]);
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const [section, setSectionRaw] = useState<Section>("runs");
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [agents, setAgents] = useState<RunAgent[]>([]);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string>("");
|
||||
const [selectedRun, setSelectedRun] = useState<{ projectId: string; name: string } | null>(null);
|
||||
const [checkmark, setCheckmark] = useState<Checkmark | null>(null);
|
||||
const [checkmarkMissing, setCheckmarkMissing] = useState(false);
|
||||
const [log, setLog] = useState<LogEntry[]>([]);
|
||||
const [logOffset, setLogOffset] = useState(0);
|
||||
const [approvals, setApprovals] = useState<Approval[]>([]);
|
||||
const [hosts, setHosts] = useState<Host[]>([]);
|
||||
const [commands, setCommands] = useState<Command[]>([]);
|
||||
const [shared, setShared] = useState<{ log: LogEntry[]; context: SharedContext[] }>({
|
||||
log: [],
|
||||
context: [],
|
||||
});
|
||||
const [cmd, setCmd] = useState<CmdState>({ text: "", error: false, busy: false });
|
||||
const [lastError, setLastError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Keep polling loop reading fresh values without re-subscribing every render.
|
||||
const sectionRef = useRef(section);
|
||||
sectionRef.current = section;
|
||||
const selectedProjectRef = useRef(selectedProjectId);
|
||||
selectedProjectRef.current = selectedProjectId;
|
||||
const selectedRunRef = useRef(selectedRun);
|
||||
selectedRunRef.current = selectedRun;
|
||||
const logOffsetRef = useRef(logOffset);
|
||||
logOffsetRef.current = logOffset;
|
||||
|
||||
const swallow = (e: unknown) => {
|
||||
if (!(e instanceof AuthError)) setLastError((e as Error).message);
|
||||
};
|
||||
|
||||
const loadProjects = useCallback(async () => {
|
||||
try {
|
||||
const ps = await clientRef.current.api<Project[]>("/projects");
|
||||
setProjects(ps);
|
||||
setLastError("");
|
||||
setSelectedProjectId((cur) => cur || (ps[0]?.id ?? ""));
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadAgents = useCallback(async (projectList: Project[]) => {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
projectList.map((p) =>
|
||||
clientRef.current
|
||||
.api<Agent[]>(`/projects/${encodeURIComponent(p.id)}/agents`)
|
||||
.catch(() => [] as Agent[]),
|
||||
),
|
||||
);
|
||||
setAgents(results.flat());
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadRun = useCallback(async (projectId: string, name: string) => {
|
||||
const path = `/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(name)}`;
|
||||
try {
|
||||
const cm = await clientRef.current.api<Checkmark>(`${path}/checkmark`);
|
||||
setCheckmark(cm);
|
||||
setCheckmarkMissing(false);
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return;
|
||||
if ((e as ApiError).status === 404) {
|
||||
setCheckmark(null);
|
||||
setCheckmarkMissing(true);
|
||||
} else swallow(e);
|
||||
}
|
||||
try {
|
||||
const entries = await clientRef.current.api<LogEntry[]>(
|
||||
`${path}/log?limit=${LOG_LIMIT}&offset=${logOffsetRef.current}`,
|
||||
);
|
||||
setLog(entries);
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadApprovals = useCallback(async (projectId: string) => {
|
||||
if (!projectId) {
|
||||
setApprovals([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setApprovals(
|
||||
await clientRef.current.api<Approval[]>(
|
||||
`/projects/${encodeURIComponent(projectId)}/approvals`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadHosts = useCallback(async () => {
|
||||
try {
|
||||
setHosts(await clientRef.current.api<Host[]>("/hosts"));
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadCommands = useCallback(async () => {
|
||||
try {
|
||||
setCommands(await clientRef.current.api<Command[]>("/commands?limit=50"));
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadShared = useCallback(async () => {
|
||||
try {
|
||||
const [logRows, context] = await Promise.all([
|
||||
clientRef.current.api<LogEntry[]>("/shared/log"),
|
||||
clientRef.current.api<SharedContext[]>("/shared/context"),
|
||||
]);
|
||||
setShared({ log: logRows, context });
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* One refresh cycle for whatever section is active (plus always-cheap projects/agents
|
||||
* so the nav counts and inbox stay live). */
|
||||
const tick = useCallback(async () => {
|
||||
const ps = await clientRef.current
|
||||
.api<Project[]>("/projects")
|
||||
.catch((e) => {
|
||||
swallow(e);
|
||||
return null;
|
||||
});
|
||||
if (ps) {
|
||||
setProjects(ps);
|
||||
setSelectedProjectId((cur) => cur || (ps[0]?.id ?? ""));
|
||||
await loadAgents(ps);
|
||||
}
|
||||
const s = sectionRef.current;
|
||||
const run = selectedRunRef.current;
|
||||
if (run) await loadRun(run.projectId, run.name);
|
||||
if (s === "approvals") await loadApprovals(selectedProjectRef.current);
|
||||
if (s === "servers") await loadHosts();
|
||||
if (s === "activity") await loadCommands();
|
||||
if (s === "shared") await loadShared();
|
||||
}, [loadAgents, loadRun, loadApprovals, loadHosts, loadCommands, loadShared]);
|
||||
|
||||
// Initial load + polling loop. The first tick populates projects *and* agents (and the
|
||||
// active section) up front, so the Runs inbox is filled without waiting a poll interval.
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
await tick();
|
||||
if (alive) setLoading(false);
|
||||
})();
|
||||
const id = setInterval(() => {
|
||||
if (!document.hidden) void tick();
|
||||
}, POLL_MS);
|
||||
return () => {
|
||||
alive = false;
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [tick]);
|
||||
|
||||
const setSection = useCallback(
|
||||
(s: Section) => {
|
||||
setSectionRaw(s);
|
||||
setCmd({ text: "", error: false, busy: false });
|
||||
if (s === "approvals") void loadApprovals(selectedProjectRef.current);
|
||||
if (s === "servers") void loadHosts();
|
||||
if (s === "activity") void loadCommands();
|
||||
if (s === "shared") void loadShared();
|
||||
},
|
||||
[loadApprovals, loadHosts, loadCommands, loadShared],
|
||||
);
|
||||
|
||||
const selectProject = useCallback(
|
||||
(id: string) => {
|
||||
setSelectedProjectId(id);
|
||||
if (sectionRef.current === "approvals") void loadApprovals(id);
|
||||
},
|
||||
[loadApprovals],
|
||||
);
|
||||
|
||||
const selectRun = useCallback(
|
||||
(projectId: string, name: string) => {
|
||||
setSelectedRun({ projectId, name });
|
||||
setLogOffset(0);
|
||||
logOffsetRef.current = 0;
|
||||
setCheckmark(null);
|
||||
setCheckmarkMissing(false);
|
||||
setLog([]);
|
||||
void loadRun(projectId, name);
|
||||
},
|
||||
[loadRun],
|
||||
);
|
||||
|
||||
const pageLog = useCallback(
|
||||
(dir: 1 | -1) => {
|
||||
const next = Math.max(0, logOffset + dir * LOG_LIMIT);
|
||||
if (next === logOffset) return;
|
||||
setLogOffset(next);
|
||||
logOffsetRef.current = next;
|
||||
const run = selectedRunRef.current;
|
||||
if (run) void loadRun(run.projectId, run.name);
|
||||
},
|
||||
[logOffset, loadRun],
|
||||
);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
void tick();
|
||||
}, [tick]);
|
||||
|
||||
// ---- control actions (enqueue + track) ----
|
||||
const enqueueAndTrack = useCallback(
|
||||
async (path: string, body: unknown, label: string): Promise<Command | null> => {
|
||||
setCmd({ text: `${label}: queued…`, error: false, busy: true });
|
||||
try {
|
||||
const command = await clientRef.current.api<Command>(path, { method: "POST", body });
|
||||
const final = await clientRef.current.trackCommand(command.id);
|
||||
if (!final) {
|
||||
setCmd({
|
||||
text: `${label}: still running (see Activity). Is the worker up?`,
|
||||
error: false,
|
||||
busy: false,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const ok = final.status === "done";
|
||||
const detail = final.error || (final.result ? JSON.stringify(final.result) : "");
|
||||
setCmd({
|
||||
text: `${label} ${ok ? "done" : "failed"}${detail ? " — " + detail : ""}`,
|
||||
error: !ok,
|
||||
busy: false,
|
||||
});
|
||||
return final;
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return null;
|
||||
setCmd({ text: `${label} failed: ${(e as Error).message}`, error: true, busy: false });
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const spawnAgent = useCallback(
|
||||
async (f: SpawnBody) => {
|
||||
const body: Record<string, unknown> = {
|
||||
name: f.name.trim(),
|
||||
role: f.role || null,
|
||||
task: f.task.trim() || null,
|
||||
};
|
||||
if (f.placement === "worktree" && f.worktree.trim()) body.worktree = f.worktree.trim();
|
||||
if (f.placement === "subdir" && f.subdir.trim()) body.subdir = f.subdir.trim();
|
||||
const p = encodeURIComponent(selectedProjectRef.current);
|
||||
const final = await enqueueAndTrack(`/projects/${p}/agents/spawn`, body, `spawn ${body.name}`);
|
||||
await loadAgents(projects);
|
||||
return final?.status === "done";
|
||||
},
|
||||
[enqueueAndTrack, loadAgents, projects],
|
||||
);
|
||||
|
||||
const killAgent = useCallback(
|
||||
async (projectId: string, name: string) => {
|
||||
const p = encodeURIComponent(projectId);
|
||||
await enqueueAndTrack(`/projects/${p}/agents/${encodeURIComponent(name)}/kill`, undefined, `kill ${name}`);
|
||||
await loadAgents(projects);
|
||||
},
|
||||
[enqueueAndTrack, loadAgents, projects],
|
||||
);
|
||||
|
||||
const deleteAgent = useCallback(
|
||||
async (projectId: string, name: string) => {
|
||||
const p = encodeURIComponent(projectId);
|
||||
try {
|
||||
await clientRef.current.api(`/projects/${p}/agents/${encodeURIComponent(name)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
setCmd({ text: `agent '${name}' row deleted`, error: false, busy: false });
|
||||
if (selectedRunRef.current?.name === name) setSelectedRun(null);
|
||||
await loadAgents(projects);
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
}
|
||||
},
|
||||
[loadAgents, projects],
|
||||
);
|
||||
|
||||
const submitAnswer = useCallback(
|
||||
async (answer: string, resume: boolean) => {
|
||||
const run = selectedRunRef.current;
|
||||
if (!run) return false;
|
||||
const path = `/projects/${encodeURIComponent(run.projectId)}/agents/${encodeURIComponent(run.name)}`;
|
||||
try {
|
||||
await clientRef.current.api(`${path}/answer`, { method: "POST", body: { answer } });
|
||||
if (resume) {
|
||||
await enqueueAndTrack(`${path}/resume`, { answer }, "resume");
|
||||
} else {
|
||||
setCmd({ text: "Answer saved (agent still paused).", error: false, busy: false });
|
||||
}
|
||||
await loadAgents(projects);
|
||||
await loadRun(run.projectId, run.name);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return false;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[enqueueAndTrack, loadAgents, loadRun, projects],
|
||||
);
|
||||
|
||||
const createProject = useCallback(
|
||||
async (b: ProjectBody) => {
|
||||
try {
|
||||
await clientRef.current.api("/projects", {
|
||||
method: "POST",
|
||||
body: {
|
||||
id: b.id.trim(),
|
||||
root_dir: b.root_dir.trim(),
|
||||
git_remote: b.git_remote.trim() || null,
|
||||
credential_ref: b.credential_ref.trim() || null,
|
||||
},
|
||||
});
|
||||
setCmd({ text: `repository '${b.id}' registered`, error: false, busy: false });
|
||||
await loadProjects();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return false;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[loadProjects],
|
||||
);
|
||||
|
||||
const updateProject = useCallback(
|
||||
async (id: string, b: Omit<ProjectBody, "id">) => {
|
||||
try {
|
||||
await clientRef.current.api(`/projects/${encodeURIComponent(id)}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
root_dir: b.root_dir.trim(),
|
||||
git_remote: b.git_remote.trim() || null,
|
||||
credential_ref: b.credential_ref.trim() || null,
|
||||
},
|
||||
});
|
||||
setCmd({ text: `repository '${id}' updated`, error: false, busy: false });
|
||||
await loadProjects();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return false;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[loadProjects],
|
||||
);
|
||||
|
||||
const deleteProject = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await clientRef.current.api(`/projects/${encodeURIComponent(id)}`, { method: "DELETE" });
|
||||
setCmd({ text: `repository '${id}' removed`, error: false, busy: false });
|
||||
setSelectedProjectId((cur) => (cur === id ? "" : cur));
|
||||
await loadProjects();
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
}
|
||||
},
|
||||
[loadProjects],
|
||||
);
|
||||
|
||||
const submitApproval = useCallback(
|
||||
async (b: ApprovalBody) => {
|
||||
const p = encodeURIComponent(selectedProjectRef.current);
|
||||
await enqueueAndTrack(
|
||||
`/projects/${p}/approvals`,
|
||||
{
|
||||
branch: b.branch.trim(),
|
||||
status: b.status,
|
||||
agent_name: b.agent_name.trim() || null,
|
||||
sha: b.sha.trim() || null,
|
||||
note: b.note.trim() || null,
|
||||
},
|
||||
`${b.status} ${b.branch}`,
|
||||
);
|
||||
await loadApprovals(selectedProjectRef.current);
|
||||
},
|
||||
[enqueueAndTrack, loadApprovals],
|
||||
);
|
||||
|
||||
const createHost = useCallback(
|
||||
async (b: HostBody) => {
|
||||
try {
|
||||
await clientRef.current.api("/hosts", {
|
||||
method: "POST",
|
||||
body: {
|
||||
hostname: b.hostname.trim(),
|
||||
forge_type: b.forge_type,
|
||||
token_env_var: b.token_env_var.trim() || null,
|
||||
base_url: b.base_url.trim() || null,
|
||||
},
|
||||
});
|
||||
setCmd({ text: `git server '${b.hostname}' added`, error: false, busy: false });
|
||||
await loadHosts();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return false;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[loadHosts],
|
||||
);
|
||||
|
||||
const updateHost = useCallback(
|
||||
async (hostname: string, b: Omit<HostBody, "hostname">) => {
|
||||
try {
|
||||
await clientRef.current.api(`/hosts/${encodeURIComponent(hostname)}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
forge_type: b.forge_type,
|
||||
token_env_var: b.token_env_var.trim() || null,
|
||||
base_url: b.base_url.trim() || null,
|
||||
},
|
||||
});
|
||||
setCmd({ text: `git server '${hostname}' updated`, error: false, busy: false });
|
||||
await loadHosts();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return false;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[loadHosts],
|
||||
);
|
||||
|
||||
const deleteHost = useCallback(
|
||||
async (hostname: string) => {
|
||||
try {
|
||||
await clientRef.current.api(`/hosts/${encodeURIComponent(hostname)}`, { method: "DELETE" });
|
||||
setCmd({ text: `git server '${hostname}' removed`, error: false, busy: false });
|
||||
await loadHosts();
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
}
|
||||
},
|
||||
[loadHosts],
|
||||
);
|
||||
|
||||
const pollCi = useCallback(async () => {
|
||||
await enqueueAndTrack("/poll-ci", undefined, "poll-ci (all projects)");
|
||||
await loadCommands();
|
||||
}, [enqueueAndTrack, loadCommands]);
|
||||
|
||||
const setSharedKey = useCallback(
|
||||
async (key: string, value: string) => {
|
||||
try {
|
||||
await clientRef.current.api(`/shared/context/${encodeURIComponent(key)}`, {
|
||||
method: "PUT",
|
||||
body: { value },
|
||||
});
|
||||
setCmd({ text: `shared context '${key}' set`, error: false, busy: false });
|
||||
await loadShared();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return false;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[loadShared],
|
||||
);
|
||||
|
||||
const value: StoreValue = {
|
||||
section,
|
||||
setSection,
|
||||
projects,
|
||||
agents,
|
||||
selectedProjectId,
|
||||
selectProject,
|
||||
selectedRun,
|
||||
selectRun,
|
||||
checkmark,
|
||||
checkmarkMissing,
|
||||
log,
|
||||
logOffset,
|
||||
pageLog,
|
||||
approvals,
|
||||
hosts,
|
||||
commands,
|
||||
shared,
|
||||
cmd,
|
||||
lastError,
|
||||
loading,
|
||||
refresh,
|
||||
spawnAgent,
|
||||
killAgent,
|
||||
deleteAgent,
|
||||
submitAnswer,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
submitApproval,
|
||||
createHost,
|
||||
updateHost,
|
||||
deleteHost,
|
||||
pollCi,
|
||||
setSharedKey,
|
||||
};
|
||||
|
||||
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
||||
}
|
||||
Reference in New Issue
Block a user