Redesign home command deck

This commit is contained in:
dirtydishes 2026-05-28 05:10:21 -04:00
parent b075a0994c
commit a35a757622
4 changed files with 1325 additions and 24 deletions

View file

@ -24,6 +24,7 @@
{"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-ddm","title":"Redesign home as command deck","description":"Implement the mock1-inspired production command deck on / while preserving focused /options and /news workspaces plus existing legacy redirects. Scope includes apps/web terminal layout, production command-deck CSS, validation, turn documentation, and Forgejo publish.","notes":"Scope: redesign / as a mock1-inspired production command deck using live useTerminal state and existing panes; preserve /options, /news, /mock1, and current legacy redirects. Leave unrelated apps/web/next-env.d.ts and piolium/ changes untouched.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-28T08:59:14Z","created_by":"dirtydishes","updated_at":"2026-05-28T09:09:43Z","started_at":"2026-05-28T08:59:29Z","closed_at":"2026-05-28T09:09:43Z","close_reason":"Implemented / as a mock1-inspired production command deck using live terminal state, preserved focused /options and /news routes plus legacy redirects, validated tests/build/screenshots, and documented the turn.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-4xb","title":"Create dashboard structure mock routes","description":"Prototype four alternate islandflow dashboard structures at /mock1 through /mock4 based on the supplied reference so the main dashboard direction can be evaluated live.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-28T08:30:33Z","created_by":"dirtydishes","updated_at":"2026-05-28T08:38:35Z","started_at":"2026-05-28T08:30:39Z","closed_at":"2026-05-28T08:38:35Z","close_reason":"Added four dashboard mock routes, documented the implementation, and validated build/tests plus route responses.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-4xb","title":"Create dashboard structure mock routes","description":"Prototype four alternate islandflow dashboard structures at /mock1 through /mock4 based on the supplied reference so the main dashboard direction can be evaluated live.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-28T08:30:33Z","created_by":"dirtydishes","updated_at":"2026-05-28T08:38:35Z","started_at":"2026-05-28T08:30:39Z","closed_at":"2026-05-28T08:38:35Z","close_reason":"Added four dashboard mock routes, documented the implementation, and validated build/tests plus route responses.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-1gq","title":"Set up Forgejo-native CI baseline","description":"Create a Forgejo-native CI workflow under .forgejo/workflows that runs the existing fast, high-signal validation checks on pull requests, pushes to main, and manual dispatch. Document the runner label expectations, scope of the job, and manual rerun path in repository docs. Keep heavier container/integration work out of the initial PR gate.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-24T00:31:55Z","created_by":"dirtydishes","updated_at":"2026-05-24T00:36:03Z","closed_at":"2026-05-24T00:36:03Z","close_reason":"Implemented a Forgejo-native CI baseline under .forgejo/workflows, documented runner expectations in the README, and synced the docker workspace snapshot so the fast validate path passes.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-1gq","title":"Set up Forgejo-native CI baseline","description":"Create a Forgejo-native CI workflow under .forgejo/workflows that runs the existing fast, high-signal validation checks on pull requests, pushes to main, and manual dispatch. Document the runner label expectations, scope of the job, and manual rerun path in repository docs. Keep heavier container/integration work out of the initial PR gate.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-24T00:31:55Z","created_by":"dirtydishes","updated_at":"2026-05-24T00:36:03Z","closed_at":"2026-05-24T00:36:03Z","close_reason":"Implemented a Forgejo-native CI baseline under .forgejo/workflows, documented runner expectations in the README, and synced the docker workspace snapshot so the fast validate path passes.","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-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}

View file

@ -769,6 +769,395 @@ h3 {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.command-deck-shell {
display: grid;
gap: 12px;
}
.command-deck-header {
min-width: 0;
display: grid;
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));
}
.command-deck-brand {
min-width: 0;
display: flex;
align-items: center;
gap: 11px;
}
.command-deck-mark {
width: 34px;
height: 34px;
flex: 0 0 auto;
border: 1px solid var(--border-strong);
border-radius: 8px;
background:
linear-gradient(135deg, oklch(0.78 0.12 74 / 0.7), oklch(0.28 0.035 250)),
var(--accent-soft);
}
.command-deck-kicker,
.command-pane-meta,
.command-health-row,
.command-replay-strip,
.command-ticker-card {
font-family: var(--font-mono), monospace;
}
.command-deck-kicker {
display: block;
color: var(--text-faint);
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: lowercase;
}
.command-deck-brand h2 {
margin: 1px 0 0;
font-family: var(--font-display), sans-serif;
font-size: 1.2rem;
line-height: 1.05;
letter-spacing: 0;
}
.command-deck-brief {
min-width: 0;
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
color: var(--text-dim);
font-size: 0.82rem;
}
.command-deck-brief strong {
color: var(--text);
font-family: var(--font-mono), monospace;
font-size: 0.86rem;
}
.command-deck-controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.command-chip {
min-height: 32px;
display: inline-flex;
align-items: center;
border: 1px solid var(--border);
border-radius: 999px;
padding: 5px 9px;
background: var(--bg-soft);
color: var(--text-dim);
font-family: var(--font-mono), monospace;
font-size: 0.68rem;
text-transform: uppercase;
}
.command-chip-connected {
color: var(--green);
background: var(--green-soft);
}
.command-chip-stale,
.command-chip-connecting {
color: var(--accent);
background: var(--accent-soft);
}
.command-chip-disconnected {
color: var(--red);
background: var(--red-soft);
}
.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);
}
.command-ticker-track {
display: grid;
grid-auto-columns: minmax(176px, 1fr);
grid-auto-flow: column;
gap: 8px;
overflow-x: auto;
padding: 7px;
}
.command-ticker-card {
min-width: 176px;
min-height: 64px;
display: grid;
grid-template-columns: 1fr auto;
gap: 4px 9px;
align-items: center;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
background: oklch(0.17 0.013 250);
color: inherit;
text-align: left;
cursor: pointer;
}
.command-ticker-card:hover,
.command-ticker-card:focus-visible {
border-color: var(--border-strong);
outline: none;
}
.command-ticker-symbol {
color: var(--text);
font-weight: 700;
}
.command-ticker-price,
.command-ticker-meta {
color: var(--text-dim);
font-size: 0.72rem;
}
.command-ticker-move {
justify-self: end;
color: var(--text-faint);
font-size: 0.68rem;
}
.command-ticker-card.is-up .command-ticker-move {
color: var(--green);
}
.command-ticker-card.is-down .command-ticker-move {
color: var(--red);
}
.command-ticker-meta {
grid-column: 1 / -1;
}
.command-deck-grid {
display: grid;
grid-template-columns: minmax(360px, 1.12fr) minmax(420px, 1.38fr) minmax(300px, 0.9fr);
grid-template-areas:
"tape chart signals"
"feed dark context"
"replay replay replay";
gap: 10px;
align-items: stretch;
}
.command-deck-grid > .terminal-pane {
border-radius: 10px;
}
.command-deck-grid > :nth-child(1) {
grid-area: tape;
min-height: 386px;
}
.command-deck-grid > :nth-child(2) {
grid-area: chart;
min-height: 386px;
}
.command-deck-grid > :nth-child(3) {
grid-area: signals;
min-height: 386px;
}
.command-deck-grid > :nth-child(4) {
grid-area: feed;
min-height: 278px;
}
.command-deck-grid > :nth-child(5) {
grid-area: dark;
min-height: 278px;
}
.command-deck-grid > :nth-child(6) {
grid-area: context;
min-height: 278px;
}
.command-deck-grid > :nth-child(7) {
grid-area: replay;
min-height: 116px;
}
.command-deck-grid .terminal-pane-head {
min-height: 42px;
padding: 10px 12px;
}
.command-deck-grid .terminal-pane-body {
padding: 10px 12px 12px;
}
.command-deck-grid .terminal-pane-title {
font-family: var(--font-mono), monospace;
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.command-deck-grid .chart-surface {
height: 300px;
}
.command-pane-meta {
color: var(--text-faint);
font-size: 0.68rem;
text-transform: uppercase;
}
.command-health-list,
.command-context-list,
.command-replay-strip {
display: grid;
gap: 8px;
}
.command-health-row {
min-height: 34px;
display: grid;
grid-template-columns: minmax(96px, 1fr) 92px 76px 92px;
gap: 8px;
align-items: center;
border-bottom: 1px solid oklch(0.72 0.012 250 / 0.09);
color: var(--text-dim);
font-size: 0.72rem;
}
.command-health-row:last-child {
border-bottom: 0;
}
.command-health-status {
width: fit-content;
max-width: 100%;
display: inline-flex;
align-items: center;
min-height: 22px;
border: 1px solid var(--border);
border-radius: 999px;
padding: 3px 7px;
font-size: 0.64rem;
}
.command-health-connected {
color: var(--green);
background: var(--green-soft);
}
.command-health-stale,
.command-health-connecting {
color: var(--accent);
background: var(--accent-soft);
}
.command-health-disconnected {
color: var(--red);
background: var(--red-soft);
}
.command-context-row {
min-width: 0;
min-height: 42px;
display: grid;
grid-template-columns: 62px 52px minmax(0, 1fr);
gap: 4px 8px;
align-items: center;
border: 0;
border-bottom: 1px solid oklch(0.72 0.012 250 / 0.09);
padding: 6px 0;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}
.command-context-row:last-child {
border-bottom: 0;
}
.command-context-row time,
.command-context-row span {
color: var(--text-dim);
font-family: var(--font-mono), monospace;
font-size: 0.68rem;
}
.command-context-row strong {
min-width: 0;
overflow: hidden;
color: var(--text);
font-size: 0.78rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.command-context-row > span:last-child {
grid-column: 3;
overflow: hidden;
color: var(--text-faint);
text-overflow: ellipsis;
white-space: nowrap;
}
.command-context-kind {
width: fit-content;
border: 1px solid var(--border);
border-radius: 999px;
padding: 2px 6px;
background: var(--bg-soft);
text-transform: uppercase;
}
.command-replay-strip {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.command-replay-strip div {
min-width: 0;
display: grid;
gap: 3px;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
background: var(--bg-soft);
}
.command-replay-strip span {
color: var(--text-faint);
font-size: 0.66rem;
text-transform: uppercase;
}
.command-replay-strip strong {
min-width: 0;
overflow: hidden;
color: var(--text);
font-size: 0.78rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.terminal-pane { .terminal-pane {
min-width: 0; min-width: 0;
height: 100%; height: 100%;
@ -2027,6 +2416,25 @@ h3 {
min-width: 136px; min-width: 136px;
padding: 10px 12px; padding: 10px 12px;
} }
.command-deck-header {
grid-template-columns: minmax(220px, 0.8fr) minmax(240px, 1fr);
}
.command-deck-controls {
grid-column: 1 / -1;
justify-content: flex-start;
}
.command-deck-grid {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas:
"signals chart"
"tape chart"
"feed context"
"dark dark"
"replay replay";
}
} }
@media (max-width: 980px) { @media (max-width: 980px) {
@ -2065,6 +2473,32 @@ h3 {
min-height: 0; min-height: 0;
} }
.command-deck-grid {
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
"signals"
"chart"
"tape"
"context"
"replay"
"feed"
"dark";
}
.command-deck-grid > .terminal-pane {
min-height: 0;
}
.command-deck-grid > :nth-child(1),
.command-deck-grid > :nth-child(2),
.command-deck-grid > :nth-child(3),
.command-deck-grid > :nth-child(4),
.command-deck-grid > :nth-child(5),
.command-deck-grid > :nth-child(6),
.command-deck-grid > :nth-child(7) {
min-height: 0;
}
.terminal-topbar { .terminal-topbar {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@ -2129,11 +2563,32 @@ h3 {
.terminal-pane-head, .terminal-pane-head,
.chart-controls, .chart-controls,
.card-controls, .card-controls,
.terminal-pane-actions { .terminal-pane-actions,
.command-deck-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.command-deck-header {
display: flex;
}
.command-deck-brief,
.command-deck-controls {
width: 100%;
justify-content: flex-start;
}
.command-deck-controls .terminal-button,
.command-chip {
width: 100%;
justify-content: center;
}
.command-ticker-track {
grid-auto-columns: minmax(164px, 78vw);
}
.terminal-pane-title-row { .terminal-pane-title-row {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@ -2311,6 +2766,37 @@ h3 {
height: 48px; height: 48px;
} }
.command-deck-grid .chart-surface {
height: 320px;
}
.command-health-row {
grid-template-columns: minmax(94px, 1fr) 92px;
}
.command-health-row span:nth-child(3),
.command-health-row span:nth-child(4) {
font-size: 0.68rem;
}
.command-context-row {
grid-template-columns: 58px minmax(0, 1fr);
}
.command-context-kind {
grid-column: 2;
grid-row: 1;
}
.command-context-row strong,
.command-context-row > span:last-child {
grid-column: 2;
}
.command-replay-strip {
grid-template-columns: minmax(0, 1fr);
}
.time { .time {
text-align: left; text-align: left;
} }

View file

@ -352,10 +352,10 @@ export const getRouteFeatures = (pathname: string): RouteFeatures => {
case "/": case "/":
default: default:
return { return {
options: false, options: true,
nbbo: false, nbbo: false,
equities: true, equities: true,
flow: false, flow: true,
news: true, news: true,
alerts: true, alerts: true,
smartMoney: true, smartMoney: true,
@ -364,17 +364,17 @@ export const getRouteFeatures = (pathname: string): RouteFeatures => {
equityJoins: true, equityJoins: true,
equityCandles: true, equityCandles: true,
equityOverlay: true, equityOverlay: true,
showOptionsPane: false, showOptionsPane: true,
showEquitiesPane: true, showEquitiesPane: true,
showFlowPane: false, showFlowPane: true,
showNewsPane: true, showNewsPane: true,
showAlertsPane: true, showAlertsPane: true,
showClassifierPane: false, showClassifierPane: false,
showDarkPane: false, showDarkPane: true,
showChartPane: true, showChartPane: true,
showFocusPane: false, showFocusPane: false,
showReplayConsole: false, showReplayConsole: false,
needsClassifierDecor: false, needsClassifierDecor: true,
needsAlertEvidencePrefetch: true, needsAlertEvidencePrefetch: true,
needsDarkUnderlying: true needsDarkUnderlying: true
}; };
@ -4215,24 +4215,24 @@ const CandleChart = ({
width, width,
height, height,
layout: { layout: {
background: { color: "#fffdf7" }, background: { color: "#0d141b" },
textColor: "#4e3e25" textColor: "#90a0b2"
}, },
grid: { grid: {
vertLines: { color: "rgba(82, 64, 36, 0.12)" }, vertLines: { color: "rgba(144, 160, 178, 0.12)" },
horzLines: { color: "rgba(82, 64, 36, 0.12)" } horzLines: { color: "rgba(144, 160, 178, 0.12)" }
}, },
crosshair: { crosshair: {
vertLine: { color: "rgba(47, 109, 79, 0.35)" }, vertLine: { color: "rgba(245, 166, 35, 0.32)" },
horzLine: { color: "rgba(47, 109, 79, 0.35)" } horzLine: { color: "rgba(245, 166, 35, 0.32)" }
}, },
timeScale: { timeScale: {
borderColor: "rgba(111, 91, 57, 0.35)", borderColor: "rgba(144, 160, 178, 0.24)",
timeVisible: true, timeVisible: true,
secondsVisible: intervalMs < 60000 secondsVisible: intervalMs < 60000
}, },
rightPriceScale: { rightPriceScale: {
borderColor: "rgba(111, 91, 57, 0.35)" borderColor: "rgba(144, 160, 178, 0.24)"
} }
}); });
@ -4250,11 +4250,11 @@ const CandleChart = ({
overlayCtxRef.current = overlayCanvas.getContext("2d"); overlayCtxRef.current = overlayCanvas.getContext("2d");
const series = chart.addCandlestickSeries({ const series = chart.addCandlestickSeries({
upColor: "#2f6d4f", upColor: "#25c17a",
downColor: "#c46f2a", downColor: "#ff6b5f",
borderVisible: false, borderVisible: false,
wickUpColor: "#2f6d4f", wickUpColor: "#25c17a",
wickDownColor: "#c46f2a" wickDownColor: "#ff6b5f"
}); });
chartRef.current = chart; chartRef.current = chart;
@ -8397,6 +8397,278 @@ const ChartPane = memo(({ state, title = "Chart" }: ChartPaneProps) => {
); );
}); });
type CommandDeckTicker = {
symbol: string;
price: number | null;
move: number | null;
options: number;
alerts: number;
};
const buildCommandDeckTickers = (state: TerminalState): CommandDeckTicker[] => {
const symbols = new Set<string>();
for (const symbol of state.activeTickers) {
symbols.add(symbol);
}
for (const print of state.filteredEquities.slice(0, 80)) {
symbols.add(print.underlying_id.toUpperCase());
}
for (const print of state.filteredOptions.slice(0, 80)) {
const parsed = parseOptionContractId(normalizeContractId(print.option_contract_id));
const symbol = (print.underlying_id ?? parsed?.root ?? extractUnderlying(print.option_contract_id))?.toUpperCase();
if (symbol) {
symbols.add(symbol);
}
}
for (const event of state.filteredSmartMoneyEvents.slice(0, 30)) {
symbols.add(event.underlying_id.toUpperCase());
}
for (const story of state.filteredNews.slice(0, 20)) {
for (const symbol of story.resolved_symbols) {
symbols.add(symbol.toUpperCase());
}
}
if (symbols.size === 0) {
symbols.add(state.chartTicker.toUpperCase());
}
return Array.from(symbols).slice(0, 10).map((symbol) => {
const equityPrints = state.filteredEquities
.filter((print) => print.underlying_id.toUpperCase() === symbol)
.slice(0, 2);
const price = equityPrints[0]?.price ?? null;
const previous = equityPrints[1]?.price ?? null;
const move = price !== null && previous !== null && previous !== 0 ? (price - previous) / previous : null;
const options = state.filteredOptions
.slice(0, 120)
.filter((print) => {
const parsed = parseOptionContractId(normalizeContractId(print.option_contract_id));
const underlying = (print.underlying_id ?? parsed?.root ?? extractUnderlying(print.option_contract_id))?.toUpperCase();
return underlying === symbol;
}).length;
const alerts = state.filteredAlerts
.slice(0, 80)
.filter((alert) => alert.trace_id.toUpperCase().includes(symbol)).length;
return { symbol, price, move, options, alerts };
});
};
const CommandDeckHeader = ({ state }: { state: TerminalState }) => {
const focus = state.activeTickers.length > 0 ? state.activeTickers.join(", ") : state.chartTicker;
const selected = state.selectedInstrumentLabel ?? "No contract lock";
const connectionLabel = state.mode === "live" ? statusLabel(state.liveSession.status, false, state.mode) : "Replay";
return (
<header className="command-deck-header" aria-label="Command deck context">
<div className="command-deck-brand">
<span className="command-deck-mark" aria-hidden="true" />
<div>
<span className="command-deck-kicker">islandflow</span>
<h2>Command Deck</h2>
</div>
</div>
<div className="command-deck-brief">
<span>Evidence console</span>
<strong>{focus}</strong>
<span>{selected}</span>
</div>
<div className="command-deck-controls" aria-label="Active command deck controls">
<span className={`command-chip command-chip-${state.liveSession.status}`}>
{state.mode === "live" ? "Live" : "Replay"}: {connectionLabel}
</span>
<span className="command-chip">Last {state.lastSeen ? formatTime(state.lastSeen) : "waiting"}</span>
<button className="terminal-button" type="button" onClick={state.toggleMode}>
{state.mode === "live" ? "Switch to Replay" : "Switch to Live"}
</button>
</div>
</header>
);
};
const TickerRail = ({ state }: { state: TerminalState }) => {
const tickers = useMemo(() => buildCommandDeckTickers(state), [state]);
return (
<div className="command-ticker-rail" aria-label="Live ticker focus rail">
<div className="command-ticker-track">
{tickers.map((ticker) => {
const direction = ticker.move === null ? "flat" : ticker.move >= 0 ? "up" : "down";
const equity = state.filteredEquities.find((print) => print.underlying_id.toUpperCase() === ticker.symbol);
return (
<button
className={`command-ticker-card is-${direction}`}
key={ticker.symbol}
type="button"
onClick={() => (equity ? state.focusEquityTicker(equity) : state.setFilterInput(ticker.symbol))}
>
<span className="command-ticker-symbol">{ticker.symbol}</span>
<span className="command-ticker-price">{ticker.price === null ? "--" : `$${formatPrice(ticker.price)}`}</span>
<span className="command-ticker-move">
{ticker.move === null
? "Move n/a"
: `${direction === "up" ? "Up" : "Down"} ${formatPct(Math.abs(ticker.move))}`}
</span>
<span className="command-ticker-meta">
{ticker.options} opt / {ticker.alerts} alerts
</span>
</button>
);
})}
</div>
</div>
);
};
const FeedHealthPane = ({ state }: { state: TerminalState }) => {
const rows = [
{ label: "Options", tape: state.options, subscribed: state.routeFeatures.options },
{ label: "Equities", tape: state.equities, subscribed: state.routeFeatures.equities },
{ label: "Flow", tape: state.flow, subscribed: state.routeFeatures.flow },
{ label: "Alerts", tape: state.alerts, subscribed: state.routeFeatures.alerts },
{ label: "News", tape: state.news, subscribed: state.routeFeatures.news },
{ label: "Dark", tape: state.inferredDark, subscribed: state.routeFeatures.inferredDark }
];
return (
<Pane
className="command-feed-pane"
title="Feed Health"
status={<span className="command-pane-meta">{state.liveSession.manifest.length} subscriptions</span>}
>
<div className="command-health-list">
{rows.map(({ label, tape, subscribed }) => (
<div className="command-health-row" key={label}>
<span>{label}</span>
<span className={`command-health-status command-health-${tape.status}`}>
{subscribed ? statusLabel(tape.status, tape.paused, state.mode) : "Idle"}
</span>
<span>{tape.lastUpdate ? formatTime(tape.lastUpdate) : "No update"}</span>
<span>{tape.dropped > 0 ? `${tape.dropped} dropped` : "Queue clear"}</span>
</div>
))}
</div>
</Pane>
);
};
const EventContextPane = ({ state }: { state: TerminalState }) => {
const events = [
...state.filteredAlerts.slice(0, 3).map((alert) => ({
key: `alert-${alert.trace_id}-${alert.seq}`,
ts: alert.source_ts,
label: "Alert",
title: alert.hits[0] ? humanizeClassifierId(alert.hits[0].classifier_id) : "Classifier alert",
detail: alert.hits[0]?.explanations?.[0] ?? `${alert.hits.length} linked hits`,
action: () => state.setSelectedAlert(alert)
})),
...state.filteredSmartMoneyEvents.slice(0, 3).map((event) => ({
key: `smart-${event.event_id}-${event.seq}`,
ts: event.source_ts,
label: "Smart",
title: smartMoneyProfileLabel(event.primary_profile_id),
detail: `${event.underlying_id} ${normalizeDirection(event.primary_direction)} / ${event.packet_ids.length} packets`,
action: () => state.openFromSmartMoneyEvent(event)
})),
...state.filteredInferredDark.slice(0, 3).map((event) => ({
key: `dark-${event.trace_id}-${event.seq}`,
ts: event.source_ts,
label: "Dark",
title: humanizeClassifierId(event.type),
detail: `${event.evidence_refs.length} evidence refs / confidence ${formatConfidence(event.confidence)}`,
action: () => state.setSelectedDarkEvent(event)
})),
...state.filteredNews.slice(0, 2).map((story) => ({
key: `news-${story.trace_id}-${story.seq}`,
ts: story.published_ts,
label: "News",
title: story.headline,
detail: story.resolved_symbols.length > 0 ? story.resolved_symbols.join(", ") : story.source,
action: () => state.setSelectedNewsStory(story)
}))
].sort((a, b) => b.ts - a.ts).slice(0, 6);
return (
<Pane
className="command-context-pane"
title="Event Context"
status={<span className="command-pane-meta">Focus evidence</span>}
>
{events.length === 0 ? (
<div className="empty">No linked evidence is available for this scope yet.</div>
) : (
<div className="command-context-list" role="list">
{events.map((event) => (
<button className="command-context-row" key={event.key} type="button" onClick={event.action}>
<time>{formatTime(event.ts)}</time>
<span className="command-context-kind">{event.label}</span>
<strong>{event.title}</strong>
<span>{event.detail}</span>
</button>
))}
</div>
)}
</Pane>
);
};
const HomeReplayRail = ({ state }: { state: TerminalState }) => {
const replayTime =
state.options.replayTime ??
state.equities.replayTime ??
state.flow.replayTime ??
state.alerts.replayTime ??
state.inferredDark.replayTime;
const replayComplete =
state.options.replayComplete ||
state.equities.replayComplete ||
state.flow.replayComplete ||
state.alerts.replayComplete ||
state.inferredDark.replayComplete;
const activeSource = state.replaySource ? state.replaySource.toUpperCase() : state.mode === "live" ? "LIVE HEAD" : "AUTO";
return (
<Pane
className="command-replay-pane"
title="Replay / Mode"
status={
<TapeStatus
status={state.mode === "live" ? state.liveSession.status : state.options.status}
lastUpdate={state.lastSeen}
replayTime={replayTime}
replayComplete={replayComplete}
paused={false}
dropped={state.options.dropped + state.equities.dropped + state.flow.dropped + state.alerts.dropped}
mode={state.mode}
/>
}
actions={
<button className="terminal-button" type="button" onClick={state.toggleMode}>
{state.mode === "live" ? "Replay" : "Live"}
</button>
}
>
<div className="command-replay-strip">
<div>
<span>Source</span>
<strong>{activeSource}</strong>
</div>
<div>
<span>Cursor</span>
<strong>{replayTime ? formatTime(replayTime) : state.lastSeen ? formatTime(state.lastSeen) : "waiting"}</strong>
</div>
<div>
<span>Chart</span>
<strong>{state.chartTicker} / {formatIntervalLabel(state.chartIntervalMs)}</strong>
</div>
<div>
<span>Scope</span>
<strong>{state.activeTickers.length > 0 ? state.activeTickers.join(", ") : "All symbols"}</strong>
</div>
</div>
</Pane>
);
};
const FocusPane = memo(({ state }: { state: TerminalState }) => { const FocusPane = memo(({ state }: { state: TerminalState }) => {
const hits = state.chartSmartMoneyEvents.slice(-10).reverse(); const hits = state.chartSmartMoneyEvents.slice(-10).reverse();
const dark = state.chartInferredDark.slice(-10).reverse(); const dark = state.chartInferredDark.slice(-10).reverse();
@ -9040,11 +9312,18 @@ export function OverviewRoute() {
const state = useTerminal(); const state = useTerminal();
return ( return (
<PageFrame title="Home"> <PageFrame title="Home">
<div className="page-grid page-grid-home"> <div className="command-deck-shell">
<ChartPane state={state} /> <CommandDeckHeader state={state} />
<EquitiesPane state={state} /> <TickerRail state={state} />
<NewsPane state={state} limit={6} /> <div className="command-deck-grid">
<AlertsPane state={state} withStrip /> <OptionsPane state={state} limit={14} />
<ChartPane state={state} title="Price / Flow" />
<AlertsPane state={state} limit={8} withStrip className="command-signals-pane" />
<FeedHealthPane state={state} />
<DarkPane state={state} limit={8} className="command-dark-pane" />
<EventContextPane state={state} />
<HomeReplayRail state={state} />
</div>
</div> </div>
</PageFrame> </PageFrame>
); );

File diff suppressed because one or more lines are too long