Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +23,7 @@ const AppLayout: React.FC = () => {
}}
>
<TopNav />
<HaltBanner />
<Stack
ref={mainRef}
component="main"
Expand Down
51 changes: 51 additions & 0 deletions src/components/layout/HaltBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import { useHaltState } from '../../api';
import { FONTS } from '../../theme';

// Full-width red banner that mounts above every page when the swap
// contract is halted. The StickyNetworkHeader does carry a tiny
// "halted" dot, but during a halt no miner earns crown and the full
// emission pool recycles — that's significant enough to warrant
// something unmissable. Renders nothing when not halted.
const HaltBanner: React.FC = () => {
const { data: halt } = useHaltState();
if (!halt?.halted) return null;

return (
<Box
role="alert"
sx={{
width: '100%',
backgroundColor: 'warning.main',
color: 'warning.contrastText',
py: 1,
px: { xs: 2, md: 4 },
borderBottom: '1px solid',
borderColor: 'warning.dark',
}}
>
<Typography
variant="mono"
sx={{
fontFamily: FONTS.mono,
fontSize: '0.78rem',
letterSpacing: '0.04em',
fontWeight: 600,
textAlign: 'center',
}}
>
System is paused for maintenance. Swaps are not being initiated.
Emissions are recycling. Scoring will resume when the system is
unpaused.
{halt.asOfBlock != null && (
<Box component="span" sx={{ ml: 1, fontWeight: 400, opacity: 0.85 }}>
(as of block #{halt.asOfBlock.toLocaleString()})
</Box>
)}
</Typography>
</Box>
);
};

export default HaltBanner;
1 change: 1 addition & 0 deletions src/components/layout/index.ts
Original file line number Diff line number Diff line change
@@ -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';
56 changes: 51 additions & 5 deletions src/components/miners/StickyNetworkHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -90,18 +105,49 @@ const StickyNetworkHeader: React.FC = () => {
<Stack direction="row" spacing={3} alignItems="center" sx={{ flex: 1 }}>
{segments}
</Stack>
<Stack direction="row" spacing={0.5} alignItems="center">
<Stack direction="row" spacing={0.75} alignItems="center">
<Box
sx={{
width: 6,
height: 6,
borderRadius: '50%',
backgroundColor: halted ? 'error.main' : 'status.active',
backgroundColor: halted ? 'warning.main' : 'status.active',
}}
/>
<Typography variant="mono" sx={{ fontSize: '0.72rem' }}>
{halted ? 'halted' : 'healthy'}
</Typography>
{halted ? (
<Typography
variant="mono"
sx={{
fontSize: '0.72rem',
color: 'warning.main',
fontWeight: 600,
letterSpacing: '0.04em',
}}
>
paused
</Typography>
) : lastRefresh != null ? (
<Typography
variant="mono"
sx={{
fontSize: '0.72rem',
color: 'text.secondary',
}}
>
last refresh #{lastRefresh.toLocaleString()}
<Box component="span" sx={{ mx: 0.5, color: 'text.disabled' }}>
·
</Box>
next ~{blocksUntilRefresh} blocks
</Typography>
) : (
<Typography
variant="mono"
sx={{ fontSize: '0.72rem', color: 'text.secondary' }}
>
healthy
</Typography>
)}
</Stack>
</Stack>
</Box>
Expand Down
Loading