gitpilot / frontend /components /SandboxStatusWidget.jsx
github-actions[bot]
Deploy from 53884f28
6078519
Raw
History Blame Contribute Delete
5.53 kB
// 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 (
<div style={s.shell}>
<div style={s.title}>Sandbox</div>
<div style={s.dim}>Checking…</div>
</div>
);
}
if (error) {
return (
<div style={s.shell}>
<div style={s.title}>Sandbox</div>
<div style={{ ...s.statusRow, color: "#fca5a5" }}>
<span></span> Status check failed
</div>
<div style={s.dim}>{error}</div>
</div>
);
}
const backend = status.backend || "subprocess";
const ok = !!status.ok;
const label = BACKEND_LABEL[backend] || backend;
const dot = ok ? "●" : "⚠";
const dotColor = ok ? "#10B981" : "#f59e0b";
const stateText = ok ? "Ready" : "Unavailable";
return (
<div style={s.shell}>
<div style={s.titleRow}>
<span style={s.title}>Sandbox</span>
<button type="button" onClick={refresh} style={s.refresh}
title="Refresh sandbox status"></button>
</div>
<div style={s.statusRow}>
<span style={{ color: dotColor, marginRight: 6 }}>{dot}</span>
<strong>{label}</strong>
<span style={s.dim}> · {stateText}</span>
</div>
{status.matrixlab_url && backend === "matrixlab" && (
<div style={s.metaRow}>
<span style={s.metaKey}>URL</span>
<span style={s.metaVal}>{status.matrixlab_url}</span>
</div>
)}
<div style={s.metaRow}>
<span style={s.metaKey}>Timeout</span>
<span style={s.metaVal}>{status.timeout_sec}s</span>
</div>
<div style={s.metaRow}>
<span style={s.metaKey}>Network</span>
<span style={s.metaVal}>
{status.allow_network ? "Enabled" : "Disabled"}
</span>
</div>
<div style={s.actions}>
{onOpenSettings && (
<button type="button" onClick={onOpenSettings} style={s.action}>
{ok ? "Change" : "Repair"}
</button>
)}
{!ok && backend === "matrixlab" && (
<button
type="button"
onClick={switchToLocal}
disabled={switching}
style={{ ...s.action, opacity: switching ? 0.6 : 1 }}
title="Switch the sandbox backend to Local subprocess"
>
{switching ? "Switching…" : "Use Local"}
</button>
)}
</div>
</div>
);
}
const s = {
shell: {
padding: "10px 12px",
background: "#0d0e17",
border: "1px solid #1f2937",
borderRadius: 8,
fontFamily: "system-ui, sans-serif",
fontSize: 12,
color: "#e4e4e7",
},
titleRow: { display: "flex", justifyContent: "space-between", alignItems: "center" },
title: {
fontSize: 10, fontWeight: 700, letterSpacing: "0.06em",
color: "#9092b5", textTransform: "uppercase",
},
refresh: {
background: "transparent", color: "#71717a", border: "none",
cursor: "pointer", fontSize: 14, padding: 0,
},
statusRow: { marginTop: 6, display: "flex", alignItems: "center" },
metaRow: { marginTop: 4, display: "flex", justifyContent: "space-between" },
metaKey: { color: "#9092b5", fontSize: 11 },
metaVal: { color: "#d4d4d8", fontSize: 11, fontFamily: "ui-monospace, monospace" },
actions: { display: "flex", gap: 6, marginTop: 8 },
action: {
flex: 1, padding: "4px 8px", fontSize: 11,
background: "transparent", color: "#a1a1aa",
border: "1px solid #3F3F46", borderRadius: 4, cursor: "pointer",
},
dim: { color: "#71717a", fontSize: 11 },
};