diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 365ddaa..7a0fe2d 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -23,7 +23,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-hoh","title":"clarify turn-doc exemptions and ambiguity rule","description":"Update AGENTS.md turn documentation rules so minor/trivial checklist takes precedence, ambiguous cases require user check-in, and completion rule applies only when turn docs are required.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-23T23:02:10Z","created_by":"dirtydishes","updated_at":"2026-05-23T23:02:10Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"islandflow-7ez","title":"rename tape to options and replace web rail with drawer shell","description":"Implement the web and desktop route transition from /tape to /options, keep /tape as a compatibility redirect, replace the persistent web rail with a shared sticky header plus overlay drawer, and update validation/docs to match.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T23:30:06Z","created_by":"dirtydishes","updated_at":"2026-05-23T23:38:59Z","started_at":"2026-05-23T23:30:24Z","closed_at":"2026-05-23T23:38:59Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"islandflow-hoh","title":"clarify turn-doc exemptions and ambiguity rule","description":"Update AGENTS.md turn documentation rules so minor/trivial checklist takes precedence, ambiguous cases require user check-in, and completion rule applies only when turn docs are required.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-23T23:02:10Z","created_by":"dirtydishes","updated_at":"2026-05-23T23:02:30Z","closed_at":"2026-05-23T23:02:30Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-t8b","title":"Update GitHub Pages docs URL target","description":"Adjust the docs Pages publish workflow so the deployed landing behavior explicitly targets dirtydishes.github.io/islandflow/docs and keeps the docs payload path consistent.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T21:18:04Z","created_by":"dirtydishes","updated_at":"2026-05-23T21:18:59Z","started_at":"2026-05-23T21:18:06Z","closed_at":"2026-05-23T21:18:59Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-kgu","title":"Reconcile PR #8 branch with current main","description":"Why this issue exists and what needs to be done: user requested reconciliation for PR #8. Identify the PR #8 branch, merge/rebase with current main, resolve conflicts, validate, and push the updated branch so the PR can merge cleanly.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T20:14:36Z","created_by":"dirtydishes","updated_at":"2026-05-23T20:24:29Z","started_at":"2026-05-23T20:14:39Z","closed_at":"2026-05-23T20:24:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-l9h","title":"stop persisting non-signal option prints in clickhouse","description":"Why: non-signal option prints are storage noise and should not be persisted by default.\\n\\nWhat: add OPTIONS_PERSIST_SIGNAL_ONLY env flag (default true), gate option_print inserts in ingest-options, add tests for persistence behavior, update env examples, and document one-off cleanup SQL for existing non-signal rows.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T03:02:32Z","created_by":"dirtydishes","updated_at":"2026-05-23T03:06:34Z","started_at":"2026-05-23T03:02:35Z","closed_at":"2026-05-23T03:06:34Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -80,6 +81,7 @@ {"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:39:58Z","closed_at":"2026-05-05T05:39:58Z","close_reason":"Completed terminal smart-money profile migration","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"auto-import","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"islandflow-3by","title":"add interaction coverage for terminal navigation drawer","description":"Add browser- or DOM-level coverage for the shared terminal header drawer so open/close behavior, Escape dismissal, backdrop dismissal, and route-change dismissal are exercised beyond pure route helper tests.","status":"open","priority":3,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-23T23:35:57Z","created_by":"dirtydishes","updated_at":"2026-05-23T23:35:57Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-gm0","title":"Default turn-doc diffs to @pierre/diffs","description":"Why this issue exists and what needs to be done\\n\\nUpdate AGENTS.md turn-documentation guidance to prefer @pierre/diffs output with an explicit fallback path when unavailable, and include the related package manifest/lock updates in the same change set.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T22:51:57Z","created_by":"dirtydishes","updated_at":"2026-05-23T22:52:23Z","started_at":"2026-05-23T22:52:00Z","closed_at":"2026-05-23T22:52:23Z","close_reason":"completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-hpf","title":"add anatomy explainer for options print and smart money flow","description":"Create a standalone docs/anatomy.html reference page that explains the end-to-end lifecycle of an options print through enrichment, signal filtering, compute clustering, flow packet creation, smart-money evaluation, classifier hits, alerts, and API/live consumption. The page should be polished, user-readable, and visually strong enough to serve as a reusable reference artifact for both technical and non-technical readers.","notes":"Added docs/anatomy.html as a standalone reference page for the options-print to smart-money pipeline, styled in the repo product register and layered for executive, mixed technical, and operator-level readers. Regenerated docs/index.html so the page is discoverable from the docs surface.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-23T02:18:48Z","created_by":"dirtydishes","updated_at":"2026-05-23T02:24:58Z","started_at":"2026-05-23T02:18:53Z","closed_at":"2026-05-23T02:24:58Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-4ca","title":"Publish May 21 standup git summary","description":"Create the daily standup-ready git activity summary for 2026-05-21, save the HTML artifact under docs/general, add the required turn document, and push the result so the automation leaves a durable record.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-22T13:03:00Z","created_by":"dirtydishes","updated_at":"2026-05-22T13:05:05Z","started_at":"2026-05-22T13:03:03Z","closed_at":"2026-05-22T13:05:05Z","close_reason":"Created the 2026-05-21 standup summary in docs/general, added the required turn document, and prepared the repo for commit/push.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 9781c00..d8166b8 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -24,6 +24,6 @@ This workspace packages a thin Electron shell around the hosted Islandflow app. ## Development Notes -- `ISLANDFLOW_DESKTOP_START_URL` controls which trusted app URL Electron loads. +- `ISLANDFLOW_DESKTOP_START_URL` controls which trusted app URL Electron loads. Prefer `/options` for deep links; `/tape` remains supported and redirects in the web app for compatibility. - `NEXT_PUBLIC_API_URL` remains a web-app setting and should typically be `https://flow.deltaisland.io` when developing the local UI inside Electron. - `assets/` currently contains placeholders only; a real `.icns` icon is deferred. diff --git a/apps/desktop/src/security.test.ts b/apps/desktop/src/security.test.ts index 3fe3e23..dacabcb 100644 --- a/apps/desktop/src/security.test.ts +++ b/apps/desktop/src/security.test.ts @@ -8,7 +8,11 @@ import { } from "./security.js"; describe("desktop URL policy", () => { - it("allows the hosted production origin", () => { + it("allows the hosted production origin on /options", () => { + expect(isTrustedAppUrl("https://flow.deltaisland.io/options?symbol=SPY")).toBe(true); + }); + + it("keeps /tape trusted as a compatibility path on the same origin", () => { expect(isTrustedAppUrl("https://flow.deltaisland.io/tape?symbol=SPY")).toBe(true); }); @@ -37,5 +41,8 @@ describe("desktop URL policy", () => { expect(resolveDesktopStartUrl(undefined)).toBe(DESKTOP_PRODUCTION_URL); expect(resolveDesktopStartUrl("https://example.com")).toBe(DESKTOP_PRODUCTION_URL); expect(resolveDesktopStartUrl("http://127.0.0.1:3000")).toBe("http://127.0.0.1:3000"); + expect(resolveDesktopStartUrl("https://flow.deltaisland.io/options")).toBe( + "https://flow.deltaisland.io/options" + ); }); }); diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index cf6746b..8c449c1 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -18,7 +18,7 @@ --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); - --rail-width: 236px; + --drawer-width: min(320px, calc(100vw - 28px)); --topbar-height: 64px; } @@ -86,22 +86,43 @@ input { } .terminal-shell { + position: relative; min-height: 100vh; - display: grid; - grid-template-columns: var(--rail-width) minmax(0, 1fr); background: linear-gradient(180deg, oklch(0.14 0.011 250) 0%, oklch(0.11 0.01 250) 100%); } -.terminal-rail { - position: sticky; - top: 0; - height: 100vh; - padding: 22px 18px; +.terminal-nav-drawer { + position: fixed; + inset: 0 auto 0 0; + z-index: 45; + width: var(--drawer-width); + padding: 20px 18px 18px; display: flex; flex-direction: column; gap: 20px; background: linear-gradient(180deg, oklch(0.16 0.012 250 / 0.98), oklch(0.13 0.011 250 / 0.98)); border-right: 1px solid var(--border); + box-shadow: 0 28px 72px rgba(0, 0, 0, 0.48); +} + +.terminal-drawer-backdrop { + position: fixed; + inset: 0; + z-index: 40; + border: 0; + background: rgba(3, 5, 8, 0.62); + cursor: pointer; +} + +.terminal-drawer-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.terminal-drawer-close { + flex: 0 0 auto; } .terminal-brand { @@ -198,6 +219,7 @@ input { .terminal-frame { min-width: 0; + min-height: 100vh; display: grid; grid-template-rows: minmax(var(--topbar-height), auto) minmax(0, 1fr); } @@ -208,11 +230,39 @@ input { z-index: 20; display: flex; align-items: center; - justify-content: flex-end; - gap: 12px; + justify-content: space-between; + gap: 16px; padding: 10px 20px; background: oklch(0.15 0.012 250 / 0.96); border-bottom: 1px solid var(--border); + backdrop-filter: blur(12px); +} + +.terminal-topbar-leading { + display: flex; + align-items: center; + gap: 12px; + flex: 0 0 auto; +} + +.terminal-menu-trigger { + display: inline-flex; + align-items: center; + gap: 10px; + min-width: 104px; +} + +.terminal-menu-trigger-icon { + display: inline-grid; + gap: 4px; +} + +.terminal-menu-trigger-icon span { + display: block; + width: 14px; + height: 1px; + border-radius: 999px; + background: currentColor; } .status-dot, @@ -463,7 +513,7 @@ input { .terminal-content { min-width: 0; - padding: 24px 24px 24px; + padding: 24px clamp(16px, 2vw, 28px) 24px; } .page-shell { @@ -689,8 +739,8 @@ h3 { grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr); } -.page-grid-tape { - grid-template-columns: minmax(0, 1.5fr) minmax(320px, 1fr); +.page-grid-options { + grid-template-columns: minmax(0, 1fr); } .page-grid-signals { @@ -714,7 +764,7 @@ h3 { .page-grid-home > :nth-child(3), .page-grid-home > :nth-child(4), -.page-grid-tape > :nth-child(1), +.page-grid-options > :nth-child(1), .page-grid-replay > :nth-child(1) { grid-column: 1 / -1; } @@ -963,11 +1013,11 @@ h3 { grid-row: 2; } -.page-grid-tape > :first-child { +.page-grid-options > :first-child { height: clamp(460px, 64vh, 880px); } -.page-grid-tape > :not(:first-child) { +.page-grid-options > :not(:first-child) { height: clamp(400px, 50vh, 680px); } @@ -1965,68 +2015,23 @@ h3 { } @media (max-width: 1180px) { - .terminal-shell { - grid-template-columns: 1fr; - } - - .terminal-rail { - position: sticky; - top: 0; - z-index: 35; - height: auto; - display: grid; - grid-template-columns: minmax(170px, auto) minmax(0, 1fr); - align-items: center; - gap: 14px 18px; - padding: 14px 16px; - border-right: 0; - border-bottom: 1px solid var(--border); - } - - .terminal-brand { - gap: 2px; + .terminal-nav-drawer { + width: min(300px, calc(100vw - 24px)); } .terminal-brand-name { font-size: 1.25rem; } - .terminal-nav { - display: flex; - min-width: 0; - gap: 8px; - overflow-x: auto; - scrollbar-width: thin; - } - - .terminal-nav-link { - flex: 0 0 auto; - white-space: nowrap; - } - - .shell-metrics { - grid-column: 1 / -1; - margin-top: 0; - grid-template-columns: repeat(4, minmax(136px, 1fr)); - gap: 8px; - overflow-x: auto; - padding-bottom: 2px; - scrollbar-width: thin; - } - .shell-metric { min-width: 136px; padding: 10px 12px; } - - .terminal-topbar { - position: static; - } } @media (max-width: 980px) { .page-grid-home, - .page-grid-tape, + .page-grid-options, .page-grid-signals, .page-grid-charts, .page-grid-replay, @@ -2037,7 +2042,7 @@ h3 { .page-grid-home > :nth-child(3), .page-grid-home > :nth-child(4), - .page-grid-tape > :nth-child(1), + .page-grid-options > :nth-child(1), .page-grid-replay > :nth-child(1) { grid-column: auto; grid-row: auto; @@ -2049,8 +2054,8 @@ h3 { .page-grid-home > :nth-child(4), .page-grid-signals > .terminal-pane, .page-grid-replay > :not(:first-child), - .page-grid-tape > :first-child, - .page-grid-tape > :not(:first-child), + .page-grid-options > :first-child, + .page-grid-options > :not(:first-child), .page-grid-charts > :last-child { height: auto; } @@ -2062,14 +2067,12 @@ h3 { .terminal-topbar { align-items: center; - justify-content: flex-end; + justify-content: space-between; padding: 10px 16px; } .terminal-topbar-actions { justify-content: flex-end; - margin-left: auto; - width: auto; } .terminal-topbar-controls { @@ -2086,11 +2089,9 @@ h3 { background-size: 24px 24px, 24px 24px, 100% 100%, auto; } - .terminal-rail { - position: static; - grid-template-columns: minmax(0, 1fr); - gap: 12px; - padding: 12px; + .terminal-nav-drawer { + width: min(340px, calc(100vw - 12px)); + padding: 16px 12px 12px; } .terminal-brand { @@ -2111,20 +2112,6 @@ h3 { padding-bottom: 2px; } - .terminal-nav-link { - padding: 12px; - font-size: 0.72rem; - } - - .shell-metrics { - display: flex; - gap: 8px; - } - - .shell-metric { - flex: 0 0 156px; - } - .terminal-content { padding: 16px 10px 22px; } @@ -2160,6 +2147,10 @@ h3 { padding: 12px 10px; } + .terminal-topbar-leading { + width: 100%; + } + .terminal-button, .mode-button, .filter-clear, @@ -2186,8 +2177,14 @@ h3 { align-items: stretch; } + .terminal-menu-trigger { + width: 100%; + justify-content: center; + } + .terminal-topbar-mode .terminal-button, .terminal-topbar-controls > .terminal-button, + .terminal-topbar-leading > .terminal-button, .page-actions > .terminal-button, .page-actions > .flow-filter-popover { width: 100%; diff --git a/apps/web/app/options/page.tsx b/apps/web/app/options/page.tsx new file mode 100644 index 0000000..abfa3fa --- /dev/null +++ b/apps/web/app/options/page.tsx @@ -0,0 +1,7 @@ +import { OptionsRoute } from "../terminal"; + +export const dynamic = "force-dynamic"; + +export default function Page() { + return ; +} diff --git a/apps/web/app/routes.test.ts b/apps/web/app/routes.test.ts index 55b29e0..e217748 100644 --- a/apps/web/app/routes.test.ts +++ b/apps/web/app/routes.test.ts @@ -28,4 +28,10 @@ describe("legacy page redirects", () => { expect(() => mod.default()).toThrow("NEXT_REDIRECT:/"); expect(redirect).toHaveBeenCalledWith("/"); }); + + it("redirects /tape to /options", async () => { + const mod = await import("./tape/page"); + expect(() => mod.default()).toThrow("NEXT_REDIRECT:/options"); + expect(redirect).toHaveBeenCalledWith("/options"); + }); }); diff --git a/apps/web/app/tape/page.tsx b/apps/web/app/tape/page.tsx index a692698..0c82e4a 100644 --- a/apps/web/app/tape/page.tsx +++ b/apps/web/app/tape/page.tsx @@ -1,7 +1,7 @@ -import { TapeRoute } from "../terminal"; +import { redirect } from "next/navigation"; export const dynamic = "force-dynamic"; export default function Page() { - return ; + redirect("/options"); } diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index 92a9904..eb666c4 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -24,6 +24,7 @@ import { getOptionScope, getLiveFeedStatus, getLiveManifest, + getTerminalNavCurrentHref, getRouteFeatures, getTapeVirtualConfig, mergeHeldTapeHistory, @@ -44,6 +45,7 @@ import { smartMoneyProfileLabel, smartMoneyToneForProfile, getAlertFlowPacketRefs, + normalizeTerminalPathname, resolveAlertFlowPacket, statusLabel, toggleFilterValue @@ -165,18 +167,24 @@ describe("alert context hydration helpers", () => { }); describe("live manifest", () => { - it("includes only tape channels on /tape", () => { + it("includes only options channels on /options", () => { const filters = buildDefaultFlowFilters(); - const channels = getLiveManifest("/tape", "SPY", 60000, filters).map( + const channels = getLiveManifest("/options", "SPY", 60000, filters).map( (subscription) => subscription.channel ); - expect(channels).toEqual(["options", "nbbo", "equities", "flow"]); + expect(channels).toEqual(["options", "nbbo", "flow"]); }); - it("dedupes tape options subscription", () => { + it("keeps /tape as a compatibility alias for /options subscriptions", () => { + expect(getLiveManifest("/tape", "SPY", 60000, buildDefaultFlowFilters())).toEqual( + getLiveManifest("/options", "SPY", 60000, buildDefaultFlowFilters()) + ); + }); + + it("dedupes options subscriptions on /options", () => { const tapeOptionsSubscriptions = getLiveManifest( - "/tape", + "/options", "SPY", 60000, buildDefaultFlowFilters() @@ -184,35 +192,35 @@ describe("live manifest", () => { expect(tapeOptionsSubscriptions).toHaveLength(1); }); - it("keeps option filters on /tape options subscriptions", () => { + it("keeps option filters on /options subscriptions", () => { const filters = { ...buildDefaultFlowFilters(), minNotional: 125_000 }; - const tapeOptionsSubscription = getLiveManifest("/tape", "SPY", 60000, filters).find( + const tapeOptionsSubscription = getLiveManifest("/options", "SPY", 60000, filters).find( (subscription) => subscription.channel === "options" ); expect(tapeOptionsSubscription?.filters).toBe(filters); }); - it("applies global flow filters to flow subscriptions on /tape", () => { + it("applies global flow filters to flow subscriptions on /options", () => { const filters = { ...buildDefaultFlowFilters(), minNotional: 50_000 }; - const tapeFlowSubscription = getLiveManifest("/tape", "SPY", 60000, filters).find( + const tapeFlowSubscription = getLiveManifest("/options", "SPY", 60000, filters).find( (subscription) => subscription.channel === "flow" ); expect(tapeFlowSubscription?.filters).toBe(filters); }); - it("includes scoped option and equity subscriptions", () => { + it("includes scoped option subscriptions on /options", () => { const manifest = getLiveManifest( - "/tape", + "/options", "AAPL", 60000, buildDefaultFlowFilters(), @@ -226,15 +234,11 @@ describe("live manifest", () => { (subscription): subscription is Extract<(typeof manifest)[number], { channel: "options" }> => subscription.channel === "options" ); - const equitiesSubscription = manifest.find( - (subscription): subscription is Extract<(typeof manifest)[number], { channel: "equities" }> => - subscription.channel === "equities" - ); expect(optionsSubscription?.underlying_ids).toEqual(["AAPL"]); expect(optionsSubscription?.option_contract_id).toBe("AAPL-2025-01-17-200-C"); expect(optionsSubscription?.snapshot_limit).toBe(100); - expect(equitiesSubscription?.underlying_ids).toEqual(["AAPL"]); + expect(manifest.some((subscription) => subscription.channel === "equities")).toBe(false); }); it("drops option-print filters for contract-focused options subscriptions but keeps flow filters", () => { @@ -244,7 +248,7 @@ describe("live manifest", () => { optionTypes: ["put"] as const }; const manifest = getLiveManifest( - "/tape", + "/options", "AAPL", 60000, filters, @@ -443,15 +447,21 @@ describe("contract-focused option helpers", () => { }); describe("route feature map", () => { - it("maps /tape to tape panes and dependencies", () => { - const features = getRouteFeatures("/tape"); + it("maps /options to the options and packets panes", () => { + const features = getRouteFeatures("/options"); expect(features.showOptionsPane).toBe(true); - expect(features.showEquitiesPane).toBe(true); + expect(features.showEquitiesPane).toBe(false); expect(features.showFlowPane).toBe(true); expect(features.needsClassifierDecor).toBe(true); expect(features.alerts).toBe(false); }); + it("keeps /tape route compatibility while normalizing to /options", () => { + expect(normalizeTerminalPathname("/tape")).toBe("/options"); + expect(getTerminalNavCurrentHref("/tape")).toBe("/options"); + expect(getRouteFeatures("/tape")).toEqual(getRouteFeatures("/options")); + }); + it("maps /signals to signal panes and dependencies", () => { const features = getRouteFeatures("/signals"); expect(features.showAlertsPane).toBe(true); @@ -506,10 +516,10 @@ describe("dark underlying route dependency helper", () => { }); describe("terminal navigation", () => { - it("exposes Home, Tape, and News as top-level destinations", () => { + it("exposes Home, Options, and News as top-level destinations", () => { expect(NAV_ITEMS).toEqual([ { href: "/", label: "Home" }, - { href: "/tape", label: "Tape" }, + { href: "/options", label: "Options" }, { href: "/news", label: "News" } ]); }); diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 3057f58..3444320 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -186,23 +186,34 @@ export const shouldIncludeEquitiesForDarkUnderlyingFallback = (): boolean => { return false; }; +const CANONICAL_OPTIONS_PATH = "/options"; +const TAPE_COMPAT_PATH = "/tape"; +const KNOWN_TERMINAL_PATHS = new Set([ + CANONICAL_OPTIONS_PATH, + TAPE_COMPAT_PATH, + "/news", + "/signals", + "/charts", + "/replay" +]); + +export const normalizeTerminalPathname = (pathname: string): string => { + if (pathname === TAPE_COMPAT_PATH) { + return CANONICAL_OPTIONS_PATH; + } + return KNOWN_TERMINAL_PATHS.has(pathname) ? pathname : "/"; +}; + export const getRouteFeatures = (pathname: string): RouteFeatures => { const includeEquitiesFallback = shouldIncludeEquitiesForDarkUnderlyingFallback(); - const normalizedPath = - pathname === "/tape" || - pathname === "/news" || - pathname === "/signals" || - pathname === "/charts" || - pathname === "/replay" - ? pathname - : "/"; + const normalizedPath = normalizeTerminalPathname(pathname); switch (normalizedPath) { - case "/tape": + case "/options": return { options: true, nbbo: true, - equities: true, + equities: false, flow: true, news: false, alerts: false, @@ -213,7 +224,7 @@ export const getRouteFeatures = (pathname: string): RouteFeatures => { equityCandles: false, equityOverlay: false, showOptionsPane: true, - showEquitiesPane: true, + showEquitiesPane: false, showFlowPane: true, showNewsPane: false, showAlertsPane: false, @@ -370,6 +381,10 @@ export const getRouteFeatures = (pathname: string): RouteFeatures => { } }; +export const getTerminalNavCurrentHref = (pathname: string): string => { + return normalizeTerminalPathname(pathname); +}; + const EMPTY_ALERT_EVENTS: AlertEvent[] = []; const EMPTY_CLASSIFIER_HIT_EVENTS: ClassifierHitEvent[] = []; const EMPTY_SMART_MONEY_EVENTS: SmartMoneyEvent[] = []; @@ -7170,7 +7185,7 @@ const useTerminal = (): TerminalState => { export const NAV_ITEMS = [ { href: "/", label: "Home" }, - { href: "/tape", label: "Tape" }, + { href: "/options", label: "Options" }, { href: "/news", label: "News" } ] as const; @@ -8812,8 +8827,31 @@ function SyntheticControlDock() { export function TerminalAppShell({ children }: { children: ReactNode }) { const state = useTerminalState(); const pathname = usePathname(); + const [drawerOpen, setDrawerOpen] = useState(false); const tickerFieldId = useId(); const tickerHintId = useId(); + const activeNavHref = getTerminalNavCurrentHref(pathname); + + useEffect(() => { + setDrawerOpen(false); + }, [pathname]); + + useEffect(() => { + if (!drawerOpen) { + return; + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setDrawerOpen(false); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [drawerOpen]); return ( @@ -8821,31 +8859,26 @@ export function TerminalAppShell({ children }: { children: ReactNode }) { Skip to terminal content -
+
+ +
{state.selectedInstrumentLabel && state.selectedInstrument?.kind !== "option-contract" ? ( @@ -8909,6 +8942,53 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
+ {drawerOpen ? ( + <> + +
+ + + + + ) : null} + {state.selectedAlert ? ( @@ -8981,11 +9061,11 @@ export function NewsRoute() { ); } -export function TapeRoute() { +export function OptionsRoute() { const state = useTerminal(); return (