ci: format the frontend with prettier and gate it
deploy / build (push) Successful in 6m44s
deploy / deploy-web (push) Successful in 8s
deploy / deploy-api (push) Successful in 15s

`cargo fmt --check` has gated the Rust half since the first commit; nothing
gated the TypeScript, so 25 of 31 files under web/src had drifted from the
.prettierrc that was already sitting in the repo. `pnpm format:check` is now the
counterpart, in the same step as `pnpm lint`.

The reformat is whitespace only — the emitted bundle hashes identically before
and after.

web/.prettierignore excludes src/api/generated. Those files are written by ts-rs
from the entities crate and CI already fails on any diff in them after `cargo
test`; formatting them here would be reverted by the next regeneration and turn
that drift gate red on every subsequent build. Verified by regenerating and
confirming the tree stays clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
2026-09-08 15:01:28 +03:00
co-authored by Claude Opus 5
parent 9cbc2cc274
commit 337d1ef516
9 changed files with 71 additions and 31 deletions
+5
View File
@@ -91,6 +91,11 @@ jobs:
# already on PATH from `npm i -g pnpm` (gitea-runners.md §4).
cd web
pnpm install --frozen-lockfile
# The web half's counterpart to `cargo fmt --check` above. It skips
# src/api/generated (.prettierignore) — ts-rs owns those files and
# regenerates them in its own style, so formatting them here would be
# undone on the next `cargo test` and fail the drift gate above.
pnpm format:check
pnpm lint
pnpm build
+7
View File
@@ -0,0 +1,7 @@
# Written by ts-rs from the Rust entities crate when `cargo test -p
# blackbeard-entities` runs, and checked for drift in CI. Formatting them here
# would be undone by the next regeneration, and the "generated types are
# current" gate would then fail on every build.
src/api/generated/
dist/
+1
View File
@@ -10,6 +10,7 @@
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --write src",
"format:check": "prettier --check src",
"typecheck": "tsc -b --noEmit"
},
"dependencies": {
+15 -11
View File
@@ -56,7 +56,10 @@ export default function App() {
// Default to the busiest chain we can actually track — the list is ordered
// by node count, and an untracked chain has nothing behind it.
const chain = route.chain ?? state.chains.find((c) => c.tracking === 'full')?.id ?? null
const info = useMemo(() => state.chains.find((c) => c.id === chain) ?? null, [state.chains, chain])
const info = useMemo(
() => state.chains.find((c) => c.id === chain) ?? null,
[state.chains, chain],
)
useWatch(chain, route.window)
@@ -67,16 +70,14 @@ export default function App() {
: `/${merged.chain ?? ''}/${merged.window}`
}, [])
const selectMiner = useCallback(
(miner: string) => {
// Keep the chain and window; the miner panel opens above the board rather
// than replacing the page, so the standings stay visible behind it.
setRoute((r) => ({ ...r, miner }))
},
[],
)
const selectMiner = useCallback((miner: string) => {
// Keep the chain and window; the miner panel opens above the board rather
// than replacing the page, so the standings stay visible behind it.
setRoute((r) => ({ ...r, miner }))
}, [])
const interval = state.summary?.block_interval_seconds ?? state.summary?.target_block_time_seconds ?? 6
const interval =
state.summary?.block_interval_seconds ?? state.summary?.target_block_time_seconds ?? 6
const activeWindow = WINDOWS.find((w) => w.id === route.window) ?? WINDOWS[1]!
return (
@@ -199,7 +200,10 @@ function Sigil() {
stroke="var(--data)"
strokeWidth="1.6"
/>
<path d="M17 8 L24 12 V21 C24 26 20.5 29.5 17 31.5 C13.5 29.5 10 26 10 21 V12 Z" fill="var(--data-wash)" />
<path
d="M17 8 L24 12 V21 C24 26 20.5 29.5 17 31.5 C13.5 29.5 10 26 10 21 V12 Z"
fill="var(--data-wash)"
/>
<path d="M17 11 V28" stroke="var(--accent)" strokeWidth="1.6" />
<path d="M13 15 L17 11 L21 15" fill="none" stroke="var(--accent)" strokeWidth="1.6" />
</svg>
+4 -6
View File
@@ -50,11 +50,7 @@ export function BlockTicker({
return (
<div
key={block.height}
className={[
'ticker-row',
mine ? 'mine' : '',
fresh.has(block.height) ? 'fresh' : '',
]
className={['ticker-row', mine ? 'mine' : '', fresh.has(block.height) ? 'fresh' : '']
.filter(Boolean)
.join(' ')}
>
@@ -77,7 +73,9 @@ export function BlockTicker({
{block.display}
</a>
<span className="ticker-gap">
{block.gap_seconds !== null ? `+${seconds(block.gap_seconds)}` : ago(block.observed_at)}
{block.gap_seconds !== null
? `+${seconds(block.gap_seconds)}`
: ago(block.observed_at)}
</span>
</div>
)
+1 -5
View File
@@ -52,11 +52,7 @@ export function ChainSwitcher({
<button
key={chain.genesis ?? chain.id}
type="button"
className={[
'chain-chip',
isActive ? 'is-active' : '',
tracked ? '' : 'is-untracked',
]
className={['chain-chip', isActive ? 'is-active' : '', tracked ? '' : 'is-untracked']
.filter(Boolean)
.join(' ')}
aria-current={isActive ? 'page' : undefined}
+27 -4
View File
@@ -60,7 +60,12 @@ export function MinerPanel({
</h2>
<div
className="numeral"
style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 4, overflowWrap: 'anywhere' }}
style={{
fontSize: 11,
color: 'var(--text-muted)',
marginTop: 4,
overflowWrap: 'anywhere',
}}
>
{miner}
</div>
@@ -68,7 +73,15 @@ export function MinerPanel({
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
<button
className="segmented"
style={{ padding: '7px 13px', font: 'inherit', fontSize: 12, letterSpacing: '0.1em', textTransform: 'uppercase', color: mine ? 'var(--accent-bright)' : 'var(--text-secondary)', cursor: 'pointer' }}
style={{
padding: '7px 13px',
font: 'inherit',
fontSize: 12,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: mine ? 'var(--accent-bright)' : 'var(--text-secondary)',
cursor: 'pointer',
}}
aria-pressed={mine}
onClick={() => toggle(miner)}
>
@@ -76,7 +89,15 @@ export function MinerPanel({
</button>
<button
className="segmented"
style={{ padding: '7px 13px', font: 'inherit', fontSize: 12, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-secondary)', cursor: 'pointer' }}
style={{
padding: '7px 13px',
font: 'inherit',
fontSize: 12,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: 'var(--text-secondary)',
cursor: 'pointer',
}}
onClick={onClose}
>
Close
@@ -121,7 +142,9 @@ export function MinerPanel({
</div>
<div className="stat">
<div className="stat-label">Best streak</div>
<div className="stat-value">{row && row.best_streak > 1 ? `×${row.best_streak}` : '—'}</div>
<div className="stat-value">
{row && row.best_streak > 1 ? `×${row.best_streak}` : '—'}
</div>
<div className="stat-note">consecutive blocks</div>
</div>
</div>
+10 -2
View File
@@ -160,7 +160,13 @@ export function ShareChart({ points }: { points: MinerSeriesPoint[] }) {
{hover && (
<>
<line className="crosshair" x1={hover.x} x2={hover.x} y1={PAD.top} y2={chart.baseline} />
<line
className="crosshair"
x1={hover.x}
x2={hover.x}
y1={PAD.top}
y2={chart.baseline}
/>
{/* 2px surface ring so the marker stays legible where it sits on the
line; the ring is part of the hit target, not just spacing. */}
<circle className="marker" cx={hover.x} cy={hover.y} r={4.5} />
@@ -196,7 +202,9 @@ export function ShareChart({ points }: { points: MinerSeriesPoint[] }) {
transform: hover.x > width / 2 ? 'translate(-100%, 0)' : 'translate(10px, 0)',
}}
>
<div className="tooltip-label">{timeLabel(new Date(hover.point.at).getTime(), chart.span)}</div>
<div className="tooltip-label">
{timeLabel(new Date(hover.point.at).getTime(), chart.span)}
</div>
<div>
<strong>{share(hover.point.share)}</strong> of blocks
</div>
+1 -3
View File
@@ -102,9 +102,7 @@ function TrendStat({
{...(hero ? { hero } : {})}
{...(reading ? { noteClass: 'reading' } : noteClass ? { noteClass } : {})}
{...(title ? { title } : {})}
{...(points
? { trend: <Sparkline points={points} label={label} onProbe={onProbe} /> }
: {})}
{...(points ? { trend: <Sparkline points={points} label={label} onProbe={onProbe} /> } : {})}
/>
)
}