Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/renderer/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
48 changes: 28 additions & 20 deletions app/renderer/lib/period.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,39 +31,47 @@ 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']],
// Window boundaries mirror src/cli-date.ts: week = now-7, 30days = now-30.
['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)
})
Expand Down
3 changes: 3 additions & 0 deletions app/renderer/lib/period.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/renderer/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
8 changes: 6 additions & 2 deletions dash/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
8 changes: 8 additions & 0 deletions mac/Sources/CodeBurnMenubar/AppStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand All @@ -1518,6 +1519,7 @@ enum Period: String, CaseIterable, Identifiable {
case .thirtyDays: "30days"
case .month: "month"
case .all: "all"
case .lifetime: "lifetime"
}
}

Expand All @@ -1530,6 +1532,7 @@ enum Period: String, CaseIterable, Identifiable {
case .thirtyDays: "30 Days"
case .month: "Month"
case .all: "6 Months"
case .lifetime: "Lifetime"
}
}

Expand All @@ -1540,6 +1543,7 @@ enum Period: String, CaseIterable, Identifiable {
case .thirtyDays: "30days"
case .month: "month"
case .all: "sixMonths"
case .lifetime: "lifetime"
}
}

Expand All @@ -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
}
}
Expand All @@ -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"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
19 changes: 19 additions & 0 deletions mac/Tests/CodeBurnMenubarTests/MenubarPeriodSettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,32 @@ 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)
#expect(Period(menubarDefaultsValue: "week") == .sevenDays)
#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)
Expand Down