Tracking doc for the chain of work uncovered while auditing how the dashboard derives
plan_nameandsubscription_expires_atfor each account card. See the audit findings at the bottom for context.
[ ]not started[~]in-flight (PR open, not merged)[x]merged
- A1. Persist
wham/usage.plan_typeintoProfileMetadataPipe the API-derived plan intosync_profile_metadata_from_auth_and_quota, preferring it over the id_token claim. Drop the#[allow(dead_code)]onChatGptApiSnapshot.plan_type. Removeapply_paid_fallback_for_free_planonce the API path supersedes it. - A2. Top-level
chatgpt_plan_typefallback in id_token decode Mirror CodexBar's defensive fallback: ifauth.chatgpt_plan_typeis missing in the nestedhttps://api.openai.com/authclaim, look at the top-levelchatgpt_plan_type. One-line guard against schema drift. - A3. Hide misleading "0 days" plan label
planLine(plan, daysLeft)should drop the days suffix when the cachedsubscription_expires_atis absent or already past. Optionally surface a subtle "needs refresh" hint instead.
PR: #19 (merged 2026-05-08)
- B1. Force OAuth refresh on user-initiated card refresh
RefreshOptions { force_token_rotation }plumbed throughrefresh_profile_via_api_with_options. mac/wintry_refresh_via_chatgpt_apinow setsforce_token_rotation: true. The 5-min silent ticker keeps the cheap path. - B2. Day-rollover background pass over all OAuth profiles
New Tauri command
refresh_all_oauth_profile_plans_silentwalks every OAuth profile, forces a token rotation, and rolls plan + quota into metadata. Frontend kicks it on bootstrap (after first dashboard render) and on every local-day rollover (10-min polling againstDate.toDateString()). - B3. Track
last_plan_check_msseparately fromlast_refreshandquota_updated_at_ms, so the UI can show plan freshness independently of quota freshness. Stamped bysync_profile_metadata_from_auth*whenever a plan is confirmed (id_token claim or API plan_type).
PR: (to be created)
- C1. Plan badge on each card with hover-time freshness tooltip.
last_plan_check_msnow flows fromProfileMetadata→ProfileIndexEntry→ProfileCard/CurrentCard. Front-endplanFreshnessTitlerenders a localized "Plan tier confirmed N min/h/d ago" tooltip, andisPlanCheckStale(>36h) drives a subtle leading dot via the.plan-check-staleCSS class. - C2. Replace silent "free → paid" fallback with explicit
unknown_paidstate. Backend constant renamed toUNKNOWN_PAID_PLAN_NAME. Front-end maps the token to a localized "Unknown paid plan" label with a "Re-login to confirm" hint surfaced in the same hover tooltip; the warning hue dot via.plan-unknown-paidseparates it from a plain stale cache.
PR: (to be created)
- D1. Decouple plan-update path from quota-update path.
sync_profile_metadata_from_auth_and_quotaremoved.sync_profile_quotaandsync_profile_metadata_from_auth(profile, api_plan_override, home)are now the only two entry points. Callers that previously bundled both arguments now make two writes; the operations touch disjointProfileMetadatafields so order is irrelevant. Disk cost is one extra ~1KB write per refresh, which is invisible in practice. - [~] D2.
InvestigateDropped. The path's hard-coded/accounts/check/v4-2023-04-27as a more authoritative plan/subscription endpoint than/wham/usage.2023-04-27date suffix telegraphs that it's a versioned snapshot endpoint OpenAI may rotate / retire without notice; reverse-engineering it carries ongoing maintenance risk that A1's/wham/usage.plan_typealready neutralizes.
auth.json
└─ tokens.id_token (JWT, decoded URL-safe base64)
└─ "https://api.openai.com/auth" claim
├─ chatgpt_plan_type → ProfileMetadata.plan_name
└─ chatgpt_subscription_active_until → ProfileMetadata.subscription_expires_at
ChatGptApiSnapshot.plan_type from /wham/usage IS populated on every silent
refresh but never persisted (#[allow(dead_code)]). That is the freshest
plan signal we have today.
| Trigger | Cadence | Profiles touched | Path |
|---|---|---|---|
| Silent ticker | every 5 min, only if quota >5min stale | active OAuth only | commands/dashboard.rs::refresh_active_profile_quota_silent → chatgpt_api::refresh_profile_via_api → metadata::sync_profile_metadata_from_auth_and_quota |
| User Refresh button | on click | one card | refresh_runtime::refresh_profile |
| User Login button | on click | one card | login_runtime::login_profile_with_home → metadata::sync_profile_metadata_from_auth |
| User Switch button | on click | active changes | indirect via profiles_index reload |
| App startup | once | all (lazy hydrate) | metadata::hydrate_profile_metadata |
refresh_oauth_tokens only fires when access_token is expired (~60min TTL)
or after a 401, so the id_token in auth.json rarely rotates for an
otherwise-quiet active profile and never rotates for inactive profiles.
| Folder | Plan | subscription_expires_at |
Age |
|---|---|---|---|
| a | plus | 2026-04-13 | -25d |
| b | team | 2026-04-27 | -11d |
| c | team | 2026-04-27 | -11d |
| d | pro | 2026-04-08 | -30d |
| e | pro (active) | 2026-05-04 | -4d |
Active profile e is only 4 days stale because the silent ticker eventually
caught it; inactive profiles are weeks stale because nothing refreshes them
until the user clicks Refresh / Login / Switch.
steipete/CodexBar— uses the sameid_token.https://api.openai.com/auth.chatgpt_plan_typesource. Has an extra fallback: if missing, pulls top-levelchatgpt_plan_typefrom the JWT payload. Does not consumewham/usage.plan_typefor plan derivation.farion1231/cc-switch— config-only switcher, no quota / plan logic.Cmochance/codex-app-transfer— protocol-forwarding tool, not OAuth-account-aware.
So A1 (persist API plan_type) is the optimization industry peers haven't taken; A2 (top-level fallback) is borrowing CodexBar's defensiveness.