// frontend/components/SandboxStatusWidget.jsx // // Always-visible sandbox health pill for the sidebar. Polls // /api/sandbox/status every 30s and surfaces: // // ● MatrixLab Ready (backend up, /health green) // ⚠ MatrixLab unavailable (network/timeout on /health) // ● Local active (using subprocess by choice) // // When degraded, offers two one-click recoveries: // * Repair — opens Settings → Sandbox to run the install/repair flow // * Use Local — flips backend to subprocess via PUT /api/sandbox/config // // Purely informational: failures here never block the chat / planner. import React, { useCallback, useEffect, useRef, useState } from "react"; import { apiUrl } from "../utils/api.js"; const POLL_MS = 30_000; const BACKEND_LABEL = { subprocess: "Local", matrixlab: "MatrixLab", off: "Pass-through", }; export default function SandboxStatusWidget({ onOpenSettings }) { const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [switching, setSwitching] = useState(false); const timerRef = useRef(null); const refresh = useCallback(async () => { try { const res = await fetch(apiUrl("/api/sandbox/status")); const data = await res.json(); if (!res.ok) { setError(data.detail || `HTTP ${res.status}`); return; } setStatus(data); setError(null); } catch (err) { setError(err.message || "Unable to reach sandbox status"); } }, []); useEffect(() => { refresh(); timerRef.current = setInterval(refresh, POLL_MS); return () => clearInterval(timerRef.current); }, [refresh]); const switchToLocal = async () => { setSwitching(true); try { await fetch(apiUrl("/api/sandbox/config"), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ backend: "subprocess" }), }); await refresh(); } finally { setSwitching(false); } }; if (!status && !error) { return (