import { useEffect, useRef, useState } from 'react'; import { Button, Callout, Intent, Tag, Spinner, NonIdealState, Collapse, HTMLTable, } from '@blueprintjs/core'; import { getProjects, getBuildHistory, type ProjectDefinition, type BuildRecord } from '../api/buildApi'; import { getGitLog, type GitCommit } from '../api/gitApi'; 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) => ( ))}
)} ); }