Say "masked" only when launchers really are
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run

The cutoff badge asserted "launchers are masked" from `blocked` alone, but
the two can disagree: the total-block branch of `playtime_tick` calls
`reconcile(should_block=False)` and leaves `blocked_at` set, so the mounts
are gone while the day still reads blocked. The panel already fetches
`rules.masked_launchers` for its "Masked now" row, so the badge now reads
the same live count instead of inferring one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-08-29 07:36:13 +02:00
parent 9062fa8c10
commit 08aa5c42f8
3 changed files with 33 additions and 17 deletions

View File

@ -40,7 +40,7 @@ export function BudgetPanel({ demo }: Props) {
<p className="error">{data.error ?? 'Budget state unavailable.'}</p>
</section>
) : (
<BudgetTodayCard today={data.today} />
<BudgetTodayCard today={data.today} maskedCount={data.rules.masked_launchers.length} />
)}
<BudgetLiveCard session={data.session} />
<BudgetHistoryChart days={data.history} budgetSeconds={data.rules.budget_seconds} />

View File

@ -5,7 +5,7 @@ import { BudgetTodayCard } from './BudgetTodayCard'
describe('BudgetTodayCard', () => {
it('shows remaining time, usage and the next warning', () => {
render(<BudgetTodayCard today={makeBudgetToday()} />)
render(<BudgetTodayCard today={makeBudgetToday()} maskedCount={0} />)
expect(screen.getByText('7h 0m')).toBeInTheDocument()
expect(screen.getByText(/1h 0m of 8h 0m used/)).toBeInTheDocument()
expect(screen.getByText(/Next warning at 1h 0m remaining/)).toBeInTheDocument()
@ -16,6 +16,7 @@ describe('BudgetTodayCard', () => {
render(
<BudgetTodayCard
today={makeBudgetToday({ seconds_remaining: 900, warned_seconds: [3600, 1800] })}
maskedCount={0}
/>,
)
expect(document.querySelector('.budget-meter--warn')).not.toBeNull()
@ -28,26 +29,33 @@ describe('BudgetTodayCard', () => {
next_warning_seconds: null,
warned_seconds: [3600, 1800, 600, 300],
})}
maskedCount={0}
/>,
)
expect(screen.getByText(/No further warnings today/)).toBeInTheDocument()
expect(document.querySelector('.budget-meter--warn')).not.toBeNull()
})
const blockedToday = () =>
makeBudgetToday({
blocked: true,
blocked_at: 1,
seconds_used: 28800,
seconds_remaining: 0,
fraction_used: 1,
next_warning_seconds: null,
})
it('shows the cutoff badge when blocked', () => {
render(
<BudgetTodayCard
today={makeBudgetToday({
blocked: true,
blocked_at: 1,
seconds_used: 28800,
seconds_remaining: 0,
fraction_used: 1,
next_warning_seconds: null,
})}
/>,
)
expect(screen.getByText(/Cutoff engaged/)).toBeInTheDocument()
render(<BudgetTodayCard today={blockedToday()} maskedCount={2} />)
expect(screen.getByText(/Cutoff engaged — launchers are masked/)).toBeInTheDocument()
expect(document.querySelector('.budget-meter--blocked')).not.toBeNull()
})
it('does not claim a mask that is not in place', () => {
render(<BudgetTodayCard today={blockedToday()} maskedCount={0} />)
expect(
screen.getByText(/Cutoff engaged — launchers are not currently masked/),
).toBeInTheDocument()
})
})

View File

@ -3,6 +3,7 @@ import type { BudgetToday } from '../types'
interface Props {
today: BudgetToday
maskedCount: number
}
/** Which colour the meter takes: normal, into the warning band, or spent.
@ -17,7 +18,7 @@ function meterState(today: BudgetToday): string {
return today.warned_seconds.length > 0 ? 'warn' : 'ok'
}
export function BudgetTodayCard({ today }: Props) {
export function BudgetTodayCard({ today, maskedCount }: Props) {
return (
<section className="budget-today">
<h2>Today</h2>
@ -39,7 +40,14 @@ export function BudgetTodayCard({ today }: Props) {
<p className="hint">
{today.blocked ? (
<span className="budget-badge budget-badge--blocked">
Cutoff engaged — launchers are masked
{/* Read the live mount count rather than inferring it from
`blocked`: the total-block branch releases every mount while
leaving `blocked_at` set, so the two can genuinely disagree —
and a badge claiming a mask that is not there is the one thing
this panel must never do. */}
{maskedCount > 0
? 'Cutoff engaged — launchers are masked'
: 'Cutoff engaged — launchers are not currently masked'}
</span>
) : today.next_warning_seconds === null ? (
'No further warnings today.'