diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 63aebba..9af4502 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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-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} @@ -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-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-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-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} diff --git a/DESIGN.md b/DESIGN.md index d1f2a68..b427ebe 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -75,11 +75,11 @@ components: typography: "{typography.label}" rounded: "{rounded.md}" padding: "12px 14px" - pane-surface: + terminal-section: backgroundColor: "{colors.bg-pane}" textColor: "{colors.text-primary}" - rounded: "{rounded.xl}" - padding: "16px 18px" + rounded: "0" + padding: "8px 10px" status-chip: backgroundColor: "{colors.bg-soft}" 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 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. **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. - Monospace-assisted precision for time, numeric, and status data. - 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 @@ -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. - **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. -- **Background:** layered dark surfaces (`#111820`, `#0d141b`) with restrained top-to-bottom sheen. -- **Shadow Strategy:** no default card shadow; only overlays and floating inspectors use lift shadows. -- **Border:** subtle perimeter lines (`rgba(255,255,255,0.08)` baseline). -- **Internal Padding:** primarily `16px-18px` with tighter inner rhythm (`8px-12px`) for controls. +- **Default Shape:** square terminal sections (`0px radius`) with border-block dividers. +- **Background:** flat dark surfaces (`#111820`, `#0d141b`) with tonal contrast only. +- **Shadow Strategy:** no shadows on production route sections; only drawers, popovers, and tooltips use lift shadows. +- **Border:** top/bottom rules carry separation; perimeter boxes are reserved for true tables, overlays, and controls that need hit-area clarity. +- **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 @@ -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** 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** 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: @@ -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** 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** default to card grids, decorative shadows, hero treatments, or static mock copy in production routes. diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index b2ca445..85651ad 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -18,6 +18,9 @@ --red-soft: oklch(0.68 0.16 28 / 0.12); --blue: oklch(0.72 0.13 247); --blue-soft: oklch(0.72 0.13 247 / 0.11); + --route-accent: var(--accent); + --route-accent-soft: var(--accent-soft); + --route-line: var(--border); --drawer-width: min(320px, calc(100vw - 28px)); --topbar-height: 64px; } @@ -36,9 +39,7 @@ body { min-height: 100vh; font-family: var(--font-sans), sans-serif; color: var(--text); - background: - radial-gradient(circle at top left, oklch(0.78 0.12 74 / 0.08), transparent 30%), - linear-gradient(180deg, oklch(0.15 0.012 250) 0%, oklch(0.11 0.01 250) 100%); + background: oklch(0.105 0.012 250); } a { @@ -88,7 +89,7 @@ input { .terminal-shell { position: relative; min-height: 100vh; - background: linear-gradient(180deg, oklch(0.14 0.011 250) 0%, oklch(0.11 0.01 250) 100%); + background: linear-gradient(180deg, oklch(0.125 0.012 250) 0%, oklch(0.095 0.01 250) 100%); } .terminal-nav-drawer { @@ -236,9 +237,8 @@ input { justify-content: space-between; gap: 16px; padding: 10px 20px; - background: oklch(0.15 0.012 250 / 0.96); + background: oklch(0.12 0.012 250 / 0.98); border-bottom: 1px solid var(--border); - backdrop-filter: blur(12px); } .terminal-topbar-leading { @@ -516,12 +516,30 @@ input { .terminal-content { min-width: 0; - padding: 24px clamp(16px, 2vw, 28px) 24px; + padding: 14px clamp(12px, 1.7vw, 24px) 18px; } .page-shell { display: grid; - gap: 18px; + gap: 10px; +} + +.page-shell-dashboard { + --route-accent: var(--accent); + --route-accent-soft: var(--accent-soft); + --route-line: oklch(0.78 0.12 74 / 0.2); +} + +.page-shell-options { + --route-accent: var(--green); + --route-accent-soft: var(--green-soft); + --route-line: oklch(0.74 0.13 151 / 0.22); +} + +.page-shell-news { + --route-accent: var(--blue); + --route-accent-soft: var(--blue-soft); + --route-line: oklch(0.72 0.13 247 / 0.22); } .page-header { @@ -529,6 +547,25 @@ input { align-items: center; justify-content: space-between; gap: 16px; + min-height: 44px; + padding: 2px 0 9px; + border-bottom: 1px solid var(--route-line); +} + +.page-heading { + min-width: 0; + display: flex; + align-items: baseline; + gap: 10px; + flex-wrap: wrap; +} + +.page-eyebrow { + color: var(--route-accent); + font-family: var(--font-mono), monospace; + font-size: 0.68rem; + letter-spacing: 0.12em; + text-transform: uppercase; } .page-title, @@ -542,8 +579,10 @@ h3 { } .page-title { - font-size: clamp(1.75rem, 2.4vw, 2.3rem); - letter-spacing: 0.06em; + font-family: var(--font-mono), monospace; + font-size: 1.18rem; + line-height: 1.1; + letter-spacing: 0.04em; } .page-actions { @@ -765,6 +804,71 @@ h3 { grid-template-columns: minmax(0, 1fr); } +.opra-intake-shell, +.wire-control-shell { + display: grid; + gap: 8px; +} + +.opra-command-rail { + min-width: 0; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)) auto; + border-top: 1px solid var(--route-line); + border-bottom: 1px solid var(--border); +} + +.opra-command-cell { + min-width: 0; + min-height: 64px; + display: grid; + align-content: center; + gap: 4px; + padding: 9px 12px; + border-right: 1px solid var(--border); +} + +.opra-command-cell span, +.opra-command-cell em { + color: var(--text-faint); + font-family: var(--font-mono), monospace; + font-size: 0.66rem; + font-style: normal; + text-transform: uppercase; +} + +.opra-command-cell strong { + min-width: 0; + overflow: hidden; + color: var(--text); + font-family: var(--font-mono), monospace; + font-size: 0.8rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.opra-command-actions { + min-width: 220px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 10px 0 10px 12px; +} + +.opra-intake-grid { + display: grid; + grid-template-columns: minmax(560px, 1.22fr) minmax(460px, 0.98fr); + gap: 8px; + align-items: stretch; + min-height: calc(100vh - var(--topbar-height) - 178px); +} + +.opra-options-pane, +.opra-flow-pane { + min-height: 620px; +} + .page-grid-home > :nth-child(3), .page-grid-home > :nth-child(4), .page-grid-options > :nth-child(1), @@ -772,9 +876,10 @@ h3 { grid-column: 1 / -1; } -.command-deck-shell { +.command-deck-shell, +.market-command-shell { display: grid; - gap: 12px; + gap: 8px; } .command-deck-header { @@ -783,10 +888,10 @@ h3 { grid-template-columns: minmax(220px, 0.8fr) minmax(260px, 1fr) auto; gap: 14px; align-items: center; - padding: 13px 14px; - border: 1px solid var(--border); - border-radius: 12px; - background: linear-gradient(180deg, oklch(0.18 0.013 250 / 0.96), oklch(0.145 0.012 250 / 0.96)); + padding: 8px 0; + border-top: 1px solid var(--route-line); + border-bottom: 1px solid var(--border); + background: transparent; } .command-deck-brand { @@ -859,9 +964,8 @@ h3 { flex-direction: column; align-items: stretch; gap: 8px; - padding: 10px 12px; - border-radius: 10px; - background: linear-gradient(180deg, oklch(0.17 0.013 250 / 0.95), oklch(0.13 0.011 250 / 0.95)); + padding: 8px 0; + background: transparent; } .compact-command-topline, @@ -996,74 +1100,353 @@ h3 { background: var(--red-soft); } +.command-metric-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border-top: 1px solid var(--route-line); + border-bottom: 1px solid var(--border); +} + +.command-metric-cell { + min-width: 0; + min-height: 72px; + display: grid; + align-content: center; + gap: 4px; + padding: 10px 12px; + border-right: 1px solid var(--border); +} + +.command-metric-cell:last-child { + border-right: 0; +} + +.command-metric-cell span, +.command-metric-cell em { + color: var(--text-faint); + font-family: var(--font-mono), monospace; + font-size: 0.68rem; + font-style: normal; + text-transform: uppercase; +} + +.command-metric-cell strong { + min-width: 0; + overflow: hidden; + color: var(--text); + font-family: var(--font-mono), monospace; + font-size: 0.86rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.command-symbol-rail, .command-ticker-rail { min-width: 0; overflow: hidden; - border: 1px solid var(--border); - border-radius: 10px; - background: oklch(0.13 0.012 250 / 0.98); + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + background: oklch(0.105 0.01 250 / 0.65); } +.command-symbol-track, .command-ticker-track { display: grid; - grid-auto-columns: minmax(176px, 1fr); + grid-auto-columns: minmax(172px, 1fr); grid-auto-flow: column; - gap: 8px; + gap: 0; overflow-x: auto; - padding: 7px; + padding: 0; } +.command-symbol-row, .command-ticker-card { - min-width: 176px; - min-height: 64px; + min-width: 172px; + min-height: 54px; display: grid; grid-template-columns: 1fr auto; - gap: 4px 9px; + gap: 3px 9px; align-items: center; - border: 1px solid var(--border); - border-radius: 8px; - padding: 8px 10px; - background: oklch(0.17 0.013 250); + border: 0; + border-right: 1px solid var(--border); + border-radius: 0; + padding: 7px 10px; + background: transparent; color: inherit; text-align: left; cursor: pointer; } +.command-symbol-row:hover, +.command-symbol-row:focus-visible, .command-ticker-card:hover, .command-ticker-card:focus-visible { - border-color: var(--border-strong); + background: var(--route-accent-soft); outline: none; } +.command-symbol-name, .command-ticker-symbol { color: var(--text); + font-family: var(--font-mono), monospace; font-weight: 700; } +.command-symbol-price, +.command-symbol-meta, .command-ticker-price, .command-ticker-meta { color: var(--text-dim); + font-family: var(--font-mono), monospace; font-size: 0.72rem; } +.command-symbol-move, .command-ticker-move { justify-self: end; color: var(--text-faint); + font-family: var(--font-mono), monospace; font-size: 0.68rem; } +.command-symbol-row.is-up .command-symbol-move, .command-ticker-card.is-up .command-ticker-move { color: var(--green); } +.command-symbol-row.is-down .command-symbol-move, .command-ticker-card.is-down .command-ticker-move { color: var(--red); } +.command-symbol-meta, .command-ticker-meta { grid-column: 1 / -1; } +.market-command-grid { + display: grid; + grid-template-columns: minmax(420px, 1.14fr) minmax(420px, 1fr) minmax(300px, 0.78fr); + grid-template-areas: + "priority chart levels" + "contracts chart health" + "context replay health"; + gap: 8px; + align-items: stretch; +} + +.market-command-grid > .terminal-pane { + min-height: 0; +} + +.command-priority-pane { + grid-area: priority; + min-height: 330px; +} + +.market-command-grid > :nth-child(2) { + grid-area: chart; + min-height: 520px; +} + +.command-levels-pane { + grid-area: levels; + min-height: 236px; +} + +.command-contracts-pane { + grid-area: contracts; + min-height: 314px; +} + +.command-feed-pane { + grid-area: health; + min-height: 420px; +} + +.command-context-pane { + grid-area: context; + min-height: 230px; +} + +.command-replay-pane { + grid-area: replay; + min-height: 140px; +} + +.market-command-grid .chart-surface { + height: 430px; +} + +.command-priority-table { + min-width: 760px; + overflow-x: auto; + border-top: 1px solid var(--border); +} + +.command-priority-row { + width: 100%; + min-height: 42px; + display: grid; + grid-template-columns: 76px 58px 112px minmax(160px, 1fr) 104px 96px 78px; + gap: 8px; + align-items: center; + padding: 0 10px; + border: 0; + border-bottom: 1px solid oklch(0.72 0.012 250 / 0.09); + background: transparent; + color: var(--text-dim); + text-align: left; +} + +.command-priority-row:not(.is-head) { + cursor: pointer; +} + +.command-priority-row:not(.is-head):hover, +.command-priority-row:not(.is-head):focus-visible { + background: var(--route-accent-soft); + color: var(--text); + outline: none; +} + +.command-priority-row.is-head { + min-height: 30px; + color: var(--text-faint); + font-family: var(--font-mono), monospace; + font-size: 0.62rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.command-priority-row time, +.command-priority-row span, +.command-priority-row strong { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.command-priority-row time, +.command-priority-row span { + font-family: var(--font-mono), monospace; + font-size: 0.7rem; +} + +.command-priority-row strong { + color: var(--text); + font-family: var(--font-mono), monospace; + font-size: 0.76rem; +} + +.command-priority-row.is-reject strong, +.command-priority-row.is-reject { + color: oklch(0.78 0.12 28); +} + +.command-priority-row.is-confirm strong { + color: var(--green); +} + +.command-score-meter { + display: grid; + grid-template-columns: minmax(0, 1fr) 28px; + gap: 7px; + align-items: center; +} + +.command-score-meter i { + height: 4px; + background: + linear-gradient(90deg, var(--route-accent) var(--score), transparent 0), + oklch(0.97 0.008 250 / 0.09); +} + +.command-score-meter em { + color: var(--text-dim); + font-style: normal; + text-align: right; +} + +.command-state { + width: fit-content; + max-width: 100%; + min-height: 22px; + display: inline-flex; + align-items: center; + border: 1px solid var(--border); + padding: 2px 7px; + color: var(--text-dim); + font-size: 0.64rem; + text-transform: uppercase; +} + +.command-state-confirm { + border-color: oklch(0.74 0.13 151 / 0.34); + color: var(--green); + background: var(--green-soft); +} + +.command-state-reject { + border-color: oklch(0.68 0.16 28 / 0.34); + color: var(--red); + background: var(--red-soft); +} + +.command-state-watch, +.command-state-hold { + border-color: var(--border-strong); + color: var(--accent); + background: var(--accent-soft); +} + +.command-state-info { + border-color: oklch(0.72 0.13 247 / 0.34); + color: var(--blue); + background: var(--blue-soft); +} + +.command-level-list { + display: grid; + margin: 0; +} + +.command-level-list div { + min-height: 44px; + display: grid; + grid-template-columns: 86px minmax(0, 1fr); + gap: 8px; + align-items: center; + padding: 7px 10px; + border-bottom: 1px solid oklch(0.72 0.012 250 / 0.09); +} + +.command-level-list div:last-child { + border-bottom: 0; +} + +.command-level-list dt, +.command-level-list dd { + min-width: 0; + margin: 0; + overflow: hidden; + font-family: var(--font-mono), monospace; + text-overflow: ellipsis; + white-space: nowrap; +} + +.command-level-list dt { + color: var(--text-faint); + font-size: 0.68rem; + text-transform: uppercase; +} + +.command-level-list dd { + color: var(--text); + font-size: 0.76rem; +} + .command-deck-grid { display: grid; grid-template-columns: minmax(360px, 1.12fr) minmax(420px, 1.38fr) minmax(300px, 0.9fr); @@ -1277,9 +1660,11 @@ h3 { height: 100%; display: flex; flex-direction: column; - border: 1px solid var(--border); - border-radius: 14px; - background: var(--bg-pane); + border: 0; + border-top: 1px solid var(--route-line); + border-bottom: 1px solid var(--border); + border-radius: 0; + background: oklch(0.13 0.012 250 / 0.74); overflow: hidden; } @@ -1288,9 +1673,10 @@ h3 { align-items: center; justify-content: space-between; gap: 12px; - padding: 15px 18px; + min-height: 38px; + padding: 8px 10px; border-bottom: 1px solid var(--border); - background: oklch(0.2 0.012 250 / 0.38); + background: oklch(0.12 0.012 250 / 0.86); } .terminal-pane-title-row { @@ -1301,10 +1687,11 @@ h3 { } .terminal-pane-title { - font-family: var(--font-sans), sans-serif; - font-size: 0.94rem; + font-family: var(--font-mono), monospace; + font-size: 0.72rem; font-weight: 600; - letter-spacing: 0.08em; + letter-spacing: 0.12em; + text-transform: uppercase; } .terminal-pane-status { @@ -1327,8 +1714,8 @@ h3 { flex: 1 1 auto; min-height: 0; flex-direction: column; - gap: 14px; - padding: 16px 18px 18px; + gap: 8px; + padding: 8px 0 0; } .chart-panel { @@ -1366,9 +1753,12 @@ h3 { .chart-surface { width: 100%; height: 460px; - border-radius: 12px; - border: 1px solid var(--border); - background: var(--bg-pane-2); + border-radius: 0; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + border-left: 0; + border-right: 0; + background: oklch(0.105 0.01 250); overflow: hidden; } @@ -1581,7 +1971,7 @@ h3 { overflow-y: hidden; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); - background: oklch(0.14 0.01 250 / 0.72); + background: oklch(0.105 0.01 250 / 0.92); } .data-table { @@ -1598,7 +1988,7 @@ h3 { min-height: 0; overflow-y: auto; overflow-x: hidden; - background-color: oklch(0.12 0.01 250); + background-color: oklch(0.105 0.01 250); } .data-table-body { @@ -1606,19 +1996,7 @@ h3 { min-width: 100%; --tape-row-height: 36px; --tape-row-double-height: 72px; - background: - repeating-linear-gradient( - to bottom, - oklch(0.98 0.008 250 / 0.01) 0, - oklch(0.98 0.008 250 / 0.01) calc(var(--tape-row-height) - 1px), - oklch(0.72 0.012 250 / 0.08) calc(var(--tape-row-height) - 1px), - oklch(0.72 0.012 250 / 0.08) var(--tape-row-height), - oklch(0.98 0.008 250 / 0.018) var(--tape-row-height), - oklch(0.98 0.008 250 / 0.018) calc(var(--tape-row-double-height) - 1px), - oklch(0.72 0.012 250 / 0.08) calc(var(--tape-row-double-height) - 1px), - oklch(0.72 0.012 250 / 0.08) var(--tape-row-double-height) - ), - oklch(0.12 0.01 250); + background: oklch(0.105 0.01 250); } .data-table-options { @@ -1645,6 +2023,10 @@ h3 { min-width: 820px; } +.data-table-news { + min-width: 1120px; +} + .data-table-head, .data-table-row { display: grid; @@ -1657,7 +2039,7 @@ h3 { height: 30px; padding: 0 10px; border-bottom: 1px solid oklch(0.72 0.012 250 / 0.12); - background: oklch(0.15 0.012 250 / 0.96); + background: oklch(0.125 0.012 250 / 0.98); color: var(--text-faint); font-size: 0.64rem; font-weight: 700; @@ -1670,7 +2052,7 @@ h3 { padding: 0 10px; border: 0; border-bottom: 1px solid oklch(0.72 0.012 250 / 0.08); - background: oklch(0.98 0.008 250 / 0.008); + background: transparent; color: inherit; font: inherit; text-align: left; @@ -1712,6 +2094,10 @@ h3 { height: 44px; } +.data-table-row-news { + height: 52px; +} + .data-table-flow .data-table-body, .data-table-alerts .data-table-body, .data-table-classifier .data-table-body, @@ -1720,6 +2106,11 @@ h3 { --tape-row-double-height: 88px; } +.data-table-news .data-table-body { + --tape-row-height: 52px; + --tape-row-double-height: 104px; +} + .data-table-row-classified { background: linear-gradient( @@ -1819,6 +2210,14 @@ h3 { ) minmax(74px, 0.65fr) minmax(260px, 2fr); } +.data-table-news .data-table-head, +.data-table-news .data-table-row { + grid-template-columns: minmax(86px, 0.75fr) minmax(92px, 0.75fr) minmax(118px, 0.9fr) minmax( + 80px, + 0.65fr + ) minmax(320px, 2.2fr) minmax(260px, 1.6fr); +} + .data-table-cell { min-width: 0; overflow: hidden; @@ -2391,57 +2790,188 @@ h3 { text-decoration: none; } -.news-list { - display: flex; - flex-direction: column; - gap: 10px; -} - -.news-row { - width: 100%; - display: flex; - flex-direction: column; +.wire-control-rails { + min-width: 0; + display: grid; + grid-template-columns: minmax(260px, 0.8fr) minmax(260px, 0.75fr) minmax(360px, 1fr); gap: 8px; - padding: 14px 16px; - border: 1px solid var(--border); - border-radius: 12px; - background: oklch(0.18 0.012 250 / 0.6); - color: var(--text); - text-align: left; - transition: - border-color 150ms ease, - background 150ms ease; } -.news-row:hover { - border-color: var(--accent-soft); - background: oklch(0.2 0.015 250 / 0.75); +.wire-status-rail, +.wire-source-rail, +.wire-symbol-rail { + min-width: 0; + border-top: 1px solid var(--route-line); + border-bottom: 1px solid var(--border); + background: oklch(0.105 0.01 250 / 0.45); } -.news-row-head, -.news-row-meta { +.wire-status-rail { + display: grid; +} + +.wire-rail-row { + min-width: 0; + min-height: 34px; + display: grid; + grid-template-columns: 70px minmax(0, 1fr) minmax(72px, 0.8fr); + gap: 8px; + align-items: center; + padding: 6px 10px; + border-bottom: 1px solid oklch(0.72 0.012 250 / 0.09); +} + +.wire-rail-row:last-child { + border-bottom: 0; +} + +.wire-source-rail, +.wire-symbol-rail { display: flex; align-items: center; - justify-content: space-between; - gap: 10px; - flex-wrap: wrap; + gap: 0; + overflow-x: auto; } -.news-row h3 { - margin: 0; - font-size: 0.96rem; - font-weight: 600; -} - -.news-row-time { - color: var(--text-dim); +.wire-rail-label, +.wire-empty-label, +.wire-source-pill, +.wire-symbol-rail button, +.wire-rail-row span, +.wire-rail-row strong, +.wire-rail-row em { font-family: var(--font-mono), monospace; - font-size: 0.78rem; } -.news-row-meta { +.wire-rail-label { + flex: 0 0 auto; + min-height: 100%; + display: inline-flex; + align-items: center; + padding: 0 10px; + border-right: 1px solid var(--border); + color: var(--text-faint); + font-size: 0.66rem; + text-transform: uppercase; +} + +.wire-empty-label { + padding: 0 10px; + color: var(--text-faint); + font-size: 0.7rem; +} + +.wire-source-pill, +.wire-symbol-rail button { + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 7px; + padding: 0 10px; + border: 0; + border-right: 1px solid var(--border); + background: transparent; + color: var(--text-dim); + white-space: nowrap; +} + +.wire-symbol-rail button { + cursor: pointer; +} + +.wire-symbol-rail button:hover, +.wire-symbol-rail button:focus-visible { + background: var(--route-accent-soft); + color: var(--text); + outline: none; +} + +.wire-source-pill strong, +.wire-symbol-rail strong, +.wire-rail-row strong { + color: var(--text); + font-size: 0.76rem; +} + +.wire-source-pill em, +.wire-symbol-rail em, +.wire-rail-row span, +.wire-rail-row em { + color: var(--text-faint); + font-size: 0.66rem; + font-style: normal; + text-transform: uppercase; +} + +.news-pane-full { + min-height: calc(100vh - var(--topbar-height) - 190px); +} + +.news-wire-shell { + min-height: 0; +} + +.history-load-muted { + border-color: oklch(0.72 0.13 247 / 0.32); + background: oklch(0.2 0.05 247 / 0.52); + color: oklch(0.86 0.08 247); +} + +.news-headline-cell, +.news-summary-cell { + white-space: normal; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.news-headline-cell { + -webkit-line-clamp: 2; + color: var(--text); + font-size: 0.74rem; + line-height: 1.28; +} + +.news-summary-cell { + -webkit-line-clamp: 2; + color: var(--text-dim); + font-size: 0.7rem; + line-height: 1.28; +} + +.news-state { + width: fit-content; + max-width: 100%; + min-height: 21px; + display: inline-flex; + align-items: center; + border: 1px solid var(--border); + padding: 2px 7px; + font-family: var(--font-mono), monospace; + font-size: 0.62rem; + text-transform: uppercase; +} + +.news-state-mapped { + border-color: oklch(0.74 0.13 151 / 0.34); + color: var(--green); + background: var(--green-soft); +} + +.news-state-updated { + border-color: oklch(0.72 0.13 247 / 0.34); + color: var(--blue); + background: var(--blue-soft); +} + +.news-state-unmapped { + border-color: var(--border-strong); + color: var(--accent); + background: var(--accent-soft); +} + +.news-wire-row-unmapped { color: var(--text-dim); - font-size: 0.78rem; } .news-drawer-body a { @@ -2637,6 +3167,35 @@ h3 { "dark dark" "replay replay"; } + + .market-command-grid { + grid-template-columns: minmax(0, 1fr) minmax(340px, 0.8fr); + grid-template-areas: + "priority levels" + "chart chart" + "contracts health" + "context replay"; + } + + .wire-control-rails { + grid-template-columns: minmax(0, 1fr); + } + + .opra-command-rail { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .opra-command-actions { + grid-column: 1 / -1; + justify-content: flex-start; + min-width: 0; + padding: 10px 0; + } + + .opra-intake-grid { + grid-template-columns: minmax(0, 1fr); + min-height: 0; + } } @media (max-width: 980px) { @@ -2687,6 +3246,18 @@ h3 { "dark"; } + .market-command-grid { + grid-template-columns: minmax(0, 1fr); + grid-template-areas: + "priority" + "chart" + "levels" + "contracts" + "health" + "context" + "replay"; + } + .command-deck-grid > .terminal-pane { min-height: 0; } @@ -2701,6 +3272,17 @@ h3 { min-height: 0; } + .market-command-grid > .terminal-pane, + .opra-options-pane, + .opra-flow-pane, + .news-pane-full { + min-height: 0; + } + + .command-metric-strip { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .terminal-topbar { align-items: center; justify-content: space-between; @@ -2761,7 +3343,7 @@ h3 { } .page-title { - font-size: 1.55rem; + font-size: 1.08rem; line-height: 1.06; } @@ -2809,6 +3391,44 @@ h3 { grid-auto-columns: minmax(164px, 78vw); } + .command-symbol-track { + grid-auto-columns: minmax(164px, 78vw); + } + + .opra-command-rail, + .command-metric-strip { + grid-template-columns: minmax(0, 1fr); + } + + .opra-command-cell, + .command-metric-cell { + border-right: 0; + border-bottom: 1px solid var(--border); + } + + .opra-command-cell:last-of-type, + .command-metric-cell:last-child { + border-bottom: 0; + } + + .opra-command-actions { + flex-direction: column; + align-items: stretch; + } + + .opra-command-actions .flow-filter-popover, + .opra-command-actions .terminal-button { + width: 100%; + } + + .wire-rail-row { + grid-template-columns: 68px minmax(0, 1fr); + } + + .wire-rail-row em { + grid-column: 2; + } + .terminal-pane-title-row { flex-direction: column; align-items: flex-start; @@ -2888,7 +3508,7 @@ h3 { } .terminal-pane { - border-radius: 12px; + border-radius: 0; } .terminal-pane-head, @@ -2967,7 +3587,8 @@ h3 { } .data-table-options, - .data-table-flow { + .data-table-flow, + .data-table-news { min-width: 1080px; } @@ -2988,10 +3609,18 @@ h3 { height: 48px; } + .data-table-row-news { + height: 56px; + } + .command-deck-grid .chart-surface { height: 320px; } + .market-command-grid .chart-surface { + height: 320px; + } + .command-health-row { grid-template-columns: minmax(94px, 1fr) 92px; } @@ -3158,8 +3787,7 @@ h3 { overflow: hidden; border: 1px solid var(--border); border-radius: 12px; - background: - linear-gradient(180deg, oklch(0.19 0.015 246 / 0.98), oklch(0.135 0.012 246 / 0.98)); + background: linear-gradient(180deg, oklch(0.19 0.015 246 / 0.98), oklch(0.135 0.012 246 / 0.98)); } .mock-panel-label { @@ -3718,8 +4346,12 @@ h3 { height: 28px; border: 1px solid var(--border); border-radius: 999px; - background: - linear-gradient(90deg, var(--blue-soft) 0 54%, var(--accent-soft) 54% 66%, oklch(0.97 0.008 250 / 0.035) 66%); + background: linear-gradient( + 90deg, + var(--blue-soft) 0 54%, + var(--accent-soft) 54% 66%, + oklch(0.97 0.008 250 / 0.035) 66% + ); } .mock-replay-track-redesign i { @@ -4704,7 +5336,12 @@ h3 { height: 28px; border: 1px solid var(--mock-line); border-radius: 999px; - background: linear-gradient(90deg, var(--mock-info-soft) 0 54%, var(--mock-accent-soft) 54% 66%, var(--mock-surface-2) 66%); + background: linear-gradient( + 90deg, + var(--mock-info-soft) 0 54%, + var(--mock-accent-soft) 54% 66%, + var(--mock-surface-2) 66% + ); } .mock-replay-rail i { @@ -4807,7 +5444,11 @@ h3 { border: 1px solid var(--mock-line-strong); border-radius: 12px; padding: 13px; - background: color-mix(in oklch, var(--mock-surface) 84%, var(--mock-accent) calc(var(--heat) * 0.16%)); + background: color-mix( + in oklch, + var(--mock-surface) 84%, + var(--mock-accent) calc(var(--heat) * 0.16%) + ); } .mock-territory-node strong, @@ -4851,7 +5492,11 @@ h3 { .mock-meter i { height: 8px; border-radius: 999px; - background: linear-gradient(90deg, var(--mock-accent) var(--value), var(--mock-line) var(--value)); + background: linear-gradient( + 90deg, + var(--mock-accent) var(--value), + var(--mock-line) var(--value) + ); } .mock-meter em { @@ -5056,7 +5701,11 @@ h3 { border-right: 1px solid var(--mock-line); padding: 12px; background: - linear-gradient(180deg, color-mix(in oklch, var(--mock-accent) calc(var(--weight) * 0.1%), transparent), transparent 78%), + linear-gradient( + 180deg, + color-mix(in oklch, var(--mock-accent) calc(var(--weight) * 0.1%), transparent), + transparent 78% + ), transparent; } @@ -5604,9 +6253,7 @@ h3 { gap: 7px; border-right: 1px solid var(--mock-line); padding: 12px; - background: - linear-gradient(180deg, var(--mock-accent-soft), transparent 72%), - transparent; + background: linear-gradient(180deg, var(--mock-accent-soft), transparent 72%), transparent; } .mock-risk-level:last-child { @@ -5614,9 +6261,7 @@ h3 { } .mock-risk-level.is-against { - background: - linear-gradient(180deg, var(--mock-bad-soft), transparent 72%), - transparent; + background: linear-gradient(180deg, var(--mock-bad-soft), transparent 72%), transparent; } .mock-risk-level span { diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index d396602..b5bf7d0 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -554,6 +554,11 @@ describe("fixed tape virtualization config", () => { overscan: 24, 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", () => { - it("exposes Home, Options, and News as top-level destinations", () => { + it("exposes Dashboard, Options, and News as top-level destinations", () => { expect(NAV_ITEMS).toEqual([ - { href: "/", label: "Home" }, + { href: "/", label: "Dashboard" }, { href: "/options", label: "Options" }, { href: "/news", label: "News" } ]); diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index ae00ab7..52e7af7 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -139,7 +139,7 @@ const LIVE_SESSION_HOT_CHANNELS = new Set([ "equity-overlay" ]); -type TapeVirtualPane = "options" | "equities" | "flow" | "alerts" | "classifier" | "dark"; +type TapeVirtualPane = "options" | "equities" | "flow" | "alerts" | "classifier" | "dark" | "news"; type TapeVirtualListConfig = { rowHeight: number; @@ -153,7 +153,8 @@ const TAPE_VIRTUAL_CONFIG: Record = { flow: { rowHeight: 44, overscan: 24, debugLabel: "flow" }, alerts: { rowHeight: 44, overscan: 24, debugLabel: "alerts" }, 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 => @@ -5602,6 +5603,7 @@ const useTerminalState = () => { const darkScroll = useListScroll(); const alertsScroll = useListScroll(); const classifierScroll = useListScroll(); + const newsScroll = useListScroll(); const optionsAnchor = useScrollAnchor(optionsScroll.listRef, optionsScroll.isAtTopRef); const equitiesAnchor = useScrollAnchor(equitiesScroll.listRef, equitiesScroll.isAtTopRef); @@ -5609,6 +5611,7 @@ const useTerminalState = () => { const darkAnchor = useScrollAnchor(darkScroll.listRef, darkScroll.isAtTopRef); const alertsAnchor = useScrollAnchor(alertsScroll.listRef, alertsScroll.isAtTopRef); const classifierAnchor = useScrollAnchor(classifierScroll.listRef, classifierScroll.isAtTopRef); + const newsAnchor = useScrollAnchor(newsScroll.listRef, newsScroll.isAtTopRef); const disableReplayGrouping = useCallback(() => null, []); const optionQueryParams = useMemo>( () => buildOptionTapeQueryParams(effectiveOptionPrintFilters, optionScope), @@ -5791,6 +5794,18 @@ const useTerminalState = () => { shouldHold: () => !flowScroll.isAtTopRef.current, resumeSignal: flowScroll.resumeTick }); + const liveNews = usePausableTapeView({ + 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( () => @@ -5831,14 +5846,7 @@ const useTerminalState = () => { ) : equityJoins; const flowFeed = mode === "live" ? liveFlow : flow; - const newsFeed = - mode === "live" - ? toStaticTapeState( - liveSession.status, - composeTapeItems([], liveSession.news, liveSession.newsHistory), - liveSession.lastUpdate - ) - : toStaticTapeState("disconnected", [], null); + const newsFeed = mode === "live" ? liveNews : toStaticTapeState("disconnected", [], null); const alertsFeed = mode === "live" ? toStaticTapeState( @@ -5896,6 +5904,10 @@ const useTerminalState = () => { classifierAnchor.apply(); }, [smartMoneyFeed.items, classifierHitsFeed.items, classifierAnchor.apply]); + useLayoutEffect(() => { + newsAnchor.apply(); + }, [newsFeed.items, newsAnchor.apply]); + const nbboMap = useMemo(() => { const map = new Map(); for (const quote of nbboFeed.items) { @@ -7248,6 +7260,7 @@ const useTerminalState = () => { darkScroll, alertsScroll, classifierScroll, + newsScroll, options: optionsFeed, equities: equitiesFeed, equityJoins: equityJoinsFeed, @@ -7320,22 +7333,30 @@ const useTerminal = (): TerminalState => { }; export const NAV_ITEMS = [ - { href: "/", label: "Home" }, + { href: "/", label: "Dashboard" }, { href: "/options", label: "Options" }, { href: "/news", label: "News" } ] as const; +type PageFrameVariant = "default" | "dashboard" | "options" | "news"; + type PageFrameProps = { title: string; + eyebrow?: string; + variant?: PageFrameVariant; actions?: 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 ( -
+
-

{title}

+
+ {eyebrow ? {eyebrow} : null} +

{title}

+
{actions ?
{actions}
: null}
{children} @@ -7611,9 +7632,11 @@ const ShellMetricStrip = () => { type OptionsPaneProps = { state: TerminalState; 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 virtual = useTapeVirtualList( items, @@ -7638,7 +7661,8 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => { return ( { +const FlowPane = memo(({ state, limit, title = "Flow", className }: FlowPaneProps) => { const items = limit ? state.filteredFlow.slice(0, limit) : state.filteredFlow; const virtual = useTapeVirtualList(items, state.flowScroll.listRef, getTapeVirtualConfig("flow")); useVirtualHistoryGate( @@ -7986,6 +8011,7 @@ const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => { return ( { + 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 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 ( + } + actions={ limit ? ( View all ) : ( -
- - {state.mode === "live" ? "Live wire" : "Live-only in v1"} -
+ ) } - actions={ - canLoadOlder ? ( - - ) : null - } > - {state.mode === "replay" ? ( -
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."} -
- ) : ( -
- {items.map((story) => ( - + ); + })} +
- {!limit && story.summary ?

{story.summary}

: null} - - ))} -
- )} + + + )} + ); }); @@ -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 ( +
+ {metrics.map((metric) => ( +
+ {metric.label} + {metric.value} + {metric.detail} +
+ ))} +
+ ); +}; + +const CommandPriorityBoard = ({ state }: { state: TerminalState }) => { + const rows = useMemo(() => buildCommandPriorityRows(state), [state]); + + return ( + {rows.length} active rows} + > + {rows.length === 0 ? ( +
No priority events are available for this scope yet.
+ ) : ( +
+
+ {["Time", "Sym", "Packet", "Read", "Score", "Decision", "State"].map((label) => ( + + {label} + + ))} +
+ {rows.map((row) => ( + + ))} +
+ )} +
+ ); +}; + +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 ( + current scope} + > +
+ {rows.map(([label, value]) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+ ); +}; + const CommandDeckHeader = ({ state }: { state: TerminalState }) => { const focus = state.activeTickers.length > 0 ? state.activeTickers.join(", ") : state.chartTicker; const activeTickerFilter = state.filterInput.trim(); @@ -8749,7 +9121,7 @@ const CommandDeckHeader = ({ state }: { state: TerminalState }) => {
islandflow - Command Deck + Market Command
@@ -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]); return ( -
-
+
+
{tickers.map((ticker) => { const direction = ticker.move === null ? "flat" : ticker.move >= 0 ? "up" : "down"; const equity = state.filteredEquities.find( @@ -8809,23 +9181,23 @@ const TickerRail = ({ state }: { state: TerminalState }) => { ); return ( @@ -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 ( +
+
+ Mode + {state.mode === "live" ? "OPRA Live" : "Replay"} + {state.options.lastUpdate ? formatTime(state.options.lastUpdate) : "waiting"} +
+
+ Scope + + {state.activeTickers.length > 0 ? state.activeTickers.join(", ") : "All symbols"} + + {state.filteredOptions.length} prints visible +
+
+ Contract + {contractLabel} + {contractActive ? "click clear to release" : "select any option row"} +
+
+ Flow Filters + {filterCount > 0 ? `${filterCount} active` : "baseline"} + {state.flowFilters.view === "raw" ? "all prints" : "signal view"} +
+
+ + +
+
+ ); +}; + +const NewsControlRails = ({ state }: { state: TerminalState }) => { + const sources = useMemo(() => { + const counts = new Map(); + 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(); + 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 ( +
+
+ {statusRows.map((row) => ( +
+ {row.label} + {row.value} + {row.detail} +
+ ))} +
+
+ Sources + {sources.length === 0 ? ( + waiting + ) : ( + sources.map(([source, count]) => ( + + {source} + {count} + + )) + )} +
+
+ Symbols + {symbols.length === 0 ? ( + unmapped + ) : ( + symbols.map(([symbol, count]) => ( + + )) + )} +
+
+ ); +}; + function SyntheticControlDock() { const visible = isSyntheticAdminVisible(); const [open, setOpen] = useState(false); @@ -9680,16 +10187,22 @@ function TerminalChrome({ children }: { children: ReactNode }) { export function OverviewRoute() { const state = useTerminal(); return ( - -
+ +
- -
- - - + + +
+ + + + -
@@ -9701,8 +10214,9 @@ export function OverviewRoute() { export function NewsRoute() { const state = useTerminal(); return ( - -
+ +
+
@@ -9712,34 +10226,13 @@ export function NewsRoute() { export function OptionsRoute() { const state = useTerminal(); return ( - - - - - } - > -
- - + +
+ +
+ + +
); diff --git a/docs/turns/2026-06-13-1022-rebuild-terminal-routes-mock9.html b/docs/turns/2026-06-13-1022-rebuild-terminal-routes-mock9.html new file mode 100644 index 0000000..0b8590e --- /dev/null +++ b/docs/turns/2026-06-13-1022-rebuild-terminal-routes-mock9.html @@ -0,0 +1,870 @@ + + + + + + Rebuild Terminal Routes Around Mock9 + + + +
+
+ Repository Turn Document +

Rebuilt Dashboard, Options, and News Around Mock9-Mock12

+

Completed the production web pivot to dense, flat, route-specific terminal surfaces while preserving production subscriptions, drawers, route helpers, TanStack virtualization, history scroll gates, and redirects.

+
+ +
+
CompletedJune 13, 2026 at 10:22 AM ET
+
Beads Issueislandflow-xkq
+
Routes/, /options, /news
+
Branchmock-redesign
+
+ +
+

Summary

+

Dashboard is now Market Command, Options is now OPRA Intake, and News is now Wire Control. The shared production terminal visual language moved away from rounded pane/card composition toward flat border-block sections, compact headers, dense table rows, and route-specific accent surfaces.

+
+ +
+

Changes Made

+
    +
  • Updated DESIGN.md to name mock9 through mock12 as the production aesthetic center and mock5 as the Options workflow reference.
  • +
  • Changed primary navigation from Home to Dashboard while keeping the same route paths.
  • +
  • Added route variants to the shared page frame so Dashboard, Options, and News can carry distinct styling without duplicating layout mechanics.
  • +
  • Rebuilt Dashboard with command metrics, a production-derived priority board, decision levels, chart context, feed health, recent contracts, replay state, and evidence context.
  • +
  • Rebuilt Options around OPRA Intake controls while preserving the real OptionsPane, FlowPane, contract focus, filters, and virtualized rows.
  • +
  • Rebuilt News as Wire Control with source, symbol, and status rails plus a virtualized news table and the existing story drawer.
  • +
  • Added getTapeVirtualConfig("news"), newsScroll, scroll anchoring, and live-history gate behavior for the news wire.
  • +
+
+ +
+

Context

+

The prior production UI already had the right data model and live session behavior, but the surface still leaned on rounded panes, stacked list cards, and generic dashboard framing. This task moves production toward the denser mock9-mock12 visual system while keeping mock routes as references only.

+
+ +
+

Important Implementation Details

+
    +
  • No backend APIs, package types, or route contracts changed.
  • +
  • News now uses the same pausable live view and scroll-anchor strategy as the tape-style panes.
  • +
  • The priority board is built from real smart-money events, alerts, flow packets, and news stories.
  • +
  • Drawers and popovers remain visually separated because they float over active workflow content.
  • +
  • The in-app Browser backend was unavailable during final verification, so Browser visual inspection is listed as incomplete below.
  • +
+
+ +
+

Relevant Diff Snippets

+

Focused excerpts rendered with @pierre/diffs/ssr using preloadPatchFile({ patch, options: {} }). The full implementation diff is intentionally not embedded to keep this document reviewable.

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Expected Impact for End-Users

+

Users should land in a more operational terminal: Dashboard shows the active market command picture, Options focuses on OPRA intake and packet fit, and News behaves like a dense wire console. The new layout reduces visual ornament and keeps live decision data closer together.

+
+ +
+

Validation

+
    +
  • bun test apps/web/app/terminal.test.ts apps/web/app/routes.test.ts: passed, 78 tests.
  • +
  • bun --cwd=apps/web run build: passed.
  • +
  • Started the web app on http://localhost:3001 because port 3000 was occupied.
  • +
  • HTTP smoke checks: /, /options, and /news returned 200; /tape returned 307 to /options.
  • +
  • Static route content checks confirmed the new labels: Market Command, OPRA Intake, Wire Control, Priority Board, OPRA Tape, Packet Fit, Sources, Symbols, and News Wire.
  • +
  • git diff --check: passed before turn-document generation.
  • +
+
+ +
+

Issues, Limitations, and Mitigations

+
    +
  • The in-app Browser backend reported no available iab browser, so desktop/mobile visual inspection in Browser could not be completed in this run.
  • +
  • Because Browser visual inspection was blocked, overlap and chart-canvas checks are covered by build/static validation but still deserve a real browser pass when the backend is available.
  • +
  • Local live data availability depends on the backend and feeds; empty states remain important for no-data sessions.
  • +
+
+ +
+

Follow-up Work

+
    +
  • Run the requested Browser desktop/mobile inspection once the in-app Browser backend is available.
  • +
  • Consider adding route-level visual regression screenshots for Dashboard, Options, and News.
  • +
  • Watch the new News scroll gate under high-volume wire bursts with live backend data.
  • +
+
+
+ +