Skip to content

Add a daily spend trend to the shared usage card - #3722

Open
Chipagosfinest wants to merge 10 commits into
steipete:mainfrom
Chipagosfinest:feat/share-card-spend-trend
Open

Chipagosfinest wants to merge 10 commits into
steipete:mainfrom
Chipagosfinest:feat/share-card-spend-trend

Conversation

@Chipagosfinest

Copy link
Copy Markdown
Contributor

Stacked on #3704, which restructured the hero this builds on. Merge that first.

Why

The card showed a total and a ranking but no movement. Reviewing how metric cards are built
elsewhere, pairing a headline value with its trend is close to universal — PatternFly's dashboard
card taxonomy defines a "trend card" as a current value plus a sparkline of its change over time,
and the component libraries that implement this pattern (Neon MetricCard, Aura DataCard, the
shadcn stats blocks) all take a value plus an optional series. A total with no shape is the one
card type none of them ship.

The data was already there. SpendDashboardModel.CurrencyGroup.dailyPoints carries per-provider
cost per day; it just never reached the payload.

Before / after

Before (#3704) After
sparse before sparse after sparse
dense before dense after dense
multi-currency before multi after multi

What it adds

ShareStatsPayload.dailySpend: [Double] — one value per covered day for the headline currency,
summed across every provider on that day, oldest first. Aggregate by construction: it exposes
nothing the subscription rows do not already show, and carries no provider, model or account
identity, so the card's LOCAL · AGGREGATE ONLY claim still holds.

The sparkline is a plain Path — no charting dependency — with a filled gradient and a 2pt stroke
in the existing accent. No axes, no gridlines, no labels on the series itself: the hero already
states the period and the total.

Two deliberate constraints

The vertical budget does not move. The hero's right column was using roughly 96pt of its 140pt,
so the label and the 30pt sparkline fit in existing slack. The dense render above confirms the card
still fits 630pt with the footer clear — which matters because SwiftUI does not clip an oversize
frame.

It is suppressed rather than faked. spendTrend returns empty for a single point or a flat
series, so no sparkline is drawn when there is no shape in the data. Inventing one would imply
movement that is not there.

One correction worth recording

The first revision put the sparkline directly beneath TRACKED TOKENS with no label, which read as
a token trend when it plots spend. It now carries a DAILY SPEND eyebrow. The render is what
caught it.

Tests

daily spend trend aggregates every provider per day in chronological order builds a
CurrencyGroup whose dailyPoints are deliberately out of order, with two providers sharing the
middle day, and asserts payload.dailySpend == [1, 5, 5] through ShareStatsBuilder.make — the
real builder, not a reimplementation of its logic in the test.

All 398 ProviderArchitectureGatekeeperTests anchors re-verified; the ShareStatsPayload.swift
Mistral anchor shifts 198 → 203 because the new field sits above it, and is updated here.

@clawsweeper

clawsweeper Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Sep 17, 2026
@clawsweeper

clawsweeper Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed September 21, 2026, 3:21 AM ET / 07:21 UTC (Revision 4).

ClawSweeper review

What this changes

Adds a daily spending sparkline to shared usage images, alongside the stacked spend-first layout, currency captions, tests, and comparison images.

Merge readiness

Blocked before merge - 5 items remain

Keep open: the trend remains useful work absent from main. One earlier finding is fixed, but two coverage defects remain, and the supplied renders do not yet establish production behavior.

Priority: P2
Reviewed head: d7c6a95f7753eba9746779fb93205863f4eb671c

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) Useful layout evidence and a focused implementation remain limited by unresolved coverage semantics and incomplete production proof.
Proof confidence 🦐 gold shrimp (3/6) Needs stronger real behavior proof before merge: Inspected sparse, dense, and multi-currency renders show ShareStatsCardView's visible layout, but the standalone rendering method bypasses ShareStatsBuilder's production inputs and the app's PNG renderer. Provide freshly built app preview/export evidence after correction; screenshots or video are preferred for the visible result, with redacted logs or terminal output acceptable for supporting behavior. Redact private details. Updating the PR body should trigger re-review; otherwise ask a maintainer to comment @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🦐 gold shrimp (3/6) 3 actionable review findings remain.

Verification

Check Result Evidence
Real behavior Needs proof Needs stronger real behavior proof before merge: Inspected sparse, dense, and multi-currency renders show ShareStatsCardView's visible layout, but the standalone rendering method bypasses ShareStatsBuilder's production inputs and the app's PNG renderer. Provide freshly built app preview/export evidence after correction; screenshots or video are preferred for the visible result, with redacted logs or terminal output acceptable for supporting behavior. Redact private details. Updating the PR body should trigger re-review; otherwise ask a maintainer to comment @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 9 items Pinned introduction and policy: Read the complete root AGENTS.md; no nested AGENTS.md or maintainer-notes directory was found. Inspected the introduced delta from ec12a02 to d7c6a95. Applied the coverage, focused-testing, and freshly built UI validation guidance. Builds and tests were not run under the read-only review contract.
Two previous coverage findings remain: The new extraction helper rejects only nil daily totals. DailySummary.hasPartialCost separately identifies incomplete requests, lower-bound costs, and mixed priced/unpriced providers. When daily summaries are empty, the helper instead fills the whole chart domain with zero-valued gaps.
Production coverage semantics: dailySummaries uses the intersection of provider coverage and returns an empty array when that intersection is unavailable. The existing disjoint-window test produces zero covered days but retains daily points, establishing a reachable trigger for the new zero-filling fallback.
Findings 3 actionable findings [P2] [P2] Suppress partially priced daily totals
[P2] [P2] Preserve unavailable coverage when summaries are empty
[P2] [P2] Update the Codex allowlist anchor to its actual source line
Security None None.

How this fits together

CodexBar builds shared usage cards from local provider spending summaries. The sharing builder supplies aggregate data to a SwiftUI preview and the PNG copy/save renderer.

flowchart LR
 A[Local provider history] --> B[Spending dashboard model]
 B --> C[Coverage and daily totals]
 C --> D[Shared card payload]
 D --> E[Card preview]
 D --> F[PNG copy or save]
Loading

Before merge

  • Add real behavior proof - Needs stronger real behavior proof before merge: Inspected sparse, dense, and multi-currency renders show ShareStatsCardView's visible layout, but the standalone rendering method bypasses ShareStatsBuilder's production inputs and the app's PNG renderer. Provide freshly built app preview/export evidence after correction; screenshots or video are preferred for the visible result, with redacted logs or terminal output acceptable for supporting behavior. Redact private details. Updating the PR body should trigger re-review; otherwise ask a maintainer to comment @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • [P2] Suppress partially priced daily totals (P2) - This prior finding remains unresolved. Production DailySummary.totalCost is a known subtotal and can be non-nil while hasPartialCost is true, including mixed priced/unpriced providers, incomplete requests, and truncated scans. The nil-only guard therefore plots changing lower bounds as a daily spend trend. Reject partial summaries as well, and exercise this through SpendDashboardModel.build rather than provider-free summary fixtures.
  • [P2] Preserve unavailable coverage when summaries are empty (P2) - This prior finding also remains unresolved. Production returns empty daily summaries when provider coverage windows do not intersect, while retaining individual daily points. The fallback then fills the entire requested period with zeros and plots isolated provider observations as a complete aggregate series. The existing disjoint-window model fixture reaches this case with coveredDayCount == 0. Suppress the trend when aggregate coverage is unavailable; do not infer covered zeros from missing points.
  • [P2] Update the Codex allowlist anchor to its actual source line (P2) - The updated entry points to line 389, which now contains the finiteCost guard; the specified let snapshots statement is at line 415. The gatekeeper compares the exact source line with the anchor and records a failure, so this branch cannot pass that suite. Refresh the anchor after the helper changes and run the focused gatekeeper tests.
  • Complete next step (P2) - Resolve the three findings and provide production preview/export proof; preserve the declared landing order after Lead the shared usage card with spend #3704.

Findings

  • [P2] [P2] Suppress partially priced daily totals — Sources/CodexBar/ShareStatsPayload.swift:364-370
  • [P2] [P2] Preserve unavailable coverage when summaries are empty — Sources/CodexBar/ShareStatsPayload.swift:372-380
  • [P2] [P2] Update the Codex allowlist anchor to its actual source line — Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift:2326-2328
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test delta Production +179/-50; tests +227/-2 The growth covers both the stacked layout and trend feature; added tests do not yet cover the two production coverage failures.

Technical review

Best possible solution:

Build the trend only from complete, covered daily summaries, preserving genuine zero days while suppressing unknown or partially priced series.

Do we have a high-confidence way to reproduce the issue?

Yes, by source inspection: production partial-cost summaries pass the new guard, and disjoint provider coverage reaches the zero-filling fallback. These are PR-head defects; no runtime reproduction was executed.

Is this the best way to solve the issue?

The existing dashboard-to-card pipeline is the right integration point, but the extraction must honor its partial-cost and coverage semantics instead of treating non-nil totals or missing points as complete observations.

Full review comments:

  • [P2] [P2] Suppress partially priced daily totals — Sources/CodexBar/ShareStatsPayload.swift:364-370
    This prior finding remains unresolved. Production DailySummary.totalCost is a known subtotal and can be non-nil while hasPartialCost is true, including mixed priced/unpriced providers, incomplete requests, and truncated scans. The nil-only guard therefore plots changing lower bounds as a daily spend trend. Reject partial summaries as well, and exercise this through SpendDashboardModel.build rather than provider-free summary fixtures.
    Confidence: 0.99
  • [P2] [P2] Preserve unavailable coverage when summaries are empty — Sources/CodexBar/ShareStatsPayload.swift:372-380
    This prior finding also remains unresolved. Production returns empty daily summaries when provider coverage windows do not intersect, while retaining individual daily points. The fallback then fills the entire requested period with zeros and plots isolated provider observations as a complete aggregate series. The existing disjoint-window model fixture reaches this case with coveredDayCount == 0. Suppress the trend when aggregate coverage is unavailable; do not infer covered zeros from missing points.
    Confidence: 0.99
  • [P2] [P2] Update the Codex allowlist anchor to its actual source line — Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift:2326-2328
    The updated entry points to line 389, which now contains the finiteCost guard; the specified let snapshots statement is at line 415. The gatekeeper compares the exact source line with the anchor and records a failure, so this branch cannot pass that suite. Refresh the anchor after the helper changes and run the focused gatekeeper tests.
    Confidence: 1

Overall correctness: patch is incorrect
Overall confidence: 0.98

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against ec12a0243d2a.

Labels

Label justifications:

  • P2: This is a bounded sharing enhancement with aggregate-data correctness defects, without evidence of an urgent core-runtime failure.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🦐 gold shrimp.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: Inspected sparse, dense, and multi-currency renders show ShareStatsCardView's visible layout, but the standalone rendering method bypasses ShareStatsBuilder's production inputs and the app's PNG renderer. Provide freshly built app preview/export evidence after correction; screenshots or video are preferred for the visible result, with redacted logs or terminal output acceptable for supporting behavior. Redact private details. Updating the PR body should trigger re-review; otherwise ask a maintainer to comment @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. Inspected sparse, dense, and multi-currency renders show ShareStatsCardView's visible layout, but the standalone rendering method bypasses ShareStatsBuilder's production inputs and the app's PNG renderer. Provide freshly built app preview/export evidence after correction; screenshots or video are preferred for the visible result, with redacted logs or terminal output acceptable for supporting behavior. Redact private details. Updating the PR body should trigger re-review; otherwise ask a maintainer to comment @clawsweeper re-review.

Evidence

What I checked:

  • Pinned introduction and policy: Read the complete root AGENTS.md; no nested AGENTS.md or maintainer-notes directory was found. Inspected the introduced delta from ec12a02 to d7c6a95. Applied the coverage, focused-testing, and freshly built UI validation guidance. Builds and tests were not run under the read-only review contract. (AGENTS.md:1, d7c6a95f7753)
  • Two previous coverage findings remain: The new extraction helper rejects only nil daily totals. DailySummary.hasPartialCost separately identifies incomplete requests, lower-bound costs, and mixed priced/unpriced providers. When daily summaries are empty, the helper instead fills the whole chart domain with zero-valued gaps. (Sources/CodexBar/ShareStatsPayload.swift:364, d7c6a95f7753)
  • Production coverage semantics: dailySummaries uses the intersection of provider coverage and returns an empty array when that intersection is unavailable. The existing disjoint-window test produces zero covered days but retains daily points, establishing a reachable trigger for the new zero-filling fallback. (Tests/CodexBarTests/SpendDashboardModelTests.swift:227, d7c6a95f7753)
  • Re-review progress and validation defect: All six added DailySummary fixtures now supply totalTokens, resolving that prior finding. The updated Codex allowlist anchor still specifies line 389, while its source statement is at line 415. The gatekeeper checks the exact anchor line and records a failure on mismatch. (Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift:2327, d7c6a95f7753)
  • Screenshots demonstrate layout, with a documented production gap: Inspected all four prepared sparse/dense images and both tracked multi-currency trend images. They visibly show the labeled sparkline fitting within the card. The documented method compiles the view separately with stubs and explicitly excludes an app build; production export instead uses NSHostingView and bitmap rendering. These images do not demonstrate the changed builder receiving real provider summaries or the freshly built app exporting them. (docs/research/share-card-layout.md:34, d7c6a95f7753)
  • Still necessary on main and latest release: Inspected the sharing payload at pinned main and latest release v0.63.0, commit f3e718c: neither includes dailySpend. Main's sharing view and payload have no spendTrend or sparkline implementation. The merged caption fix does not implement this feature. (Sources/CodexBar/ShareStatsPayload.swift:107, f3e718c897d5)

Likely related people:

  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • Chipagosfinest: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Suppress partial and unavailable coverage, with regression cases built through SpendDashboardModel.build and ShareStatsBuilder.make.
  • Correct the gatekeeper anchor and run focused sharing/gatekeeper tests, make test, and make check.
  • Attach redacted freshly built app preview and PNG-export evidence.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (3 earlier review cycles)
  • reviewed 2026-09-17T22:02:54.086Z sha 5c560a4 :: needs real behavior proof before merge. :: [P2] [P2] Preserve covered zero days in the spend trend
  • reviewed 2026-09-18T02:52:34.519Z sha 8a8ea46 :: needs real behavior proof before merge. :: [P2] [P2] Preserve unknown spend instead of converting it to zero | [P2] [P2] Give the zero-day fixture a shareable provider | [P2] [P2] End the chronological fixture after its last included day
  • reviewed 2026-09-18T03:03:00.283Z sha 6207ce8 :: needs real behavior proof before merge. :: [P2] [P2] Supply totalTokens in all daily-summary fixtures | [P2] [P2] Suppress partially priced days as well as nil totals | [P2] [P2] Preserve unavailable coverage when daily summaries are empty

@Chipagosfinest

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Sep 18, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@Chipagosfinest

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Sep 18, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@Chipagosfinest

Copy link
Copy Markdown
Contributor Author

Cross-link: #3747 restores retained top-model rows on the OG card when the USD group is incomplete. Independent of the sparkline; this branch can stay stacked on the spend-first layout. Live-checked on Homebrew 0.61.0.

@clawsweeper related ranking fix on current main — not a replacement for this trend PR.

…al disclaimer

The shared snapshot set the token count at 104pt and the spend figure at 32pt,
so the least meaningful number was the hero and the reason the card exists was
secondary. The two hero columns were also spaced independently, leaving their
labels on different baselines.

Spend now leads, tokens support it, and both columns run the same
label / value / detail rhythm. The card said it was local and aggregate four
separate times; the LOCAL SNAPSHOT badge and the aggregate sentence are gone and
the footer keeps the claim. The rankings block no longer reserves a fixed 286pt,
so a sparse snapshot centres instead of leaving a void above the footer, and the
empty model column reads as a row rather than an apology.

Also fixes a plural bug: the most common card there is rendered
"1 subscriptions".
Dropping the fixed rankings reservation removed the only guarantee that a dense
card fits 630pt. SwiftUI does not clip an oversize frame by default, so five
provider rows plus three model rows could have pushed the footer out of the
image. The reservation is back; the void this PR set out to fix came from
content hugging the top of that block, not from the block itself, so it now
centres inside it.

The 104pt slot previously held a compact token count (never more than six
characters, so it never scaled). It now holds a currency string: 654pt of
available width cannot fit "~$123,456.78" at 104pt, which would have silently
scaled the headline down. 76pt fits twelve characters unscaled and still reads
as dominant over the 42pt token count. The coverage line is pinned to one line
now that it carries explicit secondary-currency totals.
…d badge

Rendering the card at 1200x630 across sparse, dense and multi-currency payloads
showed two problems with earlier revisions of this branch.

Centring the rankings inside its reservation split the empty space on a sparse
card and left a gap directly under the divider, reading as a layout fault rather
than as absent content. Top alignment keeps one contiguous region below the
content.

Replacing the LOCAL SNAPSHOT badge with the period label put "30 DAYS" directly
above "EST. 30-DAY SPEND". The badge is removed instead, since the footer
already carries the local/aggregate claim and the spend label already carries the
period. periodLabel is now unused and removed with it.

Renders and method are recorded in docs/research/share-card-layout.md.
The card showed a total and a ranking but no movement. Every metric-card
pattern worth copying pairs a headline value with its trend, and the data was
already available: CurrencyGroup.dailyPoints carries per-provider cost per day,
it just never reached the payload.

ShareStatsPayload gains dailySpend: one value per covered day for the headline
currency, summed across providers, oldest first. It is aggregate by
construction, so it exposes nothing the rows do not already show.

The sparkline sits in the hero right column, which was using only ~96pt of its
140pt, so the vertical budget is unchanged and the dense card still fits 630pt.
It is labelled DAILY SPEND because it plots spend while sitting beneath the
token count, and it is suppressed entirely for a flat or single-point series
rather than drawing a shape the data does not support.
main added a required totalTokens field to SpendDashboardModel.DailySummary
after this branch forked; rebase surfaced the missing argument at all six
call sites in this file.
@Chipagosfinest
Chipagosfinest force-pushed the feat/share-card-spend-trend branch from 6207ce8 to d7c6a95 Compare September 21, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant