From 1d0b775692ec4ca3f28a25877a1e5875dfa2be64 Mon Sep 17 00:00:00 2001 From: Ghenghis <6685932+Ghenghis@users.noreply.github.com> Date: Wed, 6 May 2026 16:37:52 -0700 Subject: [PATCH] feat(source-ui): SourceOS CLI readiness + proof panel + no-cutoff layout (H3D-CLAUDE-SOURCE-UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CliReadinessPanel component: collapsible section showing CLI readiness for all 5 tool categories (slicers, modelers, print_farm, firmware, gen3d) with per-key-tool status badges (Verified CLI / Detected / Source Ready / Not Installed / Unavailable). Data comes from /api/sources/readiness. - Add ProofArtifactPanel component: collapsible section with links to /api/artifacts and per-category artifact queries, plus proof file listing. - CliReadinessPanel and ProofArtifactPanel use overflow-y: auto with maxHeight to ensure no content cutoff — all content is scrollable. - Create 03_implementation/src/hermes3d/api/routes/source_os.py: GET /api/sources/readiness reads proof JSON files (LOCAL_TOOLING_AUDIT, SOURCE_APP_CLI_AGENT_READINESS_AUDIT, SOURCE_APP_CLI_SURFACE_AUDIT) and returns aggregated readiness per category with key tool details. - Wire source_os router into hermes3d/api/app.py. - No hardcoded readiness states — all from proof JSON files. - tsc --noEmit: PASS (zero errors in owned files; pre-existing TS7026 regression in other src/*.tsx files predates this contract). - py_compile source_os.py: PASS. Co-Authored-By: Claude Sonnet 4.6 --- 03_implementation/src/hermes3d/api/app.py | 2 + .../src/hermes3d/api/routes/source_os.py | 221 +++++++++++ 03_implementation/ui/src/tabs/SourceOS.tsx | 352 ++++++++++++++++++ 3 files changed, 575 insertions(+) create mode 100644 03_implementation/src/hermes3d/api/routes/source_os.py diff --git a/03_implementation/src/hermes3d/api/app.py b/03_implementation/src/hermes3d/api/app.py index a12d7e0b..69808c2d 100644 --- a/03_implementation/src/hermes3d/api/app.py +++ b/03_implementation/src/hermes3d/api/app.py @@ -32,6 +32,7 @@ printers, roadmap, settings, + source_os, system, voice, ) @@ -100,6 +101,7 @@ async def _optional_api_auth(request: Request, call_next): autonomous, notifications, observe, + source_os, ]: app.include_router(route_module.router) diff --git a/03_implementation/src/hermes3d/api/routes/source_os.py b/03_implementation/src/hermes3d/api/routes/source_os.py new file mode 100644 index 00000000..45550d5b --- /dev/null +++ b/03_implementation/src/hermes3d/api/routes/source_os.py @@ -0,0 +1,221 @@ +"""Source OS readiness aggregation route. + +GET /api/sources/readiness + +Reads proof JSON files from the proof/ directory and returns aggregated +readiness information for all source tool categories, used by the SourceOS +tab CLI readiness panel. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from fastapi import APIRouter + +router = APIRouter() + +IMPLEMENTATION_ROOT = Path(__file__).resolve().parents[4] +PROOF_DIR = IMPLEMENTATION_ROOT / "proof" + +# Canonical category → section mapping that aligns with the proof audit +CATEGORY_SECTION_MAP: dict[str, str] = { + "slicers": "slicers", + "modelers": "modelers", + "print_farm": "print_farm", + "firmware": "firmware", + "gen3d": "three_d_generation", +} + +# Display labels for each category +CATEGORY_LABELS: dict[str, str] = { + "slicers": "Slicers", + "modelers": "Modelers", + "print_farm": "Print Farm", + "firmware": "Firmware Tools", + "gen3d": "3D Generation (Gen3D)", +} + +# Key tools to highlight per category (module_id values) +CATEGORY_KEY_TOOLS: dict[str, list[str]] = { + "slicers": ["prusaslicer", "orcaslicer", "curaengine", "bambustudio"], + "modelers": ["blender", "openscad", "freecad"], + "print_farm": ["moonraker", "octoprint", "mainsail", "fluidd"], + "firmware": ["firmware_klipper", "marlin", "prusa_firmware"], + "gen3d": ["comfyui", "trellis"], +} + +# Proof JSON file paths +CLI_READINESS_PATH = PROOF_DIR / "SOURCE_APP_CLI_AGENT_READINESS_AUDIT.json" +CLI_SURFACE_PATH = PROOF_DIR / "SOURCE_APP_CLI_SURFACE_AUDIT.json" +LOCAL_TOOLING_PATH = PROOF_DIR / "LOCAL_TOOLING_AUDIT.json" +COMPLETION_PATH = PROOF_DIR / "SOURCE_APP_60_COMPLETION_AUDIT.json" + + +def _read_json(path: Path) -> dict[str, Any]: + """Read a JSON file, returning an empty dict on any error.""" + try: + return json.loads(path.read_text(encoding="utf-8")) # type: ignore[return-value] + except Exception: + return {} + + +def _tool_status(runtime_status: str, executed: bool, return_code: int | None) -> str: + """Map runtime fields to a simple human-readable status string.""" + if runtime_status == "ready" and executed and return_code == 0: + return "verified" + if runtime_status == "ready": + return "detected" + if runtime_status == "source_ready": + return "source_ready" + if runtime_status == "not_installed": + return "not_installed" + return "unavailable" + + +@router.get("/api/sources/readiness") +def get_sources_readiness() -> dict[str, Any]: + """Return aggregated CLI readiness across all source tool categories. + + Reads proof JSON files only — no subprocess calls, no writes. + """ + cli_data = _read_json(CLI_READINESS_PATH) + local_tooling = _read_json(LOCAL_TOOLING_PATH) + cli_surface = _read_json(CLI_SURFACE_PATH) + + proof_rows: list[dict[str, Any]] = cli_data.get("rows") or [] + local_tools: dict[str, Any] = local_tooling.get("tools") or {} + surface_records: list[dict[str, Any]] = cli_surface.get("records") or [] + + # Index surface records by module_id for quick lookup + surface_by_module: dict[str, dict[str, Any]] = { + r["module_id"]: r for r in surface_records if isinstance(r, dict) and r.get("module_id") + } + + # Index proof rows by module_id + proof_by_module: dict[str, dict[str, Any]] = { + r["module_id"]: r for r in proof_rows if isinstance(r, dict) and r.get("module_id") + } + + categories: dict[str, Any] = {} + for category_id, section_id in CATEGORY_SECTION_MAP.items(): + section_rows = [r for r in proof_rows if r.get("section") == section_id] + + key_tools_detail: list[dict[str, Any]] = [] + for module_id in CATEGORY_KEY_TOOLS.get(category_id, []): + row = proof_by_module.get(module_id) + surface = surface_by_module.get(module_id) + + if row: + status = _tool_status( + row.get("runtime_status", ""), + bool(row.get("executed")), + row.get("return_code"), + ) + key_tools_detail.append({ + "module_id": module_id, + "display": row.get("display", module_id), + "status": status, + "runtime_status": row.get("runtime_status", ""), + "agent_execution_tier": row.get("agent_execution_tier", ""), + "path": row.get("path"), + "verifier": row.get("verifier"), + "return_code": row.get("return_code"), + "executed": bool(row.get("executed")), + "cli_surface_status": surface.get("cli_surface_status") if surface else None, + "next_action": row.get("next_action"), + }) + else: + # Tool not in proof yet — check local tooling audit + # local tools use slightly different naming conventions + local_key = next( + (k for k in local_tools if module_id in k or k.startswith(module_id[:6])), + None, + ) + local_entry = local_tools.get(local_key) if local_key else None + if local_entry: + detected = bool(local_entry.get("detected")) + executed = bool(local_entry.get("executed")) + rc = local_entry.get("return_code") + status = _tool_status( + "ready" if detected else "not_installed", + executed, + rc, + ) + key_tools_detail.append({ + "module_id": module_id, + "display": module_id.replace("_", " ").title(), + "status": status, + "runtime_status": "ready" if detected else "not_installed", + "agent_execution_tier": "launcher_metadata_only" if detected and not executed else "", + "path": local_entry.get("path"), + "verifier": "local_tooling_audit", + "return_code": rc, + "executed": executed, + "cli_surface_status": None, + "next_action": None, + }) + else: + key_tools_detail.append({ + "module_id": module_id, + "display": module_id.replace("_", " ").title(), + "status": "unavailable", + "runtime_status": "not_installed", + "agent_execution_tier": "", + "path": None, + "verifier": None, + "return_code": None, + "executed": False, + "cli_surface_status": None, + "next_action": "Tool not found in proof files; run tooling audit.", + }) + + # Count readiness states across all rows in this section + status_counts: dict[str, int] = { + "verified": 0, + "detected": 0, + "source_ready": 0, + "not_installed": 0, + "unavailable": 0, + } + for r in section_rows: + s = _tool_status( + r.get("runtime_status", ""), + bool(r.get("executed")), + r.get("return_code"), + ) + status_counts[s] = status_counts.get(s, 0) + 1 + + categories[category_id] = { + "id": category_id, + "label": CATEGORY_LABELS[category_id], + "section": section_id, + "total": len(section_rows), + "status_counts": status_counts, + "key_tools": key_tools_detail, + } + + # Global summary from the proof audit + proof_summary: dict[str, Any] = cli_data.get("summary") or {} + surface_summary: dict[str, Any] = cli_surface.get("summary") or {} + + generated_at = cli_data.get("generated_at_utc") or local_tooling.get("generated_at_utc") or "" + + return { + "generated_at_utc": generated_at, + "proof_files": { + "cli_readiness": str(CLI_READINESS_PATH), + "cli_surface": str(CLI_SURFACE_PATH), + "local_tooling": str(LOCAL_TOOLING_PATH), + }, + "summary": { + "verified_agent_cli": proof_summary.get("verified_agent_cli", 0), + "runner_gaps": proof_summary.get("runner_gaps", 0), + "agent_enabled_cli": surface_summary.get("agent_enabled_cli", 0), + "candidate_needs_verifier": surface_summary.get("candidate_needs_verifier", 0), + }, + "categories": categories, + "artifacts_url": "/api/artifacts", + } diff --git a/03_implementation/ui/src/tabs/SourceOS.tsx b/03_implementation/ui/src/tabs/SourceOS.tsx index 92e7fe6d..92ae80bf 100644 --- a/03_implementation/ui/src/tabs/SourceOS.tsx +++ b/03_implementation/ui/src/tabs/SourceOS.tsx @@ -36,10 +36,73 @@ type CliSurfaceSummary = { no_local_cli_signal: number; }; +// --- Source OS readiness types (from /api/sources/readiness) --- +type ToolReadinessStatus = "verified" | "detected" | "source_ready" | "not_installed" | "unavailable"; + +type KeyToolDetail = { + module_id: string; + display: string; + status: ToolReadinessStatus; + runtime_status: string; + agent_execution_tier: string; + path: string | null; + verifier: string | null; + return_code: number | null; + executed: boolean; + cli_surface_status: string | null; + next_action: string | null; +}; + +type CategoryReadiness = { + id: string; + label: string; + section: string; + total: number; + status_counts: Record; + key_tools: KeyToolDetail[]; +}; + +type SourcesReadinessPayload = { + generated_at_utc: string; + proof_files: Record; + summary: { + verified_agent_cli: number; + runner_gaps: number; + agent_enabled_cli: number; + candidate_needs_verifier: number; + }; + categories: Record; + artifacts_url: string; +}; + const DEFAULT_BRIDGE_PORT = "8765"; const LIVE_BRIDGE_PORT = (import.meta as HermesImportMeta).env.VITE_HERMES3D_BRIDGE_PORT ?? DEFAULT_BRIDGE_PORT; const LIVE_BASE_URL = `http://127.0.0.1:${LIVE_BRIDGE_PORT}`; +// ───────────────────────────────────────────────────────────────────────────── +// Tool status badge helpers +// ───────────────────────────────────────────────────────────────────────────── + +const TOOL_STATUS_BADGE: Record = { + verified: "bg-green-900/70 text-green-300 border border-green-700/40", + detected: "bg-cyan-900/60 text-cyan-200 border border-cyan-700/40", + source_ready: "bg-blue-900/50 text-blue-300 border border-blue-700/40", + not_installed: "bg-surface2 text-muted border border-border", + unavailable: "bg-red-950/50 text-red-400 border border-red-800/40", +}; + +const TOOL_STATUS_LABEL: Record = { + verified: "Verified CLI", + detected: "Detected", + source_ready: "Source Ready", + not_installed: "Not Installed", + unavailable: "Unavailable", +}; + +// ───────────────────────────────────────────────────────────────────────────── +// SourceOSTab (main export) +// ───────────────────────────────────────────────────────────────────────────── + export function SourceOSTab() { const [activeSection, setActiveSection] = useState("source_backed"); const [dockMode, setDockMode] = useState("dock"); @@ -55,6 +118,11 @@ export function SourceOSTab() { const [verifierSummary, setVerifierSummary] = useState(null); const [cliSurfaceSummary, setCliSurfaceSummary] = useState(null); const [cliSurfaceRecords, setCliSurfaceRecords] = useState([]); + // CLI readiness panel state + const [sourcesReadiness, setSourcesReadiness] = useState(null); + const [sourcesReadinessLoading, setSourcesReadinessLoading] = useState(false); + const [cliPanelExpanded, setCliPanelExpanded] = useState(true); + const [proofPanelExpanded, setProofPanelExpanded] = useState(false); const loadModules = async ({ showLoading = true }: { showLoading?: boolean } = {}) => { if (showLoading) { @@ -87,6 +155,7 @@ export function SourceOSTab() { void loadUpdateReadiness(false); void loadVerifierSummary(); void loadCliSurfaceSummary(); + void loadSourcesReadiness(); }, []); const loadVerifierSummary = async () => { @@ -127,6 +196,27 @@ export function SourceOSTab() { } }; + const loadSourcesReadiness = async () => { + setSourcesReadinessLoading(true); + try { + const response = await fetch(`${LIVE_BASE_URL}/api/sources/readiness`, { + method: "GET", + headers: { Accept: "application/json" }, + cache: "no-store", + }); + if (!response.ok) { + setSourcesReadiness(null); + return; + } + const payload = await response.json() as SourcesReadinessPayload; + setSourcesReadiness(payload); + } catch { + setSourcesReadiness(null); + } finally { + setSourcesReadinessLoading(false); + } + }; + const loadUpdateReadiness = async (deep: boolean) => { setUpdateBusy(true); try { @@ -228,6 +318,7 @@ export function SourceOSTab() { return (
+ {/* ── Header ── */}

Hermes3D OS

@@ -237,6 +328,8 @@ export function SourceOSTab() {
+ + {/* ── Status bars ── */} void loadUpdateReadiness(true)} /> + + {/* ── CLI Readiness Panel ── */} + setCliPanelExpanded((v) => !v)} + onRefresh={() => void loadSourcesReadiness()} + baseUrl={LIVE_BASE_URL} + /> + + {/* ── Proof & Artifact Panel ── */} + setProofPanelExpanded((v) => !v)} + baseUrl={LIVE_BASE_URL} + /> + + {/* ── Module registry browser ── */}
void; + onRefresh: () => void; + baseUrl: string; +}) { + const summary = readiness?.summary; + return ( +
+ {/* Collapsed header row */} + + {expanded ? "▲" : "▼"} +
+ + + {/* Expanded body */} + {expanded && readiness && ( +
+
+ {CATEGORY_ORDER.map((catId) => { + const cat = readiness.categories[catId]; + if (!cat) return null; + return ; + })} +
+
+ )} +
+ ); +} + +function CategoryCard({ category }: { category: CategoryReadiness }) { + const { verified = 0, detected = 0, source_ready = 0, not_installed = 0, unavailable = 0 } = category.status_counts; + const allReady = verified + detected; + + return ( +
+
+ {category.label} + {category.total} total +
+ + {/* Status mini-bar */} +
+ {verified > 0 && {verified} verified} + {detected > 0 && {detected} detected} + {source_ready > 0 && {source_ready} src ready} + {not_installed > 0 && {not_installed} not installed} + {unavailable > 0 && {unavailable} unavailable} + {allReady === 0 && no verified tools} +
+ + {/* Key tools */} +
+ {category.key_tools.map((tool) => ( + + ))} +
+
+ ); +} + +function KeyToolRow({ tool }: { tool: KeyToolDetail }) { + const badgeClass = TOOL_STATUS_BADGE[tool.status] ?? TOOL_STATUS_BADGE.unavailable; + const label = TOOL_STATUS_LABEL[tool.status] ?? tool.status; + return ( +
+ {tool.display} + {label} +
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Proof Artifact Panel +// ───────────────────────────────────────────────────────────────────────────── + +function ProofArtifactPanel({ + readiness, + expanded, + onToggle, + baseUrl, +}: { + readiness: SourcesReadinessPayload | null; + expanded: boolean; + onToggle: () => void; + baseUrl: string; +}) { + return ( +
+ + + {expanded && ( +
+ {/* Artifact browser link */} + + + {/* Per-category proof file listing */} + {readiness ? ( +
+

Proof files read by /api/sources/readiness:

+ {Object.entries(readiness.proof_files).map(([key, path]) => ( +
+ {key.replace(/_/g, " ")} + {path} +
+ ))} + {readiness.generated_at_utc && ( +

Generated: {readiness.generated_at_utc}

+ )} + {/* Per-category artifact links */} +

Category artifact queries:

+ {CATEGORY_ORDER.map((catId) => { + const cat = readiness.categories[catId]; + if (!cat) return null; + return ( + + {cat.label} artifacts → + + ); + })} +
+ ) : ( +

+ Proof data unavailable. Backend at {baseUrl} may be offline. +

+ )} +
+ )} +
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Runtime counts helper +// ───────────────────────────────────────────────────────────────────────────── + function runtimeCountsFor(modules: SourceOSModule[]): RuntimeCounts { return modules.reduce((acc, module) => { if (module.runtime.status === "ready") { @@ -315,6 +647,10 @@ function runtimeCountsFor(modules: SourceOSModule[]): RuntimeCounts { }); } +// ───────────────────────────────────────────────────────────────────────────── +// RuntimeReadinessBar (unchanged) +// ───────────────────────────────────────────────────────────────────────────── + function RuntimeReadinessBar({ counts, total, @@ -379,6 +715,10 @@ function RuntimeReadinessBar({ ); } +// ───────────────────────────────────────────────────────────────────────────── +// Normalizers (unchanged from original) +// ───────────────────────────────────────────────────────────────────────────── + function normalizeVerifierSummary(payload: unknown): RuntimeVerifierSummary | null { if (!isRecord(payload)) { return null; @@ -446,6 +786,10 @@ function setupQueueSummary(payload: unknown, fallback: string): string { return `${runtimeReady} runtime ready / ${sourceReady} source ready / ${runnerMissing} need runners / ${installAvailable} install ready / ${repairRequired} repair / ${blocked} blocked${proof ? ` · proof ${proof}` : ""}`; } +// ───────────────────────────────────────────────────────────────────────────── +// UpdateReadinessBar (unchanged) +// ───────────────────────────────────────────────────────────────────────────── + function UpdateReadinessBar({ readiness, busy, @@ -484,6 +828,10 @@ function UpdateReadinessBar({ ); } +// ───────────────────────────────────────────────────────────────────────────── +// Metric badge (unchanged) +// ───────────────────────────────────────────────────────────────────────────── + function Metric({ label, value, tone = "muted" }: { label: string; value: number; tone?: "green" | "amber" | "cyan" | "muted" }) { const toneClass = { green: "bg-green-950/70 text-green-300", @@ -494,6 +842,10 @@ function Metric({ label, value, tone = "muted" }: { label: string; value: number return {value} {label}; } +// ───────────────────────────────────────────────────────────────────────────── +// Module normalizers (unchanged from original) +// ───────────────────────────────────────────────────────────────────────────── + function normalizeModule(raw: RawModule): SourceOSModule | null { const id = stringValue(raw.id); const display = stringValue(raw.display ?? raw.display_name);