harden terminal ui error states

This commit is contained in:
dirtydishes 2026-05-29 18:16:09 -04:00
parent 5538f3faa1
commit 7835342cd3
5 changed files with 675 additions and 10 deletions

View file

@ -10,6 +10,7 @@
--text: oklch(0.93 0.014 250);
--text-dim: oklch(0.74 0.018 250);
--text-faint: oklch(0.59 0.016 250);
--text-muted: var(--text-dim);
--accent: oklch(0.78 0.12 74);
--accent-soft: oklch(0.78 0.12 74 / 0.1);
--green: oklch(0.74 0.13 151);
@ -661,6 +662,7 @@ h3 {
color: var(--text-muted);
font-size: 0.78rem;
line-height: 1.35;
overflow-wrap: anywhere;
}
.flow-filter-checkbox-grid,
@ -1249,6 +1251,12 @@ h3 {
color: var(--text-dim);
}
.drawer-note,
.drawer-empty,
.note {
overflow-wrap: anywhere;
}
.chart-surface {
width: 100%;
height: 460px;
@ -1457,6 +1465,7 @@ h3 {
color: oklch(0.91 0.08 72);
font-size: 0.78rem;
line-height: 1.35;
overflow-wrap: anywhere;
}
.data-table-wrap {
@ -2291,6 +2300,7 @@ h3 {
.synthetic-control-error {
color: var(--red);
overflow-wrap: anywhere;
}
.drawer-header {

View file

@ -14,6 +14,7 @@ import {
countActiveFlowFilterGroups,
filterOptionTapeItems,
findAnchorRestoreIndex,
formatUiErrorMessage,
formatCompactUsd,
formatOptionContractLabel,
flushPausableTapeData,
@ -80,6 +81,25 @@ describe("tape status hardening", () => {
});
});
describe("terminal error message hardening", () => {
it("normalizes whitespace and clamps oversized messages before rendering", () => {
const longMessage = `API failed\n\n${"x".repeat(320)}`;
const formatted = formatUiErrorMessage(longMessage);
expect(formatted).toHaveLength(240);
expect(formatted).toStartWith("API failed x");
expect(formatted).toEndWith("...");
expect(formatted).not.toContain("\n");
});
it("uses a fallback when an error payload is empty", () => {
expect(formatUiErrorMessage(" ", "Synthetic status could not be loaded")).toBe(
"Synthetic status could not be loaded"
);
});
});
const makeItem = (traceId: string, seq: number, ts: number) => ({
trace_id: traceId,
seq,

View file

@ -507,6 +507,20 @@ const sampleToLimit = <T,>(items: T[], limit: number): T[] => {
return sampled;
};
export const formatUiErrorMessage = (message: unknown, fallback = "Request failed"): string => {
const raw =
message instanceof Error
? message.message
: typeof message === "string"
? message
: String(message ?? "");
const normalized = raw.replace(/\s+/g, " ").trim();
if (!normalized) {
return fallback;
}
return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
};
const readErrorDetail = async (response: Response): Promise<string> => {
const statusLabel = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
const text = await response.text();
@ -530,9 +544,9 @@ const readErrorDetail = async (response: Response): Promise<string> => {
error?: string;
message?: string;
};
return payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`;
return formatUiErrorMessage(payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`);
} catch {
return `${statusLabel}: ${truncated}`;
return formatUiErrorMessage(`${statusLabel}: ${truncated}`);
}
};
@ -4479,7 +4493,7 @@ const CandleChart = ({
if (!active) {
return;
}
setError(error instanceof Error ? error.message : String(error));
setError(formatUiErrorMessage(error));
setStatus("disconnected");
setHasData(false);
}
@ -7596,8 +7610,8 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => {
>
<div className="data-table-shell">
{state.mode === "live" && optionHistoryError ? (
<div className="history-load-warning" role="status">
Older option history failed to load: {optionHistoryError}
<div className="history-load-warning" role="status" aria-live="polite">
Older option history failed to load: {formatUiErrorMessage(optionHistoryError)}
</div>
) : null}
{items.length === 0 ? (
@ -8869,6 +8883,9 @@ function SyntheticControlDock() {
setLoading(false);
return;
}
if (!response.ok) {
throw new Error(await readErrorDetail(response));
}
const nextStatus = (await response.json()) as SyntheticAdminStatusResponse;
setStatus(nextStatus);
if (!dirtyRef.current) {
@ -8879,7 +8896,7 @@ function SyntheticControlDock() {
}
} catch (loadError) {
if (!cancelled) {
setError(loadError instanceof Error ? loadError.message : String(loadError));
setError(formatUiErrorMessage(loadError, "Synthetic status could not be loaded"));
}
} finally {
if (!cancelled) {
@ -8917,8 +8934,7 @@ function SyntheticControlDock() {
})
.then(async (response) => {
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new Error(body?.detail ?? body?.error ?? "Synthetic control update failed");
throw new Error(await readErrorDetail(response));
}
return (await response.json()) as SyntheticAdminControlResponse;
})
@ -8939,7 +8955,7 @@ function SyntheticControlDock() {
})
.catch((updateError) => {
dirtyRef.current = false;
setError(updateError instanceof Error ? updateError.message : String(updateError));
setError(formatUiErrorMessage(updateError, "Synthetic control update failed"));
setDraft(savedRef.current);
})
.finally(() => {
@ -9147,7 +9163,11 @@ function SyntheticControlDock() {
</div>
</section>
{error ? <p className="drawer-note synthetic-control-error">{error}</p> : null}
{error ? (
<p className="drawer-note synthetic-control-error" role="alert">
{error}
</p>
) : null}
</>
)}
</aside>