rebuild terminal routes around dense command views
This commit is contained in:
parent
f9f732c3d1
commit
0320533628
6 changed files with 2279 additions and 248 deletions
|
|
@ -1,3 +1,6 @@
|
||||||
|
{"_type":"issue","id":"islandflow-xkq","title":"Rebuild production dashboard options news around mock9 aesthetic","description":"Reconstruct the production web UI for Dashboard, Options, and News around the mock9 through mock12 dense terminal aesthetic while preserving production data subscriptions, drawers, virtualization, route helpers, redirects, and validation.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T14:07:34Z","created_by":"dirtydishes","updated_at":"2026-06-13T14:26:46Z","started_at":"2026-06-13T14:07:53Z","closed_at":"2026-06-13T14:26:46Z","close_reason":"Rebuilt Dashboard, Options, and News around the dense mock9 to mock12 production aesthetic; tests and build passed, and Browser visual inspection was documented as blocked by the unavailable in-app browser backend.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"islandflow-u45","title":"Patch CVE-related dependency and Docker image findings","description":"Address Forgejo issues #15, #18, and #19 by upgrading the vulnerable tmp dependency resolution and moving Bun Docker images off the vulnerable oven/bun:1.3.11 base image with patched OpenSSL packages during image build.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-12T23:21:29Z","created_by":"dirtydishes","updated_at":"2026-06-12T23:23:27Z","started_at":"2026-06-12T23:22:16Z","closed_at":"2026-06-12T23:23:27Z","close_reason":"Patched Forgejo #15/#18 tmp CVE by resolving tmp@0.2.7, updated Bun Docker images and OpenSSL package upgrade layers for #19, and validated with bun audit, tests, web build, docker workspace check, and replacement image manifest inspection.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"islandflow-hut","title":"Fix tmp path traversal audit finding","description":"bun audit reports GHSA-ph9p-34f9-6g65 through workspace:@islandflow/desktop via @electron-forge/cli. Update dependency resolution so tmp is at a non-vulnerable version and verify bun audit passes.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-12T22:50:18Z","created_by":"dirtydishes","updated_at":"2026-06-12T22:58:59Z","started_at":"2026-06-12T22:58:33Z","closed_at":"2026-06-12T22:58:59Z","close_reason":"Fixed by bumping the root tmp override to ^0.2.6, refreshing bun.lock to tmp@0.2.7, and validating with bun audit plus bun test. Forgejo issue listing was inaccessible from this environment, so the branch targets the active audit finding visible on current main.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_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-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-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-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}
|
||||||
|
|
@ -27,6 +30,8 @@
|
||||||
{"_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-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-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-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-iil","title":"Replace overview with dashboard command page","description":"Turn the mock9 Market Command concept into the production root dashboard, rename the visible route from Home to Dashboard, and keep the layout dense with a chart-first command surface.","acceptance_criteria":"Root page displays Dashboard instead of Home; dashboard includes command metrics, chart area, decision levels, priority board, live context, feed health, dark context, and replay context; web tests and production build pass.","notes":"Implemented from the mock9 direction while preserving the existing / URL and using the existing ChartPane until proper chart implementation lands.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T07:37:56Z","created_by":"dirtydishes","updated_at":"2026-06-13T07:43:44Z","started_at":"2026-06-13T07:38:02Z","closed_at":"2026-06-13T07:43:44Z","close_reason":"dashboard replacement implemented, validated, and documented","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"islandflow-7l2","title":"Configure local web and desktop to use hosted Islandflow API","description":"Local web development and the Electron desktop shell are not connecting to the VPS-hosted API reliably after a recent endpoint change. Verify the active Delta Island API hostname, update local/default configuration so bun run dev:web and desktop development target it correctly, and validate the relevant web/desktop paths.","status":"closed","priority":2,"issue_type":"bug","owner":"dishes@dpdrm.com","created_at":"2026-06-13T07:32:28Z","created_by":"dirtydishes","updated_at":"2026-06-13T07:38:19Z","closed_at":"2026-06-13T07:38:19Z","close_reason":"Configured local web and desktop development to use https://api.flow.deltaisland.io as the hosted API origin, updated docs and local ignored env, verified the API host from the VPS, passed focused tests, public API route checks, and web build. Dev-web smoke confirmed the corrected API origin but port 3000 was already occupied.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-4j7","title":"replace activity matrix with alert lineage mock","description":"Rework the confusing Activity Matrix mock into a concrete alert lineage view that shows how a selected alert formed, including evidence chain, confirming/against context, invalidations, and audit state.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-12T00:10:18Z","created_by":"dirtydishes","updated_at":"2026-06-12T00:15:45Z","started_at":"2026-06-12T00:10:21Z","closed_at":"2026-06-12T00:15:45Z","close_reason":"Replaced the abstract Activity Matrix mock with an alert lineage view that supports all-symbol scope, selected alert evidence, invalidations, and audit context.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-4j7","title":"replace activity matrix with alert lineage mock","description":"Rework the confusing Activity Matrix mock into a concrete alert lineage view that shows how a selected alert formed, including evidence chain, confirming/against context, invalidations, and audit state.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-12T00:10:18Z","created_by":"dirtydishes","updated_at":"2026-06-12T00:15:45Z","started_at":"2026-06-12T00:10:21Z","closed_at":"2026-06-12T00:15:45Z","close_reason":"Replaced the abstract Activity Matrix mock with an alert lineage view that supports all-symbol scope, selected alert evidence, invalidations, and audit context.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-5bv","title":"add four dense dashboard mock routes","description":"Add four more main dashboard page mock studies in the existing terminal style: no cards, dense at-a-glance market context, and professional copy for experienced traders.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-11T23:40:25Z","created_by":"dirtydishes","updated_at":"2026-06-11T23:48:40Z","started_at":"2026-06-11T23:40:32Z","closed_at":"2026-06-11T23:48:40Z","close_reason":"Added four dense main-dashboard mock routes and validated the web build plus desktop/mobile layout overflow.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-5bv","title":"add four dense dashboard mock routes","description":"Add four more main dashboard page mock studies in the existing terminal style: no cards, dense at-a-glance market context, and professional copy for experienced traders.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-11T23:40:25Z","created_by":"dirtydishes","updated_at":"2026-06-11T23:48:40Z","started_at":"2026-06-11T23:40:32Z","closed_at":"2026-06-11T23:48:40Z","close_reason":"Added four dense main-dashboard mock routes and validated the web build plus desktop/mobile layout overflow.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-w2y","title":"tighten mock route terminal copy","description":"Revise mock route UI copy to read like an experienced trader terminal instead of explanatory walkthrough text. Keep the existing no-card mock route work and update the existing turn document for the minor follow-up.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-06-11T23:35:21Z","created_by":"dirtydishes","updated_at":"2026-06-11T23:37:45Z","closed_at":"2026-06-11T23:37:45Z","close_reason":"Tightened mock route UI copy to professional trader-terminal language and updated the existing turn document.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-w2y","title":"tighten mock route terminal copy","description":"Revise mock route UI copy to read like an experienced trader terminal instead of explanatory walkthrough text. Keep the existing no-card mock route work and update the existing turn document for the minor follow-up.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-06-11T23:35:21Z","created_by":"dirtydishes","updated_at":"2026-06-11T23:37:45Z","closed_at":"2026-06-11T23:37:45Z","close_reason":"Tightened mock route UI copy to professional trader-terminal language and updated the existing turn document.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
|
||||||
31
DESIGN.md
31
DESIGN.md
|
|
@ -75,11 +75,11 @@ components:
|
||||||
typography: "{typography.label}"
|
typography: "{typography.label}"
|
||||||
rounded: "{rounded.md}"
|
rounded: "{rounded.md}"
|
||||||
padding: "12px 14px"
|
padding: "12px 14px"
|
||||||
pane-surface:
|
terminal-section:
|
||||||
backgroundColor: "{colors.bg-pane}"
|
backgroundColor: "{colors.bg-pane}"
|
||||||
textColor: "{colors.text-primary}"
|
textColor: "{colors.text-primary}"
|
||||||
rounded: "{rounded.xl}"
|
rounded: "0"
|
||||||
padding: "16px 18px"
|
padding: "8px 10px"
|
||||||
status-chip:
|
status-chip:
|
||||||
backgroundColor: "{colors.bg-soft}"
|
backgroundColor: "{colors.bg-soft}"
|
||||||
textColor: "{colors.text-primary}"
|
textColor: "{colors.text-primary}"
|
||||||
|
|
@ -98,6 +98,8 @@ Islandflow's interface behaves like an investigation instrument, not a presentat
|
||||||
|
|
||||||
The visual atmosphere is dark and controlled, with amber used as a directional signal rather than ambient decoration. Surfaces are compact and information-dense, but each zone is explicit about purpose so the user can move from detection to validation without losing context.
|
The visual atmosphere is dark and controlled, with amber used as a directional signal rather than ambient decoration. Surfaces are compact and information-dense, but each zone is explicit about purpose so the user can move from detection to validation without losing context.
|
||||||
|
|
||||||
|
The production aesthetic is now centered on mock9 through mock12: table-first command surfaces, dense route-specific operating boards, border-block separation, minimal radius, and no dashboard-card composition as the default. Mock5 remains the workflow reference for Options only: OPRA intake, contract focus, filter controls, and packet eligibility.
|
||||||
|
|
||||||
This system explicitly rejects the anti-references in PRODUCT.md: no meme-stock hype aesthetics, no generic SaaS card fog, and no Bloomberg cosplay density unless density is earning its keep with decision value.
|
This system explicitly rejects the anti-references in PRODUCT.md: no meme-stock hype aesthetics, no generic SaaS card fog, and no Bloomberg cosplay density unless density is earning its keep with decision value.
|
||||||
|
|
||||||
**Key Characteristics:**
|
**Key Characteristics:**
|
||||||
|
|
@ -106,6 +108,8 @@ This system explicitly rejects the anti-references in PRODUCT.md: no meme-stock
|
||||||
- Accent color treated as scarce signal.
|
- Accent color treated as scarce signal.
|
||||||
- Monospace-assisted precision for time, numeric, and status data.
|
- Monospace-assisted precision for time, numeric, and status data.
|
||||||
- Readability preserved during bursty live updates.
|
- Readability preserved during bursty live updates.
|
||||||
|
- Route-specific signatures: Dashboard is Market Command, Options is OPRA Intake, News is Wire Control.
|
||||||
|
- Flat terminal sections: border-block dividers and compact headers are the default; rounded cards are not.
|
||||||
|
|
||||||
## Colors
|
## Colors
|
||||||
|
|
||||||
|
|
@ -187,13 +191,19 @@ The system is flat by default. Depth is primarily tonal (background and border d
|
||||||
- **Style:** pill chips (`999px`) with thin border and semantic soft fill.
|
- **Style:** pill chips (`999px`) with thin border and semantic soft fill.
|
||||||
- **State:** direction/severity/status chips map to green/red/blue semantic channels with text labels always present.
|
- **State:** direction/severity/status chips map to green/red/blue semantic channels with text labels always present.
|
||||||
|
|
||||||
### Cards / Containers
|
### Sections / Containers
|
||||||
|
|
||||||
- **Corner Style:** medium-soft corners (`12px` or `14px`) depending on container prominence.
|
- **Default Shape:** square terminal sections (`0px radius`) with border-block dividers.
|
||||||
- **Background:** layered dark surfaces (`#111820`, `#0d141b`) with restrained top-to-bottom sheen.
|
- **Background:** flat dark surfaces (`#111820`, `#0d141b`) with tonal contrast only.
|
||||||
- **Shadow Strategy:** no default card shadow; only overlays and floating inspectors use lift shadows.
|
- **Shadow Strategy:** no shadows on production route sections; only drawers, popovers, and tooltips use lift shadows.
|
||||||
- **Border:** subtle perimeter lines (`rgba(255,255,255,0.08)` baseline).
|
- **Border:** top/bottom rules carry separation; perimeter boxes are reserved for true tables, overlays, and controls that need hit-area clarity.
|
||||||
- **Internal Padding:** primarily `16px-18px` with tighter inner rhythm (`8px-12px`) for controls.
|
- **Internal Padding:** primarily `8px-12px` so rows, headers, and controls stay dense.
|
||||||
|
|
||||||
|
### Route Signatures
|
||||||
|
|
||||||
|
- **Dashboard / Market Command:** command metrics, priority board, decision levels, chart context, source health, recent contracts, replay state, and evidence context in one dense operating board.
|
||||||
|
- **Options / OPRA Intake:** production `OptionsPane` and `FlowPane` remain the source of truth, with TanStack virtual rows, contract focus, scroll gates, and filters tuned for option decision work.
|
||||||
|
- **News / Wire Control:** virtualized wire rows, source rails, symbol rails, live-only state, older-history scroll gates, and the existing news drawer.
|
||||||
|
|
||||||
### Inputs / Fields
|
### Inputs / Fields
|
||||||
|
|
||||||
|
|
@ -220,6 +230,8 @@ The system is flat by default. Depth is primarily tonal (background and border d
|
||||||
- **Do** use amber as a sparse decision signal for active controls, focus rails, and key counters.
|
- **Do** use amber as a sparse decision signal for active controls, focus rails, and key counters.
|
||||||
- **Do** keep overlays visually separated with dedicated shadow roles while leaving primary panes flat.
|
- **Do** keep overlays visually separated with dedicated shadow roles while leaving primary panes flat.
|
||||||
- **Do** design live updates to avoid flashing, excessive animation, and layout shifts during high-volume periods.
|
- **Do** design live updates to avoid flashing, excessive animation, and layout shifts during high-volume periods.
|
||||||
|
- **Do** prefer dense tables, rails, and flat sections for production routes.
|
||||||
|
- **Do** let each primary route have a tasteful accent and layout signature without duplicating the data model.
|
||||||
|
|
||||||
### Don't:
|
### Don't:
|
||||||
|
|
||||||
|
|
@ -228,3 +240,4 @@ The system is flat by default. Depth is primarily tonal (background and border d
|
||||||
- **Don't** make Islandflow feel like Bloomberg-style visual density used as aesthetic cosplay instead of as a genuinely useful information structure.
|
- **Don't** make Islandflow feel like Bloomberg-style visual density used as aesthetic cosplay instead of as a genuinely useful information structure.
|
||||||
- **Don't** rely on red/green alone for directional meaning or severity.
|
- **Don't** rely on red/green alone for directional meaning or severity.
|
||||||
- **Don't** use colored side-stripe accents on rows/cards as the primary signifier; use complete semantic chips and labels instead.
|
- **Don't** use colored side-stripe accents on rows/cards as the primary signifier; use complete semantic chips and labels instead.
|
||||||
|
- **Don't** default to card grids, decorative shadows, hero treatments, or static mock copy in production routes.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -554,6 +554,11 @@ describe("fixed tape virtualization config", () => {
|
||||||
overscan: 24,
|
overscan: 24,
|
||||||
debugLabel: "dark"
|
debugLabel: "dark"
|
||||||
});
|
});
|
||||||
|
expect(getTapeVirtualConfig("news")).toEqual({
|
||||||
|
rowHeight: 52,
|
||||||
|
overscan: 28,
|
||||||
|
debugLabel: "news"
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -574,9 +579,9 @@ describe("dark underlying route dependency helper", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("terminal navigation", () => {
|
describe("terminal navigation", () => {
|
||||||
it("exposes Home, Options, and News as top-level destinations", () => {
|
it("exposes Dashboard, Options, and News as top-level destinations", () => {
|
||||||
expect(NAV_ITEMS).toEqual([
|
expect(NAV_ITEMS).toEqual([
|
||||||
{ href: "/", label: "Home" },
|
{ href: "/", label: "Dashboard" },
|
||||||
{ href: "/options", label: "Options" },
|
{ href: "/options", label: "Options" },
|
||||||
{ href: "/news", label: "News" }
|
{ href: "/news", label: "News" }
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ const LIVE_SESSION_HOT_CHANNELS = new Set<LiveSubscription["channel"]>([
|
||||||
"equity-overlay"
|
"equity-overlay"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type TapeVirtualPane = "options" | "equities" | "flow" | "alerts" | "classifier" | "dark";
|
type TapeVirtualPane = "options" | "equities" | "flow" | "alerts" | "classifier" | "dark" | "news";
|
||||||
|
|
||||||
type TapeVirtualListConfig = {
|
type TapeVirtualListConfig = {
|
||||||
rowHeight: number;
|
rowHeight: number;
|
||||||
|
|
@ -153,7 +153,8 @@ const TAPE_VIRTUAL_CONFIG: Record<TapeVirtualPane, TapeVirtualListConfig> = {
|
||||||
flow: { rowHeight: 44, overscan: 24, debugLabel: "flow" },
|
flow: { rowHeight: 44, overscan: 24, debugLabel: "flow" },
|
||||||
alerts: { rowHeight: 44, overscan: 24, debugLabel: "alerts" },
|
alerts: { rowHeight: 44, overscan: 24, debugLabel: "alerts" },
|
||||||
classifier: { rowHeight: 44, overscan: 24, debugLabel: "classifier" },
|
classifier: { rowHeight: 44, overscan: 24, debugLabel: "classifier" },
|
||||||
dark: { rowHeight: 44, overscan: 24, debugLabel: "dark" }
|
dark: { rowHeight: 44, overscan: 24, debugLabel: "dark" },
|
||||||
|
news: { rowHeight: 52, overscan: 28, debugLabel: "news" }
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTapeVirtualConfig = (pane: TapeVirtualPane): TapeVirtualListConfig =>
|
export const getTapeVirtualConfig = (pane: TapeVirtualPane): TapeVirtualListConfig =>
|
||||||
|
|
@ -5602,6 +5603,7 @@ const useTerminalState = () => {
|
||||||
const darkScroll = useListScroll();
|
const darkScroll = useListScroll();
|
||||||
const alertsScroll = useListScroll();
|
const alertsScroll = useListScroll();
|
||||||
const classifierScroll = useListScroll();
|
const classifierScroll = useListScroll();
|
||||||
|
const newsScroll = useListScroll();
|
||||||
|
|
||||||
const optionsAnchor = useScrollAnchor(optionsScroll.listRef, optionsScroll.isAtTopRef);
|
const optionsAnchor = useScrollAnchor(optionsScroll.listRef, optionsScroll.isAtTopRef);
|
||||||
const equitiesAnchor = useScrollAnchor(equitiesScroll.listRef, equitiesScroll.isAtTopRef);
|
const equitiesAnchor = useScrollAnchor(equitiesScroll.listRef, equitiesScroll.isAtTopRef);
|
||||||
|
|
@ -5609,6 +5611,7 @@ const useTerminalState = () => {
|
||||||
const darkAnchor = useScrollAnchor(darkScroll.listRef, darkScroll.isAtTopRef);
|
const darkAnchor = useScrollAnchor(darkScroll.listRef, darkScroll.isAtTopRef);
|
||||||
const alertsAnchor = useScrollAnchor(alertsScroll.listRef, alertsScroll.isAtTopRef);
|
const alertsAnchor = useScrollAnchor(alertsScroll.listRef, alertsScroll.isAtTopRef);
|
||||||
const classifierAnchor = useScrollAnchor(classifierScroll.listRef, classifierScroll.isAtTopRef);
|
const classifierAnchor = useScrollAnchor(classifierScroll.listRef, classifierScroll.isAtTopRef);
|
||||||
|
const newsAnchor = useScrollAnchor(newsScroll.listRef, newsScroll.isAtTopRef);
|
||||||
const disableReplayGrouping = useCallback(() => null, []);
|
const disableReplayGrouping = useCallback(() => null, []);
|
||||||
const optionQueryParams = useMemo<Record<string, string | undefined>>(
|
const optionQueryParams = useMemo<Record<string, string | undefined>>(
|
||||||
() => buildOptionTapeQueryParams(effectiveOptionPrintFilters, optionScope),
|
() => buildOptionTapeQueryParams(effectiveOptionPrintFilters, optionScope),
|
||||||
|
|
@ -5791,6 +5794,18 @@ const useTerminalState = () => {
|
||||||
shouldHold: () => !flowScroll.isAtTopRef.current,
|
shouldHold: () => !flowScroll.isAtTopRef.current,
|
||||||
resumeSignal: flowScroll.resumeTick
|
resumeSignal: flowScroll.resumeTick
|
||||||
});
|
});
|
||||||
|
const liveNews = usePausableTapeView<NewsStory>({
|
||||||
|
enabled: mode === "live",
|
||||||
|
sourceStatus: liveSession.status,
|
||||||
|
sourceItems: liveSession.news,
|
||||||
|
historyTail: liveSession.newsHistory,
|
||||||
|
lastUpdate: liveSession.lastUpdate,
|
||||||
|
retentionLimit: LIVE_OPTIONS_HEAD_LIMIT,
|
||||||
|
captureScroll: newsAnchor.capture,
|
||||||
|
onNewItems: newsScroll.onNewItems,
|
||||||
|
shouldHold: () => !newsScroll.isAtTopRef.current,
|
||||||
|
resumeSignal: newsScroll.resumeTick
|
||||||
|
});
|
||||||
|
|
||||||
const seededLiveOptionsItems = useMemo(
|
const seededLiveOptionsItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -5831,14 +5846,7 @@ const useTerminalState = () => {
|
||||||
)
|
)
|
||||||
: equityJoins;
|
: equityJoins;
|
||||||
const flowFeed = mode === "live" ? liveFlow : flow;
|
const flowFeed = mode === "live" ? liveFlow : flow;
|
||||||
const newsFeed =
|
const newsFeed = mode === "live" ? liveNews : toStaticTapeState("disconnected", [], null);
|
||||||
mode === "live"
|
|
||||||
? toStaticTapeState(
|
|
||||||
liveSession.status,
|
|
||||||
composeTapeItems([], liveSession.news, liveSession.newsHistory),
|
|
||||||
liveSession.lastUpdate
|
|
||||||
)
|
|
||||||
: toStaticTapeState("disconnected", [], null);
|
|
||||||
const alertsFeed =
|
const alertsFeed =
|
||||||
mode === "live"
|
mode === "live"
|
||||||
? toStaticTapeState(
|
? toStaticTapeState(
|
||||||
|
|
@ -5896,6 +5904,10 @@ const useTerminalState = () => {
|
||||||
classifierAnchor.apply();
|
classifierAnchor.apply();
|
||||||
}, [smartMoneyFeed.items, classifierHitsFeed.items, classifierAnchor.apply]);
|
}, [smartMoneyFeed.items, classifierHitsFeed.items, classifierAnchor.apply]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
newsAnchor.apply();
|
||||||
|
}, [newsFeed.items, newsAnchor.apply]);
|
||||||
|
|
||||||
const nbboMap = useMemo(() => {
|
const nbboMap = useMemo(() => {
|
||||||
const map = new Map<string, OptionNBBO>();
|
const map = new Map<string, OptionNBBO>();
|
||||||
for (const quote of nbboFeed.items) {
|
for (const quote of nbboFeed.items) {
|
||||||
|
|
@ -7248,6 +7260,7 @@ const useTerminalState = () => {
|
||||||
darkScroll,
|
darkScroll,
|
||||||
alertsScroll,
|
alertsScroll,
|
||||||
classifierScroll,
|
classifierScroll,
|
||||||
|
newsScroll,
|
||||||
options: optionsFeed,
|
options: optionsFeed,
|
||||||
equities: equitiesFeed,
|
equities: equitiesFeed,
|
||||||
equityJoins: equityJoinsFeed,
|
equityJoins: equityJoinsFeed,
|
||||||
|
|
@ -7320,22 +7333,30 @@ const useTerminal = (): TerminalState => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NAV_ITEMS = [
|
export const NAV_ITEMS = [
|
||||||
{ href: "/", label: "Home" },
|
{ href: "/", label: "Dashboard" },
|
||||||
{ href: "/options", label: "Options" },
|
{ href: "/options", label: "Options" },
|
||||||
{ href: "/news", label: "News" }
|
{ href: "/news", label: "News" }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
type PageFrameVariant = "default" | "dashboard" | "options" | "news";
|
||||||
|
|
||||||
type PageFrameProps = {
|
type PageFrameProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
eyebrow?: string;
|
||||||
|
variant?: PageFrameVariant;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageFrame = ({ title, actions, children }: PageFrameProps) => {
|
const PageFrame = ({ title, eyebrow, variant = "default", actions, children }: PageFrameProps) => {
|
||||||
|
const classes = ["page-shell", `page-shell-${variant}`].join(" ");
|
||||||
return (
|
return (
|
||||||
<div className="page-shell">
|
<div className={classes} data-route-variant={variant}>
|
||||||
<header className="page-header">
|
<header className="page-header">
|
||||||
|
<div className="page-heading">
|
||||||
|
{eyebrow ? <span className="page-eyebrow">{eyebrow}</span> : null}
|
||||||
<h1 className="page-title">{title}</h1>
|
<h1 className="page-title">{title}</h1>
|
||||||
|
</div>
|
||||||
{actions ? <div className="page-actions">{actions}</div> : null}
|
{actions ? <div className="page-actions">{actions}</div> : null}
|
||||||
</header>
|
</header>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -7611,9 +7632,11 @@ const ShellMetricStrip = () => {
|
||||||
type OptionsPaneProps = {
|
type OptionsPaneProps = {
|
||||||
state: TerminalState;
|
state: TerminalState;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => {
|
const OptionsPane = memo(({ state, limit, title = "Options", className }: OptionsPaneProps) => {
|
||||||
const items = limit ? state.filteredOptions.slice(0, limit) : state.filteredOptions;
|
const items = limit ? state.filteredOptions.slice(0, limit) : state.filteredOptions;
|
||||||
const virtual = useTapeVirtualList(
|
const virtual = useTapeVirtualList(
|
||||||
items,
|
items,
|
||||||
|
|
@ -7638,7 +7661,8 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane
|
<Pane
|
||||||
title="Options"
|
className={className}
|
||||||
|
title={title}
|
||||||
status={
|
status={
|
||||||
<TapeStatus
|
<TapeStatus
|
||||||
status={state.options.status}
|
status={state.options.status}
|
||||||
|
|
@ -7972,9 +7996,10 @@ type FlowPaneProps = {
|
||||||
state: TerminalState;
|
state: TerminalState;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => {
|
const FlowPane = memo(({ state, limit, title = "Flow", className }: FlowPaneProps) => {
|
||||||
const items = limit ? state.filteredFlow.slice(0, limit) : state.filteredFlow;
|
const items = limit ? state.filteredFlow.slice(0, limit) : state.filteredFlow;
|
||||||
const virtual = useTapeVirtualList(items, state.flowScroll.listRef, getTapeVirtualConfig("flow"));
|
const virtual = useTapeVirtualList(items, state.flowScroll.listRef, getTapeVirtualConfig("flow"));
|
||||||
useVirtualHistoryGate(
|
useVirtualHistoryGate(
|
||||||
|
|
@ -7986,6 +8011,7 @@ const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane
|
<Pane
|
||||||
|
className={className}
|
||||||
title={title}
|
title={title}
|
||||||
status={
|
status={
|
||||||
<TapeStatus
|
<TapeStatus
|
||||||
|
|
@ -8272,40 +8298,96 @@ type NewsPaneProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatNewsSymbolsLabel = (story: NewsStory): string => {
|
||||||
|
if (story.resolved_symbols.length === 0) {
|
||||||
|
return story.symbol_resolution === "none" ? "unmapped" : "market";
|
||||||
|
}
|
||||||
|
const visible = story.resolved_symbols.slice(0, 4);
|
||||||
|
const extra = story.resolved_symbols.length - visible.length;
|
||||||
|
return extra > 0 ? `${visible.join(", ")} +${extra}` : visible.join(", ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewsWireStatus = (story: NewsStory): "updated" | "mapped" | "unmapped" => {
|
||||||
|
if (story.updated_ts > story.published_ts) {
|
||||||
|
return "updated";
|
||||||
|
}
|
||||||
|
return story.resolved_symbols.length > 0 ? "mapped" : "unmapped";
|
||||||
|
};
|
||||||
|
|
||||||
|
const openNewsStory = (state: TerminalState, story: NewsStory): void => {
|
||||||
|
state.setSelectedNewsStory(null);
|
||||||
|
state.setSelectedAlert(null);
|
||||||
|
state.setSelectedClassifierHit(null);
|
||||||
|
state.setSelectedSmartMoneyEvent(null);
|
||||||
|
state.setSelectedDarkEvent(null);
|
||||||
|
state.setSelectedNewsStory(story);
|
||||||
|
};
|
||||||
|
|
||||||
const NewsPane = memo(({ state, limit, className }: NewsPaneProps) => {
|
const NewsPane = memo(({ state, limit, className }: NewsPaneProps) => {
|
||||||
const items = limit ? state.filteredNews.slice(0, limit) : state.filteredNews;
|
const items = limit ? state.filteredNews.slice(0, limit) : state.filteredNews;
|
||||||
const canLoadOlder = state.mode === "live" && !limit && items.length > 0;
|
const virtual = useTapeVirtualList(items, state.newsScroll.listRef, getTapeVirtualConfig("news"));
|
||||||
|
const newsHistorySubscription = state.liveSession.manifest.find(
|
||||||
|
(subscription) => subscription.channel === "news"
|
||||||
|
);
|
||||||
|
const newsHistoryKey = newsHistorySubscription
|
||||||
|
? getLiveSubscriptionKey(newsHistorySubscription)
|
||||||
|
: null;
|
||||||
|
const newsHistoryLoading = newsHistoryKey
|
||||||
|
? Boolean(state.liveSession.historyLoading[newsHistoryKey])
|
||||||
|
: false;
|
||||||
|
const newsHistoryError = newsHistoryKey ? state.liveSession.historyErrors[newsHistoryKey] : null;
|
||||||
|
useVirtualHistoryGate(
|
||||||
|
state.mode === "live" && !limit,
|
||||||
|
items.length,
|
||||||
|
virtual.virtualItems.at(-1)?.index ?? -1,
|
||||||
|
() => void state.liveSession.loadOlder("news")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pane
|
<Pane
|
||||||
className={className}
|
className={className}
|
||||||
title="News Wire"
|
title="News Wire"
|
||||||
status={
|
status={
|
||||||
|
<TapeStatus
|
||||||
|
status={state.news.status}
|
||||||
|
lastUpdate={state.news.lastUpdate}
|
||||||
|
replayTime={state.news.replayTime}
|
||||||
|
replayComplete={state.news.replayComplete}
|
||||||
|
paused={state.news.paused}
|
||||||
|
dropped={state.news.dropped}
|
||||||
|
mode={state.mode}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
limit ? (
|
limit ? (
|
||||||
<Link className="terminal-button terminal-link-button" href="/news">
|
<Link className="terminal-button terminal-link-button" href="/news">
|
||||||
View all
|
View all
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="status-inline status-connected">
|
<TapeControls
|
||||||
<span className="status-dot" />
|
mode={state.mode}
|
||||||
<span>{state.mode === "live" ? "Live wire" : "Live-only in v1"}</span>
|
paused={state.news.paused}
|
||||||
</div>
|
onTogglePause={state.news.togglePause}
|
||||||
|
isAtTop={state.newsScroll.isAtTop}
|
||||||
|
missed={state.newsScroll.missed}
|
||||||
|
onJump={state.newsScroll.jumpToTop}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
actions={
|
|
||||||
canLoadOlder ? (
|
|
||||||
<button
|
|
||||||
className="terminal-button"
|
|
||||||
type="button"
|
|
||||||
onClick={() => void state.liveSession.loadOlder("news")}
|
|
||||||
>
|
|
||||||
Older
|
|
||||||
</button>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
<div className="data-table-shell news-wire-shell">
|
||||||
|
{state.mode === "live" && newsHistoryError ? (
|
||||||
|
<div className="history-load-warning" role="status">
|
||||||
|
Older news history failed to load: {newsHistoryError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{state.mode === "live" && newsHistoryLoading ? (
|
||||||
|
<div className="history-load-warning history-load-muted" role="status">
|
||||||
|
Loading older wire history.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{state.mode === "replay" ? (
|
{state.mode === "replay" ? (
|
||||||
<div className="empty">News is live-only in v1.</div>
|
<div className="empty">News is live only in v1.</div>
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
<div className="empty">
|
<div className="empty">
|
||||||
{state.tickerSet.size > 0
|
{state.tickerSet.size > 0
|
||||||
|
|
@ -8313,38 +8395,59 @@ const NewsPane = memo(({ state, limit, className }: NewsPaneProps) => {
|
||||||
: "Waiting for live news stories."}
|
: "Waiting for live news stories."}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="news-list" role="list" aria-label="News stories">
|
<div className="data-table-wrap">
|
||||||
{items.map((story) => (
|
<div className="data-table data-table-news" role="table" aria-label="News wire">
|
||||||
<button
|
<div className="data-table-head" role="row">
|
||||||
className="news-row"
|
<span className="data-table-cell">TIME</span>
|
||||||
key={`${story.trace_id}:${story.updated_ts}:${story.seq}`}
|
<span className="data-table-cell">SOURCE</span>
|
||||||
type="button"
|
<span className="data-table-cell">SYMBOLS</span>
|
||||||
onClick={() => {
|
<span className="data-table-cell">STATE</span>
|
||||||
state.setSelectedNewsStory(null);
|
<span className="data-table-cell">HEADLINE</span>
|
||||||
state.setSelectedAlert(null);
|
<span className="data-table-cell">SUMMARY</span>
|
||||||
state.setSelectedClassifierHit(null);
|
</div>
|
||||||
state.setSelectedSmartMoneyEvent(null);
|
<div className="data-table-scroll" ref={state.newsScroll.setListRef}>
|
||||||
state.setSelectedDarkEvent(null);
|
<div
|
||||||
state.setSelectedNewsStory(story);
|
className="data-table-body"
|
||||||
}}
|
style={{ height: `${virtual.totalSize}px` }}
|
||||||
|
aria-hidden={virtual.virtualItems.length === 0}
|
||||||
>
|
>
|
||||||
<div className="news-row-head">
|
{virtual.virtualItems.map(({ item: story, key, index, start, size }) => {
|
||||||
<h3>{story.headline}</h3>
|
const wireStatus = getNewsWireStatus(story);
|
||||||
<span className="news-row-time">{formatNewsTimestamp(story.published_ts)}</span>
|
return (
|
||||||
</div>
|
<button
|
||||||
<div className="news-row-meta">
|
className={`data-table-row data-table-row-button data-table-row-news data-table-virtual-row${index % 2 === 1 ? " is-even" : ""} news-wire-row-${wireStatus}`}
|
||||||
<span>{story.source}</span>
|
key={key}
|
||||||
{story.resolved_symbols.map((symbol) => (
|
type="button"
|
||||||
<span className="drawer-chip" key={`${story.trace_id}-${symbol}`}>
|
data-index={index}
|
||||||
{symbol}
|
data-row-start={String(start)}
|
||||||
|
data-row-size={String(size)}
|
||||||
|
data-tape-key={key}
|
||||||
|
style={{ transform: `translateY(${start}px)` }}
|
||||||
|
onClick={() => openNewsStory(state, story)}
|
||||||
|
>
|
||||||
|
<span className="data-table-cell data-table-cell-number">
|
||||||
|
{formatNewsTimestamp(story.published_ts)}
|
||||||
|
</span>
|
||||||
|
<span className="data-table-cell">{story.source}</span>
|
||||||
|
<span className="data-table-cell">{formatNewsSymbolsLabel(story)}</span>
|
||||||
|
<span className="data-table-cell">
|
||||||
|
<span className={`news-state news-state-${wireStatus}`}>
|
||||||
|
{wireStatus}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="data-table-cell news-headline-cell">{story.headline}</span>
|
||||||
|
<span className="data-table-cell news-summary-cell">
|
||||||
|
{story.summary || story.provider}
|
||||||
</span>
|
</span>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{!limit && story.summary ? <p className="drawer-note">{story.summary}</p> : null}
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -8736,6 +8839,275 @@ const buildCommandDeckTickers = (state: TerminalState): CommandDeckTicker[] => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CommandPriorityState = "confirm" | "watch" | "hold" | "reject" | "info";
|
||||||
|
|
||||||
|
type CommandPriorityRow = {
|
||||||
|
key: string;
|
||||||
|
ts: number;
|
||||||
|
symbol: string;
|
||||||
|
packet: string;
|
||||||
|
read: string;
|
||||||
|
score: number;
|
||||||
|
invalidation: string;
|
||||||
|
state: CommandPriorityState;
|
||||||
|
onOpen: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clampCommandScore = (value: number): number => {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.min(100, Math.round(value)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandStateFromDirection = (direction: string): CommandPriorityState => {
|
||||||
|
const normalized = normalizeDirection(direction);
|
||||||
|
if (normalized === "bullish") {
|
||||||
|
return "confirm";
|
||||||
|
}
|
||||||
|
if (normalized === "bearish") {
|
||||||
|
return "reject";
|
||||||
|
}
|
||||||
|
return "watch";
|
||||||
|
};
|
||||||
|
|
||||||
|
const inferCommandSymbolFromTrace = (traceId: string): string | null => {
|
||||||
|
const token = traceId
|
||||||
|
.toUpperCase()
|
||||||
|
.split(/[^A-Z0-9]+/)
|
||||||
|
.find((part) => /^[A-Z]{1,6}$/.test(part) && !["ALERT", "FLOW", "SMART"].includes(part));
|
||||||
|
return token ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildCommandPriorityRows = (state: TerminalState): CommandPriorityRow[] => {
|
||||||
|
const rows: CommandPriorityRow[] = [];
|
||||||
|
|
||||||
|
for (const event of state.filteredSmartMoneyEvents.slice(0, 8)) {
|
||||||
|
const primaryScore =
|
||||||
|
event.profile_scores.find((score) => score.profile_id === event.primary_profile_id) ??
|
||||||
|
event.profile_scores[0];
|
||||||
|
const read =
|
||||||
|
primaryScore?.reasons[0] ??
|
||||||
|
(event.primary_profile_id
|
||||||
|
? smartMoneyProfileLabel(event.primary_profile_id)
|
||||||
|
: event.event_kind);
|
||||||
|
rows.push({
|
||||||
|
key: `smart-${event.event_id}-${event.seq}`,
|
||||||
|
ts: event.source_ts,
|
||||||
|
symbol: event.underlying_id.toUpperCase(),
|
||||||
|
packet: event.packet_ids[0] ?? event.event_id,
|
||||||
|
read,
|
||||||
|
score: clampCommandScore((primaryScore?.probability ?? 0) * 100),
|
||||||
|
invalidation:
|
||||||
|
event.packet_ids.length > 0
|
||||||
|
? `${event.packet_ids.length} packet${event.packet_ids.length === 1 ? "" : "s"}`
|
||||||
|
: `${formatFlowMetric(event.features.print_count)} prints`,
|
||||||
|
state: event.abstained ? "hold" : commandStateFromDirection(event.primary_direction),
|
||||||
|
onOpen: () => state.openFromSmartMoneyEvent(event)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const alert of state.filteredAlerts.slice(0, 8)) {
|
||||||
|
const primary = alert.hits[0];
|
||||||
|
const direction = deriveAlertDirection(alert);
|
||||||
|
const severity = normalizeAlertSeverity(alert);
|
||||||
|
rows.push({
|
||||||
|
key: `alert-${alert.trace_id}-${alert.seq}`,
|
||||||
|
ts: alert.source_ts,
|
||||||
|
symbol: inferCommandSymbolFromTrace(alert.trace_id) ?? "ALERT",
|
||||||
|
packet: getAlertFlowPacketRefs(alert)[0] ?? alert.trace_id,
|
||||||
|
read: primary?.explanations?.[0] ?? primary?.classifier_id ?? "Classifier alert",
|
||||||
|
score: clampCommandScore(alert.score),
|
||||||
|
invalidation: `${alert.evidence_refs.length} refs`,
|
||||||
|
state:
|
||||||
|
severity === "high"
|
||||||
|
? commandStateFromDirection(direction)
|
||||||
|
: severity === "medium"
|
||||||
|
? "watch"
|
||||||
|
: "hold",
|
||||||
|
onOpen: () => {
|
||||||
|
state.setSelectedNewsStory(null);
|
||||||
|
state.setSelectedDarkEvent(null);
|
||||||
|
state.setSelectedClassifierHit(null);
|
||||||
|
state.setSelectedSmartMoneyEvent(null);
|
||||||
|
state.setSelectedAlert(alert);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const packet of state.filteredFlow.slice(0, 6)) {
|
||||||
|
const contract = String(packet.features.option_contract_id ?? packet.id);
|
||||||
|
const symbol = extractUnderlying(contract);
|
||||||
|
const notional = parseNumber(packet.features.total_notional, 0);
|
||||||
|
rows.push({
|
||||||
|
key: `flow-${packet.id}-${packet.seq}`,
|
||||||
|
ts: packet.source_ts,
|
||||||
|
symbol,
|
||||||
|
packet: packet.id,
|
||||||
|
read:
|
||||||
|
typeof packet.features.structure_type === "string"
|
||||||
|
? packet.features.structure_type.replace(/_/g, " ")
|
||||||
|
: "Flow packet",
|
||||||
|
score: clampCommandScore(parseNumber(packet.join_quality.nbbo_coverage_ratio, 0) * 100),
|
||||||
|
invalidation: notional > 0 ? `$${formatCompactUsd(notional)}` : "packet fit",
|
||||||
|
state: "watch",
|
||||||
|
onOpen: () => state.setFilterInput(symbol)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const story of state.filteredNews.slice(0, 4)) {
|
||||||
|
rows.push({
|
||||||
|
key: `news-${story.trace_id}-${story.seq}`,
|
||||||
|
ts: story.published_ts,
|
||||||
|
symbol: story.resolved_symbols[0]?.toUpperCase() ?? "WIRE",
|
||||||
|
packet: story.source,
|
||||||
|
read: story.headline,
|
||||||
|
score: story.resolved_symbols.length > 0 ? 55 : 25,
|
||||||
|
invalidation: getNewsWireStatus(story),
|
||||||
|
state: "info",
|
||||||
|
onOpen: () => openNewsStory(state, story)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.sort((a, b) => b.ts - a.ts).slice(0, 8);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommandMetricsStrip = ({ state }: { state: TerminalState }) => {
|
||||||
|
const priorityCount =
|
||||||
|
state.filteredSmartMoneyEvents.length + state.filteredAlerts.length + state.filteredFlow.length;
|
||||||
|
const focus = state.activeTickers.length > 0 ? state.activeTickers.join(", ") : "All symbols";
|
||||||
|
const decision =
|
||||||
|
state.selectedInstrument?.kind === "option-contract"
|
||||||
|
? (state.selectedInstrumentLabel ?? "Contract focus")
|
||||||
|
: `${state.chartTicker.toUpperCase()} / ${formatIntervalLabel(state.chartIntervalMs)}`;
|
||||||
|
const risk =
|
||||||
|
state.filteredAlerts[0]?.severity ??
|
||||||
|
(state.filteredInferredDark.length > 0 ? "dark context" : "no active alert");
|
||||||
|
const metrics = [
|
||||||
|
{
|
||||||
|
label: "Regime",
|
||||||
|
value:
|
||||||
|
state.mode === "live" ? statusLabel(state.liveSession.status, false, state.mode) : "Replay",
|
||||||
|
detail: state.lastSeen ? `last ${formatTime(state.lastSeen)}` : "waiting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Priority",
|
||||||
|
value: `${formatFlowMetric(priorityCount)} events`,
|
||||||
|
detail: focus
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Decision",
|
||||||
|
value: decision,
|
||||||
|
detail: state.selectedInstrument ? "focused instrument" : "chart context"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Risk",
|
||||||
|
value: risk,
|
||||||
|
detail: `${state.filteredNews.length} wire / ${state.filteredInferredDark.length} dark`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="command-metric-strip" aria-label="Session command metrics">
|
||||||
|
{metrics.map((metric) => (
|
||||||
|
<div className="command-metric-cell" key={metric.label}>
|
||||||
|
<span>{metric.label}</span>
|
||||||
|
<strong>{metric.value}</strong>
|
||||||
|
<em>{metric.detail}</em>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommandPriorityBoard = ({ state }: { state: TerminalState }) => {
|
||||||
|
const rows = useMemo(() => buildCommandPriorityRows(state), [state]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pane
|
||||||
|
className="command-priority-pane"
|
||||||
|
title="Priority Board"
|
||||||
|
status={<span className="command-pane-meta">{rows.length} active rows</span>}
|
||||||
|
>
|
||||||
|
{rows.length === 0 ? (
|
||||||
|
<div className="empty">No priority events are available for this scope yet.</div>
|
||||||
|
) : (
|
||||||
|
<div className="command-priority-table" role="table" aria-label="Priority board">
|
||||||
|
<div className="command-priority-row is-head" role="row">
|
||||||
|
{["Time", "Sym", "Packet", "Read", "Score", "Decision", "State"].map((label) => (
|
||||||
|
<span role="columnheader" key={label}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{rows.map((row) => (
|
||||||
|
<button
|
||||||
|
className={`command-priority-row is-${row.state}`}
|
||||||
|
key={row.key}
|
||||||
|
type="button"
|
||||||
|
onClick={row.onOpen}
|
||||||
|
>
|
||||||
|
<time>{formatTime(row.ts)}</time>
|
||||||
|
<strong>{row.symbol}</strong>
|
||||||
|
<span>{row.packet}</span>
|
||||||
|
<span>{row.read}</span>
|
||||||
|
<span
|
||||||
|
className="command-score-meter"
|
||||||
|
style={{ "--score": `${row.score}%` } as CSSProperties}
|
||||||
|
>
|
||||||
|
<i />
|
||||||
|
<em>{row.score}</em>
|
||||||
|
</span>
|
||||||
|
<span>{row.invalidation}</span>
|
||||||
|
<span className={`command-state command-state-${row.state}`}>{row.state}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Pane>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommandDecisionLevels = ({ state }: { state: TerminalState }) => {
|
||||||
|
const topOption = state.filteredOptions[0];
|
||||||
|
const topOptionLabel = topOption
|
||||||
|
? (formatOptionContractLabel(normalizeContractId(topOption.option_contract_id))?.strike ??
|
||||||
|
formatContractLabel(topOption.option_contract_id))
|
||||||
|
: "--";
|
||||||
|
const topAlert = state.filteredAlerts[0];
|
||||||
|
const topDark = state.filteredInferredDark[0];
|
||||||
|
const rows = [
|
||||||
|
["Focus", state.activeTickers.length > 0 ? state.activeTickers.join(", ") : state.chartTicker],
|
||||||
|
["Contract", state.selectedInstrumentLabel ?? topOptionLabel],
|
||||||
|
["Chart", `${state.chartTicker.toUpperCase()} ${formatIntervalLabel(state.chartIntervalMs)}`],
|
||||||
|
[
|
||||||
|
"Evidence",
|
||||||
|
topAlert
|
||||||
|
? `${normalizeAlertSeverity(topAlert)} alert at ${formatTime(topAlert.source_ts)}`
|
||||||
|
: topDark
|
||||||
|
? `${humanizeClassifierId(topDark.type)} ${formatConfidence(topDark.confidence)}`
|
||||||
|
: "waiting"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pane
|
||||||
|
className="command-levels-pane"
|
||||||
|
title="Decision Levels"
|
||||||
|
status={<span className="command-pane-meta">current scope</span>}
|
||||||
|
>
|
||||||
|
<dl className="command-level-list">
|
||||||
|
{rows.map(([label, value]) => (
|
||||||
|
<div key={label}>
|
||||||
|
<dt>{label}</dt>
|
||||||
|
<dd>{value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</Pane>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CommandDeckHeader = ({ state }: { state: TerminalState }) => {
|
const CommandDeckHeader = ({ state }: { state: TerminalState }) => {
|
||||||
const focus = state.activeTickers.length > 0 ? state.activeTickers.join(", ") : state.chartTicker;
|
const focus = state.activeTickers.length > 0 ? state.activeTickers.join(", ") : state.chartTicker;
|
||||||
const activeTickerFilter = state.filterInput.trim();
|
const activeTickerFilter = state.filterInput.trim();
|
||||||
|
|
@ -8749,7 +9121,7 @@ const CommandDeckHeader = ({ state }: { state: TerminalState }) => {
|
||||||
<div className="compact-command-topline">
|
<div className="compact-command-topline">
|
||||||
<div className="compact-command-title">
|
<div className="compact-command-title">
|
||||||
<span>islandflow</span>
|
<span>islandflow</span>
|
||||||
<strong>Command Deck</strong>
|
<strong>Market Command</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="compact-command-controls" aria-label="Active command deck controls">
|
<div className="compact-command-controls" aria-label="Active command deck controls">
|
||||||
<span className={`command-chip command-chip-${state.liveSession.status}`}>
|
<span className={`command-chip command-chip-${state.liveSession.status}`}>
|
||||||
|
|
@ -8796,12 +9168,12 @@ const CommandDeckHeader = ({ state }: { state: TerminalState }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TickerRail = ({ state }: { state: TerminalState }) => {
|
const CommandSymbolRail = ({ state }: { state: TerminalState }) => {
|
||||||
const tickers = useMemo(() => buildCommandDeckTickers(state), [state]);
|
const tickers = useMemo(() => buildCommandDeckTickers(state), [state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="command-ticker-rail" aria-label="Live ticker focus rail">
|
<div className="command-symbol-rail" aria-label="Live ticker focus rail">
|
||||||
<div className="command-ticker-track">
|
<div className="command-symbol-track">
|
||||||
{tickers.map((ticker) => {
|
{tickers.map((ticker) => {
|
||||||
const direction = ticker.move === null ? "flat" : ticker.move >= 0 ? "up" : "down";
|
const direction = ticker.move === null ? "flat" : ticker.move >= 0 ? "up" : "down";
|
||||||
const equity = state.filteredEquities.find(
|
const equity = state.filteredEquities.find(
|
||||||
|
|
@ -8809,23 +9181,23 @@ const TickerRail = ({ state }: { state: TerminalState }) => {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`command-ticker-card is-${direction}`}
|
className={`command-symbol-row is-${direction}`}
|
||||||
key={ticker.symbol}
|
key={ticker.symbol}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
equity ? state.focusEquityTicker(equity) : state.setFilterInput(ticker.symbol)
|
equity ? state.focusEquityTicker(equity) : state.setFilterInput(ticker.symbol)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="command-ticker-symbol">{ticker.symbol}</span>
|
<span className="command-symbol-name">{ticker.symbol}</span>
|
||||||
<span className="command-ticker-price">
|
<span className="command-symbol-price">
|
||||||
{ticker.price === null ? "--" : `$${formatPrice(ticker.price)}`}
|
{ticker.price === null ? "--" : `$${formatPrice(ticker.price)}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="command-ticker-move">
|
<span className="command-symbol-move">
|
||||||
{ticker.move === null
|
{ticker.move === null
|
||||||
? "Move n/a"
|
? "Move n/a"
|
||||||
: `${direction === "up" ? "Up" : "Down"} ${formatPct(Math.abs(ticker.move))}`}
|
: `${direction === "up" ? "Up" : "Down"} ${formatPct(Math.abs(ticker.move))}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="command-ticker-meta">
|
<span className="command-symbol-meta">
|
||||||
{ticker.options} opt / {ticker.alerts} alerts
|
{ticker.options} opt / {ticker.alerts} alerts
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -9122,6 +9494,141 @@ const ReplayConsole = memo(({ state }: { state: TerminalState }) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const OpraIntakeRail = ({ state }: { state: TerminalState }) => {
|
||||||
|
const contractActive = state.selectedInstrument?.kind === "option-contract";
|
||||||
|
const contractLabel = contractActive
|
||||||
|
? (state.selectedInstrumentLabel ?? "Contract focus")
|
||||||
|
: "No contract focus";
|
||||||
|
const filterCount = countActiveFlowFilterGroups(state.flowFilters);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="opra-command-rail" aria-label="OPRA intake controls">
|
||||||
|
<div className="opra-command-cell">
|
||||||
|
<span>Mode</span>
|
||||||
|
<strong>{state.mode === "live" ? "OPRA Live" : "Replay"}</strong>
|
||||||
|
<em>{state.options.lastUpdate ? formatTime(state.options.lastUpdate) : "waiting"}</em>
|
||||||
|
</div>
|
||||||
|
<div className="opra-command-cell">
|
||||||
|
<span>Scope</span>
|
||||||
|
<strong>
|
||||||
|
{state.activeTickers.length > 0 ? state.activeTickers.join(", ") : "All symbols"}
|
||||||
|
</strong>
|
||||||
|
<em>{state.filteredOptions.length} prints visible</em>
|
||||||
|
</div>
|
||||||
|
<div className="opra-command-cell">
|
||||||
|
<span>Contract</span>
|
||||||
|
<strong>{contractLabel}</strong>
|
||||||
|
<em>{contractActive ? "click clear to release" : "select any option row"}</em>
|
||||||
|
</div>
|
||||||
|
<div className="opra-command-cell">
|
||||||
|
<span>Flow Filters</span>
|
||||||
|
<strong>{filterCount > 0 ? `${filterCount} active` : "baseline"}</strong>
|
||||||
|
<em>{state.flowFilters.view === "raw" ? "all prints" : "signal view"}</em>
|
||||||
|
</div>
|
||||||
|
<div className="opra-command-actions">
|
||||||
|
<button
|
||||||
|
className={`terminal-button contract-filter-button${contractActive ? " is-active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
disabled={!contractActive}
|
||||||
|
onClick={() => state.setSelectedInstrument(null)}
|
||||||
|
title={
|
||||||
|
contractActive ? "Clear active contract filter" : "Focus a contract in the OPRA tape"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="contract-filter-button-label">
|
||||||
|
{contractActive ? "Clear Contract" : "Contract Focus"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<FlowFilterPopover filters={state.flowFilters} onChange={state.setFlowFilters} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewsControlRails = ({ state }: { state: TerminalState }) => {
|
||||||
|
const sources = useMemo(() => {
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
for (const story of state.filteredNews) {
|
||||||
|
counts.set(story.source, (counts.get(story.source) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
return Array.from(counts.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 5);
|
||||||
|
}, [state.filteredNews]);
|
||||||
|
const symbols = useMemo(() => {
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
for (const story of state.filteredNews) {
|
||||||
|
for (const symbol of story.resolved_symbols) {
|
||||||
|
const normalized = symbol.toUpperCase();
|
||||||
|
counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(counts.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 8);
|
||||||
|
}, [state.filteredNews]);
|
||||||
|
const statusRows = [
|
||||||
|
{
|
||||||
|
label: "Wire",
|
||||||
|
value:
|
||||||
|
state.mode === "live"
|
||||||
|
? statusLabel(state.news.status, state.news.paused, state.mode)
|
||||||
|
: "Live only",
|
||||||
|
detail: state.news.lastUpdate ? formatTime(state.news.lastUpdate) : "waiting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Stories",
|
||||||
|
value: formatFlowMetric(state.filteredNews.length),
|
||||||
|
detail: state.activeTickers.length > 0 ? state.activeTickers.join(", ") : "all symbols"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "History",
|
||||||
|
value: state.mode === "live" ? "scroll gate" : "disabled",
|
||||||
|
detail: state.newsScroll.isAtTop ? "at live head" : `${state.newsScroll.missed} queued`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="wire-control-rails" aria-label="Wire control rails">
|
||||||
|
<div className="wire-status-rail">
|
||||||
|
{statusRows.map((row) => (
|
||||||
|
<div className="wire-rail-row" key={row.label}>
|
||||||
|
<span>{row.label}</span>
|
||||||
|
<strong>{row.value}</strong>
|
||||||
|
<em>{row.detail}</em>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="wire-source-rail" aria-label="News sources">
|
||||||
|
<span className="wire-rail-label">Sources</span>
|
||||||
|
{sources.length === 0 ? (
|
||||||
|
<span className="wire-empty-label">waiting</span>
|
||||||
|
) : (
|
||||||
|
sources.map(([source, count]) => (
|
||||||
|
<span className="wire-source-pill" key={source}>
|
||||||
|
<strong>{source}</strong>
|
||||||
|
<em>{count}</em>
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="wire-symbol-rail" aria-label="News symbols">
|
||||||
|
<span className="wire-rail-label">Symbols</span>
|
||||||
|
{symbols.length === 0 ? (
|
||||||
|
<span className="wire-empty-label">unmapped</span>
|
||||||
|
) : (
|
||||||
|
symbols.map(([symbol, count]) => (
|
||||||
|
<button key={symbol} type="button" onClick={() => state.setFilterInput(symbol)}>
|
||||||
|
<strong>{symbol}</strong>
|
||||||
|
<em>{count}</em>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function SyntheticControlDock() {
|
function SyntheticControlDock() {
|
||||||
const visible = isSyntheticAdminVisible();
|
const visible = isSyntheticAdminVisible();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
@ -9680,16 +10187,22 @@ function TerminalChrome({ children }: { children: ReactNode }) {
|
||||||
export function OverviewRoute() {
|
export function OverviewRoute() {
|
||||||
const state = useTerminal();
|
const state = useTerminal();
|
||||||
return (
|
return (
|
||||||
<PageFrame title="Home">
|
<PageFrame title="Market Command" eyebrow="Dashboard" variant="dashboard">
|
||||||
<div className="command-deck-shell">
|
<div className="market-command-shell">
|
||||||
<CommandDeckHeader state={state} />
|
<CommandDeckHeader state={state} />
|
||||||
<TickerRail state={state} />
|
<CommandMetricsStrip state={state} />
|
||||||
<div className="command-deck-grid">
|
<CommandSymbolRail state={state} />
|
||||||
<OptionsPane state={state} limit={14} />
|
<div className="market-command-grid">
|
||||||
<ChartPane state={state} title="Price / Flow" />
|
<CommandPriorityBoard state={state} />
|
||||||
<AlertsPane state={state} limit={8} withStrip className="command-signals-pane" />
|
<ChartPane state={state} title="Chart Context" />
|
||||||
|
<CommandDecisionLevels state={state} />
|
||||||
|
<OptionsPane
|
||||||
|
state={state}
|
||||||
|
limit={12}
|
||||||
|
title="Recent Contracts"
|
||||||
|
className="command-contracts-pane"
|
||||||
|
/>
|
||||||
<FeedHealthPane state={state} />
|
<FeedHealthPane state={state} />
|
||||||
<DarkPane state={state} limit={8} className="command-dark-pane" />
|
|
||||||
<EventContextPane state={state} />
|
<EventContextPane state={state} />
|
||||||
<HomeReplayRail state={state} />
|
<HomeReplayRail state={state} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -9701,8 +10214,9 @@ export function OverviewRoute() {
|
||||||
export function NewsRoute() {
|
export function NewsRoute() {
|
||||||
const state = useTerminal();
|
const state = useTerminal();
|
||||||
return (
|
return (
|
||||||
<PageFrame title="News">
|
<PageFrame title="Wire Control" eyebrow="News" variant="news">
|
||||||
<div className="page-grid page-grid-news">
|
<div className="wire-control-shell">
|
||||||
|
<NewsControlRails state={state} />
|
||||||
<NewsPane state={state} className="news-pane-full" />
|
<NewsPane state={state} className="news-pane-full" />
|
||||||
</div>
|
</div>
|
||||||
</PageFrame>
|
</PageFrame>
|
||||||
|
|
@ -9712,34 +10226,13 @@ export function NewsRoute() {
|
||||||
export function OptionsRoute() {
|
export function OptionsRoute() {
|
||||||
const state = useTerminal();
|
const state = useTerminal();
|
||||||
return (
|
return (
|
||||||
<PageFrame
|
<PageFrame title="OPRA Intake" eyebrow="Options" variant="options">
|
||||||
title="Options"
|
<div className="opra-intake-shell">
|
||||||
actions={
|
<OpraIntakeRail state={state} />
|
||||||
<>
|
<div className="opra-intake-grid">
|
||||||
<button
|
<OptionsPane state={state} title="OPRA Tape" className="opra-options-pane" />
|
||||||
className={`terminal-button contract-filter-button${state.selectedInstrument?.kind === "option-contract" ? " is-active" : ""}`}
|
<FlowPane state={state} title="Packet Fit" className="opra-flow-pane" />
|
||||||
type="button"
|
</div>
|
||||||
disabled={state.selectedInstrument?.kind !== "option-contract"}
|
|
||||||
onClick={() => state.setSelectedInstrument(null)}
|
|
||||||
title={
|
|
||||||
state.selectedInstrument?.kind === "option-contract"
|
|
||||||
? "Clear active contract filter"
|
|
||||||
: "Contract filter activates when you focus a contract in the Options tape"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="contract-filter-button-label">
|
|
||||||
{state.selectedInstrument?.kind === "option-contract"
|
|
||||||
? state.selectedInstrumentLabel
|
|
||||||
: "Contract Filter"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<FlowFilterPopover filters={state.flowFilters} onChange={state.setFlowFilters} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="page-grid page-grid-options">
|
|
||||||
<OptionsPane state={state} />
|
|
||||||
<FlowPane state={state} title="Packets" />
|
|
||||||
</div>
|
</div>
|
||||||
</PageFrame>
|
</PageFrame>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
870
docs/turns/2026-06-13-1022-rebuild-terminal-routes-mock9.html
Normal file
870
docs/turns/2026-06-13-1022-rebuild-terminal-routes-mock9.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue