mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-28 10:25:13 +00:00
fix(clients): display CLI-computed values verbatim, align period windows across app/dash/menubar
This commit is contained in:
parent
ba56ad72f5
commit
6eb8797efa
9 changed files with 71 additions and 27 deletions
|
|
@ -119,10 +119,11 @@ const PERIOD_LABELS: Record<Period, string> = {
|
|||
week: 'Last 7 days',
|
||||
month: 'This month',
|
||||
'30days': 'Last 30 days',
|
||||
all: 'All time',
|
||||
all: 'Last 6 months',
|
||||
lifetime: 'Lifetime',
|
||||
}
|
||||
|
||||
const STANDARD_PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all']
|
||||
const STANDARD_PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all', 'lifetime']
|
||||
|
||||
// Instant-switch memo key for an overview result. Shared by the overview poll
|
||||
// and the provider prefetcher so the two never drift out of sync. Exported so
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ import { SegTabs, type SegOption } from './SegTabs'
|
|||
/** Sentinel option value: no --claude-config-source flag (aggregate all configs). */
|
||||
const ALL_CONFIGS = ''
|
||||
|
||||
/** The real CLI period vocabulary (`codeburn ... --period`). */
|
||||
/** The real CLI period vocabulary (`codeburn ... --period`, src/cli-date.ts). */
|
||||
export const PERIOD_OPTIONS: SegOption[] = [
|
||||
{ value: 'today', label: 'Today' },
|
||||
{ value: 'week', label: '7D' },
|
||||
{ value: '30days', label: '30D' },
|
||||
{ value: 'month', label: 'Month' },
|
||||
{ value: 'all', label: '6M' },
|
||||
{ value: 'lifetime', label: 'Life' },
|
||||
]
|
||||
|
||||
/** The `.bar` top bar: title, scope caption, period SegTabs, provider ProviderPop. */
|
||||
|
|
|
|||
|
|
@ -31,6 +31,20 @@ const DAILY = [
|
|||
entry('2026-07-11'),
|
||||
]
|
||||
|
||||
// All active-day entries at or before NOW's calendar day (the future 07-11 entry
|
||||
// is always excluded). Reused by the widest windows ('all', 'lifetime').
|
||||
const ALL_ACTIVE_THROUGH_NOW = [
|
||||
'2026-05-31',
|
||||
'2026-06-01',
|
||||
'2026-06-10',
|
||||
'2026-06-11',
|
||||
'2026-07-01',
|
||||
'2026-07-03',
|
||||
'2026-07-04',
|
||||
'2026-07-09',
|
||||
'2026-07-10',
|
||||
]
|
||||
|
||||
describe('sliceDailyToPeriod', () => {
|
||||
it.each<[Period, string[]]>([
|
||||
['today', ['2026-07-10']],
|
||||
|
|
@ -38,32 +52,26 @@ describe('sliceDailyToPeriod', () => {
|
|||
['week', ['2026-07-03', '2026-07-04', '2026-07-09', '2026-07-10']],
|
||||
['30days', ['2026-06-10', '2026-06-11', '2026-07-01', '2026-07-03', '2026-07-04', '2026-07-09', '2026-07-10']],
|
||||
['month', ['2026-07-01', '2026-07-03', '2026-07-04', '2026-07-09', '2026-07-10']],
|
||||
[
|
||||
'all',
|
||||
[
|
||||
'2026-05-31',
|
||||
'2026-06-01',
|
||||
'2026-06-10',
|
||||
'2026-06-11',
|
||||
'2026-07-01',
|
||||
'2026-07-03',
|
||||
'2026-07-04',
|
||||
'2026-07-09',
|
||||
'2026-07-10',
|
||||
],
|
||||
],
|
||||
['all', ALL_ACTIVE_THROUGH_NOW],
|
||||
// lifetime is unbounded below (1970), so it holds every active day up to today.
|
||||
['lifetime', ALL_ACTIVE_THROUGH_NOW],
|
||||
])('returns only in-window entries for %s', (period, expectedDates) => {
|
||||
expect(sliceDailyToPeriod(DAILY, period, NOW).map(day => day.date)).toEqual(expectedDates)
|
||||
})
|
||||
})
|
||||
|
||||
describe('periodWindowStart', () => {
|
||||
// Parity fixture: the inclusive window-start each period must produce, computed
|
||||
// exactly as src/cli-date.ts getDateRange() does for the same NOW. If cli-date
|
||||
// shifts a boundary, this table must move with it or the client will drift.
|
||||
describe('periodWindowStart matches src/cli-date.ts getDateRange', () => {
|
||||
// NOW = 2026-07-10. Values below are the local date-key of getDateRange().range.start.
|
||||
it.each<[Period, string]>([
|
||||
['today', '2026-07-10'],
|
||||
['week', '2026-07-03'],
|
||||
['30days', '2026-06-10'],
|
||||
['month', '2026-07-01'],
|
||||
['all', '2026-01-01'],
|
||||
['today', '2026-07-10'], // new Date(y, m, d)
|
||||
['week', '2026-07-03'], // new Date(y, m, d - 7)
|
||||
['30days', '2026-06-10'], // new Date(y, m, d - 30)
|
||||
['month', '2026-07-01'], // new Date(y, m, 1)
|
||||
['all', '2026-01-01'], // new Date(y, m - 6, 1)
|
||||
['lifetime', '1970-01-01'], // new Date(1970, 0, 1)
|
||||
])('aligns %s to the CLI window start', (period, expected) => {
|
||||
expect(periodWindowStart(period, NOW)).toBe(expected)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ export function periodWindowStart(period: Period, now = new Date()): string {
|
|||
return localDateKey(new Date(now.getFullYear(), now.getMonth(), 1))
|
||||
case 'all':
|
||||
return localDateKey(new Date(now.getFullYear(), now.getMonth() - ALL_TIME_MONTHS, 1))
|
||||
case 'lifetime':
|
||||
// src/cli-date.ts anchors the unbounded window at 1970-01-01.
|
||||
return localDateKey(new Date(1970, 0, 1))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
// ————— Period + IPC error contract —————
|
||||
|
||||
export type Period = 'today' | 'week' | '30days' | 'month' | 'all'
|
||||
export type Period = 'today' | 'week' | '30days' | 'month' | 'all' | 'lifetime'
|
||||
|
||||
export type DateRange = { from: string; to: string }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type Period = 'today' | 'week' | '30days' | 'month' | 'all'
|
||||
export type Period = 'today' | 'week' | '30days' | 'month' | 'all' | 'lifetime'
|
||||
|
||||
export type ModelDay = {
|
||||
name: string
|
||||
|
|
@ -171,12 +171,16 @@ export async function fetchDevices(period: Period, provider: string): Promise<{
|
|||
return { devices: (data.devices ?? []).map((d) => ({ ...d, payload: normalizePayload(d.payload) })) }
|
||||
}
|
||||
|
||||
// Keys map 1:1 to the CLI's --period values (src/cli-date.ts). Period windows
|
||||
// are computed server-side by the CLI; the dashboard only forwards the key, so
|
||||
// these can never drift from the CLI's totals.
|
||||
export const PERIODS: Array<{ key: Period; label: string }> = [
|
||||
{ key: 'today', label: 'Today' },
|
||||
{ key: 'week', label: '7 days' },
|
||||
{ key: '30days', label: '30 days' },
|
||||
{ key: 'month', label: 'Month' },
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'all', label: '6 months' },
|
||||
{ key: 'lifetime', label: 'Lifetime' },
|
||||
]
|
||||
|
||||
export type DiscoveredDevice = {
|
||||
|
|
|
|||
|
|
@ -1507,6 +1507,7 @@ enum Period: String, CaseIterable, Identifiable {
|
|||
case thirtyDays = "30 Days"
|
||||
case month = "Month"
|
||||
case all = "6 Months"
|
||||
case lifetime = "Lifetime"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
|
|
@ -1518,6 +1519,7 @@ enum Period: String, CaseIterable, Identifiable {
|
|||
case .thirtyDays: "30days"
|
||||
case .month: "month"
|
||||
case .all: "all"
|
||||
case .lifetime: "lifetime"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1530,6 +1532,7 @@ enum Period: String, CaseIterable, Identifiable {
|
|||
case .thirtyDays: "30 Days"
|
||||
case .month: "Month"
|
||||
case .all: "6 Months"
|
||||
case .lifetime: "Lifetime"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1540,6 +1543,7 @@ enum Period: String, CaseIterable, Identifiable {
|
|||
case .thirtyDays: "30days"
|
||||
case .month: "month"
|
||||
case .all: "sixMonths"
|
||||
case .lifetime: "lifetime"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1549,6 +1553,7 @@ enum Period: String, CaseIterable, Identifiable {
|
|||
case "week", "sevenDays": self = .sevenDays
|
||||
case "month": self = .month
|
||||
case "sixMonths", "all": self = .all
|
||||
case "lifetime": self = .lifetime
|
||||
default: self = .today
|
||||
}
|
||||
}
|
||||
|
|
@ -1569,6 +1574,9 @@ enum Period: String, CaseIterable, Identifiable {
|
|||
case .thirtyDays: compact ? "/30d" : " / 30d"
|
||||
case .month: compact ? "/mo" : " / mo"
|
||||
case .all: compact ? "/6mo" : " / 6mo"
|
||||
// lifetime is a panel-only period (never a menubar metric, see
|
||||
// menubarMetricCases), but the switch must stay exhaustive.
|
||||
case .lifetime: compact ? "/life" : " / life"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ private struct TrendInsight: View {
|
|||
case .today, .sevenDays: return 19
|
||||
case .thirtyDays: return 30
|
||||
case .month: return 31
|
||||
case .all: return min(days.count, 90)
|
||||
case .all, .lifetime: return min(days.count, 90)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,24 @@ struct MenubarPeriodSettingsTests {
|
|||
#expect(Period.menubarMetricCases == [.today, .sevenDays, .month, .all])
|
||||
}
|
||||
|
||||
@Test("period picker exposes the lifetime window, matching the CLI period set")
|
||||
func periodPickerExposesLifetime() {
|
||||
// The panel period selector iterates Period.allCases, so lifetime must be
|
||||
// present there while staying out of the menubar-metric subset.
|
||||
#expect(Period.allCases == [.today, .sevenDays, .thirtyDays, .month, .all, .lifetime])
|
||||
#expect(!Period.menubarMetricCases.contains(.lifetime))
|
||||
}
|
||||
|
||||
@Test("period cliArg values match src/cli-date.ts --period values")
|
||||
func cliArgsMatchCLIPeriods() {
|
||||
#expect(Period.today.cliArg == "today")
|
||||
#expect(Period.sevenDays.cliArg == "week")
|
||||
#expect(Period.thirtyDays.cliArg == "30days")
|
||||
#expect(Period.month.cliArg == "month")
|
||||
#expect(Period.all.cliArg == "all")
|
||||
#expect(Period.lifetime.cliArg == "lifetime")
|
||||
}
|
||||
|
||||
@Test("defaults values map to periods")
|
||||
func defaultsValuesMapToPeriods() {
|
||||
#expect(Period(menubarDefaultsValue: "today") == .today)
|
||||
|
|
@ -16,6 +34,7 @@ struct MenubarPeriodSettingsTests {
|
|||
#expect(Period(menubarDefaultsValue: "month") == .month)
|
||||
#expect(Period(menubarDefaultsValue: "sixMonths") == .all)
|
||||
#expect(Period(menubarDefaultsValue: "all") == .all)
|
||||
#expect(Period(menubarDefaultsValue: "lifetime") == .lifetime)
|
||||
#expect(Period(menubarDefaultsValue: "30days") == .today)
|
||||
#expect(Period(menubarDefaultsValue: "bogus") == .today)
|
||||
#expect(Period(menubarDefaultsValue: nil) == .today)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue