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() {