diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 9229f49..c755228 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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-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} diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 2718ed7..444a02d 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -234,19 +234,31 @@ const sampleToLimit = (items: T[], limit: number): T[] => { }; const readErrorDetail = async (response: Response): Promise => { + const statusLabel = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`; const text = await response.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 (/^ { .then((payload: { data?: OptionPrint[] }) => { const next = new Map(); for (const item of payload.data ?? []) { + if (!item || !item.trace_id) { + continue; + } next.set(item.trace_id, item); } if (next.size > 0) { @@ -5241,6 +5256,9 @@ const useTerminalState = () => { .then((payload: { data?: EquityPrintJoin[] }) => { const next = new Map(); for (const item of payload.data ?? []) { + if (!item || !item.id || !item.trace_id) { + continue; + } next.set(item.id, item); next.set(item.trace_id, item); if (item.print_trace_id) { @@ -5441,6 +5459,12 @@ const useTerminalState = () => { if (!response.ok) { 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<{ packets?: FlowPacket[]; smart_money?: SmartMoneyEvent[]; @@ -5455,19 +5479,28 @@ const useTerminalState = () => { const now = Date.now(); const packetMap = new Map(); for (const packet of payload.packets ?? []) { + if (!packet || !packet.id) { + continue; + } packetMap.set(packet.id, packet); } if (packetMap.size > 0) { setPinnedFlowPacketMap((prev) => upsertPinnedEntries(prev, packetMap, now)); } if (payload.smart_money?.length) { + const filtered = payload.smart_money.filter((item): item is SmartMoneyEvent => + Boolean(item && item.trace_id) + ); setOptionSupportSmartMoney((prev) => - mergeNewest(payload.smart_money ?? [], prev, PINNED_EVIDENCE_MAX_ITEMS) + mergeNewest(filtered, prev, PINNED_EVIDENCE_MAX_ITEMS) ); } if (payload.classifier_hits?.length) { + const filtered = payload.classifier_hits.filter((item): item is ClassifierHitEvent => + Boolean(item && item.trace_id) + ); setOptionSupportClassifierHits((prev) => - mergeNewest(payload.classifier_hits ?? [], prev, PINNED_EVIDENCE_MAX_ITEMS) + mergeNewest(filtered, prev, PINNED_EVIDENCE_MAX_ITEMS) ); } if (payload.nbbo_by_trace_id) { @@ -5630,11 +5663,14 @@ const useTerminalState = () => { } return response.json(); }) - .then((payload: { data?: OptionPrint[] }) => { - const next = new Map(); - for (const item of payload.data ?? []) { - next.set(item.trace_id, item); - } + .then((payload: { data?: OptionPrint[] }) => { + const next = new Map(); + for (const item of payload.data ?? []) { + if (!item || !item.trace_id) { + continue; + } + next.set(item.trace_id, item); + } if (next.size > 0) { setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, Date.now())); } @@ -5939,11 +5975,14 @@ const useTerminalState = () => { } return response.json(); }) - .then((payload: { data?: OptionPrint[] }) => { - const next = new Map(); - for (const item of payload.data ?? []) { - next.set(item.trace_id, item); - } + .then((payload: { data?: OptionPrint[] }) => { + const next = new Map(); + for (const item of payload.data ?? []) { + if (!item || !item.trace_id) { + continue; + } + next.set(item.trace_id, item); + } if (next.size > 0) { const now = Date.now(); setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, now)); @@ -6657,6 +6696,7 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => { 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}` : ""}`, style: rowStyle, + "data-index": index, "data-row-start": String(start), "data-row-size": String(size), "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" : ""}`} key={key} ref={virtual.measureElement} + data-index={index} data-row-start={String(start)} data-row-size={String(size)} 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" : ""}`} key={key} ref={virtual.measureElement} + data-index={index} data-row-start={String(start)} data-row-size={String(size)} data-tape-key={key} @@ -7061,6 +7103,7 @@ const AlertsPane = ({ limit, withStrip = false, className }: AlertsPaneProps) => key={key} type="button" ref={virtual.measureElement} + data-index={index} data-row-start={String(start)} data-row-size={String(size)} data-tape-key={key} @@ -7171,6 +7214,7 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => { key={key} type="button" ref={virtual.measureElement} + data-index={index} data-row-start={String(start)} data-row-size={String(size)} data-tape-key={key} @@ -7199,6 +7243,7 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => { key={key} type="button" ref={virtual.measureElement} + data-index={index} data-row-start={String(start)} data-row-size={String(size)} data-tape-key={key} @@ -7291,6 +7336,7 @@ const DarkPane = ({ limit, className }: DarkPaneProps) => { key={key} type="button" ref={virtual.measureElement} + data-index={index} data-row-start={String(start)} data-row-size={String(size)} data-tape-key={key}