From 7b2f31fd4debc938449f6c15186c576d5778f56e Mon Sep 17 00:00:00 2001 From: philipp Date: Tue, 19 May 2026 18:15:05 +0200 Subject: [PATCH] Keep dashboard updates live without jitter Proof: Quote lifecycle and competitiveness now update live while unpaused, with fixed row slots and clamped cells carrying the jitter prevention; static UI coverage, full npm test, and the operator dashboard build validate the correction. Assumptions: this is a UI-only operator cleanup for the active maker timing and competitiveness turn; it does not change strategy decisions, execution, DB config, persistence, live funds, or response policy. Still fake: venue-native terminal fill ids and fee-complete realized PnL remain unavailable. --- .../static/pages/StrategyPage.jsx | 55 ++++++++----------- test/operator-dashboard-ui-static.test.mjs | 8 ++- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/operator-dashboard/static/pages/StrategyPage.jsx b/src/operator-dashboard/static/pages/StrategyPage.jsx index c06bb35..4db4ddf 100644 --- a/src/operator-dashboard/static/pages/StrategyPage.jsx +++ b/src/operator-dashboard/static/pages/StrategyPage.jsx @@ -8,11 +8,9 @@ import { formatAgeFromTimestamp, formatBoolean, formatEur, formatTimestamp, trun const RESPONDED_STATES = new Set(['submitted', 'awaiting_outcome', 'not_filled', 'completed']); const TRADING_PAIR_MODES = new Set(['maker', 'taker', 'both']); -const COMPETITIVENESS_REFRESH_MS = 5_000; const COMPETITIVENESS_GROUP_ROW_COUNT = 12; const COMPETITIVENESS_DETAIL_ROW_COUNT = 8; const COMPETITIVENESS_LATENCY_ROW_COUNT = 6; -const QUOTE_LIFECYCLE_REFRESH_MS = 5_000; const QUOTE_LIFECYCLE_ROW_COUNT = 20; async function copyIdentifier(value) { @@ -24,6 +22,17 @@ async function copyIdentifier(value) { } } +function useNow(intervalMs = 1000) { + const [now, setNow] = useState(() => Date.now()); + + useEffect(() => { + const timer = window.setInterval(() => setNow(Date.now()), intervalMs); + return () => window.clearInterval(timer); + }, [intervalMs]); + + return now; +} + function formatRelativeAge(value, now) { const age = formatAgeFromTimestamp(value, now); return age === 'Unavailable' ? 'Age unavailable' : `${age} ago`; @@ -274,7 +283,6 @@ function fixedRows(items, count) { function MakerCompetitivenessSection({ summary, pairConfig }) { const latestSummaryRef = useRef(summary || {}); const [displaySummary, setDisplaySummary] = useState(() => summary || {}); - const [displayUpdatedAt, setDisplayUpdatedAt] = useState(() => new Date().toISOString()); const [updatesPaused, setUpdatesPaused] = useState(false); const total = displaySummary?.total || {}; const groups = displaySummary?.groups || []; @@ -290,23 +298,14 @@ function MakerCompetitivenessSection({ summary, pairConfig }) { useEffect(() => { latestSummaryRef.current = summary || {}; - }, [summary]); - - useEffect(() => { - if (updatesPaused) return undefined; - const timer = window.setInterval(() => { - setDisplaySummary(latestSummaryRef.current || {}); - setDisplayUpdatedAt(new Date().toISOString()); - }, COMPETITIVENESS_REFRESH_MS); - return () => window.clearInterval(timer); - }, [updatesPaused]); + if (!updatesPaused) setDisplaySummary(summary || {}); + }, [summary, updatesPaused]); function toggleUpdatesPaused() { setUpdatesPaused((current) => { const nextPaused = !current; if (current) { setDisplaySummary(latestSummaryRef.current || {}); - setDisplayUpdatedAt(new Date().toISOString()); } return nextPaused; }); @@ -325,7 +324,7 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
0 ? 'warning' : 'unknown'} /> - +
- Display snapshot {formatTimestamp(displayUpdatedAt)}. Live updates are applied every {COMPETITIVENESS_REFRESH_MS / 1000}s to keep table height stable. + Live rows update as they arrive. Fixed row slots and clamped cells keep the table height stable.
@@ -564,23 +563,17 @@ function QuoteLifecycleTable({ items }) { const latestItemsRef = useRef(items || []); const [quoteDisplayPaused, setQuoteDisplayPaused] = useState(false); const [displayItems, setDisplayItems] = useState(() => items || []); + const liveNow = useNow(); const [displayNow, setDisplayNow] = useState(() => Date.now()); - const [displayUpdatedAt, setDisplayUpdatedAt] = useState(() => new Date().toISOString()); useEffect(() => { latestItemsRef.current = items || []; - }, [items]); + if (!quoteDisplayPaused) setDisplayItems(items || []); + }, [items, quoteDisplayPaused]); useEffect(() => { - if (quoteDisplayPaused) return undefined; - const timer = window.setInterval(() => { - const now = Date.now(); - setDisplayItems(latestItemsRef.current || []); - setDisplayNow(now); - setDisplayUpdatedAt(new Date(now).toISOString()); - }, QUOTE_LIFECYCLE_REFRESH_MS); - return () => window.clearInterval(timer); - }, [quoteDisplayPaused]); + if (!quoteDisplayPaused) setDisplayNow(liveNow); + }, [liveNow, quoteDisplayPaused]); const rejectedCount = useMemo( () => displayItems.filter((item) => isStrategyRejected(item)).length, @@ -607,10 +600,8 @@ function QuoteLifecycleTable({ items }) { } function applyLatestLifecycleDisplay() { - const now = Date.now(); setDisplayItems(latestItemsRef.current || []); - setDisplayNow(now); - setDisplayUpdatedAt(new Date(now).toISOString()); + setDisplayNow(Date.now()); } function toggleQuoteDisplayPaused() { @@ -637,10 +628,10 @@ function QuoteLifecycleTable({ items }) { > {quoteDisplayPaused ? 'Resume display' : 'Pause display'} - +
- Display snapshot {formatTimestamp(displayUpdatedAt)}. Live rows are applied every {QUOTE_LIFECYCLE_REFRESH_MS / 1000}s to keep table height stable. + Live rows update as they arrive. Fixed row slots and clamped cells keep the table height stable.
diff --git a/test/operator-dashboard-ui-static.test.mjs b/test/operator-dashboard-ui-static.test.mjs index bccc565..57a1510 100644 --- a/test/operator-dashboard-ui-static.test.mjs +++ b/test/operator-dashboard-ui-static.test.mjs @@ -27,9 +27,12 @@ test('strategy page owns consolidated quote lifecycle and successful trade table assert.match(strategySource, /quoteDisplayPaused/); assert.match(strategySource, /Pause display/); assert.match(strategySource, /Resume display/); - assert.match(strategySource, /QUOTE_LIFECYCLE_REFRESH_MS/); assert.match(strategySource, /QUOTE_LIFECYCLE_ROW_COUNT/); assert.match(strategySource, /latestItemsRef/); + assert.match(strategySource, /setDisplayItems\(items \|\| \[\]\)/); + assert.match(strategySource, /useNow\(\)/); + assert.match(strategySource, /Live updates/); + assert.match(strategySource, /Live rows update as they arrive/); assert.match(strategySource, /visibleRows/); assert.match(strategySource, /quote-lifecycle-placeholder-row/); assert.match(stylesSource, /\.quote-lifecycle-table tbody tr\.quote-lifecycle-row/); @@ -144,7 +147,8 @@ test('strategy page exposes maker timing waterfall and competitiveness summaries assert.match(strategySource, /Resume updates/); assert.match(strategySource, /displaySummary/); assert.match(strategySource, /fixedRows/); - assert.match(strategySource, /COMPETITIVENESS_REFRESH_MS/); + assert.match(strategySource, /latestSummaryRef/); + assert.match(strategySource, /Live updates/); assert.match(strategySource, /latencyStageRows/); assert.match(stylesSource, /\.metric-stack/); assert.match(stylesSource, /\.competitiveness-table-stack/);