Turn document ยท 2026-05-29 18:12 EDT

Harden Terminal UI Error States

This pass made the Islandflow web terminal more stable when backend or network responses are messy: oversized error payloads are clamped, non-JSON admin failures are handled explicitly, and long warning text now wraps inside terminal surfaces.

Beads: islandflow-aq9 Surface: apps/web Command: impeccable harden

Summary

The terminal now sanitizes UI-facing error strings before rendering them, handles failed synthetic-admin status responses before parsing JSON, and gives long warnings, drawer notes, and filter copy safe wrapping behavior.

Changes Made

Context

Islandflow is a real-time market-data terminal. During live trading or replay investigation, backend failures should be clear and recoverable without breaking table lanes, drawers, charts, or operator controls. The hardened paths are intentionally small and utility-first so the product keeps its dense instrument-panel character.

Important Implementation Details

Relevant Diff Snippets

The snippets below were rendered with @pierre/diffs/ssr. They show the core UI error formatter, synthetic-admin failure handling, CSS wrapping guardrails, and regression coverage.

apps/web/app/terminal.tsx
-2+16
506 unmodified lines
507
508
509
510
511
512
17 unmodified lines
530
531
532
533
534
535
536
537
506 unmodified lines
return sampled;
};
const readErrorDetail = async (response: Response): Promise<string> => {
const statusLabel = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
const text = await response.text();
17 unmodified lines
error?: string;
message?: string;
};
return payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`;
} catch {
return `${statusLabel}: ${truncated}`;
}
};
506 unmodified lines
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
17 unmodified lines
544
545
546
547
548
549
550
551
506 unmodified lines
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();
17 unmodified lines
error?: string;
message?: string;
};
return formatUiErrorMessage(payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`);
} catch {
return formatUiErrorMessage(`${statusLabel}: ${truncated}`);
}
};
apps/web/app/terminal.tsx
-2+4
8882 unmodified lines
8869
8870
8871
8872
8873
8874
42 unmodified lines
8917
8918
8919
8920
8921
8922
8923
8924
8882 unmodified lines
setLoading(false);
return;
}
const nextStatus = (await response.json()) as SyntheticAdminStatusResponse;
setStatus(nextStatus);
if (!dirtyRef.current) {
42 unmodified lines
})
.then(async (response) => {
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new Error(body?.detail ?? body?.error ?? "Synthetic control update failed");
}
return (await response.json()) as SyntheticAdminControlResponse;
})
8882 unmodified lines
8883
8884
8885
8886
8887
8888
8889
8890
8891
42 unmodified lines
8934
8935
8936
8937
8938
8939
8940
8882 unmodified lines
setLoading(false);
return;
}
if (!response.ok) {
throw new Error(await readErrorDetail(response));
}
const nextStatus = (await response.json()) as SyntheticAdminStatusResponse;
setStatus(nextStatus);
if (!dirtyRef.current) {
42 unmodified lines
})
.then(async (response) => {
if (!response.ok) {
throw new Error(await readErrorDetail(response));
}
return (await response.json()) as SyntheticAdminControlResponse;
})
apps/web/app/globals.css
+7
9 unmodified lines
10
11
12
13
14
15
1234 unmodified lines
1249
1250
1251
1252
1253
1254
9 unmodified lines
--text: oklch(0.93 0.014 250);
--text-dim: oklch(0.74 0.018 250);
--text-faint: oklch(0.59 0.016 250);
--accent: oklch(0.78 0.12 74);
--accent-soft: oklch(0.78 0.12 74 / 0.1);
--green: oklch(0.74 0.13 151);
1234 unmodified lines
color: var(--text-dim);
}
.chart-surface {
width: 100%;
height: 460px;
9 unmodified lines
10
11
12
13
14
15
16
1234 unmodified lines
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
9 unmodified lines
--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);
1234 unmodified lines
color: var(--text-dim);
}
.drawer-note,
.drawer-empty,
.note {
overflow-wrap: anywhere;
}
.chart-surface {
width: 100%;
height: 460px;
apps/web/app/terminal.test.ts
+19
80 unmodified lines
80
81
82
83
84
85
80 unmodified lines
});
});
const makeItem = (traceId: string, seq: number, ts: number) => ({
trace_id: traceId,
seq,
80 unmodified lines
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
80 unmodified lines
});
});
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,

Expected Impact for End-Users

Users should see shorter, more useful failure messages when API routes, charts, history loading, or synthetic-admin controls fail. The terminal should remain readable even when an upstream service returns a long stack trace, an HTML error page, or text with unusual spacing.

Validation

Issues, Limitations, and Mitigations

Follow-up Work