Repository navigation
Menubar never blank, Cursor string numbers (#151), Codex reset credits (#152) - #153
Merged
Merged
Conversation
…y age A single slow /api/menubar/summary request blanked the tray title to "--" and left the popover empty or black. Every client now keeps its last good data and shows staleness only through each card's age label. Daemon: - Cache the built snapshot with singleflight; serve it immediately when fresh (12s) or when a rebuild exceeds a 3s budget, and let the build finish in the background. Settings and provider writes invalidate it - Keep the last good card per provider/account and serve it with its original capture time on storage read errors; never fabricate fresh zero cards; staleness no longer raises severity - Add latest_at and per-card updated_at_time so ages count from real capture time Tray (macOS/Windows/Linux): - Keep the last title, menu and preferences on failure; only the icon tint and "last updated" tooltip change - Single refresh worker, so requests never block, overlap or get lost - 15s timeout with one retry Popover: - Ages count up from each provider's capture time - Connection issues move to the status dot (ring + hover tooltip) - Render the last snapshot from localStorage instantly on open; ignore out-of-order responses and corrupt cache entries - Host background follows the page theme to avoid the black flash GNOME extension: keep last good state, 15s timeout with one retry, cancel in-flight requests on disable.
Cursor now returns spend fields such as spendLimitUsage.pooledLimit as
strings ("50000"), which aborted the whole usage decode and dropped the
provider on every poll, including free plans.
- Tolerant int/float types accept numbers, quoted numbers, "" and null
- A field that still cannot be parsed degrades to zero with one warning
(count only, no values) instead of failing the response
- Team is no longer inferred from pooledLimit; it requires a team or
business plan name or limitType "team"
Read-only tracking of Codex "reset credits". onWatch never redeems them. - Parse rate_limit_reset_credits (available and applicable counts) from wham/usage; an absent count stays unknown instead of 0 - Fetch the credit list (GET wham/rate-limit-reset-credits) every 30m per account, or when the count changes; failures never touch usage polling, pause state or auth counters, and never refresh tokens - Store credits per account with gone-marking; additive, idempotent migrations; account merges keep notification and live state - Dashboard: "N resets" chip with next-expiry tooltip, plan-unsupported note, and a muted dot when the list is unavailable - One notification per credit 24h before expiry, deduped across restarts; credits with no or nonsensical expiry are never notified - Test guard that no consume/redeem code path exists
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Cards whose data is older than a few poll intervals now get an amber
age label and a hollow status dot with a hover reason (for example
"Last updated 14m ago - network error"); the tray title dims and its
tooltip lists stale providers. Ages now come from honest capture times:
statusline data uses the file mtime, multi-quota cards use their oldest
quota, and OpenCode uses its cache fill time. The menubar companion now
writes its own log so a hung daemon cannot block it.
Also stop serving stale summaries as live data:
- respondJSON now sends Cache-Control: no-store on every API response.
The dashboard needed it too and no API response here is cacheable.
- Every menubar fetch uses {cache: "no-store"}.
- The popover fetches on native show (macOS and Windows), window focus
and visibilitychange, coalesced through the existing generation guard.
The localStorage instant paint still happens first.
- A companion holds an OS-level single-instance lock, and the daemon no
longer relays the companion's stdout into the log it already writes.
…panion restart waits Oldest-quota ages now take each quota's current time when it is present and only remember a quota while it is missing, so a refreshed card moves forward. Anthropic hybrid cards judge staleness on their primary windows, not the every-tenth-poll API supplements. The macOS title applies systray's plain SetTitle first so fyne switches the button out of image-only mode, then the attributed (dimmed) title, and mirrors the image position. The daemon waits for an exiting companion and the replacement retries the lock for 10s. The companion logs to stderr when it cannot open its log, and an unchanged statusline file no longer inserts duplicate rows.
…a (CI) The popover called init() from the middle of its IIFE, before the snapshot generation counters were initialized. The first render and cache restore therefore threw "Cannot access 'snapshotGeneration' before initialization": the page stayed on "Loading" and no provider card rendered, which failed the four Playwright menubar tests on every platform. Start init() at the end of the IIFE and guard the ordering with a static test. The E2E fixture now seeds Anthropic statusline data in its isolated HOME. Synthetic (real API, 401) and Anthropic (no statusline file) have no stored data, and omit-on-no-data correctly drops them; main only passed because it fabricated zero cards. Real statusline data gives the order and card tests a second provider without any network call. Make the invalidation-burst cache test deterministic: a non-positive freshness window forced late waiters to start extra builds after a successful one, so the at-most-two assertion depended on scheduling. Use a positive window, and replace the sleep-based staleness in the slow-build test with a negative one.
Lock a byte range past the pid text so the lock file stays readable on Windows, where LockFileEx locks are mandatory. Latest-snapshot providers (Cursor) drop quotas missing from the payload immediately instead of pinning the card stale for 24h. Apply the parent-pid guard on unix only and recognise renamed builds by comparing the executable base name.
…DeepSeek; drop Anthropic peak chip Command Code, OpenCode Go and DeepSeek cards now show a peak/off-peak chip for the DeepSeek schedule, peak 01:00-04:00 and 06:00-10:00 UTC Mon-Fri. The schedule is computed once in a shared helper with exact UTC math and a fractional countdown; the 30s tickers update the chip without refetching. The Anthropic promo chip and its ET-based logic are removed from the dashboard response, the popover and the menubar snapshot.
…harts Sample the selected range within the 200-row budget instead of returning only the newest polls, preserving the earliest and latest samples in individual and combined views.
Return stored format and reset metadata and render credit or currency usage against its cap instead of dropping units and limits. Keep valid zero balances and percentage-only OpenCode meters intact.
Honor the OpenCode cycle filter and dashboard JSON contract, preserve stored reset times and limits, and replace irrelevant duration and total-delta metrics with peak utilization and reset times for both window providers. Add contract and Node rendering regressions covering samples, cycles, chart datasets, and fetch-failure preservation.
A non-OK status, an error or non-array body, and a network failure now warn with the provider and range and leave the existing datasets untouched. Only a successful empty response clears the chart. The guard is shared by every provider.
The poll that detected a reset was folded into the cycle being closed, so a completed cycle could store the next window's peak and delta. Close on the last sample of the old window and give the new cycle only a future reset timestamp, avoiding stale resets and inverted cycle windows.
…mmand Code Require a 20-minute measurement window before projecting a rate, replace the misleading Idle/0.0% placeholder with an omitted stat or a measured Flat result, and add reset-pacing cards with exhaustion risk per window. Exclude Command Code's uncapped dollar balance from percent forecasts and bound the payload at 50.
A poll that still carried the previous, already-passed reset created a cycle with no reset, so the next poll read that stale reset as a fresh window. Each poll then closed a one-minute cycle and fired a duplicate reset notification; a time-based close could also set cycle_end before cycle_start. Treat a reset that is not after the sample timestamp as absent everywhere, and attach a newly published future reset to the open cycle instead of closing it unless utilization dropped. Clamp cycle_end to cycle_start.
rate.Rate is utilization percent per hour while Used and Limit are credits, so adding the rate straight to Used overstated consumption (2.8 of 14 credits at 10%/hr over 4h became 42.8 instead of 8.4) and showed the card as already exhausted. Convert the rate through the cap before comparing to the limit. OpenCode's equivalent card is already percent based.
The OpenCode and Command Code overview handlers used the generic 500-row page limit, and each cycle runs an extra quota-values query. Cap them at the project's 200-cycle budget so one request cannot fan out past the guardrail. A limit of 500 now returns at most 200 newest cycles.
Existing databases carry derived cycles written before the reset-boundary fix: inverted windows, peaks borrowed from the next window, and one-minute churn cycles. Run a one-off repair guarded by a settings marker, inside one transaction per provider table, without touching raw snapshots. Inverted cycles merge into an adjacent successor or are clamped to the last sample inside the window; short null-reset cycles fold into their successor; peak and positive-only delta are recomputed from in-window samples; active cycles lose a reset that is not after their start. Each pass reads only the newest 200 cycles per quota and logs counts.
One drifted count on the main usage payload failed the whole wham/usage decode, leaving Codex usage stale for every account. Accept numbers, quoted numbers and integral floats; degrade anything else to unknown.
Team and Business members report limitType "user", so the plan name is the only Team signal. A failed plan-info fetch classified the account as Individual for that poll, flipping total_usage from dollars to percent. Keep the last known plan name in memory; never infer Team from pooledLimit.
A future mtime (clock skew, or a file synced from another host) was clamped to now, so the timestamp changed on every poll and the same unchanged file inserted a row per poll while being labelled fresh. Clamp only the displayed captured time; dedupe on the raw filesystem mtime.
The expiry sanitizer reports an untrustworthy expiry (already past on a fast local clock, or equal to granted_at) as unknown. The next sync then wrote NULL over a stored, valid expires_at and set expiry_unknown, so a credit lost its real expiry. Keep the valid stored value; a credit genuinely reported as never-expiring still clears it.
UPDATE OR REPLACE kept the source row's status and last_seen even when the target's row was newer, so a merge could resurrect a stale status. Take both from the row with the newer last_seen; notified_at, MIN(first_seen) and the gone_at live-wins rule are unchanged.
The source-string guard could be routed around by URL building. Every Codex client request method now runs against an httptest server that fails on any non-GET, on a path containing consume, and on unexpected paths. The guard's skip list is narrowed to exact file paths, and the agent's fake server fails on any non-GET or unexpected path. The string test stays.
A non-numeric limit or percent ("N/A") degraded to zero and was stored and
shown as a fresh 0% or no-limit reading, violating never-fabricate. Drop that
quota so the last good card is kept; non-defining fields still degrade to
zero, and numbers, quoted numbers, null and "" still decode as before.
The all-accounts overview replaced a failed per-account history fetch with an empty array, then destroyed and redrew the chart without that account's lines. Treat non-OK responses, error or non-array bodies and network errors as failures: warn and leave the existing chart untouched.
A failed range or account switch left the previous lines on screen under the newly active label with only a console warning. Record the provider, account and range each chart was drawn for; when a later failure does not match, keep the lines but show an amber hollow dot whose tooltip names what is displayed. The next successful load clears the mark.
Command Code, OpenCode and DeepSeek rendered a chip in the All Providers card header and again on their first KPI card. Individual views only put it on the first card, so drop the header chip for providers that have KPI cards and keep it only where there is no KPI card to carry it (DeepSeek balance).
| if err != nil { | ||
| return 0, false | ||
| } | ||
| defer file.Close() |
Limit the unshipped v1 repair to structural corrections, preserving stored peaks and deltas despite incomplete or ambiguous samples.
Recover awaiting-rollover state from persisted cycle boundaries so stale samples cannot seed the new peak or cause another reset after a restart.
The delayed-rollover bug seeded churn cycles with the old window's sample, so folding or merging them forward wrote a spurious 100% peak into the real successor. Let the successor keep its own stored statistics instead.
Stop holding samples once the grace window ends, restart an idle OpenCode window at the next session instead of stretching it across days, and count usage recorded between the boundary and the API acknowledgment.
An idle 5h window that waited past the rollover grace kept its old start when the next session opened, stretching the cycle across days. Mirror OpenCode's idle restart and keep the next session's first sample. Fold churn rows without dragging a distant successor's start, and seed usage in a new window whose predecessor barely registered any.
Only restart an idle Command Code window when it never learned its reset, so a boundary published during the grace period is not moved forward. Seed the restart with its first sample so the delta matches OpenCode.
historyBoth silently dropped providers whose read failed and still returned 200, so the All Providers view saw a missing provider and replaced its drawn rows with "Collecting data...". The handler now lists per-provider read failures in an errors map, and the dashboard merges successful providers into the previous snapshot, keeping failed providers' last rows behind an amber stale dot with the data age in its tooltip. A successful empty read still clears a chart. A repeated failure of the chart's current selection also kept its stale mark now: the render key records the last successful fetch time, and every failure of the current request marks the retained data with its age. Failures from obsolete requests no longer touch the marker.
The peak test helpers interpolated a filesystem path straight into a JS string literal. On Windows filepath.Join produces backslashes, JavaScript read them as escapes, and require() failed with a mangled module name (D:aonWatchonWatchinternalwebstaticpeak-hours.js). Render the path with JSON string quoting instead, which is valid JavaScript on every platform, and unit-test the quoting with a Windows path so the logic runs on macOS. The other node-driven web tests read their sources through staticFS, so no path is interpolated and nothing else needed changing.
TestDaemonChildRun_DebugModeAllProvidersWithCopilot failed on Windows only because t.TempDir's cleanup reported 'unlinkat ...: The directory is not empty'. The child is terminated abruptly there (Windows cannot deliver os.Interrupt), and a write from it can land between the cleanup's directory scan and rmdir; Go retries sharing violations on Windows but not ENOTEMPTY. The daemon subprocess helper now removes its sandbox home itself, retrying until it converges, before t.TempDir's own removal runs.
The dashboard rendered applicable_available_count (0 while a reset is banked) as a stray grid cell. Use available_count, the number the Codex CLI shows, and render a compact chip in the first card header, the account header, and the menubar Codex card, with the next expiry in the tooltip.
DISTINCT over the per-snapshot value tables scanned 80k-230k rows per call (100-200ms each, several per insights and menubar build). Seek between values through an index instead, adding the missing codex_quota_values(quota_name) index. Antigravity range loads ran one query per snapshot; batch them, sample chart ranges in SQL, and compute the coverage insight from COUNT/MIN/MAX.
Build the menubar snapshot shortly after each poll (debounced, only while a menubar client is active) and serve aged snapshots immediately while a rebuild runs, with an X-Onwatch-Refreshing hint so the popover re-fetches once. The popover now writes DOM only when its markup changes and repaints once per refresh, opening the popover refreshes the tray from the same snapshot, and the macOS panel fades in after WebKit repaints instead of flashing the bare background.
Dashboard init read /api/settings three times in sequence (hidden insights, timezone, provider settings) before any data request could start. Share one request and clear it before any settings write.
Insights, history, cycle overview and logging history are pure functions of stored data and settings, yet were recomputed on every load, tab switch and refresh tick. Cache each 200 response until the provider it covers polls again (any poll for the combined view), drop everything on a settings write, cap entries at 60s, 128 entries and 8MB, and never cache partial answers.
Range loads ran one SELECT per snapshot on every provider's value table, so a 30-day history read issued thousands of tiny queries. Load each range's values with batched WHERE snapshot_id IN (...) queries (400 ids per query), ordered by snapshot_id then the existing inner order, and attach rows through an id->snapshot map. The same batching removes the per-cycle value queries from every provider's cycle overview. Chart callers also get ...RangeSampled methods that pick about 200 snapshots evenly across a range in SQL, using windowChartQuery, plus a variant that carries an extra account predicate.
History endpoints and historyBoth loaded a full range and then downsampled to maxChartPoints, so the query cost grew with every poll in the window. Switch the chart handlers to the store's ...RangeSampled methods, which pick about 200 points in SQL. Logging-history endpoints, which need every row, and Cursor/Ollama/Muse (already capped at the 200 most recent) are unchanged.
The window sampler numbered full rows, so a 30-day range materialized every snapshot (raw_json included) to keep about 200. Pick the sampled ids from (captured_at, id), which the index covers, then read only those rows: 30 days of Command Code drops from about 0.7s to about 10ms.
…d reads Responses over 1MB are served uncached and stored bodies are exact copies, so the 8MB budget holds. Agents publish a poll outcome just before writing the snapshot, so invalidate the provider again 3s later to drop answers computed in that gap. Grok, Kimi and Antigravity history now answer an error status on a failed read instead of an empty 200, which the dashboard keeps its last chart for and the cache never stores.
…nswers Every settings write (including hiding an insight) clears the response cache, and insight builders that swallow a read error mark their answer partial so it is served but not cached. Background menubar rebuilds keep a 5s minimum gap, deferring poll-driven prebuilds and never retrying a failing build per request; the popover's refresh follow-up cannot chain. DeepSeek and Moonshot now report poll outcomes after their writes, and the dashboard reset chip's expiry tooltip updates on the 30s ticker.
This was referenced Oct 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Menubar never goes blank
X-Onwatch-Refreshinghint so the popover re-fetches once.Cursor numeric fields as JSON strings (Fixes #151)
""andnullall decode. A bad non-quota field degrades to zero; an unreadable limit or percent drops that quota for the poll instead of storing a fake 0%.pooledLimit; the last known plan name is reused when the plan lookup fails.Codex banked reset credits, phase 1 (Refs #152)
Peak hours
Command Code and OpenCode Go dashboard
Performance
DISTINCTscans replaced with index seeks, adding the missingcodex_quota_values(quota_name)index.captured_atindex.Test plan
./app.sh --test(race), full suitegofmtcleango vet -tags menubaron darwin (cgo), linux and windows