diff --git a/README.md b/README.md index d929df01..78ac72f1 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Everything runs locally. No wrapper, no proxy, no API keys, nothing leaves your npx codeburn ``` -That opens the interactive dashboard (last 7 days by default). Arrow keys switch periods, `q` quits. That is the 30-second version. You now know where your AI budget goes. +That opens the interactive dashboard (today by default, or the last 7 days when today has no usage yet). Arrow keys switch periods, `q` quits. That is the 30-second version. You now know where your AI budget goes. **Install it** for a permanent `codeburn` command: @@ -440,7 +440,7 @@ Run `codeburn` for the dashboard, or use a subcommand below. Most commands also | Command | What it does | |---------|--------------| -| `codeburn` | Interactive dashboard, last 7 days (the default view) | +| `codeburn` | Interactive dashboard, today (falls back to the last 7 days when today is empty) | | `codeburn today` | Today's usage | | `codeburn month` | This calendar month's usage | | `codeburn overview` | Plain-text monthly summary, copy-pasteable (`--no-color`, `--from`/`--to`) | diff --git a/app/renderer/App.test.tsx b/app/renderer/App.test.tsx index 2a6aacac..2fb475ad 100644 --- a/app/renderer/App.test.tsx +++ b/app/renderer/App.test.tsx @@ -241,6 +241,25 @@ describe('App shortcuts', () => { await waitFor(() => expect(mocks.getOverview).toHaveBeenCalledWith('today', 'all')) }) + it('falls back to 7 days when the boot payload shows today has no sessions', async () => { + localStorage.removeItem('codeburn.defaultPeriod') + const empty = overviewPayload() + mocks.getOverview.mockResolvedValue({ ...empty, current: { ...empty.current, sessions: 0 } }) + render() + await waitFor(() => expect(mocks.getOverview).toHaveBeenCalledWith('today', 'all')) + await waitFor(() => expect(mocks.getOverview).toHaveBeenCalledWith('week', 'all')) + }) + + it('leaves a persisted default period alone when today has no sessions', async () => { + localStorage.setItem('codeburn.defaultPeriod', 'today') + const empty = overviewPayload() + mocks.getOverview.mockResolvedValue({ ...empty, current: { ...empty.current, sessions: 0 } }) + render() + await waitFor(() => expect(mocks.getOverview).toHaveBeenCalledWith('today', 'all')) + await act(async () => { await Promise.resolve() }) + expect(mocks.getOverview).not.toHaveBeenCalledWith('week', 'all') + }) + it('switches sections with command-number shortcuts', async () => { render() diff --git a/app/renderer/App.tsx b/app/renderer/App.tsx index e72777b6..8837ffc7 100644 --- a/app/renderer/App.tsx +++ b/app/renderer/App.tsx @@ -146,11 +146,16 @@ function isPeriod(value: string): value is Period { return (STANDARD_PERIODS as string[]).includes(value) } -/** Boot period = the persisted "Default period" Settings writes, else today. */ -function initialPeriod(): Period { +/** The persisted "Default period" Settings writes, when there is one. */ +function savedPeriod(): Period | null { let saved: string | null = null try { saved = globalThis.localStorage?.getItem('codeburn.defaultPeriod') ?? null } catch { /* storage can be unavailable */ } - return saved && isPeriod(saved) ? saved : 'today' + return saved && isPeriod(saved) ? saved : null +} + +/** Boot period = the persisted "Default period" Settings writes, else today. */ +function initialPeriod(): Period { + return savedPeriod() ?? 'today' } /** Persisted Claude config override (empty/absent = aggregate all configs). */ @@ -254,6 +259,17 @@ function AppMain() { // every section to spawn its own read behind the still-running parse, and each // one then died on its own timeout. Stay gated (and keep the splash) until the // hydration actually settles. + // #1111: with no persisted default the app opens on Today and falls back to 7 + // days once, when the first payload shows today has no sessions yet. Disarmed + // by the period picker, so it can never move a period the user chose. + const autoPeriod = useRef(savedPeriod() === null) + useEffect(() => { + const sessions = overview.data?.current.sessions + if (!autoPeriod.current || sessions === undefined) return + autoPeriod.current = false + if (period === 'today' && sessions === 0) setPeriod('week') + }, [overview.data, period]) + const overviewCold = isColdHydrating(overview.error) const [ready, setReady] = useState(false) useEffect(() => { @@ -460,6 +476,7 @@ function AppMain() { const onPeriodChange = (value: string) => { if (isPeriod(value)) { + autoPeriod.current = false setCustomRange(null) setPeriod(value) } diff --git a/dash/src/App.tsx b/dash/src/App.tsx index 24d58518..58ac3485 100644 --- a/dash/src/App.tsx +++ b/dash/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, type ReactNode } from 'react' +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { @@ -495,6 +495,17 @@ export function App() { const primary = viewing ?? local const c0 = primary?.payload?.current + // #1111: the dashboard opens on Today and falls back to 7 days once, when the + // first local payload shows today still has no sessions. Disarmed by the + // period picker, so it can never move a period the user chose. + const autoPeriod = useRef(true) + useEffect(() => { + const sessions = local?.payload?.current?.sessions + if (!autoPeriod.current || sessions === undefined) return + autoPeriod.current = false + if (period === 'today' && sessions === 0) setPeriod('week') + }, [local, period]) + const providerOptions = useMemo( () => c0 @@ -589,7 +600,7 @@ export function App() {