Redesign home command deck
This commit is contained in:
parent
b075a0994c
commit
a35a757622
4 changed files with 1325 additions and 24 deletions
|
|
@ -769,6 +769,395 @@ h3 {
|
|||
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 {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
|
|
@ -2027,6 +2416,25 @@ h3 {
|
|||
min-width: 136px;
|
||||
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) {
|
||||
|
|
@ -2065,6 +2473,32 @@ h3 {
|
|||
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 {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -2129,11 +2563,32 @@ h3 {
|
|||
.terminal-pane-head,
|
||||
.chart-controls,
|
||||
.card-controls,
|
||||
.terminal-pane-actions {
|
||||
.terminal-pane-actions,
|
||||
.command-deck-header {
|
||||
flex-direction: column;
|
||||
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 {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
|
@ -2311,6 +2766,37 @@ h3 {
|
|||
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 {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -352,10 +352,10 @@ export const getRouteFeatures = (pathname: string): RouteFeatures => {
|
|||
case "/":
|
||||
default:
|
||||
return {
|
||||
options: false,
|
||||
options: true,
|
||||
nbbo: false,
|
||||
equities: true,
|
||||
flow: false,
|
||||
flow: true,
|
||||
news: true,
|
||||
alerts: true,
|
||||
smartMoney: true,
|
||||
|
|
@ -364,17 +364,17 @@ export const getRouteFeatures = (pathname: string): RouteFeatures => {
|
|||
equityJoins: true,
|
||||
equityCandles: true,
|
||||
equityOverlay: true,
|
||||
showOptionsPane: false,
|
||||
showOptionsPane: true,
|
||||
showEquitiesPane: true,
|
||||
showFlowPane: false,
|
||||
showFlowPane: true,
|
||||
showNewsPane: true,
|
||||
showAlertsPane: true,
|
||||
showClassifierPane: false,
|
||||
showDarkPane: false,
|
||||
showDarkPane: true,
|
||||
showChartPane: true,
|
||||
showFocusPane: false,
|
||||
showReplayConsole: false,
|
||||
needsClassifierDecor: false,
|
||||
needsClassifierDecor: true,
|
||||
needsAlertEvidencePrefetch: true,
|
||||
needsDarkUnderlying: true
|
||||
};
|
||||
|
|
@ -4215,24 +4215,24 @@ const CandleChart = ({
|
|||
width,
|
||||
height,
|
||||
layout: {
|
||||
background: { color: "#fffdf7" },
|
||||
textColor: "#4e3e25"
|
||||
background: { color: "#0d141b" },
|
||||
textColor: "#90a0b2"
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: "rgba(82, 64, 36, 0.12)" },
|
||||
horzLines: { color: "rgba(82, 64, 36, 0.12)" }
|
||||
vertLines: { color: "rgba(144, 160, 178, 0.12)" },
|
||||
horzLines: { color: "rgba(144, 160, 178, 0.12)" }
|
||||
},
|
||||
crosshair: {
|
||||
vertLine: { color: "rgba(47, 109, 79, 0.35)" },
|
||||
horzLine: { color: "rgba(47, 109, 79, 0.35)" }
|
||||
vertLine: { color: "rgba(245, 166, 35, 0.32)" },
|
||||
horzLine: { color: "rgba(245, 166, 35, 0.32)" }
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: "rgba(111, 91, 57, 0.35)",
|
||||
borderColor: "rgba(144, 160, 178, 0.24)",
|
||||
timeVisible: true,
|
||||
secondsVisible: intervalMs < 60000
|
||||
},
|
||||
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");
|
||||
|
||||
const series = chart.addCandlestickSeries({
|
||||
upColor: "#2f6d4f",
|
||||
downColor: "#c46f2a",
|
||||
upColor: "#25c17a",
|
||||
downColor: "#ff6b5f",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#2f6d4f",
|
||||
wickDownColor: "#c46f2a"
|
||||
wickUpColor: "#25c17a",
|
||||
wickDownColor: "#ff6b5f"
|
||||
});
|
||||
|
||||
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 hits = state.chartSmartMoneyEvents.slice(-10).reverse();
|
||||
const dark = state.chartInferredDark.slice(-10).reverse();
|
||||
|
|
@ -9040,11 +9312,18 @@ export function OverviewRoute() {
|
|||
const state = useTerminal();
|
||||
return (
|
||||
<PageFrame title="Home">
|
||||
<div className="page-grid page-grid-home">
|
||||
<ChartPane state={state} />
|
||||
<EquitiesPane state={state} />
|
||||
<NewsPane state={state} limit={6} />
|
||||
<AlertsPane state={state} withStrip />
|
||||
<div className="command-deck-shell">
|
||||
<CommandDeckHeader state={state} />
|
||||
<TickerRail state={state} />
|
||||
<div className="command-deck-grid">
|
||||
<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>
|
||||
</PageFrame>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue