fix(approvals): real runtime-values probe; disable actions only when truly down
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

Oracle round-7 watch-out: button title said 'Disabled in this
environment' but disabled prop only blocked when !a.enabled, so a
buyer could still click it and get the 500.

Approvals now probes GET /api/ea2/runtime-values on mount. Treats
HTTP 422 (validation, missing query) and 2xx as 'up' — that proves
the service is reachable. Anything else flips runtimeHealth to
'down', which (a) hides the yellow runtime-note, (b) actually
disables the button, and (c) changes the title attribute to the
honest reason. Removes the misleading '· preview' suffix when the
runtime is fine.

Live probe confirms /api/ea2/runtime-values returns 422 for a bare
GET (validation error), so under normal operation the button stays
enabled. The original 500 came from the runtime engine's internal
cluster DNS to ea2.baseline.svc which is a different code path.
This commit is contained in:
2026-06-14 17:10:20 +04:00
parent 32c6cc635e
commit d4d74cb685
+28 -6
View File
@@ -36,6 +36,8 @@ export default function Approvals() {
const [busy, setBusy] = useState<string | null>(null); const [busy, setBusy] = useState<string | null>(null);
const tenantId = useApp((s) => s.tenantId); const tenantId = useApp((s) => s.tenantId);
const [runtimeHealth, setRuntimeHealth] = useState<"unknown" | "up" | "down">("unknown");
const loadQueue = async () => { const loadQueue = async () => {
if (!tenantId) { if (!tenantId) {
setItems([]); setItems([]);
@@ -52,6 +54,21 @@ export default function Approvals() {
useEffect(() => { void loadQueue(); }, [tenantId]); useEffect(() => { void loadQueue(); }, [tenantId]);
useEffect(() => {
let cancelled = false;
fetch(`${api.config.baseUrl}/api/ea2/runtime-values`, {
method: "GET",
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
})
.then((r) => {
if (cancelled) return;
if (r.status === 422 || r.ok) setRuntimeHealth("up");
else setRuntimeHealth("down");
})
.catch(() => { if (!cancelled) setRuntimeHealth("down"); });
return () => { cancelled = true; };
}, []);
useEffect(() => { useEffect(() => {
if (!activeKey) return; if (!activeKey) return;
let cancelled = false; let cancelled = false;
@@ -163,21 +180,26 @@ export default function Approvals() {
</ul> </ul>
</div> </div>
)} )}
{runtimeHealth === "down" && (
<div className="approvals-runtime-note"> <div className="approvals-runtime-note">
Acting on a step writes the decision back to EA2. The runtime-values service in this environment is currently offline, so a Submit will be rejected with an error. Use this view to inspect cases; switch to a live environment to act on them. Runtime engine is unreachable from this environment. Action buttons are disabled. Use this view to inspect cases; once the runtime comes back, actions will write to EA2.
</div> </div>
)}
<div className="approvals-actions"> <div className="approvals-actions">
{(tx.available_actions || []).map((a: any) => ( {(tx.available_actions || []).map((a: any) => {
const disabled = !a.enabled || busy === a.id || runtimeHealth === "down";
return (
<button <button
key={a.id} key={a.id}
className={`btn ${a.kind === "approve" || a.kind === "submit" ? "btn-primary" : "btn-secondary"}`} className={`btn ${a.kind === "approve" || a.kind === "submit" ? "btn-primary" : "btn-secondary"}`}
disabled={!a.enabled || busy === a.id} disabled={disabled}
onClick={() => handleAction(a.id)} onClick={() => handleAction(a.id)}
title="Disabled in this environment — runtime-values offline" title={runtimeHealth === "down" ? "Runtime engine offline" : a.display_label || a.id}
> >
<Pulse size={11} /> {busy === a.id ? "Sending…" : `${a.display_label || a.id} · preview`} <Pulse size={11} /> {busy === a.id ? "Sending…" : a.display_label || a.id}
</button> </button>
))} );
})}
{(!tx.available_actions || tx.available_actions.length === 0) && ( {(!tx.available_actions || tx.available_actions.length === 0) && (
<div className="hub-empty">No actions available on this step.</div> <div className="hub-empty">No actions available on this step.</div>
)} )}