From 08aa5c42f82b3d9a5e200cdbe512a810051cfcd9 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 29 Aug 2026 07:36:13 +0200 Subject: [PATCH] Say "masked" only when launchers really are 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) --- web/src/components/BudgetPanel.tsx | 2 +- web/src/components/BudgetTodayCard.test.tsx | 36 +++++++++++++-------- web/src/components/BudgetTodayCard.tsx | 12 +++++-- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/web/src/components/BudgetPanel.tsx b/web/src/components/BudgetPanel.tsx index ce7b38b..f1edd2f 100644 --- a/web/src/components/BudgetPanel.tsx +++ b/web/src/components/BudgetPanel.tsx @@ -40,7 +40,7 @@ export function BudgetPanel({ demo }: Props) {

{data.error ?? 'Budget state unavailable.'}

) : ( - + )} diff --git a/web/src/components/BudgetTodayCard.test.tsx b/web/src/components/BudgetTodayCard.test.tsx index 7e08481..dd0ce2e 100644 --- a/web/src/components/BudgetTodayCard.test.tsx +++ b/web/src/components/BudgetTodayCard.test.tsx @@ -5,7 +5,7 @@ import { BudgetTodayCard } from './BudgetTodayCard' describe('BudgetTodayCard', () => { it('shows remaining time, usage and the next warning', () => { - render() + render() 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( , ) 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( - , - ) - expect(screen.getByText(/Cutoff engaged/)).toBeInTheDocument() + render() + 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() + expect( + screen.getByText(/Cutoff engaged — launchers are not currently masked/), + ).toBeInTheDocument() + }) }) diff --git a/web/src/components/BudgetTodayCard.tsx b/web/src/components/BudgetTodayCard.tsx index 6e42f9f..65b5557 100644 --- a/web/src/components/BudgetTodayCard.tsx +++ b/web/src/components/BudgetTodayCard.tsx @@ -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 (

Today

@@ -39,7 +40,14 @@ export function BudgetTodayCard({ today }: Props) {

{today.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'} ) : today.next_warning_seconds === null ? ( 'No further warnings today.'