From bd13b217fa3d4b872a7c13da4b5ab3a6ae98e037 Mon Sep 17 00:00:00 2001 From: anderdc Date: Wed, 27 May 2026 15:42:21 -0500 Subject: [PATCH 1/5] crown: note that round-bounded panels refresh every ~2h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three dashboard surfaces — Crown History grid, Crown rate chart, and the Miner Leaderboard's crown_share column — read from the per-round crown_holders / rate_history tables. Today the alw-utils sync-validator-state daemon hides this by re-deriving crown rows every 12s and writing them incrementally; once that daemon retires (in favor of the validator owning these writes directly), the same panels will visibly stale between flushes and snap in at end of each SCORING_WINDOW_BLOCKS round (~2h). The behavior isn't a regression — it's the validator becoming the single source of truth — but it changes user expectation, so we surface it inline. New small CrownRoundFillNote component renders "{subject} refreshes at end of each ~2h scoring round · last refresh #L · next ~M blocks" using the current chain head to compute the window boundaries. Distinct from the top-bar current-crown widget, which keeps the live ~12s cadence via the new current_crown_holders table — that one needs no note. See companion validator + schema + das-allways PRs. --- src/components/miners/CrownHistoryGrid.tsx | 6 ++ src/components/miners/CrownRateChart.tsx | 6 ++ src/components/miners/CrownRoundFillNote.tsx | 61 ++++++++++++++++++++ src/components/miners/MinerLeaderboard.tsx | 9 +++ 4 files changed, 82 insertions(+) create mode 100644 src/components/miners/CrownRoundFillNote.tsx diff --git a/src/components/miners/CrownHistoryGrid.tsx b/src/components/miners/CrownHistoryGrid.tsx index 8bfb302..0ee5963 100644 --- a/src/components/miners/CrownHistoryGrid.tsx +++ b/src/components/miners/CrownHistoryGrid.tsx @@ -14,6 +14,7 @@ import { useCrownHistory, useHaltState, type Direction } from '../../api'; import { FONTS } from '../../theme'; import CrownGridHoverCard from './CrownGridHoverCard'; import CrownGridRangeInputs from './CrownGridRangeInputs'; +import CrownRoundFillNote from './CrownRoundFillNote'; import { buildCells, buildTiers, type CellState } from './crownGridCells'; // Mirrors SCORING_WINDOW_BLOCKS in allways/constants.py — validator sets @@ -718,6 +719,11 @@ const CrownHistoryGrid: React.FC<{ as of #{headBlock.toLocaleString()} · each cell = 1 block (12s) · each row = 60 blocks (12m) + ); }; diff --git a/src/components/miners/CrownRateChart.tsx b/src/components/miners/CrownRateChart.tsx index 34696ab..ab855a3 100644 --- a/src/components/miners/CrownRateChart.tsx +++ b/src/components/miners/CrownRateChart.tsx @@ -14,6 +14,7 @@ import { type Direction, } from '../../api'; import { FONTS } from '../../theme'; +import CrownRoundFillNote from './CrownRoundFillNote'; const PANEL_W = 800; const PANEL_H = 140; @@ -665,6 +666,11 @@ const CrownRateChart: React.FC<{ )} #{head.toLocaleString()} + ); }; diff --git a/src/components/miners/CrownRoundFillNote.tsx b/src/components/miners/CrownRoundFillNote.tsx new file mode 100644 index 0000000..a0cb926 --- /dev/null +++ b/src/components/miners/CrownRoundFillNote.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +// Mirrors allways/constants.py SCORING_WINDOW_BLOCKS — the validator +// flushes crown rows to the dashboard DB once per round, so panels that +// read crown_holders / rate_history won't show new data between flushes. +// (The top-bar current-crown widget is live ~12s — see current_crown_holders.) +const SCORING_WINDOW_BLOCKS = 600; + +interface Props { + // Current chain head. When null/0 the note renders without the + // last/next refresh figures. + headBlock?: number | null; + // What this panel actually shows ("crown data", "rate history", + // "crown share"...). Goes at the start of the sentence. + subject?: string; + sx?: object; +} + +const CrownRoundFillNote: React.FC = ({ + headBlock, + subject = 'crown data', + sx, +}) => { + const head = headBlock ?? 0; + const lastRefresh = + head > 0 + ? Math.floor(head / SCORING_WINDOW_BLOCKS) * SCORING_WINDOW_BLOCKS + : null; + const nextRefresh = + lastRefresh != null ? lastRefresh + SCORING_WINDOW_BLOCKS : null; + const blocksLeft = + nextRefresh != null ? Math.max(0, nextRefresh - head) : null; + + return ( + + {subject} refreshes at end of each ~2h scoring round + {lastRefresh != null && ( + <> + {' · last refresh #'} + {lastRefresh.toLocaleString()} + {' · next ~'} + {blocksLeft} + {' blocks'} + + )} + + ); +}; + +export default CrownRoundFillNote; diff --git a/src/components/miners/MinerLeaderboard.tsx b/src/components/miners/MinerLeaderboard.tsx index 3287e74..0cfa263 100644 --- a/src/components/miners/MinerLeaderboard.tsx +++ b/src/components/miners/MinerLeaderboard.tsx @@ -14,11 +14,13 @@ import { } from '@mui/material'; import { useNavigate } from 'react-router-dom'; import { + useHaltState, useMinerLeaderboard, type LeaderboardRow, type Range, } from '../../api'; import CrownIcon from './CrownIcon'; +import CrownRoundFillNote from './CrownRoundFillNote'; import SortHeader, { type SortDir } from './SortHeader'; import { FONTS } from '../../theme'; import { formatTao, shortHotkey } from '../../utils/format'; @@ -95,6 +97,8 @@ const MinerLeaderboard: React.FC<{ const navigate = useNavigate(); const theme = useTheme(); const { data, isLoading } = useMinerLeaderboard(range); + const { data: halt } = useHaltState(); + const headBlock = halt?.asOfBlock ?? 0; const [sortKey, setSortKey] = useState('crownShare'); const [sortDir, setSortDir] = useState('desc'); const [query, setQuery] = useState(''); @@ -383,6 +387,11 @@ const MinerLeaderboard: React.FC<{ })} + ); }; From 8e2c7e91a1ea07391797937854fcaa7e4ec9685d Mon Sep 17 00:00:00 2001 From: anderdc Date: Wed, 27 May 2026 17:02:33 -0500 Subject: [PATCH 2/5] layout: full-width HaltBanner above every page when contract halted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StickyNetworkHeader carries a 6px dot + tiny "halted" label that's easy to miss. During a halt no miner earns crown and the full emission pool recycles — significant enough to warrant something unmissable. Mounts in AppLayout right under TopNav so it sits above every routed page, including miner detail. Renders nothing when halt.halted is false. Uses the existing useHaltState() hook (already polling in StickyNetworkHeader and elsewhere) so no new network traffic. Shows "as of block #N" detail when asOfBlock is set, so users can see freshness. Part of the same dashboard-truthiness rollout that adds the live current_crown_holders writer + round-fill notes; pairs with the validator-side halt fixes (validator now clears current_crown_holders during halt so the live header doesn't contradict the recycle). --- src/components/layout/AppLayout.tsx | 2 ++ src/components/layout/HaltBanner.tsx | 50 ++++++++++++++++++++++++++++ src/components/layout/index.ts | 1 + 3 files changed, 53 insertions(+) create mode 100644 src/components/layout/HaltBanner.tsx diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index 7106e64..f0f5f1d 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -4,6 +4,7 @@ import { Outlet } from 'react-router-dom'; import { useOnNavigate, useSSE } from '../../hooks'; import LoadingPage from '../../pages/LoadingPage'; import { TopNav, Footer } from '../nav'; +import HaltBanner from './HaltBanner'; const AppLayout: React.FC = () => { // Single SSE subscription for the whole app — every routed page @@ -22,6 +23,7 @@ const AppLayout: React.FC = () => { }} > + { + const { data: halt } = useHaltState(); + if (!halt?.halted) return null; + + return ( + + + SWAP CONTRACT HALTED — emissions are recycling, no miner is currently + earning. Rate posts still accepted; scoring resumes on unhalt. + {halt.asOfBlock != null && ( + + (as of block #{halt.asOfBlock.toLocaleString()}) + + )} + + + ); +}; + +export default HaltBanner; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index b168660..0e93839 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -1,3 +1,4 @@ export { default as AppLayout } from './AppLayout'; +export { default as HaltBanner } from './HaltBanner'; export { default as Page } from './Page'; export type { PageProps } from './Page'; From 1dfe754eabb146d0a301cc7b26373157f16e929f Mon Sep 17 00:00:00 2001 From: anderdc Date: Wed, 27 May 2026 17:09:22 -0500 Subject: [PATCH 3/5] ui: consolidate refresh status into top-right StickyNetworkHeader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous per-panel CrownRoundFillNote (added on Crown History grid, Crown rate chart, Miner Leaderboard) said the same thing three times in three places. Replaces it with a single status line on the right side of StickyNetworkHeader where "healthy"/"halted" already lives — one source of truth for system state. Behavior: - not halted, head known: "last refresh #L · next ~M blocks" - not halted, head unknown (halt query loading): "healthy" (existing fallback so the indicator never disappears mid-load) - halted: "paused" in error.main, bold Refresh math (last/next) is the same formula as the deleted note: floor(head / 600) and (last + 600 - head). One module-level constant mirrors allways/constants.py SCORING_WINDOW_BLOCKS so a future scoring window change updates here in one place. Removes CrownRoundFillNote.tsx entirely (no remaining callers). Removes the now-unused useHaltState import + headBlock plumbing from MinerLeaderboard. Full HaltBanner stays — top-right is for ongoing/quieter signal; the banner is for "everything is recycling, you need to notice this." --- src/components/miners/CrownHistoryGrid.tsx | 6 -- src/components/miners/CrownRateChart.tsx | 6 -- src/components/miners/CrownRoundFillNote.tsx | 61 ------------------- src/components/miners/MinerLeaderboard.tsx | 9 --- src/components/miners/StickyNetworkHeader.tsx | 54 ++++++++++++++-- 5 files changed, 50 insertions(+), 86 deletions(-) delete mode 100644 src/components/miners/CrownRoundFillNote.tsx diff --git a/src/components/miners/CrownHistoryGrid.tsx b/src/components/miners/CrownHistoryGrid.tsx index 0ee5963..8bfb302 100644 --- a/src/components/miners/CrownHistoryGrid.tsx +++ b/src/components/miners/CrownHistoryGrid.tsx @@ -14,7 +14,6 @@ import { useCrownHistory, useHaltState, type Direction } from '../../api'; import { FONTS } from '../../theme'; import CrownGridHoverCard from './CrownGridHoverCard'; import CrownGridRangeInputs from './CrownGridRangeInputs'; -import CrownRoundFillNote from './CrownRoundFillNote'; import { buildCells, buildTiers, type CellState } from './crownGridCells'; // Mirrors SCORING_WINDOW_BLOCKS in allways/constants.py — validator sets @@ -719,11 +718,6 @@ const CrownHistoryGrid: React.FC<{ as of #{headBlock.toLocaleString()} · each cell = 1 block (12s) · each row = 60 blocks (12m) - ); }; diff --git a/src/components/miners/CrownRateChart.tsx b/src/components/miners/CrownRateChart.tsx index ab855a3..34696ab 100644 --- a/src/components/miners/CrownRateChart.tsx +++ b/src/components/miners/CrownRateChart.tsx @@ -14,7 +14,6 @@ import { type Direction, } from '../../api'; import { FONTS } from '../../theme'; -import CrownRoundFillNote from './CrownRoundFillNote'; const PANEL_W = 800; const PANEL_H = 140; @@ -666,11 +665,6 @@ const CrownRateChart: React.FC<{ )} #{head.toLocaleString()} - ); }; diff --git a/src/components/miners/CrownRoundFillNote.tsx b/src/components/miners/CrownRoundFillNote.tsx deleted file mode 100644 index a0cb926..0000000 --- a/src/components/miners/CrownRoundFillNote.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -// Mirrors allways/constants.py SCORING_WINDOW_BLOCKS — the validator -// flushes crown rows to the dashboard DB once per round, so panels that -// read crown_holders / rate_history won't show new data between flushes. -// (The top-bar current-crown widget is live ~12s — see current_crown_holders.) -const SCORING_WINDOW_BLOCKS = 600; - -interface Props { - // Current chain head. When null/0 the note renders without the - // last/next refresh figures. - headBlock?: number | null; - // What this panel actually shows ("crown data", "rate history", - // "crown share"...). Goes at the start of the sentence. - subject?: string; - sx?: object; -} - -const CrownRoundFillNote: React.FC = ({ - headBlock, - subject = 'crown data', - sx, -}) => { - const head = headBlock ?? 0; - const lastRefresh = - head > 0 - ? Math.floor(head / SCORING_WINDOW_BLOCKS) * SCORING_WINDOW_BLOCKS - : null; - const nextRefresh = - lastRefresh != null ? lastRefresh + SCORING_WINDOW_BLOCKS : null; - const blocksLeft = - nextRefresh != null ? Math.max(0, nextRefresh - head) : null; - - return ( - - {subject} refreshes at end of each ~2h scoring round - {lastRefresh != null && ( - <> - {' · last refresh #'} - {lastRefresh.toLocaleString()} - {' · next ~'} - {blocksLeft} - {' blocks'} - - )} - - ); -}; - -export default CrownRoundFillNote; diff --git a/src/components/miners/MinerLeaderboard.tsx b/src/components/miners/MinerLeaderboard.tsx index 0cfa263..3287e74 100644 --- a/src/components/miners/MinerLeaderboard.tsx +++ b/src/components/miners/MinerLeaderboard.tsx @@ -14,13 +14,11 @@ import { } from '@mui/material'; import { useNavigate } from 'react-router-dom'; import { - useHaltState, useMinerLeaderboard, type LeaderboardRow, type Range, } from '../../api'; import CrownIcon from './CrownIcon'; -import CrownRoundFillNote from './CrownRoundFillNote'; import SortHeader, { type SortDir } from './SortHeader'; import { FONTS } from '../../theme'; import { formatTao, shortHotkey } from '../../utils/format'; @@ -97,8 +95,6 @@ const MinerLeaderboard: React.FC<{ const navigate = useNavigate(); const theme = useTheme(); const { data, isLoading } = useMinerLeaderboard(range); - const { data: halt } = useHaltState(); - const headBlock = halt?.asOfBlock ?? 0; const [sortKey, setSortKey] = useState('crownShare'); const [sortDir, setSortDir] = useState('desc'); const [query, setQuery] = useState(''); @@ -387,11 +383,6 @@ const MinerLeaderboard: React.FC<{ })} - ); }; diff --git a/src/components/miners/StickyNetworkHeader.tsx b/src/components/miners/StickyNetworkHeader.tsx index b8cf919..f0996fd 100644 --- a/src/components/miners/StickyNetworkHeader.tsx +++ b/src/components/miners/StickyNetworkHeader.tsx @@ -5,9 +5,24 @@ import { useCurrentCrown, useHaltState } from '../../api'; import CrownIcon from './CrownIcon'; import { FONTS } from '../../theme'; +// Mirrors allways/constants.py SCORING_WINDOW_BLOCKS — the validator +// flushes crown_holders / rate_history once per round, so most panels +// only refresh at that cadence. The top-right indicator surfaces the +// math (last/next) so we don't need to label every panel individually. +const SCORING_WINDOW_BLOCKS = 600; + const StickyNetworkHeader: React.FC = () => { const { data: crown } = useCurrentCrown(); const { data: halt } = useHaltState(); + const head = halt?.asOfBlock ?? 0; + const lastRefresh = + head > 0 + ? Math.floor(head / SCORING_WINDOW_BLOCKS) * SCORING_WINDOW_BLOCKS + : null; + const blocksUntilRefresh = + lastRefresh != null + ? Math.max(0, lastRefresh + SCORING_WINDOW_BLOCKS - head) + : null; const segments: React.ReactNode[] = []; if (crown) { @@ -90,7 +105,7 @@ const StickyNetworkHeader: React.FC = () => { {segments} - + { backgroundColor: halted ? 'error.main' : 'status.active', }} /> - - {halted ? 'halted' : 'healthy'} - + {halted ? ( + + paused + + ) : lastRefresh != null ? ( + + last refresh #{lastRefresh.toLocaleString()} + + · + + next ~{blocksUntilRefresh} blocks + + ) : ( + + healthy + + )} From 4584786302b89daccc119300daceb090fb04cfc7 Mon Sep 17 00:00:00 2001 From: anderdc Date: Wed, 27 May 2026 17:34:16 -0500 Subject: [PATCH 4/5] HaltBanner: soften wording to "paused for maintenance" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "SWAP CONTRACT HALTED — emissions are recycling, no miner is currently earning. Rate posts still accepted; scoring resumes on unhalt." reads as an alert about something broken. Halt is almost always a planned admin action (upgrade window, parameter change), so user-facing copy frames it as scheduled rather than emergency: "System is paused for maintenance. Swaps are not being initiated. Emissions are recycling. Scoring will resume when the system is unpaused. (as of block #N)" Same banner styling, same /halt data source, same gating. --- src/components/layout/HaltBanner.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/layout/HaltBanner.tsx b/src/components/layout/HaltBanner.tsx index dba9dd9..2905eeb 100644 --- a/src/components/layout/HaltBanner.tsx +++ b/src/components/layout/HaltBanner.tsx @@ -35,8 +35,9 @@ const HaltBanner: React.FC = () => { textAlign: 'center', }} > - SWAP CONTRACT HALTED — emissions are recycling, no miner is currently - earning. Rate posts still accepted; scoring resumes on unhalt. + System is paused for maintenance. Swaps are not being initiated. + Emissions are recycling. Scoring will resume when the system is + unpaused. {halt.asOfBlock != null && ( (as of block #{halt.asOfBlock.toLocaleString()}) From e62f2b03d1af38c9f16b705675d22bb8b9f2c466 Mon Sep 17 00:00:00 2001 From: anderdc Date: Wed, 27 May 2026 17:37:35 -0500 Subject: [PATCH 5/5] halt UI: warning amber instead of error red Red signals critical-failure semantics; halt is a planned admin action ("paused for maintenance"). Amber/warning palette communicates "heads up, things are different" without the alarm bell. Switches both the full-width HaltBanner background and the top-right "paused" dot+text from error.main/error.contrastText to warning.main/warning.contrastText. Same MUI palette path, same contrast guarantees (warning.contrastText auto-resolves). --- src/components/layout/HaltBanner.tsx | 6 +++--- src/components/miners/StickyNetworkHeader.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/layout/HaltBanner.tsx b/src/components/layout/HaltBanner.tsx index 2905eeb..67e4328 100644 --- a/src/components/layout/HaltBanner.tsx +++ b/src/components/layout/HaltBanner.tsx @@ -17,12 +17,12 @@ const HaltBanner: React.FC = () => { role="alert" sx={{ width: '100%', - backgroundColor: 'error.main', - color: 'error.contrastText', + backgroundColor: 'warning.main', + color: 'warning.contrastText', py: 1, px: { xs: 2, md: 4 }, borderBottom: '1px solid', - borderColor: 'error.dark', + borderColor: 'warning.dark', }} > { width: 6, height: 6, borderRadius: '50%', - backgroundColor: halted ? 'error.main' : 'status.active', + backgroundColor: halted ? 'warning.main' : 'status.active', }} /> {halted ? ( @@ -119,7 +119,7 @@ const StickyNetworkHeader: React.FC = () => { variant="mono" sx={{ fontSize: '0.72rem', - color: 'error.main', + color: 'warning.main', fontWeight: 600, letterSpacing: '0.04em', }}