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 tenantId = useApp((s) => s.tenantId);
const [runtimeHealth, setRuntimeHealth] = useState<"unknown" | "up" | "down">("unknown");
const loadQueue = async () => {
if (!tenantId) {
setItems([]);
@@ -52,6 +54,21 @@ export default function Approvals() {
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(() => {
if (!activeKey) return;
let cancelled = false;
@@ -163,21 +180,26 @@ export default function Approvals() {
</ul>
</div>
)}
{runtimeHealth === "down" && (
<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 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
key={a.id}
className={`btn ${a.kind === "approve" || a.kind === "submit" ? "btn-primary" : "btn-secondary"}`}
disabled={!a.enabled || busy === a.id}
disabled={disabled}
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>
))}
);
})}
{(!tx.available_actions || tx.available_actions.length === 0) && (
<div className="hub-empty">No actions available on this step.</div>
)}