diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 58e5b6b..570dd9a 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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-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-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-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-9en","title":"Install Impeccable skill for Codex","description":"Install the Impeccable skill in the Codex-compatible project locations after the upstream installer selected unused harness folders.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T07:59:10Z","created_by":"dirtydishes","updated_at":"2026-05-29T07:59:22Z","started_at":"2026-05-29T07:59:18Z","closed_at":"2026-05-29T07:59:22Z","close_reason":"Installed Impeccable into .agents and mirrored it into .codex/skills for Codex use.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index a454a20..7cbc952 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1675,6 +1675,7 @@ h3 { text-overflow: ellipsis; white-space: nowrap; font-size: 0.72rem; + unicode-bidi: plaintext; } .data-table-cell-number { @@ -2010,11 +2011,16 @@ h3 { } .empty { + display: flex; + align-items: center; + min-height: 76px; padding: 18px; border-radius: 12px; border: 1px dashed var(--border); background: var(--bg-soft); color: var(--text-dim); + line-height: 1.4; + overflow-wrap: anywhere; } .drawer { diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index e6ed106..80d727f 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -4,6 +4,7 @@ import { NAV_ITEMS, appendHistoryTail, buildAlertContextPath, + buildTapeStatusAnnouncement, buildDefaultFlowFilters, buildOptionTapeQueryParams, classifierToneForFamily, @@ -51,6 +52,34 @@ import { toggleFilterValue } from "./terminal"; +describe("tape status hardening", () => { + it("builds a screen-reader announcement with replay state and queued rows", () => { + expect( + buildTapeStatusAnnouncement({ + status: "connected", + replayTime: null, + replayComplete: false, + paused: true, + dropped: 12, + mode: "replay" + }) + ).toBe("Replay feed paused, time not available, 12 queued rows"); + }); + + it("announces stale live feeds without relying on the colored dot", () => { + expect( + buildTapeStatusAnnouncement({ + status: "stale", + replayTime: null, + replayComplete: false, + paused: false, + dropped: 0, + mode: "live" + }) + ).toBe("Live feed behind"); + }); +}); + const makeItem = (traceId: string, seq: number, ts: number) => ({ trace_id: traceId, seq, diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 5375688..2826266 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -2145,6 +2145,59 @@ export const statusLabel = (status: WsStatus, paused: boolean, mode: TapeMode): } }; +export const buildTapeStatusAnnouncement = ({ + status, + replayTime, + replayComplete, + paused, + dropped, + mode +}: Pick): string => { + const label = replayComplete ? "Replay Complete" : statusLabel(status, paused, mode); + const feedLabel = mode === "live" && label.toLowerCase().startsWith("feed ") + ? label.toLowerCase() + : `feed ${label.toLowerCase()}`; + const parts = [`${mode === "live" ? "Live" : "Replay"} ${feedLabel}`]; + + if (mode === "replay") { + parts.push(`time ${replayTime ? formatTime(replayTime) : "not available"}`); + } + + if (paused && dropped > 0) { + parts.push(`${dropped} queued rows`); + } + + return parts.join(", "); +}; + +const DataCell = ({ + children, + className = "", + title, + numeric = false +}: { + children: ReactNode; + className?: string; + title?: string; + numeric?: boolean; +}) => { + const classes = ["data-table-cell", numeric ? "data-table-cell-number" : "", className] + .filter(Boolean) + .join(" "); + + return ( + + {children} + + ); +}; + +const EmptyState = ({ children }: { children: ReactNode }) => ( +
+ {children} +
+); + type TapeConfig = { mode: TapeMode; wsPath: string; @@ -3893,17 +3946,33 @@ const TapeStatus = ({ }: TapeStatusProps) => { const label = replayComplete ? "Replay Complete" : statusLabel(status, paused, mode); const pausedLabel = paused && dropped > 0 ? `+${dropped} queued` : ""; + const announcement = buildTapeStatusAnnouncement({ + status, + replayTime, + replayComplete, + paused, + dropped, + mode + }); return ( -
- +
+
@@ -7532,7 +7601,7 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => {
) : null} {items.length === 0 ? ( -
+ {state.mode === "live" ? state.options.status === "stale" ? "Feed behind. Waiting for fresh option prints." @@ -7544,29 +7613,23 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => { : state.tickerSet.size > 0 ? "No option prints match the current filter." : "Replay queue empty. Ensure ClickHouse has data."} -
+ ) : (
- TIME - SYM - EXP - STRIKE - C/P - SPOT - DETAILS - TYPE - VALUE - SIDE - IV - CLASSIFIER + {["TIME", "SYM", "EXP", "STRIKE", "C/P", "SPOT", "DETAILS", "TYPE", "VALUE", "SIDE", "IV", "CLASSIFIER"].map((header) => ( + + {header} + + ))}
{virtual.virtualItems.map(({ item: print, key, index, start, size }) => { const contractId = normalizeContractId(print.option_contract_id); @@ -7602,42 +7665,42 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => { }; const cells = ( <> - {formatTime(print.ts)} - + {formatTime(print.ts)} + - - + + - - + + - - + + - - {typeof spot === "number" ? formatPrice(spot) : "--"} - + + {typeof spot === "number" ? formatPrice(spot) : "--"} + {formatSize(print.size)}@{formatPrice(print.price)}_{nbboSide ?? "--"} - - {print.option_type ?? "--"} - ${formatCompactUsd(notional)} - + + {print.option_type ?? "--"} + ${formatCompactUsd(notional)} + {nbboSide ? ( {nbboSide} ) : ( "--" )} - - {typeof iv === "number" ? formatPct(iv) : "--"} - {decor ? humanizeClassifierId(decor.family) : "--"} + + {typeof iv === "number" ? formatPct(iv) : "--"} + {decor ? humanizeClassifierId(decor.family) : "--"} ); @@ -7721,7 +7784,7 @@ const EquitiesPane = memo(({ state, limit }: EquitiesPaneProps) => { >
{items.length === 0 ? ( -
+ {state.mode === "live" ? state.equities.status === "stale" ? "Feed behind. Waiting for fresh equity prints." @@ -7735,23 +7798,23 @@ const EquitiesPane = memo(({ state, limit }: EquitiesPaneProps) => { : state.tickerSet.size > 0 ? "No equity prints match the current filter." : "Replay queue empty. Ensure ClickHouse has data."} -
+ ) : (
- TIME - SYM - PRICE - SIZE - VENUE - TAPE + {["TIME", "SYM", "PRICE", "SIZE", "VENUE", "TAPE"].map((header) => ( + + {header} + + ))}
-
+
{virtual.virtualItems.map(({ item: print, key, index, start, size }) => (
{ data-tape-key={key} style={{ transform: `translateY(${start}px)` }} > - {formatTime(print.ts)} - + {formatTime(print.ts)} + - - ${formatPrice(print.price)} - {formatSize(print.size)}x - {print.exchange} - {print.offExchangeFlag ? "Off-Ex" : "Lit"} + + ${formatPrice(print.price)} + {formatSize(print.size)}x + {print.exchange} + {print.offExchangeFlag ? "Off-Ex" : "Lit"}
))}
@@ -7825,7 +7888,7 @@ const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => { >
{items.length === 0 ? ( -
+ {state.tickerSet.size > 0 ? "No flow packets match the current filter." : state.mode === "live" @@ -7833,23 +7896,19 @@ const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => { ? "Feed behind. Waiting for fresh flow packets." : "No flow packets yet. Start compute." : "Replay queue empty. Ensure ClickHouse has data."} -
+ ) : (
- TIME - CONTRACT - PRINTS - SIZE - NOTIONAL - WINDOW - STRUCTURE - NBBO - QUALITY + {["TIME", "CONTRACT", "PRINTS", "SIZE", "NOTIONAL", "WINDOW", "STRUCTURE", "NBBO", "QUALITY"].map((header) => ( + + {header} + + ))}
-
+
{virtual.virtualItems.map(({ item: packet, key, index, start, size }) => { const features = packet.features ?? {}; const contract = String(features.option_contract_id ?? packet.id ?? "unknown"); @@ -7904,6 +7963,7 @@ const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => { return (
{ data-tape-key={key} style={{ transform: `translateY(${start}px)` }} > - {formatTime(startTs)} → {formatTime(endTs)} - {contract} - {formatFlowMetric(count)} - {formatFlowMetric(totalSize)} - ${formatUsd(notional)} - {windowMs > 0 ? formatFlowMetric(windowMs, "ms") : "--"} - {structureLabel} - {nbboLabel} - {qualityLabel || "--"} + {formatTime(startTs)} → {formatTime(endTs)} + {contract} + {formatFlowMetric(count)} + {formatFlowMetric(totalSize)} + ${formatUsd(notional)} + {windowMs > 0 ? formatFlowMetric(windowMs, "ms") : "--"} + {structureLabel} + {nbboLabel} + {qualityLabel || "--"}
); })} diff --git a/docs/turns/2026-05-29-harden-web-terminal-ui-states.html b/docs/turns/2026-05-29-harden-web-terminal-ui-states.html new file mode 100644 index 0000000..a303937 --- /dev/null +++ b/docs/turns/2026-05-29-harden-web-terminal-ui-states.html @@ -0,0 +1,293 @@ + + + + + + Harden Web Terminal UI States + + + +
+
+
+ 2026-05-29 18:04 EDT + Beads: islandflow-ggm + Web terminal hardening +
+

Harden Web Terminal UI States

+

+ The terminal UI now handles live and replay status, empty panes, clipped market data, and table semantics more reliably for real users, assistive technology, and unusual input values. +

+
+ +
+

Summary

+

+ I hardened the main web terminal surface by adding accessible feed announcements, reusable empty-state markup, safer data cells for clipped values, and stronger table semantics on the busiest tape panes. +

+
+ +
+

Changes Made

+
    +
  • Added buildTapeStatusAnnouncement so live and replay feed states have complete screen-reader labels instead of relying on colored dots or terse visible labels.
  • +
  • Added reusable DataCell and EmptyState helpers for terminal panes.
  • +
  • Updated Options, Equities, and Flow panes with semantic column headers, rowgroups, cells, and useful title fallbacks for clipped values.
  • +
  • Improved empty-state layout so long messages wrap cleanly without collapsing the pane.
  • +
  • Added unicode-bidi: plaintext to table cells so mixed-direction symbols, ticker text, and unusual copied values are less likely to reorder confusingly.
  • +
  • Added focused tests for the new status-announcement helper.
  • +
+
+ +
+

Context

+

+ Islandflow is an evidence console for live market investigation. The UI has to remain useful when feeds are stale, paused, empty, or carrying long contract identifiers and numeric values. Hardening here focused on making the existing dense terminal more robust without changing its visual identity. +

+
+ +
+

Important Implementation Details

+
    +
  • TapeStatus now exposes a polite status region with an aria-label such as Live feed behind or Replay feed paused, time not available, 12 queued rows.
  • +
  • The visible status dot is marked aria-hidden, keeping color as a visual cue rather than the only status carrier.
  • +
  • Table headers are generated from arrays to keep repeated header markup consistent.
  • +
  • Clipped values such as option contracts, exact timestamps, full notional values, NBBO quality strings, and venue labels now expose fuller details through title where useful.
  • +
+
+ +
+

Relevant Diff Snippets

+

+ Diff snippets are presented in the format expected by diffs.com-style unified diff rendering. @pierre/diffs is installed in this repo, but it does not expose a CLI binary, so the relevant unified snippets are embedded directly. +

+
diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx
+@@
++export const buildTapeStatusAnnouncement = ({
++  status,
++  replayTime,
++  replayComplete,
++  paused,
++  dropped,
++  mode
++}: Pick<TapeStatusProps, "status" | "replayTime" | "replayComplete" | "paused" | "dropped" | "mode">): string => {
++  const label = replayComplete ? "Replay Complete" : statusLabel(status, paused, mode);
++  const feedLabel = mode === "live" && label.toLowerCase().startsWith("feed ")
++    ? label.toLowerCase()
++    : `feed ${label.toLowerCase()}`;
++  const parts = [`${mode === "live" ? "Live" : "Replay"} ${feedLabel}`];
++  ...
++};
++
++const EmptyState = ({ children }: { children: ReactNode }) => (
++  <div className="empty" role="status" aria-live="polite">
++    {children}
++  </div>
++);
+
+@@
+-    <div className={`status-inline status-${status} ${mode === "replay" ? "status-replay" : ""}`.trim()}>
+-      <span className="status-dot" />
++    <div
++      className={`status-inline status-${status} ${mode === "replay" ? "status-replay" : ""}`.trim()}
++      role="status"
++      aria-live="polite"
++      aria-label={announcement}
++    >
++      <span className="status-dot" aria-hidden="true" />
+ +
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
+@@
+ .data-table-cell {
+   min-width: 0;
+   overflow: hidden;
+   text-overflow: ellipsis;
+   white-space: nowrap;
+   font-size: 0.72rem;
++  unicode-bidi: plaintext;
+ }
+
+ .empty {
++  display: flex;
++  align-items: center;
++  min-height: 76px;
+   padding: 18px;
+   border-radius: 12px;
+   border: 1px dashed var(--border);
+   background: var(--bg-soft);
+   color: var(--text-dim);
++  line-height: 1.4;
++  overflow-wrap: anywhere;
+ }
+ +
diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts
+@@
++describe("tape status hardening", () => {
++  it("announces stale live feeds without relying on the colored dot", () => {
++    expect(
++      buildTapeStatusAnnouncement({
++        status: "stale",
++        replayTime: null,
++        replayComplete: false,
++        paused: false,
++        dropped: 0,
++        mode: "live"
++      })
++    ).toBe("Live feed behind");
++  });
++});
+
+ +
+

Expected Impact for End-Users

+

+ Traders and researchers should get a steadier terminal under imperfect feed conditions. Screen-reader users get explicit live and replay status changes, empty panes announce themselves clearly, and clipped market values are easier to inspect without widening the layout. +

+
+ +
+

Validation

+
    +
  • bun test apps/web/app/terminal.test.ts: 76 passing tests.
  • +
  • bun --cwd=apps/web run build: production build completed successfully.
  • +
  • Browser verification at http://localhost:3000/options: confirmed status regions, table semantics, column headers, rowgroup, and cells are present in the rendered page.
  • +
+
+ +
+

Issues, Limitations, and Mitigations

+
    +
  • The Options pane can still be wider than a narrow viewport by design; the table remains inside its horizontal scroll container.
  • +
  • Alert, classifier, dark-event, and news panes still have some older one-off markup. This task hardened the highest-traffic tape panes first.
  • +
  • The browser check observed a history-load warning because backend history was unavailable locally. That state rendered cleanly and was not a build blocker.
  • +
+
+ +
+

Follow-up Work

+
+

+ No additional Beads issue was required during this turn. A sensible future pass would extend the same DataCell and EmptyState treatment to Alerts, Smart Money, Dark Events, and the chart evidence lists. +

+
+
+
+ +