Skip to content

fix(date-range): unify 'all' period semantics between CLI and dashboard#221

Merged
iamtoruk merged 2 commits intogetagentseal:mainfrom
ozymandiashh:fix/unify-all-period-semantics
May 5, 2026
Merged

fix(date-range): unify 'all' period semantics between CLI and dashboard#221
iamtoruk merged 2 commits intogetagentseal:mainfrom
ozymandiashh:fix/unify-all-period-semantics

Conversation

@ozymandiashh
Copy link
Copy Markdown
Contributor

Summary

getDateRange was duplicated across src/cli.ts and src/dashboard.tsx with conflicting semantics for 'all':

  • CLI: intentionally bounded 'all' to the last 6 months (justified inline: keeps Codex/Cursor parses responsive on sparse multi-year history).
  • Dashboard: returned new Date(0) instead, so the same --period all flag silently meant two different windows depending on which entry point you hit.

Period, PERIODS, PERIOD_LABELS, and toPeriod were duplicated as well. cli-date.ts already existed for date helpers (parseDateRangeFlags), so the consolidation lives there.

After this PR

  • 'all' consistently means Last 6 months in both CLI and dashboard.
  • Dashboard tab strip label changes from 'All Time' to '6 Months' (short form for tight UI; long-form 'Last 6 months' from getDateRange().label continues to drive CLI text output).
  • Existing --from / --to remains the escape hatch for users who need an unbounded historical window.
  • Single source of truth in src/cli-date.ts.

Changes

  • src/cli-date.ts: add Period, PERIODS, PERIOD_LABELS, toPeriod, getDateRange. Pull the existing 6-month rationale into a named ALL_TIME_MONTHS constant.
  • src/cli.ts: drop the local copies and import from cli-date.
  • src/dashboard.tsx: drop the local copies, route through a thin getPeriodRange(period: Period) wrapper to keep the strict Period type at the React boundary while letting the CLI continue to accept extras like 'yesterday'. Aliased the shared getDateRange import to getDateRangeShared to avoid shadowing the wrapper.
  • tests/cli-date.test.ts: 13 new cases covering:
    • 'all' regression guard (must never silently fall back to Date(0))
    • CLI/dashboard agreement on 'all'
    • end-of-month clamping tolerance (5–7 month window, so the test stays green when run on May 31 / Jul 31 / etc.)
    • 'yesterday' support
    • unknown-input fallback
  • README.md, CHANGELOG.md: surface the bound and point heavy users at --from/--to.

Backward compatibility

The CLI flag --period all continues to be accepted. Only the dashboard window changes to match what the CLI was already doing. No public API or schema change.

Alternatives considered

  • Make both unbounded: rejected. Preserves the existing CLI rationale (Codex/Cursor parsing performance on multi-year history).
  • Rename the period from all to 6months: rejected for backward compatibility. Users have shell aliases and menubar configs passing --period all. Capping all seamlessly + documenting --from/--to in the changelog felt less disruptive.
  • Emit a deprecation warning on --period all: rejected as overkill for a tactical bug fix. Open to revisit if maintainers prefer.

Notes for reviewer

  • Only the period meaning of 'all' is touched. The provider filter 'all' (e.g. dashboard.tsx:672, cli.ts --provider default) is unrelated and unchanged.
  • Main thing to review: whether 'all' should remain bounded to 6 months. This PR intentionally preserves the CLI's existing bounded behavior and fixes the dashboard drift. Users who need true historical ranges can use --from/--to.

Test plan

npx tsc --noEmit   # 0 errors
npx vitest run     # 33 files, 440 passed (was 427 baseline + 13 new in tests/cli-date.test.ts)
npm run build      # success
node dist/cli.js report -p all   # tab strip "[ 6 Months ]", header "6 Months", correct totals

Refs #93

`getDateRange` was duplicated across `src/cli.ts` and `src/dashboard.tsx`
with conflicting semantics for `'all'`. The CLI intentionally bounded
`'all'` to the last 6 months (justified inline: keeps Codex/Cursor parses
responsive on sparse multi-year history). The dashboard returned
`new Date(0)` instead, so the same `--period all` flag silently meant
two different windows depending on which entry point you hit.

`Period`, `PERIODS`, `PERIOD_LABELS`, and `toPeriod` were duplicated as
well, and `cli-date.ts` already existed for date helpers
(`parseDateRangeFlags`) so the consolidation lives there.

Both call sites now go through a single `getDateRange(period: string)`
in `cli-date.ts` that returns `{ range, label }`. The dashboard wraps it
as `getPeriodRange(period: Period)` to keep the strict `Period` type at
the React boundary while letting the CLI continue to accept extras like
`'yesterday'`.

`PERIOD_LABELS.all` becomes `'6 Months'` (short, for the dashboard tab
strip; the previous `'All Time'` was misleading and the long-form
`'Last 6 months'` from `getDateRange().label` already drives CLI output).

Changes:
- src/cli-date.ts: add `Period`, `PERIODS`, `PERIOD_LABELS`, `toPeriod`,
  `getDateRange`. Pull the existing 6-month rationale into a named
  `ALL_TIME_MONTHS` constant.
- src/cli.ts: drop the local copies and import from cli-date.
- src/dashboard.tsx: drop the local copies, route through
  `getPeriodRange`, alias the shared `getDateRange` import to
  `getDateRangeShared` to avoid shadowing the wrapper.
- tests/cli-date.test.ts: 13 cases covering `'all'` regression guard
  (must never silently fall back to `Date(0)`), CLI/dashboard agreement,
  end-of-month clamping tolerance, `'yesterday'` support, and
  unknown-input fallback.
- README.md, CHANGELOG.md: surface the bound and point heavy users at
  `--from`/`--to` for unbounded windows.

The CLI flag `--period all` continues to be accepted; only the dashboard
window changes to match what the CLI was already doing. No public API
or schema change.

Refs getagentseal#93
@ozymandiashh ozymandiashh force-pushed the fix/unify-all-period-semantics branch from 6c6c2be to 3dc3e32 Compare May 5, 2026 00:53
Copy link
Copy Markdown
Member

@iamtoruk iamtoruk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review across CLI/menubar impact, data accuracy, security, and code quality.

Impact

CLI: No behavioral change. The CLI already capped 'all' to 6 months.

macOS menubar / GNOME extension: No change. Both call codeburn status --period all which was already bounded to 6 months. Their local tab labels ('All') are separate constants and unaffected.

Dashboard only: Intentional fix. new Date(0) -> 6-month cap.

Security

No vulnerabilities found. toPeriod whitelists inputs with safe fallback, getDateRange has a default branch for defense in depth, the ISO regex is anchored (no ReDoS), no dynamic property access with user input, no prototype pollution vectors.

Minor note: cli-date.ts importing toDateString from daily-cache.ts pulls in transitive deps on crypto, fs, os. Not exploitable, but if this module is ever used in a browser context it would break. Consider inlining toDateString (it's a 1-line formatter) or extracting it to a tiny shared module.

Data accuracy bug: end-of-month overflow

The 'all' case computes:

const start = new Date(now.getFullYear(), now.getMonth() - ALL_TIME_MONTHS, now.getDate())

JS rolls overflow days into the next month. On Aug 31: new Date(2026, 2, 31) becomes Mar 3 -- silently excluding Feb 28, Mar 1, Mar 2 (3 days of data). On Oct 31: skips Apr 30. On May 31: skips Nov 30.

The data isn't deleted (it's in the cache), but the date range excludes it. The test accepts a 5-7 month tolerance which masks this.

Fix: Use day 1 of the target month:

const start = new Date(now.getFullYear(), now.getMonth() - ALL_TIME_MONTHS, 1)

This always produces a valid date and gives users slightly more than 6 months (safer direction for a cost tracker).

Code quality nits

  1. Unnecessary alias: getDateRange as getDateRangeShared in dashboard.tsx adds noise. The wrapper is called getPeriodRange -- there's no name conflict. Just import getDateRange directly.

  2. Tautological test: The "CLI and dashboard agree" test calls getDateRange('all') twice from the same function and asserts equality. It tests idempotency, not cross-module agreement. Consider renaming it to match what it actually verifies, or making it an integration test that imports through dashboard.tsx's wrapper.

  3. gnome/prefs.js still says "All Time" (line 29): After this PR the canonical label is "6 Months". Not blocking, but a follow-up for consistency.

Verdict

The consolidation is well-executed and the fix is correct. The PR description is excellent (alternatives considered, backward compat notes, test plan). The one material issue is the end-of-month overflow -- please fix before merge. The rest are minor suggestions.

@ozymandiashh
Copy link
Copy Markdown
Contributor Author

Thanks for the deep review. You were right about the end-of-month overflow: using the current day when subtracting 6 months can roll into the next month and silently exclude a few days.

I pushed a follow-up fix in 9a258a8:

  • all now starts at day 1 of the target month: new Date(now.getFullYear(), now.getMonth() - ALL_TIME_MONTHS, 1)
  • replaced the loose 5-7 month tolerance with an exact month check + start-day assertion
  • added a fake-timer regression test for Aug 31, 2026 so this can't regress quietly
  • cleaned up the dashboard import alias nit
  • updated the GNOME prefs label from All Time to 6 Months

Local verification:

  • npx vitest run tests/cli-date.test.ts passes
  • npx tsc --noEmit passes
  • npx vitest run passes: 33 files, 440 tests
  • npm run build passes

GitHub checks are green too. Could you take another look when you have a chance?

Copy link
Copy Markdown
Member

@iamtoruk iamtoruk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clean refactoring, solid tests, well-documented rationale.

One thing to address before merge: the label unification is incomplete across all surfaces. Please ensure "All Time" / "All" is updated to "6 Months" in all of these:

  • TUI dashboard (src/dashboard.tsx) - PERIOD_LABELS.all and the hotkey hint bar (line ~611 hardcodes "all time")
  • macOS menubar (mac/Sources/CodeBurnMenubar/AppStore.swift) - Period enum case all = "All" (line 331)
  • macOS menubar phrase (mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift) —-periodPhrase returns "all time" for .all
  • GNOME indicator (gnome/indicator.js) - line 21 still says { id: 'all', label: 'All' }
  • GNOME prefs (gnome/prefs.js) - you got this one already

Otherwise users will see "All" or "All Time" on some surfaces while the behavior is actually capped to 6 months —-misleading for a cost tracker where accuracy matters.

@iamtoruk iamtoruk merged commit 32dfa8e into getagentseal:main May 5, 2026
3 checks passed
iamtoruk added a commit that referenced this pull request May 5, 2026
PR #221 unified the period logic but missed the TUI hotkey bar,
GNOME indicator popup, and macOS menubar app. All surfaces now
consistently show '6 Months' instead of 'All' or 'all time'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants