Add Report Card to ClaudeBar
Add report cards that turn a login's usage history into metrics with comparison deltas, shown in the existing card style, test first.
When to Use
This skill covers report-style features — cards that:
- Aggregate a login's days of usage (cost, tokens, time, sessions)
- Compare periods (today vs yesterday, this week vs last week)
- Read another tool's usage logs (as data in its definition)
- Display results in cards matching the existing UI
First, which kind of report is it?
Usage history is data now (daily-usage design). A report is almost always a new view over days the login already reads, not new reading code. Place it before anything else:
| The person wants… | It is | You write |
|---|---|---|
| a new look at usage this app already reads — this week vs last, cost per model, a 30-day chart | a report over days: the page asks account.usageHistory?.days(in: range) and a value in Quotas turns the days into the report | a Quotas value + its tests, a card. No reading code |
| the same history for another tool (its own log files) | a usageHistory block in that provider's definition | JSON (paths, where, fields, prices), its golden tests — no Swift |
| history from a log format no reader understands yet | a new reader, named for the format, in Modules/DataSources/Sources/Internal/Logs/ | the reader, test-first in DataSourcesTests, then the JSON; a row in ENGINE_DESIGN §1 |
| an answer to a different question than "how much did I use?" | a capability (CANONICAL §2.1): declared in the definition, a handle on Account that is nil when not declared | design first; a runner outside the modules is supplied by the Engine (TARGET_ARCHITECTURE §10) |
Never: a field on UsageSnapshot (the kernel is shrinking toward Usage,
CANONICAL_MODEL §6),
an XxxAnalyzer / XxxAnalyzing protocol, a parser in Sources/Infrastructure, or a
vendor-named type in a module.
Reference implementation:
references/daily-usage-pattern.md— TODAY'S USAGE: Claude's logs and prices as data,UsageLogin DataSources,account.usageHistory,DailyUsageReportin Quotas,DailyUsageCardView. The Leaderboard reads 30 days the same way (login.history.days(in:)→DailyTokens.summed).
Check the design first — the docs are the source of truth (AGENTS.md): USER_JOURNEYS, CANONICAL_MODEL, TARGET_ARCHITECTURE, then the feature's
design.md.
How a report over days flows
text<id>.json "usageHistory" ──► UsageLog (DataSources) one per login, built by reader · PriceList · ProviderFactory from the definition DayAggregator │ popover opens (never in the background, #204) │ ▼ account.usageHistory?.days(in: .last(14)) ──► [DailyUsageStat] (closed days from the ledger) │ ▼ WeeklyReport(days:) a rich value in Modules/Quotas: deltas, percentages, formatting │ ▼ WeeklyCardView renders what the report says — never compares or counts
Workflow
Phase 0: Place it and design it (get user approval) ↓ Phase 1: The report value + tests (TDD Red→Green) ↓ Phase 2: Only if needed — the definition's usageHistory, a new reader, or a capability ↓ Phase 3: Mockup, card view, reading it on popover open ↓ Phase 4: Verify, screenshots on mock data, docs
Phase 0: Place it and design it (MANDATORY)
Step 1: Define the report
- What does the person want to know? In their words (USER_JOURNEYS).
- Which kind is it? The table above.
- Which days?
DateRange(.last(n), a week, today vs yesterday). - Which logins? A report is per login — two logins are two reports, never summed.
- How many cards? One per metric, or one combined card.
Step 2: Diagram it
Example: this week vs last week, per login ┌────────────────────────────────────────────────────────────────────┐ │ Providers (module) Quotas (module) App │ │ │ │ account.usageHistory ─► WeeklyReport(days:) ─► WeeklyCardView │ │ .days(in: .last(14)) thisWeek / lastWeek × 3 metrics │ │ → [DailyUsageStat] deltas, %, formatted │ │ │ │ No reading code: claude.json's usageHistory already reads the logs│ └────────────────────────────────────────────────────────────────────┘
Step 3: The pieces
| Piece | Module | One job | Test |
|---|---|---|---|
WeeklyReport | Quotas | turns days into the comparison the card shows | Tests/DomainTests/<Feature>/ |
WeeklyCardView | App | renders it | AppTests / screenshots |
(only if needed) usageHistory block, a reader, a capability | definition / DataSources / Providers | — | golden tests / DataSourcesTests / ProvidersTests |
Step 4: Mockup and approval
A UI change gets a mockup in design-concept/<feature>/ on mock data first
(AGENTS.md). Present the doc change, the diagram, the pieces and the mockup;
use AskUserQuestion and wait for approval.
Phase 1: The report value (TDD)
Name each test should <outcome> [when <situation>], in the person's words, never a method, type or mechanism verb → Naming tests.
Location: Modules/Quotas/Sources/{Name}Report.swift (beside DailyUsageReport),
tests in Tests/DomainTests/{Feature}/{Name}ReportTests.swift (beside DailyUsageReportTests).
It is built from days — [DailyUsageStat] — so it never reads a file:
swiftpublic struct {Name}Report: Sendable, Equatable { public let current: [DailyUsageStat] // e.g. this week public let previous: [DailyUsageStat] // e.g. last week /// The days split at `boundary`, as the page asks for them. public init(days: [DailyUsageStat], splitAt boundary: Date) { … } public var cost: Decimal { current.reduce(0) { $0 + $1.totalCost } } public var costChangePercent: Double? { let before = previous.reduce(Decimal(0)) { $0 + $1.totalCost } guard before > 0 else { return nil } // nil when there was nothing before … } public var formattedCostDelta: String { … } // "+$5.00", "-$1.20": the sign always shown public var costProgress: Double { … } // current / (current + previous), 0…1 }
Rules:
Decimalfor money,TimeIntervalfor durations;Locale(identifier: "en_US")for currency.- Every formatted string and every comparison lives in the value — the view only reads them.
- A change percent is
nilwhen the previous period is zero; deltas always carry a sign. isEmptymeans all zero (&&, not||).
Write the tests first (state and return values, Chicago school), then the value.
Phase 2: Only if the report needs reading it doesn't have
2a. Another tool's history → its definition
Add a usageHistory block to <id>.json — files, format, where, fields,
prices, sessionGap — the shape is in the daily-usage design §2.
Golden tests in Modules/Providers/Tests/ over fixture logs (see ClaudeUsageHistoryTests).
No Swift.
2b. A log format no reader knows → one reader in DataSources
A reader named for the format (JSONLinesReader, never AcmeLogReader) in
Modules/DataSources/Sources/Internal/Logs/, test-first in
Modules/DataSources/Tests/Logs/ with neutral fixtures, yielding the one
LogRecord every reader yields. Then the JSON uses it. Add its row to
ENGINE_DESIGN §1. Only scan files changed within the range (LogFileFinder).
2c. A different question → a capability
Design it in the docs first (CANONICAL §2.1, TARGET_ARCHITECTURE): declared in
the definition, its own types in Modules/Providers, an @Observable handle on
Account that is nil when not declared — like usageHistory and
guestPasses. ProviderFactory.make builds it from the definition; a runner
that lives outside the modules (a CLI the App runs, the Keychain) is supplied by
the Engine the App builds once. The App never passes anything per provider.
Phase 3: The card, read on popover open
3a. The card view
Location: Sources/App/Views/{Name}CardView.swift. Match the existing
cards; DailyUsageCardView is the reference:
swiftstruct {Name}CardView: View { let metric: {Name}Metric // one case per card let report: {Name}Report let delay: Double // cascading entrance @Environment(\.appTheme) private var theme var body: some View { VStack(alignment: .leading, spacing: 6) { // header: icon + LABEL · the large value · the progress bar · the delta line } .padding(12) .background(/* theme.cardGradient fill + theme.glassBorder stroke */) } }
Card styling checklist: .padding(12) · theme.cardGradient + theme.glassBorder (1pt) ·
theme.cardCornerRadius · theme.fontDesign on text · theme.textPrimary/Secondary/Tertiary ·
theme.progressTrack · hover scale 1.015 · progress bar animated with delay.
3b. Read the days when the popover opens
Usage history is read when the popover opens, never in the background (#204),
and per login — the page asks the selected login, as MenuContentView does:
swift.task(id: login.id) { guard let history = login.usageHistory else { return } // nil: this login offers none let days = await history.days(in: .last(14)) report = {Name}Report(days: days, splitAt: weekStart) }
The view renders report; it never compares, counts or decides from the days itself.
3c. Nothing to register
ProviderFactory.make builds usageHistory from the definition, and every
provider is found by ProviderCatalog.detect() (TARGET_ARCHITECTURE §10). The
App passes nothing per provider.
Phase 4: Verify
tuist generatexcodebuild test -workspace ClaudeBar.xcworkspace -scheme ClaudeBar-Workspace -destination 'platform=macOS,arch=arm64'— the full run;tuist testskips cached targets- Run the real popover on mock data (
scripts/demo-screenshots.sh) — screenshots, never real names, emails or usage - Docs in the same change: the feature's
README.md/design.md, one CHANGELOG line (≤300 chars),python3 scripts/gen-docs.py && python3 scripts/check-docs.py --strict
Checklist
Phase 0
- Placed: report over days · definition
usageHistory· new reader · capability - Design in the docs, diagram, pieces table, mockup in
design-concept/<feature>/ - User approved
Phase 1
-
{Name}ReportTestswritten first (deltas, nil percent, signs, progress, empty) -
{Name}ReportinModules/Quotas, built from[DailyUsageStat]— green
Phase 2 (only if needed)
-
usageHistoryblock + golden tests, or a format-named reader inDataSources+ its ENGINE_DESIGN row, or a declared capability - No
UsageSnapshotfield, no analyzer protocol, no vendor-named Swift
Phase 3
-
{Name}CardViewmatching the card style - Days read on popover open, per login; nothing passed in
ClaudeBarApp
Phase 4
- Full
xcodebuild testgreen - Screenshots on mock data
- Docs and CHANGELOG line; docs check passes


