Fix terminal virtualization and hydration crash handling

This commit is contained in:
dirtydishes 2026-05-07 02:17:17 -04:00
parent dc0aeaa7d2
commit 088bd37e84
2 changed files with 62 additions and 15 deletions

View file

@ -1,3 +1,4 @@
{"_type":"issue","id":"islandflow-9xs","title":"Fix terminal hydration and virtual row measurement crash","description":"Fix client crash caused by options-support hydration on non-JSON/404 responses and satisfy tanstack virtual measured-row data-index requirement across virtualized tables.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T06:14:33Z","created_by":"dirtydishes","updated_at":"2026-05-07T06:17:09Z","started_at":"2026-05-07T06:14:43Z","closed_at":"2026-05-07T06:17:09Z","close_reason":"Completed: added data-index attributes on measured virtual rows, hardened options-support hydration error handling/content-type validation, and guarded trace-id hydration loops against malformed payload entries.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-35g","title":"Fix Docker deployment workspace lockfile drift","description":"Refresh deployment/docker workspace lockfile for Docker builds, add a drift guard for Docker-built workspaces, and document the separate deployment snapshot so frozen Bun installs cannot fail when repo dependencies change.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T06:02:06Z","created_by":"dirtydishes","updated_at":"2026-05-07T06:07:50Z","started_at":"2026-05-07T06:02:15Z","closed_at":"2026-05-07T06:07:50Z","close_reason":"Completed: synced deployment Docker workspace snapshot from repo root, refreshed deployment bun.lock, added sync/check scripts, and documented maintenance workflow. Local docker compose build validation is blocked here because Docker daemon is unavailable.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-35g","title":"Fix Docker deployment workspace lockfile drift","description":"Refresh deployment/docker workspace lockfile for Docker builds, add a drift guard for Docker-built workspaces, and document the separate deployment snapshot so frozen Bun installs cannot fail when repo dependencies change.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T06:02:06Z","created_by":"dirtydishes","updated_at":"2026-05-07T06:07:50Z","started_at":"2026-05-07T06:02:15Z","closed_at":"2026-05-07T06:07:50Z","close_reason":"Completed: synced deployment Docker workspace snapshot from repo root, refreshed deployment bun.lock, added sync/check scripts, and documented maintenance workflow. Local docker compose build validation is blocked here because Docker daemon is unavailable.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-2ij","title":"Harden tape virtualization, scoped focus, and live feed health","description":"Implement the coordinated tape stability plan across web and API.\n\nScope:\n- replace fixed-height tape virtualization with measured virtualization and virtual-end history loading\n- replace scrollHeight anchoring with key-based anchor restore\n- compose canonical tape lists across seed/live/history sources\n- preserve clicked contract/ticker context during scoped focus transitions\n- separate backend hot-channel health from scoped quiet empty states\n- shrink browser hot windows and modestly reduce server cache limits\n- add regression tests and development instrumentation\n\nAcceptance:\n- no giant blank spacer gaps during tape scrolling\n- scroll remains stable while live data and history mutate the list\n- clicked deep-history option/equity rows remain visible immediately after focus\n- narrow scopes do not surface Feed behind unless backend channel health is stale\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T05:35:18Z","created_by":"dirtydishes","updated_at":"2026-05-07T05:52:14Z","started_at":"2026-05-07T05:35:21Z","closed_at":"2026-05-07T05:52:14Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-2ij","title":"Harden tape virtualization, scoped focus, and live feed health","description":"Implement the coordinated tape stability plan across web and API.\n\nScope:\n- replace fixed-height tape virtualization with measured virtualization and virtual-end history loading\n- replace scrollHeight anchoring with key-based anchor restore\n- compose canonical tape lists across seed/live/history sources\n- preserve clicked contract/ticker context during scoped focus transitions\n- separate backend hot-channel health from scoped quiet empty states\n- shrink browser hot windows and modestly reduce server cache limits\n- add regression tests and development instrumentation\n\nAcceptance:\n- no giant blank spacer gaps during tape scrolling\n- scroll remains stable while live data and history mutate the list\n- clicked deep-history option/equity rows remain visible immediately after focus\n- narrow scopes do not surface Feed behind unless backend channel health is stale\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T05:35:18Z","created_by":"dirtydishes","updated_at":"2026-05-07T05:52:14Z","started_at":"2026-05-07T05:35:21Z","closed_at":"2026-05-07T05:52:14Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-uj7","title":"Fix home to tape navigation","description":"Home rail Tape navigation was not reliably switching to the tape route. Use browser-native top-level navigation for Home/Tape rail links so /tape remains reachable even if client router handling stalls.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T03:18:14Z","created_by":"dirtydishes","updated_at":"2026-05-07T03:18:21Z","started_at":"2026-05-07T03:18:20Z","closed_at":"2026-05-07T03:18:21Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-uj7","title":"Fix home to tape navigation","description":"Home rail Tape navigation was not reliably switching to the tape route. Use browser-native top-level navigation for Home/Tape rail links so /tape remains reachable even if client router handling stalls.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T03:18:14Z","created_by":"dirtydishes","updated_at":"2026-05-07T03:18:21Z","started_at":"2026-05-07T03:18:20Z","closed_at":"2026-05-07T03:18:21Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -234,19 +234,31 @@ const sampleToLimit = <T,>(items: T[], limit: number): T[] => {
}; };
const readErrorDetail = async (response: Response): Promise<string> => { const readErrorDetail = async (response: Response): Promise<string> => {
const statusLabel = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
const text = await response.text(); const text = await response.text();
if (!text) { if (!text) {
return ""; return statusLabel;
} }
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
const trimmed = text.trimStart();
const truncated = text.length > 600 ? `${text.slice(0, 600)}...` : text;
if (!contentType.includes("application/json")) {
if (/^<!doctype html/i.test(trimmed) || /^<html/i.test(trimmed)) {
return `${statusLabel}: received HTML response instead of JSON`;
}
return `${statusLabel}: ${truncated}`;
}
try { try {
const payload = JSON.parse(text) as { const payload = JSON.parse(text) as {
detail?: string; detail?: string;
error?: string; error?: string;
message?: string; message?: string;
}; };
return payload.detail ?? payload.error ?? payload.message ?? text; return payload.detail ?? payload.error ?? payload.message ?? `${statusLabel}: ${truncated}`;
} catch { } catch {
return text; return `${statusLabel}: ${truncated}`;
} }
}; };
@ -5194,6 +5206,9 @@ const useTerminalState = () => {
.then((payload: { data?: OptionPrint[] }) => { .then((payload: { data?: OptionPrint[] }) => {
const next = new Map<string, OptionPrint>(); const next = new Map<string, OptionPrint>();
for (const item of payload.data ?? []) { for (const item of payload.data ?? []) {
if (!item || !item.trace_id) {
continue;
}
next.set(item.trace_id, item); next.set(item.trace_id, item);
} }
if (next.size > 0) { if (next.size > 0) {
@ -5241,6 +5256,9 @@ const useTerminalState = () => {
.then((payload: { data?: EquityPrintJoin[] }) => { .then((payload: { data?: EquityPrintJoin[] }) => {
const next = new Map<string, EquityPrintJoin>(); const next = new Map<string, EquityPrintJoin>();
for (const item of payload.data ?? []) { for (const item of payload.data ?? []) {
if (!item || !item.id || !item.trace_id) {
continue;
}
next.set(item.id, item); next.set(item.id, item);
next.set(item.trace_id, item); next.set(item.trace_id, item);
if (item.print_trace_id) { if (item.print_trace_id) {
@ -5441,6 +5459,12 @@ const useTerminalState = () => {
if (!response.ok) { if (!response.ok) {
throw new Error(await readErrorDetail(response)); throw new Error(await readErrorDetail(response));
} }
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
if (!contentType.includes("application/json")) {
throw new Error(
`Unexpected content type from /lookup/options-support: ${contentType || "unknown"}`
);
}
return response.json() as Promise<{ return response.json() as Promise<{
packets?: FlowPacket[]; packets?: FlowPacket[];
smart_money?: SmartMoneyEvent[]; smart_money?: SmartMoneyEvent[];
@ -5455,19 +5479,28 @@ const useTerminalState = () => {
const now = Date.now(); const now = Date.now();
const packetMap = new Map<string, FlowPacket>(); const packetMap = new Map<string, FlowPacket>();
for (const packet of payload.packets ?? []) { for (const packet of payload.packets ?? []) {
if (!packet || !packet.id) {
continue;
}
packetMap.set(packet.id, packet); packetMap.set(packet.id, packet);
} }
if (packetMap.size > 0) { if (packetMap.size > 0) {
setPinnedFlowPacketMap((prev) => upsertPinnedEntries(prev, packetMap, now)); setPinnedFlowPacketMap((prev) => upsertPinnedEntries(prev, packetMap, now));
} }
if (payload.smart_money?.length) { if (payload.smart_money?.length) {
const filtered = payload.smart_money.filter((item): item is SmartMoneyEvent =>
Boolean(item && item.trace_id)
);
setOptionSupportSmartMoney((prev) => setOptionSupportSmartMoney((prev) =>
mergeNewest(payload.smart_money ?? [], prev, PINNED_EVIDENCE_MAX_ITEMS) mergeNewest(filtered, prev, PINNED_EVIDENCE_MAX_ITEMS)
); );
} }
if (payload.classifier_hits?.length) { if (payload.classifier_hits?.length) {
const filtered = payload.classifier_hits.filter((item): item is ClassifierHitEvent =>
Boolean(item && item.trace_id)
);
setOptionSupportClassifierHits((prev) => setOptionSupportClassifierHits((prev) =>
mergeNewest(payload.classifier_hits ?? [], prev, PINNED_EVIDENCE_MAX_ITEMS) mergeNewest(filtered, prev, PINNED_EVIDENCE_MAX_ITEMS)
); );
} }
if (payload.nbbo_by_trace_id) { if (payload.nbbo_by_trace_id) {
@ -5633,6 +5666,9 @@ const useTerminalState = () => {
.then((payload: { data?: OptionPrint[] }) => { .then((payload: { data?: OptionPrint[] }) => {
const next = new Map<string, OptionPrint>(); const next = new Map<string, OptionPrint>();
for (const item of payload.data ?? []) { for (const item of payload.data ?? []) {
if (!item || !item.trace_id) {
continue;
}
next.set(item.trace_id, item); next.set(item.trace_id, item);
} }
if (next.size > 0) { if (next.size > 0) {
@ -5942,6 +5978,9 @@ const useTerminalState = () => {
.then((payload: { data?: OptionPrint[] }) => { .then((payload: { data?: OptionPrint[] }) => {
const next = new Map<string, OptionPrint>(); const next = new Map<string, OptionPrint>();
for (const item of payload.data ?? []) { for (const item of payload.data ?? []) {
if (!item || !item.trace_id) {
continue;
}
next.set(item.trace_id, item); next.set(item.trace_id, item);
} }
if (next.size > 0) { if (next.size > 0) {
@ -6657,6 +6696,7 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => {
const commonProps = { const commonProps = {
className: `data-table-row data-table-row-button data-table-row-classified data-table-row-options data-table-virtual-row${index % 2 === 1 ? " is-even" : ""}${decor ? ` is-classified classifier-${decor.tone}` : ""}`, className: `data-table-row data-table-row-button data-table-row-classified data-table-row-options data-table-virtual-row${index % 2 === 1 ? " is-even" : ""}${decor ? ` is-classified classifier-${decor.tone}` : ""}`,
style: rowStyle, style: rowStyle,
"data-index": index,
"data-row-start": String(start), "data-row-start": String(start),
"data-row-size": String(size), "data-row-size": String(size),
"data-tape-key": key, "data-tape-key": key,
@ -6813,6 +6853,7 @@ const EquitiesPane = ({ limit }: EquitiesPaneProps) => {
className={`data-table-row data-table-row-equities data-table-virtual-row${index % 2 === 1 ? " is-even" : ""}`} className={`data-table-row data-table-row-equities data-table-virtual-row${index % 2 === 1 ? " is-even" : ""}`}
key={key} key={key}
ref={virtual.measureElement} ref={virtual.measureElement}
data-index={index}
data-row-start={String(start)} data-row-start={String(start)}
data-row-size={String(size)} data-row-size={String(size)}
data-tape-key={key} data-tape-key={key}
@ -6962,6 +7003,7 @@ const FlowPane = ({ limit, title = "Flow" }: FlowPaneProps) => {
className={`data-table-row data-table-row-flow data-table-virtual-row${index % 2 === 1 ? " is-even" : ""}${nbboStale || nbboMissing ? " data-table-row-warn" : ""}`} className={`data-table-row data-table-row-flow data-table-virtual-row${index % 2 === 1 ? " is-even" : ""}${nbboStale || nbboMissing ? " data-table-row-warn" : ""}`}
key={key} key={key}
ref={virtual.measureElement} ref={virtual.measureElement}
data-index={index}
data-row-start={String(start)} data-row-start={String(start)}
data-row-size={String(size)} data-row-size={String(size)}
data-tape-key={key} data-tape-key={key}
@ -7061,6 +7103,7 @@ const AlertsPane = ({ limit, withStrip = false, className }: AlertsPaneProps) =>
key={key} key={key}
type="button" type="button"
ref={virtual.measureElement} ref={virtual.measureElement}
data-index={index}
data-row-start={String(start)} data-row-start={String(start)}
data-row-size={String(size)} data-row-size={String(size)}
data-tape-key={key} data-tape-key={key}
@ -7171,6 +7214,7 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
key={key} key={key}
type="button" type="button"
ref={virtual.measureElement} ref={virtual.measureElement}
data-index={index}
data-row-start={String(start)} data-row-start={String(start)}
data-row-size={String(size)} data-row-size={String(size)}
data-tape-key={key} data-tape-key={key}
@ -7199,6 +7243,7 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
key={key} key={key}
type="button" type="button"
ref={virtual.measureElement} ref={virtual.measureElement}
data-index={index}
data-row-start={String(start)} data-row-start={String(start)}
data-row-size={String(size)} data-row-size={String(size)}
data-tape-key={key} data-tape-key={key}
@ -7291,6 +7336,7 @@ const DarkPane = ({ limit, className }: DarkPaneProps) => {
key={key} key={key}
type="button" type="button"
ref={virtual.measureElement} ref={virtual.measureElement}
data-index={index}
data-row-start={String(start)} data-row-start={String(start)}
data-row-size={String(size)} data-row-size={String(size)}
data-tape-key={key} data-tape-key={key}