The /subscription-window endpoint cached the last `tracker.state` snapshot
verbatim. The tracker polls Anthropic's /api/oauth/usage every 5 minutes
(aggressive polling risks 429s / OAuth-token flagging). When the user's
5-hour window rolls over between two polls, the cached `utilization_pct`
described the OLD window — the dashboard rendered e.g. 44% while Claude
Code itself showed 0%. (Issue #281)
Fix: display-time synthesis. We already have all the data needed locally:
- snapshot.five_hour.resets_at — the API-reported reset boundary.
- session_tracking.compute_window_tokens — transcript-derived token
counts we can scope to [resets_at, now] to estimate usage in the new
window.
When `now >= resets_at`, render_state() synthesizes:
- used = local transcript tokens since resets_at (capped at limit; we
undercount tokens spent on Claude Code outside this proxy and must
never report >100%).
- utilization_pct = used / limit * 100.
- resets_at = next_reset (advanced by window_duration; marked
`resets_at_estimated=True`).
- synthesized=True.
When `now < resets_at`, the cached snapshot is returned verbatim with
`synthesized=False`. Backward compatible: every existing tracker.state
key is preserved; only new keys (synthesized, resets_at_estimated,
optional render_warning) are added per window dict.
Also adds maybe_poll_on_demand(): a 60s-floored singleton poll triggered
on dashboard load. Bounded across users (well within Anthropic
tolerance), wrapped in asyncio.wait_for(2s) so a slow upstream never
blocks the request handler. Exceptions are swallowed and logged.
All synthesis decisions emit structured logs:
event=subscription_window_synthesized window=... used=... limit=...
event=subscription_render_synthesis_failed (warn fallback)
event=subscription_on_demand_poll_triggered/skipped_floor/timeout/failed
Tests (12 new, all green):
- render within window returns cached pct
- render after reset synthesizes from local tokens
- render after reset with zero local tokens => 0%
- render capped at 100%
- render handles missing resets_at gracefully
- render preserves existing state keys (backward compat)
- render with no snapshot returns base state
- render synthesis fallback path logs and returns cached
- synthesize helper handles None window
- synthesize helper advances reset multiple windows when dashboard
loaded long after reset (e.g. machine asleep)
- maybe_poll_on_demand singleton 60s floor (mock-counted)
- maybe_poll_on_demand swallows API failures
Closes#281