From a71ad56f3c2fa8f9a952f2610c16499cf3435c51 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Tue, 11 Aug 2026 22:22:09 +0100 Subject: [PATCH] Expose browsing daily target range --- .../src/features/growth/growth-page.tsx | 49 +++++++++++++++++++ docs/configuration.md | 2 +- docs/frontend.md | 2 +- tests/dashboard/growth.test.tsx | 32 ++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/src/features/growth/growth-page.tsx b/apps/dashboard/src/features/growth/growth-page.tsx index 8ac59e7..9c6b1b4 100644 --- a/apps/dashboard/src/features/growth/growth-page.tsx +++ b/apps/dashboard/src/features/growth/growth-page.tsx @@ -178,12 +178,27 @@ function BrowsingSimulationSection({ account }: { account: GrowthAccountDetail } const runNow = useRunBrowsingSimulationMutation(account.accountId); const { settings, stats, todayTarget, recentDecisions } = account.browsing; const selected = recentDecisions.filter((decision) => decision.decision === 'simulated-upvote'); + const [dailyMinimum, setDailyMinimum] = useState(String(settings.dailyTargetMin)); + const [dailyMaximum, setDailyMaximum] = useState(String(settings.dailyTargetMax)); const [gapMinutes, setGapMinutes] = useState(String(settings.minimumGapMs / 60_000)); const [selectionsPerSession, setSelectionsPerSession] = useState(String(settings.selectionsPerSession)); + useEffect(() => setDailyMinimum(String(settings.dailyTargetMin)), [settings.dailyTargetMin]); + useEffect(() => setDailyMaximum(String(settings.dailyTargetMax)), [settings.dailyTargetMax]); useEffect(() => setGapMinutes(String(settings.minimumGapMs / 60_000)), [settings.minimumGapMs]); useEffect(() => setSelectionsPerSession(String(settings.selectionsPerSession)), [settings.selectionsPerSession]); + const parsedDailyMinimum = Number.parseInt(dailyMinimum, 10); + const parsedDailyMaximum = Number.parseInt(dailyMaximum, 10); + const dailyRangeValid = Number.isInteger(parsedDailyMinimum) + && Number.isInteger(parsedDailyMaximum) + && parsedDailyMinimum >= 1 + && parsedDailyMaximum <= 12 + && parsedDailyMinimum <= parsedDailyMaximum; + const dailyRangeError = dailyMinimum !== '' && dailyMaximum !== '' && !dailyRangeValid + ? 'Use a range from 1 to 12, with the minimum no greater than the maximum.' + : undefined; + return (
@@ -212,6 +227,40 @@ function BrowsingSimulationSection({ account }: { account: GrowthAccountDetail } label="Submit selected upvotes" onChange={(autoVoteEnabled) => update.mutate({ autoVoteEnabled })} /> +
+ + + +
{ ]))); }); + it('updates the browsing daily target range together', async () => { + renderGrowth(); + const user = userEvent.setup(); + + const minimum = await screen.findByLabelText('Daily minimum'); + await user.clear(minimum); + await user.type(minimum, '6'); + const maximum = screen.getByLabelText('Daily maximum'); + await user.clear(maximum); + await user.type(maximum, '9'); + await user.click(screen.getByRole('button', { name: 'Save daily range' })); + + await waitFor(() => expect(requests).toContainEqual(expect.objectContaining({ + method: 'PATCH', body: { dailyTargetMin: 6, dailyTargetMax: 9 }, + }))); + }); + + it('rejects an inverted browsing daily target range in the dashboard', async () => { + renderGrowth(); + const user = userEvent.setup(); + + const minimum = await screen.findByLabelText('Daily minimum'); + await user.clear(minimum); + await user.type(minimum, '8'); + const maximum = screen.getByLabelText('Daily maximum'); + await user.clear(maximum); + await user.type(maximum, '4'); + + expect(screen.getByRole('button', { name: 'Save daily range' })).toBeDisabled(); + expect(screen.getAllByText(/minimum no greater than the maximum/i)).toHaveLength(2); + }); + it('shows an empty state when there are no accounts', async () => { overviewPayload = []; renderGrowth();