diff --git a/clarity.controlplane/src/pages/BuildMonitorPage.tsx b/clarity.controlplane/src/pages/BuildMonitorPage.tsx index 1d49de3..d9ecf75 100644 --- a/clarity.controlplane/src/pages/BuildMonitorPage.tsx +++ b/clarity.controlplane/src/pages/BuildMonitorPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { AnchorButton, Button, Callout, Collapse, HTMLTable, Intent, NonIdealState, Spinner, Tag, } from '@blueprintjs/core'; @@ -26,7 +26,7 @@ const STATUS_INTENT: Record = { Running: Intent.PRIMARY, }; -// ── Inline log viewer ───────────────────────────────────────────────────────── +// ── Inline log viewer ───────────────────────────────────────────────────────── function LogRow({ lines }: { lines: string[] }) { const ref = useRef(null); @@ -47,7 +47,7 @@ function LogRow({ lines }: { lines: string[] }) { ); } -// ── Manual project row ──────────────────────────────────────────────────────── +// ── Manual project row ──────────────────────────────────────────────────────── function ProjectRow({ project, lastBuild, onBuilt, @@ -96,8 +96,8 @@ function ProjectRow({ }; const statusIntent = lastBuild ? STATUS_INTENT[lastBuild.status] : Intent.NONE; - const duration = lastBuild?.durationMs != null ? `${(lastBuild.durationMs / 1000).toFixed(1)}s` : '—'; - const lastRun = lastBuild ? new Date(lastBuild.startedAt).toLocaleString() : '—'; + const duration = lastBuild?.durationMs != null ? `${(lastBuild.durationMs / 1000).toFixed(1)}s` : '—'; + const lastRun = lastBuild ? new Date(lastBuild.startedAt).toLocaleString() : '—'; return ( <> @@ -129,17 +129,17 @@ function ProjectRow({ ); } -// ── CI (SolutionBuild) row ─────────────────────────────────────────────────── +// ── CI (SolutionBuild) row ─────────────────────────────────────────────────── function CiRow({ build, giteaBase }: { build: BuildRecord; giteaBase: string }) { const [open, setOpen] = useState(false); - const repo = build.target.replace(/\.slnx?$/, ''); // "ControlPlane.slnx" → "ControlPlane" + const repo = build.target.replace(/\.slnx?$/, ''); // "ControlPlane.slnx" → "ControlPlane" const shortSha = build.commitSha?.slice(0, 7); const shaUrl = build.commitSha ? `${giteaBase}/ClarityStack/${repo === 'ControlPlane' ? 'OPC' : repo}/commit/${build.commitSha}` : null; - const duration = build.durationMs != null ? `${(build.durationMs / 1000).toFixed(1)}s` : '—'; + const duration = build.durationMs != null ? `${(build.durationMs / 1000).toFixed(1)}s` : '—'; const lastRun = new Date(build.startedAt).toLocaleString(); return ( @@ -157,7 +157,7 @@ function CiRow({ build, giteaBase }: { build: BuildRecord; giteaBase: string }) style={{ fontFamily: 'monospace', fontSize: '0.72rem', padding: '0 4px' }}> {shortSha} - ) : } + ) : —} {build.status} {lastRun} @@ -175,7 +175,7 @@ function CiRow({ build, giteaBase }: { build: BuildRecord; giteaBase: string }) ); } -// ── Page ────────────────────────────────────────────────────────────────────── +// ── Page ────────────────────────────────────────────────────────────────────── export default function BuildMonitorPage() { const [projects, setProjects] = useState([]); @@ -261,298 +261,3 @@ export default function BuildMonitorPage() { ); } - -const BASE_URL = import.meta.env.VITE_API_URL ?? ''; - -const KIND_INTENT: Record = { - DotnetProject: Intent.PRIMARY, - NpmProject: Intent.WARNING, - DockerImage: Intent.NONE, - SolutionBuild: Intent.SUCCESS, -}; - -const STATUS_INTENT: Record = { - Succeeded: Intent.SUCCESS, - Failed: Intent.DANGER, - Running: Intent.PRIMARY, -}; - -// ── Git history panel ───────────────────────────────────────────────────────── - -function GitHistoryPanel({ relativePath }: { relativePath: string }) { - const [commits, setCommits] = useState([]); - const [loading, setLoading] = useState(false); - const [open, setOpen] = useState(false); - const [fetched, setFetched] = useState(false); - const [error, setError] = useState(null); - - const toggle = async () => { - if (!open && !fetched) { - setLoading(true); - setError(null); - try { - const data = await getGitLog(relativePath, 10); - setCommits(data); - setFetched(true); - } catch (e) { - setError(e instanceof Error ? e.message : 'Failed to load git history'); - } finally { - setLoading(false); - } - } - setOpen((o) => !o); - }; - - return ( -
-
- ); -} - -// ── Per-project card ────────────────────────────────────────────────────────── - -function ProjectCard({ - project, - lastBuild, - onBuilt, -}: { - project: ProjectDefinition; - lastBuild: BuildRecord | undefined; - onBuilt: () => void; -}) { - const [building, setBuilding] = useState(false); - const [logs, setLogs] = useState([]); - const [open, setOpen] = useState(false); - const [error, setError] = useState(null); - const logRef = useRef(null); - - useEffect(() => { - if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; - }, [logs]); - - const handleBuild = async () => { - setBuilding(true); - setOpen(true); - setLogs([]); - setError(null); - - try { - const res = await fetch( - `${BASE_URL}/api/builds/${encodeURIComponent(project.name)}`, - { method: 'POST' } - ); - if (!res.ok || !res.body) throw new Error(res.statusText); - - const reader = res.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const parts = buffer.split('\n\n'); - buffer = parts.pop() ?? ''; - for (const chunk of parts) { - const dataLine = chunk.replace(/^data:\s*/m, '').trim(); - if (!dataLine) continue; - try { - const msg = JSON.parse(dataLine); - if (msg.done) onBuilt(); - else if (typeof msg.line === 'string') - setLogs((p) => [...p.slice(-1000), msg.line]); - } catch { /* ignore */ } - } - } - } catch (e) { - setError(e instanceof Error ? e.message : 'Unknown error'); - } finally { - setBuilding(false); - } - }; - - const statusIntent = lastBuild ? STATUS_INTENT[lastBuild.status] : Intent.NONE; - const statusLabel = lastBuild?.status ?? 'Never built'; - const lastRun = lastBuild ? new Date(lastBuild.startedAt).toLocaleString() : '—'; - const duration = lastBuild?.durationMs != null - ? `${(lastBuild.durationMs / 1000).toFixed(1)}s` : null; - - return ( -
-
-
- {project.name} - - {project.relativePath} - -
-
- {project.kind} - {statusLabel} - {duration && {duration}} -
-
- -
-
- - {error && {error}} - - {open && logs.length > 0 && ( -
- {logs.map((l, i) =>
{l}
)} -
- )} - - -
- ); -} - -// ── Page ────────────────────────────────────────────────────────────────────── - -export default function BuildMonitorPage() { - const [projects, setProjects] = useState([]); - const [history, setHistory] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const load = async () => { - setLoading(true); - setError(null); - try { - const [p, h] = await Promise.all([getProjects(), getBuildHistory()]); - setProjects(p); - setHistory(h); - } catch (e) { - setError(e instanceof Error ? e.message : 'Failed to load'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { (async () => { await load(); })(); }, []); - - // Find latest build per project — match exactly by relativePath (= build target) - const lastBuildFor = (project: ProjectDefinition): BuildRecord | undefined => - history.find((b) => b.target === project.relativePath); - - return ( - <> -
-
-

Build Monitor

-

Trigger and track builds for every project in the solution.

-
-
-
-
- - {error && ( - - {error} - - )} - - {loading && } title="Loading projects..." />} - - {!loading && !error && ( -
- {projects.map((p) => ( - - ))} -
- )} - - ); -}