mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-19 07:43:09 +00:00
fix(date-range): unify 'all' period semantics between CLI and dashboard
`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 #93
This commit is contained in:
parent
18335a1f9d
commit
3dc3e32715
6 changed files with 217 additions and 76 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import type { DateRange } from './types.js'
|
||||
import { toDateString } from './daily-cache.js'
|
||||
|
||||
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/
|
||||
|
||||
|
|
@ -7,6 +8,35 @@ const END_OF_DAY_MINUTES = 59
|
|||
const END_OF_DAY_SECONDS = 59
|
||||
const END_OF_DAY_MS = 999
|
||||
|
||||
// "All Time" is intentionally bounded to the last 6 months. Older data is
|
||||
// rarely actionable for a cost tracker, and capping the range keeps the parse
|
||||
// path bounded so providers like Codex/Cursor with sparse multi-year history
|
||||
// still load in seconds. Users who need an unbounded window can use
|
||||
// `--from` / `--to`.
|
||||
const ALL_TIME_MONTHS = 6
|
||||
|
||||
export type Period = 'today' | 'week' | '30days' | 'month' | 'all'
|
||||
|
||||
export const PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all']
|
||||
|
||||
// Short labels suitable for the dashboard tab strip. Long-form labels for
|
||||
// header text come from `getDateRange().label`.
|
||||
export const PERIOD_LABELS: Record<Period, string> = {
|
||||
today: 'Today',
|
||||
week: '7 Days',
|
||||
'30days': '30 Days',
|
||||
month: 'This Month',
|
||||
all: '6 Months',
|
||||
}
|
||||
|
||||
export function toPeriod(s: string): Period {
|
||||
if (s === 'today') return 'today'
|
||||
if (s === 'month') return 'month'
|
||||
if (s === '30days') return '30days'
|
||||
if (s === 'all') return 'all'
|
||||
return 'week'
|
||||
}
|
||||
|
||||
function parseLocalDate(s: string): Date {
|
||||
if (!ISO_DATE_RE.test(s)) {
|
||||
throw new Error(`Invalid date format "${s}": expected YYYY-MM-DD`)
|
||||
|
|
@ -37,3 +67,58 @@ export function parseDateRangeFlags(from: string | undefined, to: string | undef
|
|||
}
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date range and a human-readable label for a named period.
|
||||
*
|
||||
* Accepts a string (rather than the strict `Period` type) because the CLI
|
||||
* surfaces a few extra inputs not exposed in the dashboard tab strip
|
||||
* (e.g. `'yesterday'`). Unknown values fall back to `'week'`.
|
||||
*
|
||||
* Note: `'all'` is bounded to the last 6 months. Use `--from`/`--to` for
|
||||
* an unbounded historical window.
|
||||
*/
|
||||
export function getDateRange(period: string): { range: DateRange; label: string } {
|
||||
const now = new Date()
|
||||
const end = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
END_OF_DAY_HOURS,
|
||||
END_OF_DAY_MINUTES,
|
||||
END_OF_DAY_SECONDS,
|
||||
END_OF_DAY_MS,
|
||||
)
|
||||
|
||||
switch (period) {
|
||||
case 'today': {
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
return { range: { start, end }, label: `Today (${toDateString(start)})` }
|
||||
}
|
||||
case 'yesterday': {
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
|
||||
const yesterdayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, END_OF_DAY_HOURS, END_OF_DAY_MINUTES, END_OF_DAY_SECONDS, END_OF_DAY_MS)
|
||||
return { range: { start, end: yesterdayEnd }, label: `Yesterday (${toDateString(start)})` }
|
||||
}
|
||||
case 'week': {
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
|
||||
return { range: { start, end }, label: 'Last 7 Days' }
|
||||
}
|
||||
case 'month': {
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
return { range: { start, end }, label: `${now.toLocaleString('default', { month: 'long' })} ${now.getFullYear()}` }
|
||||
}
|
||||
case '30days': {
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30)
|
||||
return { range: { start, end }, label: 'Last 30 Days' }
|
||||
}
|
||||
case 'all': {
|
||||
const start = new Date(now.getFullYear(), now.getMonth() - ALL_TIME_MONTHS, now.getDate())
|
||||
return { range: { start, end }, label: 'Last 6 months' }
|
||||
}
|
||||
default: {
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
|
||||
return { range: { start, end }, label: 'Last 7 Days' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue