harden terminal ui states and drawer focus handling #16
5 changed files with 675 additions and 10 deletions
|
|
@ -24,6 +24,7 @@
|
||||||
{"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"islandflow-aq9","title":"Harden terminal UI error and overflow states","description":"Harden the web terminal against oversized API errors, non-JSON synthetic admin failures, and long status text so live trading panes remain stable under bad network/backend responses.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-29T22:10:16Z","created_by":"dirtydishes","updated_at":"2026-05-29T22:13:37Z","closed_at":"2026-05-29T22:13:37Z","close_reason":"Hardened terminal UI error rendering, synthetic admin failure parsing, long-message wrapping, and added focused tests.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-ggm","title":"Harden web terminal UI states","description":"Improve the web terminal surface so it handles loading, empty data, API failures, overflow, and accessible live-status behavior more robustly.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T21:59:45Z","created_by":"dirtydishes","updated_at":"2026-05-29T22:05:45Z","started_at":"2026-05-29T21:59:59Z","closed_at":"2026-05-29T22:05:45Z","close_reason":"Hardened web terminal status announcements, empty states, table semantics, clipped-cell fallbacks, tests, validation, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-ggm","title":"Harden web terminal UI states","description":"Improve the web terminal surface so it handles loading, empty data, API failures, overflow, and accessible live-status behavior more robustly.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T21:59:45Z","created_by":"dirtydishes","updated_at":"2026-05-29T22:05:45Z","started_at":"2026-05-29T21:59:59Z","closed_at":"2026-05-29T22:05:45Z","close_reason":"Hardened web terminal status announcements, empty states, table semantics, clipped-cell fallbacks, tests, validation, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-dk5","title":"Remove frontend cooker route","description":"Remove the experimental /frontend-cooker page and update repository references that still list it as an available public route.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T13:50:38Z","created_by":"dirtydishes","updated_at":"2026-05-29T13:53:05Z","started_at":"2026-05-29T13:50:48Z","closed_at":"2026-05-29T13:53:05Z","close_reason":"Removed the /frontend-cooker Next.js route, cleaned route/scanner references, documented the work, and validated the web build.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-dk5","title":"Remove frontend cooker route","description":"Remove the experimental /frontend-cooker page and update repository references that still list it as an available public route.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T13:50:38Z","created_by":"dirtydishes","updated_at":"2026-05-29T13:53:05Z","started_at":"2026-05-29T13:50:48Z","closed_at":"2026-05-29T13:53:05Z","close_reason":"Removed the /frontend-cooker Next.js route, cleaned route/scanner references, documented the work, and validated the web build.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-ep2","title":"Configure Impeccable live mode","description":"Initialize the repository's Impeccable live-mode configuration so future design iteration can start without first-time setup.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T08:03:47Z","created_by":"dirtydishes","updated_at":"2026-05-29T08:05:01Z","started_at":"2026-05-29T08:03:52Z","closed_at":"2026-05-29T08:05:01Z","close_reason":"Configured Impeccable live mode and documented validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-ep2","title":"Configure Impeccable live mode","description":"Initialize the repository's Impeccable live-mode configuration so future design iteration can start without first-time setup.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T08:03:47Z","created_by":"dirtydishes","updated_at":"2026-05-29T08:05:01Z","started_at":"2026-05-29T08:03:52Z","closed_at":"2026-05-29T08:05:01Z","close_reason":"Configured Impeccable live mode and documented validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
--text: oklch(0.93 0.014 250);
|
--text: oklch(0.93 0.014 250);
|
||||||
--text-dim: oklch(0.74 0.018 250);
|
--text-dim: oklch(0.74 0.018 250);
|
||||||
--text-faint: oklch(0.59 0.016 250);
|
--text-faint: oklch(0.59 0.016 250);
|
||||||
|
--text-muted: var(--text-dim);
|
||||||
--accent: oklch(0.78 0.12 74);
|
--accent: oklch(0.78 0.12 74);
|
||||||
--accent-soft: oklch(0.78 0.12 74 / 0.1);
|
--accent-soft: oklch(0.78 0.12 74 / 0.1);
|
||||||
--green: oklch(0.74 0.13 151);
|
--green: oklch(0.74 0.13 151);
|
||||||
|
|
@ -661,6 +662,7 @@ h3 {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-filter-checkbox-grid,
|
.flow-filter-checkbox-grid,
|
||||||
|
|
@ -1249,6 +1251,12 @@ h3 {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawer-note,
|
||||||
|
.drawer-empty,
|
||||||
|
.note {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-surface {
|
.chart-surface {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 460px;
|
height: 460px;
|
||||||
|
|
@ -1457,6 +1465,7 @@ h3 {
|
||||||
color: oklch(0.91 0.08 72);
|
color: oklch(0.91 0.08 72);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table-wrap {
|
.data-table-wrap {
|
||||||
|
|
@ -2291,6 +2300,7 @@ h3 {
|
||||||
|
|
||||||
.synthetic-control-error {
|
.synthetic-control-error {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-header {
|
.drawer-header {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
countActiveFlowFilterGroups,
|
countActiveFlowFilterGroups,
|
||||||
filterOptionTapeItems,
|
filterOptionTapeItems,
|
||||||
findAnchorRestoreIndex,
|
findAnchorRestoreIndex,
|
||||||
|
formatUiErrorMessage,
|
||||||
formatCompactUsd,
|
formatCompactUsd,
|
||||||
formatOptionContractLabel,
|
formatOptionContractLabel,
|
||||||
flushPausableTapeData,
|
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) => ({
|
const makeItem = (traceId: string, seq: number, ts: number) => ({
|
||||||
trace_id: traceId,
|
trace_id: traceId,
|
||||||
seq,
|
seq,
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,20 @@ const sampleToLimit = <T,>(items: T[], limit: number): T[] => {
|
||||||
return sampled;
|
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 readErrorDetail = async (response: Response): Promise<string> => {
|
||||||
const statusLabel = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
|
const statusLabel = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
@ -530,9 +544,9 @@ const readErrorDetail = async (response: Response): Promise<string> => {
|
||||||
error?: string;
|
error?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
return payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`;
|
return formatUiErrorMessage(payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`);
|
||||||
} catch {
|
} catch {
|
||||||
return `${statusLabel}: ${truncated}`;
|
return formatUiErrorMessage(`${statusLabel}: ${truncated}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -4479,7 +4493,7 @@ const CandleChart = ({
|
||||||
if (!active) {
|
if (!active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(error instanceof Error ? error.message : String(error));
|
setError(formatUiErrorMessage(error));
|
||||||
setStatus("disconnected");
|
setStatus("disconnected");
|
||||||
setHasData(false);
|
setHasData(false);
|
||||||
}
|
}
|
||||||
|
|
@ -7596,8 +7610,8 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => {
|
||||||
>
|
>
|
||||||
<div className="data-table-shell">
|
<div className="data-table-shell">
|
||||||
{state.mode === "live" && optionHistoryError ? (
|
{state.mode === "live" && optionHistoryError ? (
|
||||||
<div className="history-load-warning" role="status">
|
<div className="history-load-warning" role="status" aria-live="polite">
|
||||||
Older option history failed to load: {optionHistoryError}
|
Older option history failed to load: {formatUiErrorMessage(optionHistoryError)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
|
|
@ -8869,6 +8883,9 @@ function SyntheticControlDock() {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await readErrorDetail(response));
|
||||||
|
}
|
||||||
const nextStatus = (await response.json()) as SyntheticAdminStatusResponse;
|
const nextStatus = (await response.json()) as SyntheticAdminStatusResponse;
|
||||||
setStatus(nextStatus);
|
setStatus(nextStatus);
|
||||||
if (!dirtyRef.current) {
|
if (!dirtyRef.current) {
|
||||||
|
|
@ -8879,7 +8896,7 @@ function SyntheticControlDock() {
|
||||||
}
|
}
|
||||||
} catch (loadError) {
|
} catch (loadError) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
setError(formatUiErrorMessage(loadError, "Synthetic status could not be loaded"));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
|
@ -8917,8 +8934,7 @@ function SyntheticControlDock() {
|
||||||
})
|
})
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const body = await response.json().catch(() => null);
|
throw new Error(await readErrorDetail(response));
|
||||||
throw new Error(body?.detail ?? body?.error ?? "Synthetic control update failed");
|
|
||||||
}
|
}
|
||||||
return (await response.json()) as SyntheticAdminControlResponse;
|
return (await response.json()) as SyntheticAdminControlResponse;
|
||||||
})
|
})
|
||||||
|
|
@ -8939,7 +8955,7 @@ function SyntheticControlDock() {
|
||||||
})
|
})
|
||||||
.catch((updateError) => {
|
.catch((updateError) => {
|
||||||
dirtyRef.current = false;
|
dirtyRef.current = false;
|
||||||
setError(updateError instanceof Error ? updateError.message : String(updateError));
|
setError(formatUiErrorMessage(updateError, "Synthetic control update failed"));
|
||||||
setDraft(savedRef.current);
|
setDraft(savedRef.current);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -9147,7 +9163,11 @@ function SyntheticControlDock() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</aside>
|
||||||
|
|
|
||||||
614
docs/turns/2026-05-29-harden-terminal-ui-errors.html
Normal file
614
docs/turns/2026-05-29-harden-terminal-ui-errors.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue