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.
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
-
Added
formatUiErrorMessageinapps/web/app/terminal.tsxto normalize whitespace, provide fallback text, and clamp rendered error messages to 240 characters. - Routed chart, option-history, and synthetic-control error displays through the formatter so server payloads cannot flood or distort the interface.
-
Updated synthetic admin status loading to check
response.okbefore JSON parsing, reusing the existing response-detail reader for JSON, HTML, and plain-text failures. -
Added
aria-liveandrole="alert"where warning/error text changes need to be announced by assistive technology. - Added wrapping containment for drawer notes, empty states, flow-filter copy, synthetic errors, and history-load warnings.
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
-
The formatter accepts
unknown, so callers can safely pass native errors, strings, empty payloads, or unexpected values without branching at every render site. -
readErrorDetailstill preserves useful HTTP status context, but now returns a UI-safe string even when the backend sends HTML, a huge plain-text body, or malformed JSON. - The CSS keeps the terminal layout dense. It wraps only message-like text surfaces, not the horizontally scrollable market-data tables.
-
The dev-server smoke test showed desktop
/optionshad no visible-overflow offenders. Mobile still reports a document width wider than the viewport because the options data table intentionally keeps a minimum width and scrolls horizontally.
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.
506 unmodified lines50750850951051151217 unmodified lines530531532533534535536537506 unmodified linesreturn sampled;};const readErrorDetail = async (response: Response): Promise<string> => {const statusLabel = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;const text = await response.text();17 unmodified lineserror?: string;message?: string;};return payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`;} catch {return `${statusLabel}: ${truncated}`;}};506 unmodified lines50750850951051151251351451551651751851952052152252352452552617 unmodified lines544545546547548549550551506 unmodified linesreturn 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 lineserror?: string;message?: string;};return formatUiErrorMessage(payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`);} catch {return formatUiErrorMessage(`${statusLabel}: ${truncated}`);}};
8882 unmodified lines88698870887188728873887442 unmodified lines891789188919892089218922892389248882 unmodified linessetLoading(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 lines88838884888588868887888888898890889142 unmodified lines89348935893689378938893989408882 unmodified linessetLoading(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;})
9 unmodified lines1011121314151234 unmodified lines1249125012511252125312549 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 linescolor: var(--text-dim);}.chart-surface {width: 100%;height: 460px;9 unmodified lines101112131415161234 unmodified lines1251125212531254125512561257125812591260126112629 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 linescolor: var(--text-dim);}.drawer-note,.drawer-empty,.note {overflow-wrap: anywhere;}.chart-surface {width: 100%;height: 460px;
80 unmodified lines80818283848580 unmodified lines});});const makeItem = (traceId: string, seq: number, ts: number) => ({trace_id: traceId,seq,80 unmodified lines8182838485868788899091929394959697989910010110210310410580 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
- Passed:
bun test apps/web/app/terminal.test.ts - Passed:
bun --cwd=apps/web run build -
Passed: Playwright smoke loaded
http://localhost:3000/optionsat desktop and mobile widths; desktop scan found no visible-overflow offenders.
Issues, Limitations, and Mitigations
- This hardens UI rendering of failures. It does not change backend retry behavior or websocket reconnection policy.
-
Mobile
/optionskeeps a wider-than-viewport table by design. The mitigation is the existing horizontal table scroll, which preserves data-column legibility.
Follow-up Work
- islandflow-3by: add browser or DOM coverage for the shared terminal navigation drawer interactions.
- Consider adding a small DOM-level test for synthetic-admin non-JSON failure rendering if the project adds a React component test harness.