diff --git a/.beads/config.yaml b/.beads/config.yaml index 8344bac..12fdcdb 100644 --- a/.beads/config.yaml +++ b/.beads/config.yaml @@ -53,4 +53,7 @@ # - github.org # - github.repo -sync.remote: "http://dolt.deltaisland.io/islandflow" \ No newline at end of file +sync: + remote: git+https://git.deltaisland.io/dirtydishes/islandflow.git + +sync.remote: "git+https://github.com/dirtydishes/islandflow.git" \ No newline at end of file diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b1ab2c6..3a3f069 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,5 +1,3 @@ -{"_type":"issue","id":"islandflow-9ur","title":"address forgejo issue 15 tmp cve","description":"Track remediation for Forgejo issue #15: update tmp from vulnerable 0.2.5 to patched 0.2.6+ via root override and refreshed Bun lockfile, then validate with audit/tests.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-01T17:32:18Z","created_by":"dirtydishes","updated_at":"2026-06-01T17:36:01Z","started_at":"2026-06-01T17:32:23Z","closed_at":"2026-06-01T17:36:01Z","close_reason":"Resolved Forgejo issue #15 by bumping the tmp override to ^0.2.6, refreshing bun.lock to tmp@0.2.7, and validating with bun audit, bun why tmp, and bun test.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-m3d","title":"fix docs mirroring to github pages","description":"The repository docs folder is supposed to mirror to dirtydishes.github.io for GitHub Pages, but the mirroring is not working. Investigate the docs publishing workflow and repair the configuration or scripts so docs can be published reliably.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-31T22:05:48Z","created_by":"dirtydishes","updated_at":"2026-05-31T22:12:26Z","started_at":"2026-05-31T22:05:56Z","closed_at":"2026-05-31T22:12:26Z","close_reason":"Updated docs Pages workflow to publish into dirtydishes/dirtydishes.github.io under islandflow/docs, tightened docs index generation, regenerated docs index, and documented validation/limitations.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-2op","title":"[bug] Desktop app unclickable and no live data in hosted shell","description":"## Summary\\nDesktop Electron shell appears fully non-interactive (clicks do not work) and no live market data reaches the UI.\\n\\n## Why this matters\\nDesktop wrapper is currently unusable for core workflow and blocks users from validating market streams outside browser.\\n\\n## Scope\\nReproduce issue locally, identify root cause(s) in Electron shell and frontend integration, implement fix, and validate interactivity + data flow end-to-end.\\n\\n## Acceptance Criteria\\n- Desktop app responds to pointer interactions (navigation/actions clickable)\\n- Live data stream connects and updates UI in desktop mode\\n- Regression coverage or guardrails added where practical\\n- Findings and validation documented","status":"in_progress","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-24T04:23:55Z","created_by":"dirtydishes","updated_at":"2026-05-24T04:23:57Z","started_at":"2026-05-24T04:23:57Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-jad","title":"Sync docs pages workflow fix to github mirror","description":"GitHub is still running an older docs Pages workflow with configure-pages because github/main is behind forgejo/main. Push the already-fixed workflow commit to the GitHub mirror so Actions runs the gh-pages branch deployment flow instead.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T22:27:46Z","created_by":"dirtydishes","updated_at":"2026-05-23T22:28:24Z","started_at":"2026-05-23T22:28:10Z","closed_at":"2026-05-23T22:28:24Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-bc7","title":"Fix docs Pages workflow configure-pages failure","description":"Replace the current docs Pages deployment flow so workflow runs succeed even when configure-pages cannot read or enable the site. Keep published docs target behavior for dirtydishes.github.io/islandflow/docs.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T22:23:28Z","created_by":"dirtydishes","updated_at":"2026-05-23T22:25:19Z","started_at":"2026-05-23T22:23:31Z","closed_at":"2026-05-23T22:25:19Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -11,7 +9,6 @@ {"_type":"issue","id":"islandflow-yza","title":"Persist historical flow packets for alert detail replay","description":"## Why\nAlert details can show a missing persisted flow packet when the packet is no longer present in the Redis hot cache, even though the associated historical alert and evidence were loaded from ClickHouse.\n\n## What needs to be done\nTrace the API path that resolves alert detail flow packets, compare Redis hot-cache lookups with ClickHouse historical fetches, and ensure historical flow packet payloads are treated as first-class persisted data with context preserved when replaying or loading older alerts.\n\n## Acceptance Criteria\n- Alert detail flow packets load for historical alerts even when the packet is absent from Redis hot cache\n- Historical ClickHouse-backed flow packet responses preserve the context required by the UI\n- Relevant automated tests cover the regression or the gap is explicitly documented","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T06:52:04Z","created_by":"dirtydishes","updated_at":"2026-05-20T06:59:26Z","started_at":"2026-05-20T06:52:09Z","closed_at":"2026-05-20T06:59:26Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-jor","title":"Support Forgejo pull request status in desktop git panel","description":"The desktop app currently reports pull request status unavailable when a repository only has a Forgejo remote. Add native Forgejo/Gitea-style remote detection and pull request status lookup so Forgejo-only repositories can show PR state in the Codex app git panel.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T20:55:15Z","created_by":"dirtydishes","updated_at":"2026-05-19T20:59:46Z","started_at":"2026-05-19T20:55:25Z","closed_at":"2026-05-19T20:59:46Z","close_reason":"Patched the installed Codex desktop app bundle with a Forgejo PR status fallback and documented the local change.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-g3a","title":"Reconcile PR merge conflicts","description":"Resolve the current pull request conflicts for the nextjs-upgrade branch, validate the result, document the turn, and push the reconciled branch.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T18:44:51Z","created_by":"dirtydishes","updated_at":"2026-05-19T18:47:35Z","started_at":"2026-05-19T18:44:56Z","closed_at":"2026-05-19T18:47:35Z","close_reason":"Merged forgejo/main into nextjs-upgrade, resolved README and Beads conflicts, updated JetStream retention tests, validated deploy help, Docker workspace sync, API/bus tests, and web build, and added turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-9rc","title":"Implement native fast iterative deploy plan","description":"Implement the checked-in plan at plans/2026-05-18-native-fast-iterative-deploy-plan.md. Cover deploy-phase timing instrumentation, native deployment operational assets, deploy guardrails, validation/cutover documentation, and any required live VPS remediation that is safely actionable from this session. Track follow-up items separately if anything cannot be completed in-repo or on the live host.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T07:15:19Z","created_by":"dirtydishes","updated_at":"2026-05-18T07:34:03Z","started_at":"2026-05-18T07:15:25Z","closed_at":"2026-05-18T07:34:03Z","close_reason":"Implemented the native fast iterative deploy plan with deploy timing summaries, worker-only native fast mode, edge-cutover guardrails, local-on-server execution support, checked-in native ops assets, live audit findings, and turn documentation. Remaining cutover work is tracked in islandflow-vvw.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-jbi","title":"Hydrate alert evidence details from ClickHouse","description":"Alert detail drawers need to fetch persisted alert context from ClickHouse by trace id, including linked flow packets, option prints, preserved execution context, and explicit missing refs for UI diagnostics.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T14:55:43Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:01:58Z","started_at":"2026-05-17T14:55:53Z","closed_at":"2026-05-17T15:01:58Z","close_reason":"Implemented ClickHouse-backed alert context hydration across storage, API, terminal drawer, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-8kj","title":"Configure persistent beads Dolt remote on deltaisland server","description":"Install the beads and Dolt CLIs on the server, configure a persistent Dolt sync remote backed by the server-hosted Forgejo repository, verify refs/dolt/data publication, and document Nginx Proxy Manager / firewall considerations.","status":"closed","priority":1,"issue_type":"task","assignee":"delta","created_at":"2026-05-17T10:31:31Z","created_by":"delta","updated_at":"2026-05-17T10:37:47Z","started_at":"2026-05-17T10:32:16Z","closed_at":"2026-05-17T10:37:47Z","close_reason":"Installed bd and dolt on the server, configured the Forgejo-backed Dolt remote, published refs/dolt/data, and documented the setup.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-200","title":"Implement durable options tape history","description":"Implement the plan from docs/plans/2026-05-16-1711-durable-options-tape-history.html: durable ClickHouse-backed options history, signal/all prints view selection, preserved execution context, stale semantics limited to live health, reset runbook, tests, and turn documentation.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-16T21:21:30Z","created_by":"dirtydishes","updated_at":"2026-05-16T21:26:51Z","started_at":"2026-05-16T21:21:33Z","closed_at":"2026-05-16T21:26:51Z","close_reason":"Implemented durable options tape history, signal/raw view selection, reset runbook, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -27,11 +24,6 @@ {"_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-xmi","title":"Resolve conflicts in PR 45","description":"Resolve the merge conflicts blocking Forgejo PR 45, validate the affected code paths, and push the reconciled branch back to Forgejo.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-06T03:33:52Z","created_by":"dirtydishes","updated_at":"2026-06-06T03:35:16Z","started_at":"2026-06-06T03:33:58Z","closed_at":"2026-06-06T03:35:16Z","close_reason":"Resolved the PR 45 merge conflict in .beads/issues.jsonl and validated the reconciled tracker file.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-8a6","title":"verify github pages token for docs mirror","description":"The docs mirror workflow now publishes islandflow/docs into dirtydishes/dirtydishes.github.io, but the GitHub Actions secret DOCS_PAGES_TOKEN must exist and have permission to push to that Pages repository. Verify the secret is configured and manually run the Publish Docs workflow after the mirror branch lands.","notes":"Direct manual publish to dirtydishes/dirtydishes.github.io succeeded on 2026-06-01 and https://dirtydishes.github.io/islandflow/docs/ returned HTTP 200. Remaining work is to verify DOCS_PAGES_TOKEN so the islandflow docs mirror workflow can publish future updates automatically.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-31T22:12:27Z","created_by":"dirtydishes","updated_at":"2026-06-01T13:45:34Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-5jt","title":"Add anatomy reference page","description":"Create a standalone docs/anatomy.html reference explaining how prints move through ingest, tape, flow packets, smart-money events, classifier hits, and alerts, including diagrams.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-31T21:20:34Z","created_by":"dirtydishes","updated_at":"2026-05-31T21:25:54Z","started_at":"2026-05-31T21:20:44Z","closed_at":"2026-05-31T21:25:54Z","close_reason":"Added the standalone anatomy reference page and linked it from the docs index.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-cig","title":"Expand CI quality gates","description":"Add a more robust CI workflow for the Bun/TypeScript monorepo, including formatting, linting, type checking, builds, and tests where appropriate.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-30T06:29:33Z","created_by":"dirtydishes","updated_at":"2026-05-30T06:34:11Z","started_at":"2026-05-30T06:29:41Z","closed_at":"2026-05-30T06:34:11Z","close_reason":"Expanded CI quality gates with Biome formatting/linting, public API route checks, Docker snapshot validation, tests, typecheck, and web build validation.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-3l6","title":"fix ci typecheck bun path resolution","description":"Forgejo CI fails in scripts/typecheck.ts because the script shells out to bunx, which expects bun on PATH. The runner installs Bun by absolute path, so the typecheck helper should use the current Bun executable instead of PATH lookup.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-30T05:34:55Z","created_by":"dirtydishes","updated_at":"2026-05-30T06:00:31Z","started_at":"2026-05-30T05:35:02Z","closed_at":"2026-05-30T06:00:31Z","close_reason":"Fixed the Forgejo CI terminal import mismatch by switching the terminal client component to a namespace import; verified locally and on Forgejo run #56.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-wtg","title":"Harden drawer dialog focus behavior","description":"Fix terminal drawers so they expose modal dialog semantics, trap keyboard focus while open, and restore focus to the invoking control after close.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T22:55:25Z","created_by":"dirtydishes","updated_at":"2026-05-29T23:09:45Z","started_at":"2026-05-29T22:56:22Z","closed_at":"2026-05-29T23:09:45Z","close_reason":"Implemented modal dialog semantics, focus trapping, Escape dismissal, focus restoration, validation, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-833","title":"Improve narrow options table responsiveness","description":"Adapt the Options route for narrow screens so dense tape tables remain contained in their panes, preserve row identity while horizontally panning, and keep the mobile ticker/filter controls readable.","acceptance_criteria":"Options tape panes have bounded heights on narrow screens; table body scrolls internally; first table column remains visible while panning; mobile topbar and filter controls have adequate spacing; web production build passes.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T22:34:05Z","created_by":"dirtydishes","updated_at":"2026-05-29T22:36:20Z","started_at":"2026-05-29T22:34:24Z","closed_at":"2026-05-29T22:36:20Z","close_reason":"Implemented narrow-screen options pane containment, sticky row context, touch-scroll affordances, and mobile control spacing. Validated with web build and in-browser narrow viewport checks.","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} @@ -50,8 +42,6 @@ {"_type":"issue","id":"islandflow-kgu","title":"Reconcile PR #8 branch with current main","description":"Why this issue exists and what needs to be done: user requested reconciliation for PR #8. Identify the PR #8 branch, merge/rebase with current main, resolve conflicts, validate, and push the updated branch so the PR can merge cleanly.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T20:14:36Z","created_by":"dirtydishes","updated_at":"2026-05-23T20:24:29Z","started_at":"2026-05-23T20:14:39Z","closed_at":"2026-05-23T20:24:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-l9h","title":"stop persisting non-signal option prints in clickhouse","description":"Why: non-signal option prints are storage noise and should not be persisted by default.\\n\\nWhat: add OPTIONS_PERSIST_SIGNAL_ONLY env flag (default true), gate option_print inserts in ingest-options, add tests for persistence behavior, update env examples, and document one-off cleanup SQL for existing non-signal rows.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T03:02:32Z","created_by":"dirtydishes","updated_at":"2026-05-23T03:06:34Z","started_at":"2026-05-23T03:02:35Z","closed_at":"2026-05-23T03:06:34Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-2cj","title":"Add Forgejo-first agent workflow guidance to AGENTS.md","description":"Why this issue exists and what needs to be done:\\n- The repository’s canonical home is Forgejo at git.deltaisland.io, but AGENTS.md does not currently direct agents to prefer Forgejo-specific workflows.\\n- Update AGENTS.md so agents treat Forgejo as primary and use the fj CLI for pull request workflows.\\n- Keep existing Beads and completion instructions intact while clarifying remote preference and command usage.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-23T02:51:31Z","created_by":"dirtydishes","updated_at":"2026-05-23T02:55:42Z","closed_at":"2026-05-23T02:55:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-6ub","title":"Fix LiveStateManager default hot-head test expectation after recent API changes","description":"bun test v1.3.13 (bf2e2cec) currently fails on the case after the latest pulled changes. The failure appears unrelated to the server-load tuning work and should be investigated separately so targeted validation can pass cleanly again.","status":"open","priority":2,"issue_type":"bug","owner":"dishes@dpdrm.com","created_at":"2026-05-22T06:09:44Z","created_by":"dirtydishes","updated_at":"2026-05-22T06:09:44Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-qke","title":"Tune healthchecks and Redis flush cadence to reduce server load","description":"Containerd and dockerd are consuming significant CPU due to frequent Docker healthcheck exec churn across multiple stacks, and the host Islandflow Redis instance is hot from aggressive live-cache rewrite behavior. Tune external stack healthcheck intervals and Islandflow Redis flush cadence to reduce steady-state load while preserving service behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-22T06:06:58Z","created_by":"dirtydishes","updated_at":"2026-05-22T06:11:40Z","started_at":"2026-05-22T06:07:03Z","closed_at":"2026-05-22T06:11:40Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-xc5","title":"One-time bidirectional git remote backfill between github and forgejo","description":"Perform a one-time sync so github and forgejo contain the same branch/tag refs and historical commits, including pre-transition github history and newer forgejo commits. Document exact commands and validation results.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-21T01:25:05Z","created_by":"dirtydishes","updated_at":"2026-05-21T01:26:19Z","started_at":"2026-05-21T01:25:16Z","closed_at":"2026-05-21T01:26:19Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-y7b","title":"Fix false browser fallback in Electron renderer","description":"Why this issue exists and what needs to be done:\\nElectron sessions can briefly or permanently render browser-only fallback copy when runtime detection depends on async desktop AI state loading.\\n\\nImplement a runtime snapshot that is resolved synchronously on the client (shell marker + bridge presence) and kept independent from bridge.ai state fetch/subscribe behavior. Add bounded runtime resync/retry and lifecycle-triggered resync on focus/pageshow so late bridge exposure flips to desktop mode.\\n\\nUpdate desktop-ai tests to cover: runtime marker present before AI state resolves, bridge present with pending/rejected getState, and late runtime availability. Keep preload/IPC contract unchanged unless a verified failure requires it.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-21T00:06:52Z","created_by":"dirtydishes","updated_at":"2026-05-21T00:11:21Z","started_at":"2026-05-21T00:06:55Z","closed_at":"2026-05-21T00:11:21Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-xtg","title":"implement ai alert copilot ux refinements","description":"Implement the AI alert Copilot UX plan: markdown result rendering, reusable task result states, in-session result caching with regenerate, task cancellation through the desktop bridge, tests, and required turn documentation.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T23:30:50Z","created_by":"dirtydishes","updated_at":"2026-05-20T23:37:58Z","started_at":"2026-05-20T23:30:58Z","closed_at":"2026-05-20T23:37:58Z","close_reason":"Implemented markdown Copilot rendering, session result caching, regenerate controls, task cancellation plumbing, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -68,16 +58,12 @@ {"_type":"issue","id":"islandflow-lm6","title":"Clarify repo turn documentation scope","description":"Update AGENTS.md so repository turn documentation clearly uses repo-local docs/turns and impeccable styling, without inheriting global non-repo computer-task styling.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T12:05:07Z","created_by":"dirtydishes","updated_at":"2026-05-19T12:06:12Z","started_at":"2026-05-19T12:05:14Z","closed_at":"2026-05-19T12:06:12Z","close_reason":"Verified AGENTS.md now scopes repo turn docs to docs/turns and makes impeccable the styling authority; added turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-6iq","title":"Update README for current project state","description":"Resolve README merge conflicts and document the current project state, including the smart money classification taxonomy, Next.js update, and deployment workflow changes.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T11:37:24Z","created_by":"dirtydishes","updated_at":"2026-05-19T11:40:01Z","started_at":"2026-05-19T11:37:31Z","closed_at":"2026-05-19T11:40:01Z","close_reason":"README conflict resolved and current project state documented, including smart-money taxonomy, Next.js update, and deployment workflow.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-lib","title":"Upgrade apps/web to Next.js 16.2.6","description":"Upgrade the web app dependency stack to Next.js 16.2.6 with React 19, refresh Bun and mirrored Docker workspace lockfiles, keep runtime behavior unchanged, fix any focused web test fallout, validate the web build and targeted route tests, and document the completed work.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T11:04:51Z","created_by":"dirtydishes","updated_at":"2026-05-19T11:31:23Z","started_at":"2026-05-19T11:04:57Z","closed_at":"2026-05-19T11:31:23Z","close_reason":"Upgraded apps/web to Next.js 16.2.6 with React 19, refreshed Bun lockfiles including the Docker workspace mirror, fixed the React 19 nullable ref type issue, and validated the web build, focused tests, Docker workspace sync, and route smoke checks.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-fl5","title":"Decide final public posture for api.flow.deltaisland.io after native cutover","description":"Why this issue exists and what needs to be done:\\n- Native cutover now works end-to-end through Nginx Proxy Manager and the public API hostname now resolves directly to the VPS\\n- The API hostname was left DNS-only in Cloudflare during incident resolution, while the web hostname still uses the Cloudflare proxy\\n- We need to decide whether api.flow.deltaisland.io should remain direct-to-origin or be re-proxied through Cloudflare, then validate TLS, websocket, and operational behavior for the chosen posture","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-18T23:51:21Z","created_by":"dirtydishes","updated_at":"2026-05-18T23:51:21Z","dependencies":[{"issue_id":"islandflow-fl5","depends_on_id":"islandflow-vvw","type":"discovered-from","created_at":"2026-05-18T19:52:32Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-8fn","title":"implement alpaca-backed news wire view","description":"Why this issue exists and what needs to be done:\\nAdd an Alpaca-powered live news pipeline, API, storage, and web experience, including a dedicated /news route, Home preview, live fanout, history pagination, ticker resolution, and replay-mode live-only empty states.\\n\\nAcceptance criteria:\\n- normalized NewsStory contract and live channel exist\\n- ingest-news service backfills and streams Alpaca news\\n- API persists, serves, and fans out news\\n- web app exposes /news plus Home preview and drawer\\n- tests cover types, storage, API, and key UI behaviors\\n- turn documentation is added\\n\\nDesign:\\nReuse Islandflow drawer, chips, panes, and terminal styling; keep news live-only in v1 replay mode.\\n\\nNotes:\\nImplement client-side ticker filtering in v1 and expose latest revision only per provider+story_id.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T20:37:13Z","created_by":"dirtydishes","updated_at":"2026-05-18T20:55:11Z","started_at":"2026-05-18T20:37:20Z","closed_at":"2026-05-18T20:55:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-k8i","title":"Fix duplicate alert context import in API entrypoint","description":"Recent alert-context work introduced a duplicate fetchAlertContextByTraceId import in services/api/src/index.ts, which risks breaking TypeScript compilation and API startup. Remove the duplicate import and validate the affected API/web tests.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T13:01:58Z","created_by":"dirtydishes","updated_at":"2026-05-18T13:03:40Z","started_at":"2026-05-18T13:02:02Z","closed_at":"2026-05-18T13:03:40Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-lk9","title":"Fix PR creation workflow after Forgejo migration","description":"## Why\\nCreating pull requests with fails after the repository moved primary collaboration from GitHub to Forgejo. The current workflow still assumes GitHub GraphQL PR creation semantics, which do not work against the Forgejo remote.\\n\\n## What\\nInvestigate the current PR creation path, identify remaining GitHub-specific assumptions, and update the repo workflow/scripts/docs so contributors can reliably publish branches and open PRs in the Forgejo-based setup.\\n\\n## Acceptance Criteria\\n- The repo no longer instructs contributors to use a broken GitHub-specific PR creation path for Forgejo branches\\n- There is a documented and preferably scripted way to create the equivalent review request against Forgejo\\n- Validation demonstrates the new workflow behaves correctly or clearly documents any remaining platform limitation","status":"in_progress","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T10:26:47Z","created_by":"dirtydishes","updated_at":"2026-05-18T10:26:53Z","started_at":"2026-05-18T10:26:53Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-vvw","title":"Stage native public-edge cutover after worker soak","description":"Why this issue exists and what needs to be done:\\n- The native deploy path is now provisioned for worker-first iteration, with checked-in user units, rollback helpers, and edge guardrails\\n- Remaining work is to enable and soak native worker units, validate duplicate-processing behavior, then deliberately cut over the public web/api edge if warranted\\n- Final acceptance should include deciding whether Docker or native becomes the default runtime after operational evidence","notes":"2026-05-18: native infra, native app services, NPM public-edge retargeting, Docker rollback helpers, and Cloudflare/DNS API hostname recovery were implemented and verified. Public checks now pass for flow.deltaisland.io and api.flow.deltaisland.io. Remaining follow-up: decide whether api.flow.deltaisland.io should remain DNS-only or be re-proxied through Cloudflare under islandflow-fl5.","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T07:32:35Z","created_by":"dirtydishes","updated_at":"2026-05-18T23:52:32Z","started_at":"2026-05-18T23:51:20Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-bsg","title":"Fix public /replay/options proxy regression","description":"Restore correct public routing for GET /replay/options on flow.deltaisland.io. The app currently serves HTML for that API path, which indicates edge/proxy routing drift. Update the live proxy topology or deployment assets as needed, then validate with bun run scripts/check-public-api-routes.ts.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T07:15:19Z","created_by":"dirtydishes","updated_at":"2026-05-18T07:32:51Z","started_at":"2026-05-18T07:15:24Z","closed_at":"2026-05-18T07:32:51Z","close_reason":"Audited the live VPS and reverse proxy on 2026-05-18: public /replay/options now returns JSON, bun run scripts/check-public-api-routes.ts passes, and the active Nginx Proxy Manager config includes /replay in the API route matcher. No in-repo app code change was required.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-1ei","title":"Make deploy helper remote-aware for Forgejo","description":"Why: scripts/deploy.ts hardcodes git remote name origin for fetch/pull/push and branch verification, but this repository now uses forgejo/github remotes and may not have an origin remote. What: update deploy.ts to resolve the deploy git remote robustly (Forgejo-aware), use it across local prechecks, branch publish, and remote rollout git operations, and keep behavior explicit in output.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T03:20:12Z","created_by":"dirtydishes","updated_at":"2026-05-18T03:22:39Z","started_at":"2026-05-18T03:20:16Z","closed_at":"2026-05-18T03:22:39Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-xod","title":"Add --fast mode to deploy helper","description":"Why: full main deploys rebuild all images and run full verification, which is slow for routine rollouts. What: add a --fast flag to scripts/deploy.ts with explicit behavior that short-circuits slow steps while preserving basic safety checks; update help text/docs for discoverability.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T02:50:47Z","created_by":"dirtydishes","updated_at":"2026-05-18T02:53:41Z","started_at":"2026-05-18T02:50:50Z","closed_at":"2026-05-18T02:53:41Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-cif","title":"hydrate alert evidence context from clickhouse","description":"Implement alert detail hydration from ClickHouse with a new context endpoint and frontend drawer evidence resolution. Includes storage lookup by alert trace_id/evidence refs, unresolved refs diagnostics, API route GET /flow/alerts/:trace_id/context, terminal evidence hydration + loading states/copy updates, and tests across storage/api/web.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T00:15:55Z","created_by":"dirtydishes","updated_at":"2026-05-18T00:17:38Z","started_at":"2026-05-18T00:16:00Z","closed_at":"2026-05-18T00:17:38Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-9j5","title":"Prepare PR for deploy allowlist cleanup","description":"Why this issue exists and what needs to be done:\\n- Package current deploy allowlist cleanup into a reviewable PR with multiple commits\\n- Add required turn documentation in docs/turns\\n- Run validation and push all artifacts","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T15:44:12Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:53:55Z","started_at":"2026-05-17T15:44:22Z","closed_at":"2026-05-17T15:53:55Z","close_reason":"Packaged deploy allowlist cleanup into multi-commit PR branch with required turn documentation and push workflow.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-4e9","title":"Polish terminal view","description":"Improve the Islandflow web terminal view with a focused UI polish pass aligned to the product design system.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T15:18:18Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:25:02Z","started_at":"2026-05-17T15:18:21Z","closed_at":"2026-05-17T15:25:02Z","close_reason":"Polished terminal shell styling, responsive Tape actions, and documented the turn.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-lyt","title":"Summarize 2026-05-16 git activity for standup","description":"Create a grounded standup summary for yesterday's git activity, anchored to commits, changed files, and any linked PR context if present. Produce the required HTML document in docs/general and complete the beads + git handoff workflow.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T14:02:57Z","created_by":"dirtydishes","updated_at":"2026-05-17T14:05:37Z","started_at":"2026-05-17T14:03:09Z","closed_at":"2026-05-17T14:05:37Z","close_reason":"Created docs/general standup summary for 2026-05-16 git activity, grounded to commits and changed files, and prepared the repo handoff workflow.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-sz8","title":"Fix public /replay/options proxy regression","description":"## Summary\nThe new deploy-time public route checker added in commit 1424a27 (\"fix durable options history routing\") currently fails against https://flow.deltaisland.io because GET /replay/options returns HTML instead of JSON.\n\n## Evidence\n- `bun run scripts/check-public-api-routes.ts https://flow.deltaisland.io` fails on `/replay/options?view=signal\u0026after_ts=0\u0026after_seq=0\u0026limit=1` with `returned non-JSON content (text/html; charset=UTF-8)`\n- `services/api/src/index.ts` implements `GET /replay/options`, so the HTML response indicates the request is landing on the web app instead of the API service\n- `deployment/docker/README.md` documents that same-origin proxy mode must include `/replay/*` in the API route matcher\n\n## Minimal Fix\nUpdate the live reverse proxy / edge route matcher for flow.deltaisland.io so `/replay/*` is forwarded to the API host, then rerun `bun run check:public-api-routes`.\n\n## Notes\nThis looks like a production proxy configuration regression rather than an in-repo application bug.","status":"open","priority":2,"issue_type":"bug","owner":"dishes@dpdrm.com","created_at":"2026-05-17T13:06:11Z","created_by":"dirtydishes","updated_at":"2026-05-17T13:06:11Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -108,8 +94,6 @@ {"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:39:58Z","closed_at":"2026-05-05T05:39:58Z","close_reason":"Completed terminal smart-money profile migration","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"auto-import","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-5rt","title":"Summarize June 2 git activity for standup","description":"Create the daily standup summary in docs/general for 2026-06-02 activity, anchored to yesterday's commits and touched files.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-03T16:30:03Z","created_by":"dirtydishes","updated_at":"2026-06-03T16:31:33Z","started_at":"2026-06-03T16:31:26Z","closed_at":"2026-06-03T16:31:33Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-3f4","title":"Publish May 31 standup git summary","description":"## Summary\nCreate the daily standup HTML summary for 2026-05-31 git activity in docs/general and regenerate any supporting docs index entries.\n\n## Why this matters\nThe team needs a grounded, commit-anchored standup artifact for May 31 repository activity.\n\n## Scope\nInspect May 31 git history, write the summary document in docs/general, update related generated docs metadata if needed, and close out the task.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-01T13:02:21Z","created_by":"dirtydishes","updated_at":"2026-06-01T13:04:45Z","started_at":"2026-06-01T13:02:29Z","closed_at":"2026-06-01T13:04:45Z","close_reason":"Added docs/general standup summary for 2026-05-31 and verified docs index discovery.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-6ak","title":"Clarify turn doc diff rendering instructions","description":"Make AGENTS.md explicit that turn documents should render diffs with the @pierre/diffs/ssr library import instead of attempting to run @pierre/diffs through bunx.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-30T02:01:59Z","created_by":"dirtydishes","updated_at":"2026-05-30T02:02:27Z","started_at":"2026-05-30T02:02:00Z","closed_at":"2026-05-30T02:02:27Z","close_reason":"Updated AGENTS.md to require @pierre/diffs/ssr rendering, forbid bunx @pierre/diffs attempts, and include a known-good preloadPatchDiff recipe.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-3kn","title":"Summarize 2026-05-28 git activity","description":"Prepare the standup-ready summary of yesterday's git activity, grounded in commits, PRs, and touched files, and store the HTML report in docs/general.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-29T13:02:25Z","created_by":"dirtydishes","updated_at":"2026-05-29T13:04:23Z","started_at":"2026-05-29T13:02:33Z","closed_at":"2026-05-29T13:04:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-3ys","title":"Expand Forgejo CI beyond the fast validate path","description":"Add follow-on Forgejo CI jobs after the initial baseline is stable. This should cover deferred work such as Docker image builds for deployment/docker, service-container integration tests for NATS/Redis/ClickHouse paths, and any later deploy or release automation that should not block the first fast PR gate.","status":"open","priority":3,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-24T00:34:09Z","created_by":"dirtydishes","updated_at":"2026-05-24T00:34:09Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -127,6 +111,4 @@ {"_type":"issue","id":"islandflow-zsy","title":"Expose Forgejo SSH on a direct DNS hostname","description":"git.deltaisland.io currently resolves through Cloudflare's proxy, so SSH on port 2222 does not complete even though the Forgejo container is listening on the host. If SSH-based git/beads workflows are desired, add a DNS-only hostname (or adjust the existing record) that points directly at the server for Forgejo SSH.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-17T10:34:06Z","created_by":"delta","updated_at":"2026-05-17T10:34:06Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-38p","title":"Add native deployment unit templates and rollback helpers","description":"The deploy helper now supports --runtime native, but the repo still relies on operator-managed systemd units and manual rollback. Add checked-in native deployment templates or provisioning guidance for the expected units, and consider lightweight rollback/smoke-test helpers once the host-native path is exercised on the real VPS.","status":"open","priority":3,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:46:42Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:46:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-575","title":"Document smart-money event calendar env","description":"Document smart-money event-calendar environment configuration in env examples and README.\n","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T06:57:14Z","created_by":"dirtydishes","updated_at":"2026-05-05T06:57:57Z","started_at":"2026-05-05T06:57:17Z","closed_at":"2026-05-05T06:57:57Z","close_reason":"Documented event-calendar env variables","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-a1m","title":"Publish June 3 standup summary","description":"Why this issue exists and what needs to be done:\\n- Produce the daily standup summary for git activity on 2026-06-03.\\n- Ground every statement in commits and touched files only.\\n- Save the HTML artifact under docs/general and complete the automation handoff workflow.","status":"closed","priority":4,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-04T13:02:04Z","created_by":"dirtydishes","updated_at":"2026-06-04T13:03:43Z","started_at":"2026-06-04T13:03:34Z","closed_at":"2026-06-04T13:03:43Z","close_reason":"Created docs/general/2026-06-04-standup-summary-2026-06-03.html with a commit-grounded summary of June 3 git activity.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"islandflow-0jb","title":"Publish June 1 standup summary","description":"Why this issue exists and what needs to be done:\\n- Produce the daily standup summary for git activity on 2026-06-01.\\n- Ground every statement in commits and touched files only.\\n- Save the HTML artifact under docs/general and complete the automation handoff workflow.","status":"closed","priority":4,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-02T13:03:01Z","created_by":"dirtydishes","updated_at":"2026-06-02T13:05:51Z","started_at":"2026-06-02T13:03:16Z","closed_at":"2026-06-02T13:05:51Z","close_reason":"Created docs/general/2026-06-02-standup-summary-2026-06-01.html with a commit-grounded June 1 standup summary.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-1tu","title":"Publish 2026-05-24 standup summary","description":"Why this issue exists and what needs to be done\n\nCreate the daily standup summary for git activity on 2026-05-24, grounded in commits and touched files, then store the HTML report in docs/general.","status":"closed","priority":4,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-25T13:02:56Z","created_by":"dirtydishes","updated_at":"2026-05-25T13:04:31Z","closed_at":"2026-05-25T13:04:31Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 01724f6..c746164 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -30,27 +30,17 @@ jobs: apt-get install --yes --no-install-recommends curl unzip rm -rf /var/lib/apt/lists/* curl -fsSL https://bun.sh/install | bash - echo "$HOME/.bun/bin" >> "$GITHUB_PATH" ~/.bun/bin/bun --version - name: Install dependencies run: ~/.bun/bin/bun install --frozen-lockfile - - name: Check formatting - run: ~/.bun/bin/bun run fmt:check - - - name: Run lint - run: ~/.bun/bin/bun run lint - - name: Run typecheck run: ~/.bun/bin/bun run typecheck - name: Run tests run: ~/.bun/bin/bun test - - name: Check public API routes - run: ~/.bun/bin/bun run check:public-api-routes - - name: Check Docker workspace snapshot run: ~/.bun/bin/bun run check:docker-workspace diff --git a/AGENTS.md b/AGENTS.md index 72a0e65..225cfda 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,15 +86,39 @@ Agent expectations: ## Required Turn Documentation -Follow the global turn-documentation rules in `~/.codex/AGENTS.md` for repository implementation tasks, plan documents, and non-repo computer tasks. +At the end of every completed implementation task, before final handoff, create a user-readable HTML document describing the work. -For this repository, the repo-specific requirements are: +This documentation is mandatory whenever code, configuration, tests, or project files were changed. -- Save repository implementation turn documents in `docs/turns/`. -- Use the `impeccable` skill to structure and style repository implementation turn documents when available. -- Render "Relevant Diff Snippets" with `@pierre/diffs/ssr`; use https://diffs.com/docs as the SSR reference. -- For minor updates to a previous change, update the existing turn document instead of creating a new one. -- The minor/trivial exemptions below override the general documentation requirement for this repository. +### Precedence and classification + +Use this decision order before creating a turn document: + +1. Check the minor/trivial exemption checklist below first. +2. If the task clearly matches an exemption, do not create a turn document. +3. If the task is a clearly substantive implementation change, create a turn document. +4. If classification is ambiguous or mixed, ask the user before creating a turn document. + +The minor/trivial exemptions override the general mandatory turn-document rule. + +For diff content in turn documentation (including "Code diffs" and "Relevant Diff Snippets"), render the diff as HTML with the `@pierre/diffs/ssr` library by default. Do not try to run `bunx @pierre/diffs`; this package is installed as a library and does not expose a CLI. A plain diff/code block fallback is only acceptable if importing or rendering with `@pierre/diffs/ssr` fails because of a real tool or blocking error, and the document must say why. + +Known-good `@pierre/diffs/ssr` pattern: + +```js +import { preloadPatchDiff } from "@pierre/diffs/ssr"; +import { execSync } from "node:child_process"; + +const patch = execSync("git diff -- path/to/file", { encoding: "utf8" }); +const rendered = ( + await preloadPatchDiff({ + patch, + options: { maxContextLines: 4 } + }) +).prerenderedHTML; +``` + +Embed `rendered` directly into the turn document inside a clearly labeled diff container. ### No turn document for minor/trivial checklist matches @@ -109,9 +133,69 @@ Do not create a turn document when the change is minor/trivial and cleanly match If a change does not cleanly fit either exempt or substantive buckets, ask the user before creating a turn document. +### When making a minor update to a previous change, update the existing documentation instead of creating a new file. Use the following format: + +**"New Changes as of {time and date at which the change was made}"** +- **Summary of changes** +- **Why this change was made** +- **Code diffs** (render with `@pierre/diffs/ssr` by default; if importing or rendering fails, include a clearly labeled plain diff/code block and note why) +- **Related issues or PRs** + +Additionally, add a note to each section explaining why the changes were made. + +### Location + +Save the document in: + +```text +docs/turns/ +``` + +Use a clear timestamped filename: + +```text +docs/turns/YYYY-MM-DD-short-task-name.html +``` + +Example: + +```text +docs/turns/2026-05-14-add-market-replay-controls.html +``` + +### Format + +Use the `impeccable` skill to structure and style the document as clean, readable HTML. + +For this repository, `impeccable` is the styling and layout authority for turn documents when available. Do not apply global non-repo computer-task house styling to repository turn documents. + +If the `impeccable` skill is unavailable or blocked by an actual tool/file error, still create a well-structured standalone HTML file with: + +- A concise summary at the top +- A detailed explanation of what changed +- Relevant context or background +- Specific code snippets or examples when helpful +- Issues, limitations, tradeoffs, or mitigations +- Validation performed, including tests, builds, linters, or manual checks +- Any remaining follow-up work, with corresponding Beads issue IDs when applicable + +### Required Sections + +Each turn document must include these sections: + +1. **Summary** +2. **Changes Made** +3. **Context** +4. **Important Implementation Details** +5. **Relevant Diff Snippets** (render with `@pierre/diffs/ssr` by default; if importing or rendering fails, include a clearly labeled plain diff/code block and note why) +6. **Expected Impact for End-Users** +7. **Validation** +8. **Issues, Limitations, and Mitigations** +9. **Follow-up Work** + ### Completion Rule -For repository implementation tasks that require turn documentation, the task is not complete until: +A task that requires a turn document is not complete until: 1. The Beads workflow is updated 2. The turn document is created in `docs/turns` @@ -120,3 +204,40 @@ For repository implementation tasks that require turn documentation, the task is 5. `bd dolt push` succeeds 6. `git push forgejo ` succeeds 7. `git status` shows the branch is up to date with `forgejo/` + +For tasks that do require turn documentation, the document may be brief when scope is small, but it must clearly explain what changed and how it was validated. + +## Plan Mode Documentation + +When working in plan mode, do not modify implementation files. + +At the end of plan mode, provide a concise summary of the plan and ask the user whether they want to proceed with implementation. + +If the user asks to save the plan, create a user-readable HTML plan document in: + +```text +docs/plans/ +``` + +Use a clear timestamped filename: + +```text +docs/plans/YYYY-MM-DD-short-plan-name.html +``` + +The plan document should be labeled clearly as a plan and should include: + +1. **Plan Summary** +2. **Goals** +3. **Proposed Changes** +4. **Relevant Context** +5. **Implementation Steps** +6. **Risks, Limitations, and Mitigations** +7. **Open Questions** + +Always do the following when you finish a task, finish the beads workflow and and make a commit: +- Document the changes in a user-readable format +- Use the impeccable skill to structure the document as HTML +- Create a clear, concise summary of the changes at the top, followed by a detailed description of the changes, including any relevant context or background as well as specific code snippets or examples. +- Note any relevant issues or limitations that were addressed or mitigated by the changes. +- The HTML file should be stored in the `docs/turns` directory. It should include the current date and time, as well as a brief explanation of changes. e.g. docs/turns/YYYY-MM-DD-{description}.html diff --git a/apps/web/app/api/admin/synthetic/control/route.ts b/apps/web/app/api/admin/synthetic/control/route.ts index 578df3a..09f5629 100644 --- a/apps/web/app/api/admin/synthetic/control/route.ts +++ b/apps/web/app/api/admin/synthetic/control/route.ts @@ -9,8 +9,11 @@ export async function GET(): Promise { } export async function PUT(req: Request): Promise { - return proxySyntheticAdminRequest("/admin/synthetic/control", { - method: "PUT", - body: await req.text() - }); + return proxySyntheticAdminRequest( + "/admin/synthetic/control", + { + method: "PUT", + body: await req.text() + } + ); } diff --git a/apps/web/app/api/admin/synthetic/routes.test.ts b/apps/web/app/api/admin/synthetic/routes.test.ts index ee50525..eec575d 100644 --- a/apps/web/app/api/admin/synthetic/routes.test.ts +++ b/apps/web/app/api/admin/synthetic/routes.test.ts @@ -1,5 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; -import { getSyntheticAdminProxyConfig, isSyntheticAdminFeatureEnabled } from "./shared"; +import { + getSyntheticAdminProxyConfig, + isSyntheticAdminFeatureEnabled +} from "./shared"; const originalFetch = globalThis.fetch; diff --git a/apps/web/app/dashboard-mocks.tsx b/apps/web/app/dashboard-mocks.tsx index 1c23bb1..101141c 100644 --- a/apps/web/app/dashboard-mocks.tsx +++ b/apps/web/app/dashboard-mocks.tsx @@ -18,29 +18,25 @@ const variants: Record< > = { mock1: { title: "Command Deck", - premise: - "Closest to the reference: left navigation, ticker ribbon, dense evidence panes, replay rail.", + premise: "Closest to the reference: left navigation, ticker ribbon, dense evidence panes, replay rail.", mode: "Dense ops", layout: "classic" }, mock2: { title: "Investigation Stack", - premise: - "A calmer analyst layout with the selected symbol story in the center and context wrapped around it.", + premise: "A calmer analyst layout with the selected symbol story in the center and context wrapped around it.", mode: "Forensic", layout: "focus" }, mock3: { title: "Signal Wall", - premise: - "Prioritizes alert triage and cross-symbol scanning before a user drills into price action.", + premise: "Prioritizes alert triage and cross-symbol scanning before a user drills into price action.", mode: "Triage", layout: "signals" }, mock4: { title: "Replay Lab", - premise: - "A replay-first structure with timeline, event tape, and causality context always visible.", + premise: "A replay-first structure with timeline, event tape, and causality context always visible.", mode: "Replay", layout: "replay" } @@ -97,10 +93,7 @@ export function DashboardMock({ variant }: DashboardMockProps) { const config = variants[variant]; return ( -
+
{variant === "mock1" ? : null} @@ -284,11 +277,7 @@ function OptionTape({ condensed = false }: { condensed?: boolean }) { function ChartPanel({ compact = false }: { compact?: boolean }) { return ( - +
194.88 +2.34 (+1.22%) @@ -317,24 +306,16 @@ function ChartPanel({ compact = false }: { compact?: boolean }) { function SignalPanel({ hero = false }: { hero?: boolean }) { return ( - +
{signals.map(([time, title, symbol, value, tag]) => (
{title} - - {symbol} / {value} - + {symbol} / {value}
- + {tag}
@@ -351,9 +332,7 @@ function FeedHealth() { {feedHealth.map(([feed, status, lag, rate]) => (
{feed} - - {status} - + {status} {lag} {rate}/s
@@ -371,9 +350,7 @@ function DarkFlow() {
{time} {symbol} - - {side} - + {side} {size} {notional} {type} @@ -425,11 +402,7 @@ function EventContext() { function ReplayRail({ compact = false }: { compact?: boolean }) { return ( - +
@@ -457,9 +430,8 @@ function SymbolBrief() { +1.22%

- Dark sweep pressure aligns with short-window momentum and a fresh news catalyst. Context - confidence is high, but the largest block remains off-exchange and should be checked against - next print behavior. + Dark sweep pressure aligns with short-window momentum and a fresh news catalyst. Context confidence is high, but + the largest block remains off-exchange and should be checked against next print behavior.

Bullish @@ -472,12 +444,7 @@ function SymbolBrief() { function Sparkline({ direction }: { direction: string }) { return ( - + span { @@ -1817,39 +1761,17 @@ h3 { font-variant-numeric: tabular-nums; } -.classifier-green { - --classifier-rgb: 37, 193, 122; -} -.classifier-red { - --classifier-rgb: 255, 107, 95; -} -.classifier-amber { - --classifier-rgb: 245, 166, 35; -} -.classifier-copper { - --classifier-rgb: 198, 122, 75; -} -.classifier-blue { - --classifier-rgb: 77, 163, 255; -} -.classifier-teal { - --classifier-rgb: 64, 210, 190; -} -.classifier-yellowgreen { - --classifier-rgb: 174, 210, 78; -} -.classifier-violet { - --classifier-rgb: 170, 130, 255; -} -.classifier-cyan { - --classifier-rgb: 94, 214, 255; -} -.classifier-magenta { - --classifier-rgb: 255, 92, 205; -} -.classifier-neutral { - --classifier-rgb: 192, 200, 210; -} +.classifier-green { --classifier-rgb: 37, 193, 122; } +.classifier-red { --classifier-rgb: 255, 107, 95; } +.classifier-amber { --classifier-rgb: 245, 166, 35; } +.classifier-copper { --classifier-rgb: 198, 122, 75; } +.classifier-blue { --classifier-rgb: 77, 163, 255; } +.classifier-teal { --classifier-rgb: 64, 210, 190; } +.classifier-yellowgreen { --classifier-rgb: 174, 210, 78; } +.classifier-violet { --classifier-rgb: 170, 130, 255; } +.classifier-cyan { --classifier-rgb: 94, 214, 255; } +.classifier-magenta { --classifier-rgb: 255, 92, 205; } +.classifier-neutral { --classifier-rgb: 192, 200, 210; } .contract, .drawer-row-title { @@ -1999,9 +1921,7 @@ h3 { opacity: 0; pointer-events: none; transform: translateY(8px); - transition: - opacity 0.15s ease, - transform 0.15s ease; + transition: opacity 0.15s ease, transform 0.15s ease; z-index: 5; } @@ -2127,10 +2047,7 @@ h3 { color: var(--text-dim); box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28); z-index: 45; - transition: - border-color 0.16s ease, - background-color 0.16s ease, - color 0.16s ease; + transition: border-color 0.16s ease, background-color 0.16s ease, color 0.16s ease; } .synthetic-control-gear:hover, @@ -2296,9 +2213,7 @@ h3 { background: oklch(0.18 0.012 250 / 0.6); color: var(--text); text-align: left; - transition: - border-color 150ms ease, - background 150ms ease; + transition: border-color 150ms ease, background 150ms ease; } .news-row:hover { @@ -2605,11 +2520,7 @@ h3 { @media (max-width: 720px) { .terminal-shell { - background-size: - 24px 24px, - 24px 24px, - 100% 100%, - auto; + background-size: 24px 24px, 24px 24px, 100% 100%, auto; } .terminal-nav-drawer { @@ -2966,7 +2877,9 @@ h3 { width: 34px; height: 34px; border-radius: 9px; - background: linear-gradient(135deg, oklch(0.68 0.14 246), oklch(0.68 0.12 164)), var(--blue-soft); + background: + linear-gradient(135deg, oklch(0.68 0.14 246), oklch(0.68 0.12 164)), + var(--blue-soft); box-shadow: inset 0 0 0 1px oklch(0.94 0.02 240 / 0.24); } diff --git a/apps/web/app/routes.test.ts b/apps/web/app/routes.test.ts index 5206d51..e217748 100644 --- a/apps/web/app/routes.test.ts +++ b/apps/web/app/routes.test.ts @@ -4,8 +4,7 @@ const redirect = mock((path: string) => { throw new Error(`NEXT_REDIRECT:${path}`); }); -mock.module("next/navigation", () => ({ default: { redirect }, redirect })); -mock.module("next/navigation.js", () => ({ default: { redirect }, redirect })); +mock.module("next/navigation", () => ({ redirect })); describe("legacy page redirects", () => { beforeEach(() => { diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index d396602..e6ed106 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -1,36 +1,6 @@ -import { describe, expect, it, mock } from "bun:test"; +import { describe, expect, it } from "bun:test"; import { getSubscriptionKey as getLiveSubscriptionKey } from "@islandflow/types"; - -const redirect = mock((path: string) => { - throw new Error(`NEXT_REDIRECT:${path}`); -}); - -const nextNavigationMock = { - default: { - redirect, - usePathname: () => "/options" - }, - redirect, - usePathname: () => "/options" -}; - -const nextNavigationResolved = import.meta.resolve("next/navigation"); -const nextNavigationJsResolved = import.meta.resolve("next/navigation.js"); - -mock.module("next/navigation", () => ({ - ...nextNavigationMock -})); -mock.module("next/navigation.js", () => ({ - ...nextNavigationMock -})); -mock.module(nextNavigationResolved, () => ({ - ...nextNavigationMock -})); -mock.module(nextNavigationJsResolved, () => ({ - ...nextNavigationMock -})); - -const { +import { NAV_ITEMS, appendHistoryTail, buildAlertContextPath, @@ -79,7 +49,7 @@ const { resolveAlertFlowPacket, statusLabel, toggleFilterValue -} = await import("./terminal"); +} from "./terminal"; const makeItem = (traceId: string, seq: number, ts: number) => ({ trace_id: traceId, @@ -311,16 +281,12 @@ describe("live manifest", () => { }); it("includes news subscriptions on home and /news", () => { - expect( - getLiveManifest("/", "SPY", 60000, buildDefaultFlowFilters()).map( - (subscription) => subscription.channel - ) - ).toContain("news"); - expect( - getLiveManifest("/news", "SPY", 60000, buildDefaultFlowFilters()).map( - (subscription) => subscription.channel - ) - ).toEqual(["news"]); + expect(getLiveManifest("/", "SPY", 60000, buildDefaultFlowFilters()).map((subscription) => subscription.channel)).toContain( + "news" + ); + expect(getLiveManifest("/news", "SPY", 60000, buildDefaultFlowFilters()).map((subscription) => subscription.channel)).toEqual([ + "news" + ]); }); it("scopes /charts subscriptions to chart channels only", () => { @@ -524,36 +490,12 @@ describe("route feature map", () => { describe("fixed tape virtualization config", () => { it("uses expected fixed row heights and overscan by table", () => { - expect(getTapeVirtualConfig("options")).toEqual({ - rowHeight: 36, - overscan: 44, - debugLabel: "options" - }); - expect(getTapeVirtualConfig("equities")).toEqual({ - rowHeight: 36, - overscan: 36, - debugLabel: "equities" - }); - expect(getTapeVirtualConfig("flow")).toEqual({ - rowHeight: 44, - overscan: 24, - debugLabel: "flow" - }); - expect(getTapeVirtualConfig("alerts")).toEqual({ - rowHeight: 44, - overscan: 24, - debugLabel: "alerts" - }); - expect(getTapeVirtualConfig("classifier")).toEqual({ - rowHeight: 44, - overscan: 24, - debugLabel: "classifier" - }); - expect(getTapeVirtualConfig("dark")).toEqual({ - rowHeight: 44, - overscan: 24, - debugLabel: "dark" - }); + expect(getTapeVirtualConfig("options")).toEqual({ rowHeight: 36, overscan: 44, debugLabel: "options" }); + expect(getTapeVirtualConfig("equities")).toEqual({ rowHeight: 36, overscan: 36, debugLabel: "equities" }); + expect(getTapeVirtualConfig("flow")).toEqual({ rowHeight: 44, overscan: 24, debugLabel: "flow" }); + expect(getTapeVirtualConfig("alerts")).toEqual({ rowHeight: 44, overscan: 24, debugLabel: "alerts" }); + expect(getTapeVirtualConfig("classifier")).toEqual({ rowHeight: 44, overscan: 24, debugLabel: "classifier" }); + expect(getTapeVirtualConfig("dark")).toEqual({ rowHeight: 44, overscan: 24, debugLabel: "dark" }); }); }); @@ -740,11 +682,7 @@ describe("live tape history helpers", () => { }); it("promotes hot-window overflow into the history tail", () => { - const currentHot = [ - makeItem("hot-3", 3, 300), - makeItem("hot-2", 2, 200), - makeItem("hot-1", 1, 100) - ]; + const currentHot = [makeItem("hot-3", 3, 300), makeItem("hot-2", 2, 200), makeItem("hot-1", 1, 100)]; const incoming = [makeItem("hot-4", 4, 400)]; const { kept, evicted } = mergeNewestWithOverflow(incoming, currentHot, 3); @@ -759,11 +697,7 @@ describe("live tape history helpers", () => { let history: Array> = []; for (let seq = 1; seq <= 5; seq += 1) { - const { kept, evicted } = mergeNewestWithOverflow( - [makeItem(`row-${seq}`, seq, seq * 100)], - hot, - 2 - ); + const { kept, evicted } = mergeNewestWithOverflow([makeItem(`row-${seq}`, seq, seq * 100)], hot, 2); hot = kept; history = appendHistoryTail(history, evicted, hot, 5000); } @@ -798,24 +732,13 @@ describe("live tape history helpers", () => { }); it("dedupes the seam between promoted overflow and fetched history", () => { - const currentHot = [ - makeItem("hot-3", 3, 300), - makeItem("hot-2", 2, 200), - makeItem("hot-1", 1, 100) - ]; + const currentHot = [makeItem("hot-3", 3, 300), makeItem("hot-2", 2, 200), makeItem("hot-1", 1, 100)]; const { kept, evicted } = mergeNewestWithOverflow([makeItem("hot-4", 4, 400)], currentHot, 3); const promoted = appendHistoryTail([], evicted, kept, 5000); - const merged = appendHistoryTail( - promoted, - [makeItem("hot-1", 1, 100), makeItem("older", 0, 50)], - kept, - 5000 - ); + const merged = appendHistoryTail(promoted, [makeItem("hot-1", 1, 100), makeItem("older", 0, 50)], kept, 5000); expect(merged.map((item) => item.trace_id)).toEqual(["hot-1", "older"]); - expect(new Set([...kept, ...merged].map((item) => item.trace_id)).size).toBe( - kept.length + merged.length - ); + expect(new Set([...kept, ...merged].map((item) => item.trace_id)).size).toBe(kept.length + merged.length); }); it("trims the history tail to the soft cap", () => { @@ -868,9 +791,10 @@ describe("live tape history helpers", () => { makeItem("hist-2", 2, 200) ]; - expect( - mergeHeldTapeHistory(displayed, incoming, frozenLive).map((item) => item.trace_id) - ).toEqual(["hist-3", "hist-2"]); + expect(mergeHeldTapeHistory(displayed, incoming, frozenLive).map((item) => item.trace_id)).toEqual([ + "hist-3", + "hist-2" + ]); }); it("appends truly older lazy-loaded rows to the held history tail", () => { @@ -883,9 +807,12 @@ describe("live tape history helpers", () => { makeItem("older-0", 0, 50) ]; - expect( - mergeHeldTapeHistory(displayed, incoming, frozenLive).map((item) => item.trace_id) - ).toEqual(["hist-3", "hist-2", "older-1", "older-0"]); + expect(mergeHeldTapeHistory(displayed, incoming, frozenLive).map((item) => item.trace_id)).toEqual([ + "hist-3", + "hist-2", + "older-1", + "older-0" + ]); }); it("resyncs buffered live history by replacing the held segment after resume", () => { @@ -898,12 +825,7 @@ describe("live tape history helpers", () => { const resynced = appendHistoryTail([], [makeItem("overflow-newer", 6, 600), ...held], [], 0); expect(held.map((item) => item.trace_id)).toEqual(["hist-3", "hist-2", "older-1"]); - expect(resynced.map((item) => item.trace_id)).toEqual([ - "overflow-newer", - "hist-3", - "hist-2", - "older-1" - ]); + expect(resynced.map((item) => item.trace_id)).toEqual(["overflow-newer", "hist-3", "hist-2", "older-1"]); }); }); @@ -983,21 +905,9 @@ describe("classifier row decoration helpers", () => { it("selects primary hits by confidence, source timestamp, then seq", () => { const hit = selectPrimaryClassifierHit([ - { - ...makeAlert({ classifier_id: "old", confidence: 0.9, source_ts: 1_000, seq: 1 }), - direction: "bullish", - explanations: [] - }, - { - ...makeAlert({ classifier_id: "new", confidence: 0.9, source_ts: 2_000, seq: 1 }), - direction: "bullish", - explanations: [] - }, - { - ...makeAlert({ classifier_id: "low", confidence: 0.5, source_ts: 3_000, seq: 9 }), - direction: "bullish", - explanations: [] - } + { ...makeAlert({ classifier_id: "old", confidence: 0.9, source_ts: 1_000, seq: 1 }), direction: "bullish", explanations: [] }, + { ...makeAlert({ classifier_id: "new", confidence: 0.9, source_ts: 2_000, seq: 1 }), direction: "bullish", explanations: [] }, + { ...makeAlert({ classifier_id: "low", confidence: 0.5, source_ts: 3_000, seq: 9 }), direction: "bullish", explanations: [] } ]); expect(hit?.classifier_id).toBe("new"); @@ -1070,9 +980,9 @@ describe("signals helpers", () => { ) ).toBe("bearish"); - expect( - deriveAlertDirection(makeAlert({ hits: [{ direction: "weird", confidence: 0.4 }] })) - ).toBe("neutral"); + expect(deriveAlertDirection(makeAlert({ hits: [{ direction: "weird", confidence: 0.4 }] }))).toBe( + "neutral" + ); expect(deriveAlertDirection(makeAlert({ hits: [] }))).toBe("neutral"); }); diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index d7afe6e..5375688 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import * as nextNavigation from "next/navigation"; +import { usePathname } from "next/navigation"; import { createContext, memo, @@ -54,12 +54,7 @@ import { matchesFlowPacketFilters, matchesOptionPrintFilters } from "@islandflow/types"; -import { - createChart, - type IChartApi, - type SeriesMarker, - type UTCTimestamp -} from "lightweight-charts"; +import { createChart, type IChartApi, type SeriesMarker, type UTCTimestamp } from "lightweight-charts"; const parseBoundedInt = ( value: string | undefined, @@ -661,9 +656,8 @@ const frontendTapeDebugMetrics: Record = { const bumpTapeDebugMetric = (key: TapeDebugMetricKey, count = 1): void => { frontendTapeDebugMetrics[key] += count; if (DEV_TAPE_DEBUG && typeof window !== "undefined") { - ( - window as typeof window & { __IF_TAPE_DEBUG__?: Record } - ).__IF_TAPE_DEBUG__ = frontendTapeDebugMetrics; + (window as typeof window & { __IF_TAPE_DEBUG__?: Record }).__IF_TAPE_DEBUG__ = + frontendTapeDebugMetrics; } }; @@ -1053,8 +1047,9 @@ const buildApiUrl = (path: string): string => { return `${httpProtocol}://${host}${path}`; }; -export const isSyntheticAdminVisible = (value = process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN): boolean => - value === "1"; +export const isSyntheticAdminVisible = ( + value = process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN +): boolean => value === "1"; type SyntheticAdminStatusResponse = { enabled: boolean; @@ -1087,7 +1082,10 @@ const SYNTHETIC_PROFILE_ORDER: Array = { +const SYNTHETIC_PROFILE_LABELS: Record< + keyof SyntheticControlState["profile_weights"], + string +> = { institutional_directional: "Institutional Directional", retail_whale: "Retail Whale", event_driven: "Event Driven", @@ -1268,17 +1266,10 @@ export const formatNewsTimestamp = (ts: number, now = Date.now()): string => { const date = new Date(ts); return isSameLocalDay(ts, now) ? date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) - : date.toLocaleString([], { - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit" - }); + : date.toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); }; -const sanitizeNewsHtml = ( - value: string -): { html: string; fallbackText: string; sanitized: boolean } => { +const sanitizeNewsHtml = (value: string): { html: string; fallbackText: string; sanitized: boolean } => { const fallbackText = value .replace(//gi, " ") .replace(//gi, " ") @@ -1292,10 +1283,7 @@ const sanitizeNewsHtml = ( .replace(//gi, "") .replace(/\son\w+=(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "") .replace(/\shref=(["'])javascript:[\s\S]*?\1/gi, ' href="#"') - .replace( - /<(?!\/?(p|div|section|article|span|strong|em|b|i|ul|ol|li|br|a|h1|h2|h3|h4|blockquote)\b)[^>]*>/gi, - "" - ); + .replace(/<(?!\/?(p|div|section|article|span|strong|em|b|i|ul|ol|li|br|a|h1|h2|h3|h4|blockquote)\b)[^>]*>/gi, ""); return { html: sanitized, fallbackText, sanitized: true }; } catch { return { html: "", fallbackText, sanitized: false }; @@ -1362,11 +1350,9 @@ export const deriveAlertDirection = (alert: AlertEvent): "bullish" | "bearish" | totals[direction].confidence += Number.isFinite(hit.confidence) ? hit.confidence : 0; } - const ranked = ( - Object.entries(totals) as Array< - ["bullish" | "bearish" | "neutral", { count: number; confidence: number }] - > - ).sort((a, b) => { + const ranked = (Object.entries(totals) as Array< + ["bullish" | "bearish" | "neutral", { count: number; confidence: number }] + >).sort((a, b) => { if (b[1].count !== a[1].count) { return b[1].count - a[1].count; } @@ -1380,10 +1366,7 @@ export const getAlertWindowAnchorTs = (alerts: AlertEvent[], fallbackNow = Date. if (alerts.length === 0) { return fallbackNow; } - return alerts.reduce( - (max, alert) => Math.max(max, alert.source_ts), - alerts[0]?.source_ts ?? fallbackNow - ); + return alerts.reduce((max, alert) => Math.max(max, alert.source_ts), alerts[0]?.source_ts ?? fallbackNow); }; const extractUnderlying = (contractId: string): string => { @@ -1527,13 +1510,14 @@ export const buildDefaultFlowFilters = (): OptionFlowFilters => ({ nbboSides: DEFAULT_FLOW_SIDES, optionTypes: DEFAULT_FLOW_OPTION_TYPES, minNotional: - FLOW_FILTER_PRESET === "all" ? undefined : FLOW_FILTER_PRESET === "balanced" ? 5_000 : undefined + FLOW_FILTER_PRESET === "all" + ? undefined + : FLOW_FILTER_PRESET === "balanced" + ? 5_000 + : undefined }); -const sameFilterValues = ( - left: T[] | undefined, - right: T[] | undefined -): boolean => { +const sameFilterValues = (left: T[] | undefined, right: T[] | undefined): boolean => { const leftValues = [...(left ?? [])].sort(); const rightValues = [...(right ?? [])].sort(); if (leftValues.length !== rightValues.length) { @@ -1732,7 +1716,7 @@ export const classifierToneForFamily = (classifierId: string): string => CLASSIFIER_FAMILY_TONES[classifierId] ?? "neutral"; export const smartMoneyToneForProfile = (profileId: SmartMoneyProfileId | null): string => - profileId ? (SMART_MONEY_PROFILE_TONES[profileId] ?? "neutral") : "neutral"; + profileId ? SMART_MONEY_PROFILE_TONES[profileId] ?? "neutral" : "neutral"; export const smartMoneyProfileLabel = (profileId: SmartMoneyProfileId | null): string => profileId ? humanizeClassifierId(profileId) : "Abstained"; @@ -1771,10 +1755,7 @@ export const getOptionTableSnapshot = ( ): { spot: string; iv: string; side: string; details: string; value: string } => { const side = print.execution_nbbo_side ?? print.nbbo_side ?? fallbackSide ?? "--"; return { - spot: - typeof print.execution_underlying_spot === "number" - ? formatPrice(print.execution_underlying_spot) - : "--", + spot: typeof print.execution_underlying_spot === "number" ? formatPrice(print.execution_underlying_spot) : "--", iv: typeof print.execution_iv === "number" ? formatPct(print.execution_iv) : "--", side, details: `${formatSize(print.size)}@${formatPrice(print.price)}_${side}`, @@ -1898,9 +1879,7 @@ const useScrollAnchor = ( } | null>(null); const readRenderedRows = useCallback((element: HTMLDivElement) => { - return Array.from( - element.querySelectorAll("[data-tape-key][data-row-start][data-row-size]") - ) + return Array.from(element.querySelectorAll("[data-tape-key][data-row-start][data-row-size]")) .map((node) => { const key = node.dataset.tapeKey; const start = Number(node.dataset.rowStart); @@ -2185,7 +2164,9 @@ type TapeConfig = { hotWindowLimit?: number; }; -const useTape = (config: TapeConfig): TapeState => { +const useTape = ( + config: TapeConfig +): TapeState => { const { mode, wsPath, replayPath, expectedType, latestPath, onNewItems, captureScroll } = config; const batchSize = config.batchSize ?? 40; const pollMs = config.pollMs ?? 1000; @@ -2731,16 +2712,20 @@ const usePausableTapeView = ( }; }; -const useLiveStream = (config: { - enabled: boolean; - wsPath: string; - expectedType: MessageType; - onNewItems?: (count: number) => void; - captureScroll?: () => void; - shouldHold?: () => boolean; - resumeSignal?: number; -}): TapeState => { - const [status, setStatus] = useState(config.enabled ? "connecting" : "disconnected"); +const useLiveStream = ( + config: { + enabled: boolean; + wsPath: string; + expectedType: MessageType; + onNewItems?: (count: number) => void; + captureScroll?: () => void; + shouldHold?: () => boolean; + resumeSignal?: number; + } +): TapeState => { + const [status, setStatus] = useState( + config.enabled ? "connecting" : "disconnected" + ); const [items, setItems] = useState([]); const [lastUpdate, setLastUpdate] = useState(null); const [replayTime] = useState(null); @@ -2799,7 +2784,8 @@ const useLiveStream = (config: { return; } - const nextBatch = holdRef.current.length > 0 ? [...holdRef.current, ...buffered] : buffered; + const nextBatch = + holdRef.current.length > 0 ? [...holdRef.current, ...buffered] : buffered; holdRef.current = []; setItems((prev) => @@ -3016,10 +3002,7 @@ const LIVE_HISTORY_ENDPOINTS: Partial { +const appendOptionFlowFilters = (params: URLSearchParams, filters: OptionFlowFilters | undefined): void => { if (!filters) { return; } @@ -3136,10 +3119,7 @@ export const shouldClearOptionFocusSeed = ( }; const appendLiveScopeParams = (params: URLSearchParams, subscription: LiveSubscription): void => { - if ( - (subscription.channel === "options" || subscription.channel === "equities") && - subscription.underlying_ids?.length - ) { + if ((subscription.channel === "options" || subscription.channel === "equities") && subscription.underlying_ids?.length) { params.set("underlying_ids", subscription.underlying_ids.join(",")); } if (subscription.channel === "options" && subscription.option_contract_id) { @@ -3177,7 +3157,7 @@ export const getLiveManifest = ( filters: optionScope?.option_contract_id && optionPrintFilters === undefined ? undefined - : (optionPrintFilters ?? flowFilters), + : optionPrintFilters ?? flowFilters, ...optionScope, snapshot_limit: LIVE_OPTIONS_HEAD_LIMIT }); @@ -3432,8 +3412,7 @@ const useLiveSession = ( return; } - const subscription = - message.op === "snapshot" ? message.snapshot.subscription : message.subscription; + const subscription = message.op === "snapshot" ? message.snapshot.subscription : message.subscription; const items = message.op === "snapshot" ? message.snapshot.items : [message.item]; const subscriptionKey = getLiveSubscriptionKey(subscription); const updateAt = Date.now(); @@ -3541,16 +3520,10 @@ const useLiveSession = ( }); break; case "inferred-dark": - mergeItems( - setInferredDark, - inferredDarkRef, - items as InferredDarkEvent[], - LIVE_HOT_WINDOW, - { - setter: setInferredDarkHistory, - ref: inferredDarkHistoryRef - } - ); + mergeItems(setInferredDark, inferredDarkRef, items as InferredDarkEvent[], LIVE_HOT_WINDOW, { + setter: setInferredDarkHistory, + ref: inferredDarkHistoryRef + }); break; case "equity-candles": mergeItems(setChartCandles, chartCandlesRef, items as EquityCandle[]); @@ -3922,9 +3895,7 @@ const TapeStatus = ({ const pausedLabel = paused && dropped > 0 ? `+${dropped} queued` : ""; return ( -
+
{label} {mode === "replay" ? ( @@ -3932,9 +3903,7 @@ const TapeStatus = ({ Replay time {replayTime ? formatTime(replayTime) : "—"} ) : null} - + {pausedLabel || "+000 queued"}
@@ -3950,14 +3919,7 @@ type TapeControlsProps = { onJump: () => void; }; -const TapeControls = ({ - mode, - paused, - onTogglePause, - isAtTop, - missed, - onJump -}: TapeControlsProps) => { +const TapeControls = ({ mode, paused, onTogglePause, isAtTop, missed, onJump }: TapeControlsProps) => { const active = !isAtTop && missed > 0; return (
@@ -3969,10 +3931,7 @@ const TapeControls = ({ - + +{missed} new
@@ -4161,7 +4120,11 @@ const CandleChart = ({ ? "#c46f2a" : "rgba(111, 91, 57, 0.9)", shape: - direction === "bullish" ? "arrowUp" : direction === "bearish" ? "arrowDown" : "circle", + direction === "bullish" + ? "arrowUp" + : direction === "bearish" + ? "arrowDown" + : "circle", text: event.abstained ? "ABS" : event.primary_profile_id @@ -4418,7 +4381,9 @@ const CandleChart = ({ const response = await fetch(url.toString()); if (!response.ok) { const detail = await readErrorDetail(response); - throw new Error(`Candle fetch failed (${response.status})${detail ? `: ${detail}` : ""}`); + throw new Error( + `Candle fetch failed (${response.status})${detail ? `: ${detail}` : ""}` + ); } const payload = (await response.json()) as { data?: EquityCandle[] }; if (!active || !seriesRef.current) { @@ -4451,6 +4416,7 @@ const CandleChart = ({ } }; + const ensureOverlayListener = () => { if (!chartRef.current) { return; @@ -4597,7 +4563,7 @@ const CandleChart = ({ return; } - const sortedCandles = [...liveCandles].sort((a, b) => a.ts - b.ts || a.seq - b.seq); + const sortedCandles = [...liveCandles].sort((a, b) => (a.ts - b.ts) || (a.seq - b.seq)); if (sortedCandles.length > 0) { seriesRef.current.setData(sortedCandles.map(toChartCandle)); const last = sortedCandles.at(-1); @@ -4802,7 +4768,9 @@ export const collectAlertContextEvidence = ( return { packets, prints }; }; -export const getAlertFlowPacketRefs = (alert: Pick): string[] => { +export const getAlertFlowPacketRefs = ( + alert: Pick +): string[] => { return alert.evidence_refs.filter((ref) => ref.startsWith("flowpacket:")); }; @@ -4871,10 +4839,7 @@ const AlertDrawer = ({ alert, flowPacket, evidence, contextStatus, onClose }: Al {isContextLoading ? Loading context : null}
{isContextLoading ? ( -
+
@@ -4915,12 +4880,7 @@ const AlertDrawer = ({ alert, flowPacket, evidence, contextStatus, onClose }: Al {String(flowPacket.features.option_contract_id ?? flowPacket.id ?? "Flow packet")}
- - {formatFlowMetric( - parseNumber(flowPacket.features.count, flowPacket.members.length) - )}{" "} - prints - + {formatFlowMetric(parseNumber(flowPacket.features.count, flowPacket.members.length))} prints {formatFlowMetric(parseNumber(flowPacket.features.total_size, 0))} size Notional $ @@ -4946,9 +4906,7 @@ const AlertDrawer = ({ alert, flowPacket, evidence, contextStatus, onClose }: Al

Evidence prints

{evidencePrints.length === 0 ? ( -

- Persisted evidence prints are not available for this alert. -

+

Persisted evidence prints are not available for this alert.

) : (
{evidencePrints.slice(0, 6).map((item) => ( @@ -4958,9 +4916,7 @@ const AlertDrawer = ({ alert, flowPacket, evidence, contextStatus, onClose }: Al ${formatPrice(item.print.price)} {formatSize(item.print.size)}x {item.print.exchange} - {item.print.execution_nbbo_side ? ( - Side {item.print.execution_nbbo_side} - ) : null} + {item.print.execution_nbbo_side ? Side {item.print.execution_nbbo_side} : null} {formatOptionalMs(item.print.execution_nbbo_age_ms) ? ( Quote {formatOptionalMs(item.print.execution_nbbo_age_ms)} ) : null} @@ -4997,9 +4953,7 @@ const AlertDrawer = ({ alert, flowPacket, evidence, contextStatus, onClose }: Al
)} {unknownCount > 0 ? ( -

- +{unknownCount} evidence refs unresolved in persisted context. -

+

+{unknownCount} evidence refs unresolved in persisted context.

) : null} {missingRefs.length > 0 ? (

Missing refs: {missingRefs.slice(0, 4).join(", ")}

@@ -5025,9 +4979,7 @@ const NewsDrawer = ({ story, onClose }: NewsDrawerProps) => {

{story.headline}

{story.source} · Published {formatDateTime(story.published_ts)} - {story.updated_ts !== story.published_ts - ? ` · Updated ${formatDateTime(story.updated_ts)}` - : ""} + {story.updated_ts !== story.published_ts ? ` · Updated ${formatDateTime(story.updated_ts)}` : ""}

@@ -5436,26 +5377,20 @@ export const parseTickerFilterInput = (value: string): string[] => { }; const useTerminalState = () => { - const pathname = nextNavigation.usePathname(); + const pathname = usePathname(); const routeFeatures = useMemo(() => getRouteFeatures(pathname), [pathname]); const [mode, setMode] = useState("live"); const [replaySource, setReplaySource] = useState(null); const [selectedAlert, setSelectedAlert] = useState(null); const [selectedNewsStory, setSelectedNewsStory] = useState(null); const [selectedDarkEvent, setSelectedDarkEvent] = useState(null); - const [selectedClassifierHit, setSelectedClassifierHit] = useState( - null - ); - const [selectedSmartMoneyEvent, setSelectedSmartMoneyEvent] = useState( - null - ); + const [selectedClassifierHit, setSelectedClassifierHit] = useState(null); + const [selectedSmartMoneyEvent, setSelectedSmartMoneyEvent] = useState(null); const [selectedInstrument, setSelectedInstrument] = useState(null); const [optionFocusSeed, setOptionFocusSeed] = useState | null>(null); const [equityFocusSeed, setEquityFocusSeed] = useState | null>(null); const [filterInput, setFilterInput] = useState(""); - const [flowFilters, setFlowFilters] = useState(() => - buildDefaultFlowFilters() - ); + const [flowFilters, setFlowFilters] = useState(() => buildDefaultFlowFilters()); const [chartIntervalMs, setChartIntervalMs] = useState(CANDLE_INTERVALS[0].ms); const activeTickers = useMemo(() => parseTickerFilterInput(filterInput), [filterInput]); const tickerSet = useMemo(() => new Set(activeTickers), [activeTickers]); @@ -5463,9 +5398,8 @@ const useTerminalState = () => { const isOptionContractFocused = selectedInstrument?.kind === "option-contract"; const focusedOptionContractId = selectedInstrument?.kind === "option-contract" ? selectedInstrument.contractId : null; - const optionFocusScopeKey = focusedOptionContractId - ? `option-contract:${focusedOptionContractId}` - : null; + const optionFocusScopeKey = + focusedOptionContractId ? `option-contract:${focusedOptionContractId}` : null; const equityFocusScopeKey = selectedInstrument?.kind === "equity" ? `equity:${selectedInstrument.underlyingId.toUpperCase()}` @@ -5480,12 +5414,7 @@ const useTerminalState = () => { ); const equityScope = useMemo( () => ({ - underlying_ids: - activeTickers.length > 0 - ? activeTickers - : instrumentUnderlying - ? [instrumentUnderlying] - : undefined + underlying_ids: activeTickers.length > 0 ? activeTickers : instrumentUnderlying ? [instrumentUnderlying] : undefined }), [activeTickers, instrumentUnderlying] ); @@ -5550,13 +5479,7 @@ const useTerminalState = () => { }, [mode]); useEffect(() => { - if ( - !selectedAlert && - !selectedNewsStory && - !selectedClassifierHit && - !selectedDarkEvent && - !selectedSmartMoneyEvent - ) { + if (!selectedAlert && !selectedNewsStory && !selectedClassifierHit && !selectedDarkEvent && !selectedSmartMoneyEvent) { return; } @@ -5588,13 +5511,7 @@ const useTerminalState = () => { document.removeEventListener("mousedown", handlePointerDown); document.removeEventListener("keydown", handleKeyDown); }; - }, [ - selectedAlert, - selectedNewsStory, - selectedClassifierHit, - selectedDarkEvent, - selectedSmartMoneyEvent - ]); + }, [selectedAlert, selectedNewsStory, selectedClassifierHit, selectedDarkEvent, selectedSmartMoneyEvent]); const optionsScroll = useListScroll(); const equitiesScroll = useListScroll(); @@ -5608,7 +5525,10 @@ const useTerminalState = () => { const flowAnchor = useScrollAnchor(flowScroll.listRef, flowScroll.isAtTopRef); const darkAnchor = useScrollAnchor(darkScroll.listRef, darkScroll.isAtTopRef); const alertsAnchor = useScrollAnchor(alertsScroll.listRef, alertsScroll.isAtTopRef); - const classifierAnchor = useScrollAnchor(classifierScroll.listRef, classifierScroll.isAtTopRef); + const classifierAnchor = useScrollAnchor( + classifierScroll.listRef, + classifierScroll.isAtTopRef + ); const disableReplayGrouping = useCallback(() => null, []); const optionQueryParams = useMemo>( () => buildOptionTapeQueryParams(effectiveOptionPrintFilters, optionScope), @@ -5744,18 +5664,12 @@ const useTerminalState = () => { getReplayKey: disableReplayGrouping }); - const optionsChannelStatus = getHotChannelFeedStatus( - liveSession.status, - liveSession.channelHealth.options - ); + const optionsChannelStatus = getHotChannelFeedStatus(liveSession.status, liveSession.channelHealth.options); const equitiesChannelStatus = getHotChannelFeedStatus( liveSession.status, liveSession.channelHealth.equities ); - const flowChannelStatus = getHotChannelFeedStatus( - liveSession.status, - liveSession.channelHealth.flow - ); + const flowChannelStatus = getHotChannelFeedStatus(liveSession.status, liveSession.channelHealth.flow); const liveOptions = usePausableTapeView({ enabled: mode === "live", @@ -5811,7 +5725,8 @@ const useTerminalState = () => { [equityFocusScopeKey, equityFocusSeed, liveEquities.historyItems, liveEquities.liveItems] ); - const optionsFeed = mode === "live" ? { ...liveOptions, items: seededLiveOptionsItems } : options; + const optionsFeed = + mode === "live" ? { ...liveOptions, items: seededLiveOptionsItems } : options; const nbboFeed = mode === "live" ? toStaticTapeState( @@ -5953,12 +5868,10 @@ const useTerminalState = () => { error: null }); const [optionSupportSmartMoney, setOptionSupportSmartMoney] = useState([]); - const [optionSupportClassifierHits, setOptionSupportClassifierHits] = useState< - ClassifierHitEvent[] - >([]); - const [historicalNbboByTraceId, setHistoricalNbboByTraceId] = useState< - Map - >(() => new Map()); + const [optionSupportClassifierHits, setOptionSupportClassifierHits] = useState([]); + const [historicalNbboByTraceId, setHistoricalNbboByTraceId] = useState>( + () => new Map() + ); const resolvedOptionPrintMap = useMemo(() => { const merged = new Map(); @@ -6452,16 +6365,11 @@ const useTerminalState = () => { } return { kind: "unknown", id }; }); - }, [ - resolvedFlowPacketMap, - resolvedOptionPrintMap, - selectedClassifierHit, - selectedClassifierPacketId - ]); + }, [resolvedFlowPacketMap, resolvedOptionPrintMap, selectedClassifierHit, selectedClassifierPacketId]); const selectedSmartMoneyFlowPacket = useMemo(() => { const packetId = selectedSmartMoneyEvent?.packet_ids[0]; - return packetId ? (resolvedFlowPacketMap.get(packetId) ?? null) : null; + return packetId ? resolvedFlowPacketMap.get(packetId) ?? null : null; }, [resolvedFlowPacketMap, selectedSmartMoneyEvent]); const selectedSmartMoneyEvidence = useMemo((): EvidenceItem[] => { @@ -6482,16 +6390,12 @@ const useTerminalState = () => { return; } - const missingPacketIds = selectedSmartMoneyEvent.packet_ids.filter( - (id) => !resolvedFlowPacketMap.has(id) - ); + const missingPacketIds = selectedSmartMoneyEvent.packet_ids.filter((id) => !resolvedFlowPacketMap.has(id)); if (missingPacketIds.length > 0) { incrementRetentionMetric("pinnedFetchMisses", missingPacketIds.length); void Promise.all( missingPacketIds.map(async (packetId) => { - const response = await fetch( - buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`) - ); + const response = await fetch(buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`)); if (!response.ok) { throw new Error(await readErrorDetail(response)); } @@ -6516,9 +6420,7 @@ const useTerminalState = () => { }); } - const missingPrintIds = selectedSmartMoneyEvent.member_print_ids.filter( - (id) => !resolvedOptionPrintMap.has(id) - ); + const missingPrintIds = selectedSmartMoneyEvent.member_print_ids.filter((id) => !resolvedOptionPrintMap.has(id)); if (missingPrintIds.length === 0) { return; } @@ -6573,12 +6475,7 @@ const useTerminalState = () => { return null; }, - [ - extractPacketContract, - extractUnderlyingFromTrace, - resolvedFlowPacketMap, - resolvedOptionPrintMap - ] + [extractPacketContract, extractUnderlyingFromTrace, resolvedFlowPacketMap, resolvedOptionPrintMap] ); const matchesTicker = useCallback( @@ -6613,9 +6510,7 @@ const useTerminalState = () => { const filteredEquities = useMemo(() => { if (tickerSet.size === 0) { if (instrumentUnderlying) { - return equitiesFeed.items.filter( - (print) => print.underlying_id.toUpperCase() === instrumentUnderlying - ); + return equitiesFeed.items.filter((print) => print.underlying_id.toUpperCase() === instrumentUnderlying); } return equitiesFeed.items; } @@ -6653,11 +6548,7 @@ const useTerminalState = () => { setEquityFocusSeed(null); return; } - const composedBaseItems = composeTapeItems( - [], - liveEquities.liveItems ?? [], - liveEquities.historyItems ?? [] - ); + const composedBaseItems = composeTapeItems([], liveEquities.liveItems ?? [], liveEquities.historyItems ?? []); const liveKeys = new Set(composedBaseItems.map((item) => getTapeItemKey(item))); if (equityFocusSeed.items.every((item) => liveKeys.has(getTapeItemKey(item)))) { setEquityFocusSeed(null); @@ -6668,11 +6559,7 @@ const useTerminalState = () => { (print: OptionPrint) => { const contractId = normalizeContractId(print.option_contract_id); const parsed = parseOptionContractId(contractId); - const underlyingId = ( - print.underlying_id ?? - parsed?.root ?? - extractUnderlying(contractId) - ).toUpperCase(); + const underlyingId = (print.underlying_id ?? parsed?.root ?? extractUnderlying(contractId)).toUpperCase(); const scopeKey = `option-contract:${contractId}`; const subscriptionKey = getLiveSubscriptionKey({ channel: "options", @@ -6681,9 +6568,7 @@ const useTerminalState = () => { }); const seedItems = composeTapeItems( [print], - filteredOptions.filter( - (candidate) => normalizeContractId(candidate.option_contract_id) === contractId - ), + filteredOptions.filter((candidate) => normalizeContractId(candidate.option_contract_id) === contractId), [] ); setOptionFocusSeed({ scopeKey, subscriptionKey, items: seedItems }); @@ -6708,9 +6593,7 @@ const useTerminalState = () => { const scopeKey = `equity:${underlyingId}`; const seedItems = composeTapeItems( [print], - filteredEquities.filter( - (candidate) => candidate.underlying_id.toUpperCase() === underlyingId - ), + filteredEquities.filter((candidate) => candidate.underlying_id.toUpperCase() === underlyingId), [] ); setEquityFocusSeed({ scopeKey, items: seedItems }); @@ -6824,9 +6707,7 @@ const useTerminalState = () => { if (tickerSet.size === 0) { return newsFeed.items; } - return newsFeed.items.filter((story) => - story.resolved_symbols.some((symbol) => matchesTicker(symbol)) - ); + return newsFeed.items.filter((story) => story.resolved_symbols.some((symbol) => matchesTicker(symbol))); }, [matchesTicker, newsFeed.items, routeFeatures.news, routeFeatures.showNewsPane, tickerSet]); const visibleAlerts = useMemo(() => { @@ -6850,11 +6731,7 @@ const useTerminalState = () => { }, [visibleAlerts]); useEffect(() => { - if ( - !routeFeatures.needsAlertEvidencePrefetch || - mode !== "live" || - visibleAlerts.length === 0 - ) { + if (!routeFeatures.needsAlertEvidencePrefetch || mode !== "live" || visibleAlerts.length === 0) { return; } @@ -6867,9 +6744,7 @@ const useTerminalState = () => { incrementRetentionMetric("pinnedFetchMisses", missingPacketIds.length); void Promise.all( missingPacketIds.map(async (packetId) => { - const response = await fetch( - buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`) - ); + const response = await fetch(buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`)); if (!response.ok) { throw new Error(await readErrorDetail(response)); } @@ -6980,12 +6855,7 @@ const useTerminalState = () => { keys.add(id); } return keys; - }, [ - selectedAlert, - selectedClassifierFlowPacket, - selectedSmartMoneyEvent, - visibleAlertEvidenceRefs - ]); + }, [selectedAlert, selectedClassifierFlowPacket, selectedSmartMoneyEvent, visibleAlertEvidenceRefs]); const activePinnedJoinKeys = useMemo(() => { const keys = new Set(); @@ -7104,8 +6974,7 @@ const useTerminalState = () => { const desiredTrace = `alert:${packetId}`; return ( alertsFeed.items.find( - (item) => - item.trace_id === desiredTrace || getAlertFlowPacketRefs(item).includes(packetId) + (item) => item.trace_id === desiredTrace || getAlertFlowPacketRefs(item).includes(packetId) ) ?? null ); }, @@ -7176,20 +7045,15 @@ const useTerminalState = () => { if (routeFeatures.alerts || routeFeatures.showAlertsPane) { updates.push(alertsFeed.lastUpdate); } - if ( - routeFeatures.smartMoney || - routeFeatures.showClassifierPane || - routeFeatures.showChartPane || - routeFeatures.showFocusPane - ) { + if (routeFeatures.smartMoney || routeFeatures.showClassifierPane || routeFeatures.showChartPane || routeFeatures.showFocusPane) { updates.push(smartMoneyFeed.lastUpdate); } if (routeFeatures.classifierHits || routeFeatures.showClassifierPane) { updates.push(classifierHitsFeed.lastUpdate); } - return ( - updates.filter((value): value is number => value !== null).sort((a, b) => b - a)[0] ?? null - ); + return updates + .filter((value): value is number => value !== null) + .sort((a, b) => b - a)[0] ?? null; }, [ routeFeatures.options, routeFeatures.showOptionsPane, @@ -7348,7 +7212,13 @@ type FlowFilterPopoverProps = { onChange: Dispatch>; }; -const FlowFilterSection = ({ title, children }: { title: string; children: ReactNode }) => { +const FlowFilterSection = ({ + title, + children +}: { + title: string; + children: ReactNode; +}) => { return (
{title}
@@ -7358,7 +7228,7 @@ const FlowFilterSection = ({ title, children }: { title: string; children: React }; export const FlowFilterPopover = ({ filters, onChange }: FlowFilterPopoverProps) => { - const pathname = nextNavigation.usePathname(); + const pathname = usePathname(); const [open, setOpen] = useState(false); const rootRef = useRef(null); const activeCount = countActiveFlowFilterGroups(filters); @@ -7395,8 +7265,7 @@ export const FlowFilterPopover = ({ filters, onChange }: FlowFilterPopoverProps) onChange((prev) => ({ ...prev, view, - securityTypes: - view === "raw" ? undefined : (prev.securityTypes ?? DEFAULT_FLOW_SECURITY_TYPES), + securityTypes: view === "raw" ? undefined : prev.securityTypes ?? DEFAULT_FLOW_SECURITY_TYPES, nbboSides: view === "raw" ? undefined : prev.nbboSides, optionTypes: view === "raw" ? undefined : prev.optionTypes, minNotional: view === "raw" ? undefined : prev.minNotional @@ -7447,7 +7316,11 @@ export const FlowFilterPopover = ({ filters, onChange }: FlowFilterPopoverProps) {open ? ( -
+
Flow Filters
@@ -7615,25 +7488,16 @@ type OptionsPaneProps = { const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => { const items = limit ? state.filteredOptions.slice(0, limit) : state.filteredOptions; - const virtual = useTapeVirtualList( - items, - state.optionsScroll.listRef, - getTapeVirtualConfig("options") - ); + const virtual = useTapeVirtualList(items, state.optionsScroll.listRef, getTapeVirtualConfig("options")); const optionHistorySubscription = state.liveSession.manifest.find( (subscription) => subscription.channel === "options" ); - const optionHistoryKey = optionHistorySubscription - ? getLiveSubscriptionKey(optionHistorySubscription) - : null; + const optionHistoryKey = optionHistorySubscription ? getLiveSubscriptionKey(optionHistorySubscription) : null; const optionHistoryError = optionHistoryKey ? state.liveSession.historyErrors[optionHistoryKey] : null; - useVirtualHistoryGate( - state.mode === "live" && !limit, - items.length, - virtual.virtualItems.at(-1)?.index ?? -1, - () => void state.liveSession.loadOlder("options") + useVirtualHistoryGate(state.mode === "live" && !limit, items.length, virtual.virtualItems.at(-1)?.index ?? -1, () => + void state.liveSession.loadOlder("options") ); return ( @@ -7708,9 +7572,7 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => { const contractId = normalizeContractId(print.option_contract_id); const parsed = parseOptionContractId(contractId); const contractDisplay = formatOptionContractLabel(contractId); - const quote = - state.historicalNbboByTraceId.get(print.trace_id) ?? - state.nbboMap.get(contractId); + const quote = state.historicalNbboByTraceId.get(print.trace_id) ?? state.nbboMap.get(contractId); const hasPreservedNbbo = typeof print.execution_nbbo_side === "string"; const nbboSide = print.execution_nbbo_side ?? @@ -7740,72 +7602,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)} - + ${formatCompactUsd(notional)} {nbboSide ? ( - - {nbboSide} - + {nbboSide} ) : ( "--" )} - - {typeof iv === "number" ? formatPct(iv) : "--"} - - - {decor ? humanizeClassifierId(decor.family) : "--"} - + {typeof iv === "number" ? formatPct(iv) : "--"} + {decor ? humanizeClassifierId(decor.family) : "--"} ); @@ -7857,16 +7689,9 @@ type EquitiesPaneProps = { const EquitiesPane = memo(({ state, limit }: EquitiesPaneProps) => { const items = limit ? state.filteredEquities.slice(0, limit) : state.filteredEquities; - const virtual = useTapeVirtualList( - items, - state.equitiesScroll.listRef, - getTapeVirtualConfig("equities") - ); - useVirtualHistoryGate( - state.mode === "live" && !limit, - items.length, - virtual.virtualItems.at(-1)?.index ?? -1, - () => void state.liveSession.loadOlder("equities") + const virtual = useTapeVirtualList(items, state.equitiesScroll.listRef, getTapeVirtualConfig("equities")); + useVirtualHistoryGate(state.mode === "live" && !limit, items.length, virtual.virtualItems.at(-1)?.index ?? -1, () => + void state.liveSession.loadOlder("equities") ); return ( @@ -7934,9 +7759,7 @@ const EquitiesPane = memo(({ state, limit }: EquitiesPaneProps) => { data-tape-key={key} style={{ transform: `translateY(${start}px)` }} > - - {formatTime(print.ts)} - + {formatTime(print.ts)} - - ${formatPrice(print.price)} - - - {formatSize(print.size)}x - + ${formatPrice(print.price)} + {formatSize(print.size)}x {print.exchange} - - {print.offExchangeFlag ? "Off-Ex" : "Lit"} - + {print.offExchangeFlag ? "Off-Ex" : "Lit"}
))}
@@ -7977,11 +7794,8 @@ type FlowPaneProps = { const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => { const items = limit ? state.filteredFlow.slice(0, limit) : state.filteredFlow; const virtual = useTapeVirtualList(items, state.flowScroll.listRef, getTapeVirtualConfig("flow")); - useVirtualHistoryGate( - state.mode === "live" && !limit, - items.length, - virtual.virtualItems.at(-1)?.index ?? -1, - () => void state.liveSession.loadOlder("flow") + useVirtualHistoryGate(state.mode === "live" && !limit, items.length, virtual.virtualItems.at(-1)?.index ?? -1, () => + void state.liveSession.loadOlder("flow") ); return ( @@ -8052,26 +7866,18 @@ const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => { typeof features.structure_type === "string" ? features.structure_type : ""; const structureLegs = parseNumber(features.structure_legs, 0); const structureRights = - typeof features.structure_rights === "string" - ? features.structure_rights - : ""; + typeof features.structure_rights === "string" ? features.structure_rights : ""; const structureStrikes = parseNumber(features.structure_strikes, 0); const nbboBid = parseNumber(features.nbbo_bid, Number.NaN); const nbboAsk = parseNumber(features.nbbo_ask, Number.NaN); const nbboMid = parseNumber(features.nbbo_mid, Number.NaN); const nbboSpread = parseNumber(features.nbbo_spread, Number.NaN); - const aggressiveBuyRatio = parseNumber( - features.nbbo_aggressive_buy_ratio, - Number.NaN - ); + const aggressiveBuyRatio = parseNumber(features.nbbo_aggressive_buy_ratio, Number.NaN); const aggressiveSellRatio = parseNumber( features.nbbo_aggressive_sell_ratio, Number.NaN ); - const aggressiveCoverage = parseNumber( - features.nbbo_coverage_ratio, - Number.NaN - ); + const aggressiveCoverage = parseNumber(features.nbbo_coverage_ratio, Number.NaN); const insideRatio = parseNumber(features.nbbo_inside_ratio, Number.NaN); const nbboAge = parseNumber(packet.join_quality.nbbo_age_ms, Number.NaN); const nbboStale = parseNumber(packet.join_quality.nbbo_stale, 0) > 0; @@ -8079,26 +7885,21 @@ const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => { const structureLabel = structureType ? `${structureType.replace(/_/g, " ")}${structureRights ? ` ${structureRights}` : ""}${structureLegs > 0 ? ` ${structureLegs}L` : ""}${structureStrikes > 0 ? ` ${structureStrikes}K` : ""}` : "--"; - const nbboLabel = - Number.isFinite(nbboBid) && Number.isFinite(nbboAsk) - ? `${formatPrice(nbboBid)} x ${formatPrice(nbboAsk)}` - : Number.isFinite(nbboMid) - ? `Mid ${formatPrice(nbboMid)}` - : "--"; + const nbboLabel = Number.isFinite(nbboBid) && Number.isFinite(nbboAsk) + ? `${formatPrice(nbboBid)} x ${formatPrice(nbboAsk)}` + : Number.isFinite(nbboMid) + ? `Mid ${formatPrice(nbboMid)}` + : "--"; const qualityLabel = [ Number.isFinite(aggressiveCoverage) && aggressiveCoverage > 0 ? `Agg ${formatPct(aggressiveBuyRatio)}/${formatPct(aggressiveSellRatio)} ${formatPct(aggressiveCoverage)} cov` : null, - Number.isFinite(insideRatio) && insideRatio > 0 - ? `In ${formatPct(insideRatio)}` - : null, + Number.isFinite(insideRatio) && insideRatio > 0 ? `In ${formatPct(insideRatio)}` : null, Number.isFinite(nbboSpread) ? `Spr ${formatPrice(nbboSpread)}` : null, Number.isFinite(nbboAge) ? `${Math.round(nbboAge)}ms` : null, nbboStale ? "Stale" : null, nbboMissing ? "Missing" : null - ] - .filter(Boolean) - .join(" | "); + ].filter(Boolean).join(" | "); return (
{ data-tape-key={key} style={{ transform: `translateY(${start}px)` }} > - - {formatTime(startTs)} → {formatTime(endTs)} - + {formatTime(startTs)} → {formatTime(endTs)} {contract} - - {formatFlowMetric(count)} - - - {formatFlowMetric(totalSize)} - - - ${formatUsd(notional)} - - - {windowMs > 0 ? formatFlowMetric(windowMs, "ms") : "--"} - + {formatFlowMetric(count)} + {formatFlowMetric(totalSize)} + ${formatUsd(notional)} + {windowMs > 0 ? formatFlowMetric(windowMs, "ms") : "--"} {structureLabel} {nbboLabel} {qualityLabel || "--"} @@ -8151,16 +7942,9 @@ type AlertsPaneProps = { const AlertsPane = memo(({ state, limit, withStrip = false, className }: AlertsPaneProps) => { const items = limit ? state.filteredAlerts.slice(0, limit) : state.filteredAlerts; - const virtual = useTapeVirtualList( - items, - state.alertsScroll.listRef, - getTapeVirtualConfig("alerts") - ); - useVirtualHistoryGate( - state.mode === "live" && !limit, - items.length, - virtual.virtualItems.at(-1)?.index ?? -1, - () => void state.liveSession.loadOlder("alerts") + const virtual = useTapeVirtualList(items, state.alertsScroll.listRef, getTapeVirtualConfig("alerts")); + useVirtualHistoryGate(state.mode === "live" && !limit, items.length, virtual.virtualItems.at(-1)?.index ?? -1, () => + void state.liveSession.loadOlder("alerts") ); return ( @@ -8236,23 +8020,13 @@ const AlertsPane = memo(({ state, limit, withStrip = false, className }: AlertsP state.setSelectedAlert(alert); }} > - - {formatTime(alert.source_ts)} - - - {primary ? humanizeClassifierId(primary.classifier_id) : "Alert"} - + {formatTime(alert.source_ts)} + {primary ? humanizeClassifierId(primary.classifier_id) : "Alert"} {severity} - - {Math.round(alert.score)} - - - {alert.hits.length} - + {Math.round(alert.score)} + {alert.hits.length} {direction} - - {primary?.explanations?.[0] ?? "--"} - + {primary?.explanations?.[0] ?? "--"} ); })} @@ -8294,11 +8068,7 @@ const NewsPane = memo(({ state, limit, className }: NewsPaneProps) => { } actions={ canLoadOlder ? ( - ) : null @@ -8308,9 +8078,7 @@ const NewsPane = memo(({ state, limit, className }: NewsPaneProps) => {
News is live-only in v1.
) : items.length === 0 ? (
- {state.tickerSet.size > 0 - ? "No news stories match the current filter." - : "Waiting for live news stories."} + {state.tickerSet.size > 0 ? "No news stories match the current filter." : "Waiting for live news stories."}
) : (
@@ -8356,9 +8124,7 @@ type ClassifierPaneProps = { }; const ClassifierPane = memo(({ state, limit, className }: ClassifierPaneProps) => { - const smartMoneyItems = limit - ? state.filteredSmartMoneyEvents.slice(0, limit) - : state.filteredSmartMoneyEvents; + const smartMoneyItems = limit ? state.filteredSmartMoneyEvents.slice(0, limit) : state.filteredSmartMoneyEvents; const legacyItems = smartMoneyItems.length === 0 ? limit @@ -8367,20 +8133,11 @@ const ClassifierPane = memo(({ state, limit, className }: ClassifierPaneProps) = : []; const items: Array = smartMoneyItems.length > 0 ? smartMoneyItems : legacyItems; - const virtual = useTapeVirtualList( - items, - state.classifierScroll.listRef, - getTapeVirtualConfig("classifier") - ); - useVirtualHistoryGate( - state.mode === "live" && !limit, - items.length, - virtual.virtualItems.at(-1)?.index ?? -1, - () => { - void state.liveSession.loadOlder("smart-money"); - void state.liveSession.loadOlder("classifier-hits"); - } - ); + const virtual = useTapeVirtualList(items, state.classifierScroll.listRef, getTapeVirtualConfig("classifier")); + useVirtualHistoryGate(state.mode === "live" && !limit, items.length, virtual.virtualItems.at(-1)?.index ?? -1, () => { + void state.liveSession.loadOlder("smart-money"); + void state.liveSession.loadOlder("classifier-hits"); + }); const showingSmartMoney = smartMoneyItems.length > 0; return ( @@ -8420,11 +8177,7 @@ const ClassifierPane = memo(({ state, limit, className }: ClassifierPaneProps) =
) : (
-
+
TIME PROFILE @@ -8434,75 +8187,60 @@ const ClassifierPane = memo(({ state, limit, className }: ClassifierPaneProps) =
- {showingSmartMoney - ? virtual.virtualItems.map(({ item, key, index, start, size }) => { - const event = item as SmartMoneyEvent; - const primaryScore = - event.profile_scores.find( - (score) => score.profile_id === event.primary_profile_id - ) ?? event.profile_scores[0]; - const direction = normalizeDirection(event.primary_direction); - return ( - - ); - }) - : virtual.virtualItems.map(({ item, key, index, start, size }) => { - const hit = item as ClassifierHitEvent; - const direction = normalizeDirection(hit.direction); - return ( - - ); - })} + {showingSmartMoney ? virtual.virtualItems.map(({ item, key, index, start, size }) => { + const event = item as SmartMoneyEvent; + const primaryScore = + event.profile_scores.find((score) => score.profile_id === event.primary_profile_id) ?? + event.profile_scores[0]; + const direction = normalizeDirection(event.primary_direction); + return ( + + ); + }) : virtual.virtualItems.map(({ item, key, index, start, size }) => { + const hit = item as ClassifierHitEvent; + const direction = normalizeDirection(hit.direction); + return ( + + ); + })}
@@ -8522,11 +8260,8 @@ type DarkPaneProps = { const DarkPane = memo(({ state, limit, className }: DarkPaneProps) => { const items = limit ? state.filteredInferredDark.slice(0, limit) : state.filteredInferredDark; const virtual = useTapeVirtualList(items, state.darkScroll.listRef, getTapeVirtualConfig("dark")); - useVirtualHistoryGate( - state.mode === "live" && !limit, - items.length, - virtual.virtualItems.at(-1)?.index ?? -1, - () => void state.liveSession.loadOlder("inferred-dark") + useVirtualHistoryGate(state.mode === "live" && !limit, items.length, virtual.virtualItems.at(-1)?.index ?? -1, () => + void state.liveSession.loadOlder("inferred-dark") ); return ( @@ -8599,20 +8334,12 @@ const DarkPane = memo(({ state, limit, className }: DarkPaneProps) => { state.setSelectedDarkEvent(event); }} > - - {formatTime(event.source_ts)} - + {formatTime(event.source_ts)} {humanizeClassifierId(event.type)} {underlying ?? "Unknown"} - - {formatConfidence(event.confidence)} - - - {evidenceCount} - - - {underlying ? "--" : "Underlying not in current join cache."} - + {formatConfidence(event.confidence)} + {evidenceCount} + {underlying ? "--" : "Underlying not in current join cache."} ); })} @@ -8632,6 +8359,7 @@ type ChartPaneProps = { }; const ChartPane = memo(({ state, title = "Chart" }: ChartPaneProps) => { + return ( { } for (const print of state.filteredOptions.slice(0, 80)) { const parsed = parseOptionContractId(normalizeContractId(print.option_contract_id)); - const symbol = ( - print.underlying_id ?? - parsed?.root ?? - extractUnderlying(print.option_contract_id) - )?.toUpperCase(); + const symbol = (print.underlying_id ?? parsed?.root ?? extractUnderlying(print.option_contract_id))?.toUpperCase(); if (symbol) { symbols.add(symbol); } @@ -8708,39 +8432,31 @@ const buildCommandDeckTickers = (state: TerminalState): CommandDeckTicker[] => { symbols.add(state.chartTicker.toUpperCase()); } - return Array.from(symbols) - .slice(0, 10) - .map((symbol) => { - const equityPrints = state.filteredEquities - .filter((print) => print.underlying_id.toUpperCase() === symbol) - .slice(0, 2); - const price = equityPrints[0]?.price ?? null; - const previous = equityPrints[1]?.price ?? null; - const move = - price !== null && previous !== null && previous !== 0 - ? (price - previous) / previous - : null; - const options = state.filteredOptions.slice(0, 120).filter((print) => { + return Array.from(symbols).slice(0, 10).map((symbol) => { + const equityPrints = state.filteredEquities + .filter((print) => print.underlying_id.toUpperCase() === symbol) + .slice(0, 2); + const price = equityPrints[0]?.price ?? null; + const previous = equityPrints[1]?.price ?? null; + const move = price !== null && previous !== null && previous !== 0 ? (price - previous) / previous : null; + const options = state.filteredOptions + .slice(0, 120) + .filter((print) => { const parsed = parseOptionContractId(normalizeContractId(print.option_contract_id)); - const underlying = ( - print.underlying_id ?? - parsed?.root ?? - extractUnderlying(print.option_contract_id) - )?.toUpperCase(); + const underlying = (print.underlying_id ?? parsed?.root ?? extractUnderlying(print.option_contract_id))?.toUpperCase(); return underlying === symbol; }).length; - const alerts = state.filteredAlerts - .slice(0, 80) - .filter((alert) => alert.trace_id.toUpperCase().includes(symbol)).length; - return { symbol, price, move, options, alerts }; - }); + const alerts = state.filteredAlerts + .slice(0, 80) + .filter((alert) => alert.trace_id.toUpperCase().includes(symbol)).length; + return { symbol, price, move, options, alerts }; + }); }; const CommandDeckHeader = ({ state }: { state: TerminalState }) => { const focus = state.activeTickers.length > 0 ? state.activeTickers.join(", ") : state.chartTicker; const selected = state.selectedInstrumentLabel ?? "No contract lock"; - const connectionLabel = - state.mode === "live" ? statusLabel(state.liveSession.status, false, state.mode) : "Replay"; + const connectionLabel = state.mode === "live" ? statusLabel(state.liveSession.status, false, state.mode) : "Replay"; return (
@@ -8760,9 +8476,7 @@ const CommandDeckHeader = ({ state }: { state: TerminalState }) => { {state.mode === "live" ? "Live" : "Replay"}: {connectionLabel} - - Last {state.lastSeen ? formatTime(state.lastSeen) : "waiting"} - + Last {state.lastSeen ? formatTime(state.lastSeen) : "waiting"} @@ -8779,22 +8493,16 @@ const TickerRail = ({ state }: { state: TerminalState }) => {
{tickers.map((ticker) => { const direction = ticker.move === null ? "flat" : ticker.move >= 0 ? "up" : "down"; - const equity = state.filteredEquities.find( - (print) => print.underlying_id.toUpperCase() === ticker.symbol - ); + const equity = state.filteredEquities.find((print) => print.underlying_id.toUpperCase() === ticker.symbol); return (
Cursor - - {replayTime - ? formatTime(replayTime) - : state.lastSeen - ? formatTime(state.lastSeen) - : "waiting"} - + {replayTime ? formatTime(replayTime) : state.lastSeen ? formatTime(state.lastSeen) : "waiting"}
Chart - - {state.chartTicker} / {formatIntervalLabel(state.chartIntervalMs)} - + {state.chartTicker} / {formatIntervalLabel(state.chartIntervalMs)}
Scope - - {state.activeTickers.length > 0 ? state.activeTickers.join(", ") : "All symbols"} - + {state.activeTickers.length > 0 ? state.activeTickers.join(", ") : "All symbols"}
@@ -9016,9 +8696,7 @@ const FocusPane = memo(({ state }: { state: TerminalState }) => {
{smartMoneyProfileLabel(hit.primary_profile_id)}
- + {normalizeDirection(hit.primary_direction)} {formatTime(hit.source_ts)} @@ -9066,11 +8744,7 @@ const ReplayConsole = memo(({ state }: { state: TerminalState }) => { + } @@ -9226,7 +8900,9 @@ function SyntheticControlDock() { const disabled = !status?.enabled; const derived = status?.derived; - const updateControl = (patch: SyntheticControlPatch) => { + const updateControl = ( + patch: SyntheticControlPatch + ) => { dirtyRef.current = true; setDraft((current) => createSyntheticControlDraft(current ?? buildDefaultSyntheticControl(), patch) @@ -9313,7 +8989,9 @@ function SyntheticControlDock() {