Fix terminal virtualization and hydration crash handling
This commit is contained in:
parent
dc0aeaa7d2
commit
088bd37e84
2 changed files with 62 additions and 15 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -5630,11 +5663,14 @@ const useTerminalState = () => {
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.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 ?? []) {
|
||||||
next.set(item.trace_id, item);
|
if (!item || !item.trace_id) {
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
next.set(item.trace_id, item);
|
||||||
|
}
|
||||||
if (next.size > 0) {
|
if (next.size > 0) {
|
||||||
setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, Date.now()));
|
setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, Date.now()));
|
||||||
}
|
}
|
||||||
|
|
@ -5939,11 +5975,14 @@ const useTerminalState = () => {
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.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 ?? []) {
|
||||||
next.set(item.trace_id, item);
|
if (!item || !item.trace_id) {
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
next.set(item.trace_id, item);
|
||||||
|
}
|
||||||
if (next.size > 0) {
|
if (next.size > 0) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, now));
|
setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, now));
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue