From 6eb8797efab2fe1499e4a4b0cdbf9e04415f3fcc Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Mon, 20 Jul 2026 06:26:15 -0700 Subject: [PATCH] fix(clients): display CLI-computed values verbatim, align period windows across app/dash/menubar --- app/renderer/App.tsx | 5 +- app/renderer/components/TopBar.tsx | 3 +- app/renderer/lib/period.test.ts | 48 +++++++++++-------- app/renderer/lib/period.ts | 3 ++ app/renderer/lib/types.ts | 2 +- dash/src/lib/api.ts | 8 +++- mac/Sources/CodeBurnMenubar/AppStore.swift | 8 ++++ .../Views/HeatmapSection.swift | 2 +- .../MenubarPeriodSettingsTests.swift | 19 ++++++++ 9 files changed, 71 insertions(+), 27 deletions(-) diff --git a/app/renderer/App.tsx b/app/renderer/App.tsx index 41898a9a..8dae38a9 100644 --- a/app/renderer/App.tsx +++ b/app/renderer/App.tsx @@ -119,10 +119,11 @@ const PERIOD_LABELS: Record = { 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 diff --git a/app/renderer/components/TopBar.tsx b/app/renderer/components/TopBar.tsx index 4c789b24..48a3d5e9 100644 --- a/app/renderer/components/TopBar.tsx +++ b/app/renderer/components/TopBar.tsx @@ -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. */ diff --git a/app/renderer/lib/period.test.ts b/app/renderer/lib/period.test.ts index 6a32ff2f..24567cb1 100644 --- a/app/renderer/lib/period.test.ts +++ b/app/renderer/lib/period.test.ts @@ -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) }) diff --git a/app/renderer/lib/period.ts b/app/renderer/lib/period.ts index c024f251..0c81c47c 100644 --- a/app/renderer/lib/period.ts +++ b/app/renderer/lib/period.ts @@ -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)) } } diff --git a/app/renderer/lib/types.ts b/app/renderer/lib/types.ts index c003a0e9..94d766d0 100644 --- a/app/renderer/lib/types.ts +++ b/app/renderer/lib/types.ts @@ -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 } diff --git a/dash/src/lib/api.ts b/dash/src/lib/api.ts index ffc66246..dbeffa11 100644 --- a/dash/src/lib/api.ts +++ b/dash/src/lib/api.ts @@ -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 = { diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift index 7476c92c..eb63fa3b 100644 --- a/mac/Sources/CodeBurnMenubar/AppStore.swift +++ b/mac/Sources/CodeBurnMenubar/AppStore.swift @@ -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" } } } diff --git a/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift b/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift index c28c5dab..fbd5871d 100644 --- a/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift +++ b/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift @@ -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) } } diff --git a/mac/Tests/CodeBurnMenubarTests/MenubarPeriodSettingsTests.swift b/mac/Tests/CodeBurnMenubarTests/MenubarPeriodSettingsTests.swift index e52a9875..bfc6ebc5 100644 --- a/mac/Tests/CodeBurnMenubarTests/MenubarPeriodSettingsTests.swift +++ b/mac/Tests/CodeBurnMenubarTests/MenubarPeriodSettingsTests.swift @@ -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)