Skip to content

fix: Monthly usage window respects subscription anchor, fix setManualSpentTargets#69

Merged
ltmoerdani merged 1 commit into
ltmoerdani:mainfrom
Wallacy:fix/monthly-usage-anchor-window
Jul 3, 2026
Merged

fix: Monthly usage window respects subscription anchor, fix setManualSpentTargets#69
ltmoerdani merged 1 commit into
ltmoerdani:mainfrom
Wallacy:fix/monthly-usage-anchor-window

Conversation

@Wallacy

@Wallacy Wallacy commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

fix: Monthly usage window respects subscription anchor, fix setManualSpentTargets — fixes monthly cost drift

Goal

Fix the monthly usage percentage jumping backward (e.g. 30% → 8% → 46%) due
to two bugs introduced in the SQLite wiring (PR #60).

Root cause

Bug 1 — Auto-anchor lost. buildSummaryFromRows originally derived the
monthly window from the earliest SQLite row's day-of-month (auto-anchor),
matching the actual billing start. My previous change replaced this with
calendar month boundaries, causing the window to shift when crossing the
anchor boundary and dropping accumulated costs.

Bug 2 — setManualSpentTargets window mismatch. When the user set a monthly
target with a new anchor day, getSummary() computed the monthly cost using
the CURRENT window (no anchor). After storing the anchor, the window changed,
so tracked + baseline no longer equaled the target — the displayed value
diverged from what the user entered.

What was implemented

  • buildMonthlyWindow(nowMs, baseline, earliestMs?) with 3-tier priority:
    1. User-configured anchor from baseline (set via "Set spent targets")
    2. Auto-anchor from earliest SQLite row (restores original behavior)
    3. Calendar month fallback (tracked-only path)
  • buildSummaryFromRows passes the earliest SQLite timestamp for auto-anchor
  • setManualSpentTargets computes trackedMonthly using the PROSPECTIVE
    window (with the new anchor), reading SQLite costs directly instead of
    relying on getSummary() which uses the old window
  • Both SQLite and tracked aggregation paths use the same window logic

Testing

86/86 unit tests pass. Anchor simulation:

earliest row day=20 → on Jul 2: window = [Jun 20, Jul 20) ✓
set anchor day=21 hour=21 → window = [Jun 21 21:00, Jul 21 21:00) ✓

Notes

Users who previously set manual targets with an anchor may want to clear and
re-set them after applying this fix, since the tracked monthly cost is now
computed against the correct window. The auto-anchor should work out of the
box — no manual config needed if the earliest SQLite row matches the
subscription start date.

…SpentTargets window mismatch

The monthly cost aggregation was using calendar month boundaries after my
previous change, but the OpenCode Go billing is anchor-based (resets on the
subscription day/hour). The auto-anchor from the earliest SQLite row was also
lost, causing the window to shift when crossing calendar boundaries and making
the percentage jump backward.

Root cause of the setManualSpentTargets bug: getSummary() computed the monthly
cost for the CURRENT window (no anchor). After storing the anchor, the window
changed, so tracked + baseline no longer equaled the target.

Changes:
- buildMonthlyWindow(nowMs, baseline, earliestMs?) with 3-tier priority:
  1. User-configured anchor from baseline (set via Set Spent Targets)
  2. Auto-anchor from earliest SQLite row (restores original behavior)
  3. Calendar month fallback (for tracked-only path)
- buildSummaryFromRows passes earliest SQLite timestamp for auto-anchor
- buildSummaryFromTracked uses baseline anchor or calendar month fallback
- setManualSpentTargets computes trackedMonthly using the PROSPECTIVE window
  (with the new anchor), reading SQLite costs directly instead of relying on
  getSummary() which would use the old window
- CHANGELOG updated
@ltmoerdani

Copy link
Copy Markdown
Owner

Nice one @Wallacy , clean fix. I checked out the branch locally: compile passes, 86/86 tests pass.

Root cause analysis is solid, especially bug 2 (window shifts after the anchor gets stored). The 3-tier fallback in buildMonthlyWindow reads well too: manual > auto-anchor > calendar. Invariant display = tracked + baseline = target holds.

Two minor notes, neither a merge blocker:

  1. Test gap. The 86/86 that pass are pre-existing tests. The anchor logic (buildMonthlyWindow 3-tier + setManualSpentTargets prospective window) has no coverage in src/test/goUsageTracker.test.ts. The mock interface there doesn't even expose setManualSpentTargets. A regression like this deserves a preventive test so it doesn't come back. Fine to do in a follow-up.

  2. tempBaseline duplication. The block that builds tempBaseline from targets.monthlyAnchorDay appears twice, identical (around lines 605-615 and 628-638). Could extract into a small helper, but readability is fine for now.

Merging as-is. If you want to add tests or extract the helper, happy to take a follow-up PR.

Thanks for the fix 🙏

@ltmoerdani ltmoerdani merged commit 4a8e1c3 into ltmoerdani:main Jul 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants