tokentracker-cli 1.5.3 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-uuDGO9Mq.js → AchievementBadge-CKNb_cwg.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-BCTVT7Jo.js → AchievementsPage-u9P8DXJI.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-B6fJSJSr.js → AchievementsSection-BSCUJ4DR.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-FjUN__9G.js → BadgeDetailModal-Bg7cE3Zq.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-B-KqY_Wr.js → BotAnimated-C7D2WbQA.js} +1 -1
  6. package/dashboard/dist/assets/{Card-DS8T8piP.js → Card-B4IqAhai.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-_0UjsFPu.js → CloudCheckoutPage-P75pH9Ym.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-BstgVBaV.js → CloudPage-DDEZfmZg.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-DtVsYZgc.js → CommandPalette-CER_AklA.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-DTpEJqV3.js → CommunityStatsModal-Cbb74eib.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-Lfdh2vr7.js → ConfirmModal-Q1ZQdZHt.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-DpwQ7pkq.js → Controls-DaGOfcoz.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-CXI7RmxO.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-CrWgrzT2.js → DateRangePopover-DSS1Ira_.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-DbK5KWJS.js → DevicePage-6zPLYydh.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-Dd2B_o03.js → DialogClose-DdCQ36LK.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-DztHQvj-.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-3JQBVha3.js → DialogTitle-BjP217Y1.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-LYAqG8pG.js → ElectricLogo-Dhq8PM4h.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-DhDvUor7.js → FadeIn-CGNbED4E.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-9f1XmgYC.js → HeaderGithubStar-DwqEFzb6.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-BXhHAkP7.js → HoverTooltip-CQZTgyQy.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-ChPdDeMl.js → IpCheckPage-Bz09HXLn.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DnJ7Y9zw.js → LandingPage-DC-lZsYR.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BF9isLzu.js → LeaderboardPage-D8MpLM_t.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-BM4JKgkC.js → LeaderboardProBadge-pdXgokhE.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-DfZABOpN.js → LeaderboardProfileModal-nHZLiE0m.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-BaZ38iT5.js → LeaderboardProfilePage-BKv3oIAW.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-TmRTgSCb.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-DvhlYoqQ.js → LocalOnlyNotice-aF01udyR.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-g_Gbnzhs.js → LoginCard-BaroD0Ct.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-DkJrI6b7.js → LoginModal-0jgyMGVM.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-CqCKSo0n.js → LoginPage-BUx8U58i.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DbF7ROsj.js → PetPage-CpOjtJqg.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-4WuH41ZT.js → PopoverPopup-BYjfer-M.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-6j0A2KD9.js → ProviderIcon-NtYa2h0u.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DBd-iwuM.js → ResetPasswordPage-CYUg7TR1.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BBEpLeiG.js → Select-CYP95o5y.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-Cn_U_rrO.js → SelectItemText-BCDfusOM.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-2WZc6Rml.js → SelfHostPage-BmVc2rIT.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-Dyp8NwIn.js +1 -0
  42. package/dashboard/dist/assets/SessionsPage-tGEj6Ztz.js +3 -0
  43. package/dashboard/dist/assets/{SettingsPage-rXNr96i-.js → SettingsPage-CCMbq5wc.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CudTvbZL.js → SkillsPage-C4O4fbaQ.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BRKrbTsJ.js → TokenGalaxy-D2d738xr.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-CsLKXhWC.js → TooltipPopup-BJfFYesG.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-8onUTbkk.js → TrendMonitor-CElakFSU.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-CcwE0lz8.js → WidgetsPage-Cvd1LbJm.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-Cj9LLYqP.js → WrappedPage-wRC1tE_a.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +1 -0
  51. package/dashboard/dist/assets/__vite-browser-external-V4TPLCJG.js +45 -0
  52. package/dashboard/dist/assets/{agent-logos-ChBA1nAH.js → agent-logos-Ba57HF3w.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-Bb5mfoPc.js → arrow-left-B1bKnxXL.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-D1Ti2XUI.js → arrow-right-DrOlKzX5.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-B_CjA_y8.js → arrow-up-right-hlYpo3yw.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-DdfrAqie.js → calendar-days-CSY6SEsI.js} +1 -1
  57. package/dashboard/dist/assets/{check-29zGw1MK.js → check-xrP1gLw5.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-DAzlFX7g.js → chevron-down-6-zT5n9d.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-C2SeCjFM.js → cloud-checkout-BZymZD4K.js} +1 -1
  60. package/dashboard/dist/assets/{composite-DIf735Hq.js → composite-C9izWafa.js} +1 -1
  61. package/dashboard/dist/assets/{copy-BOuOM0HQ.js → copy-Dkas5wZX.js} +1 -1
  62. package/dashboard/dist/assets/{download-50tQRpGe.js → download-CagGRs0_.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-BhlQClVe.js → external-link-DRl10hxd.js} +1 -1
  64. package/dashboard/dist/assets/{flame-CeGfA-0L.js → flame-CkvmsTFB.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-Cq7YiAkJ.js → format-tokens-BtwhO3mV.js} +1 -1
  66. package/dashboard/dist/assets/{gift-Do-UFhiT.js → gift-YzjXkMQx.js} +1 -1
  67. package/dashboard/dist/assets/{icons-C6EZ_HmP.js → icons-CHhKYs0u.js} +1 -1
  68. package/dashboard/dist/assets/{index-BWhbBAd9.js → index-B0MQZTrM.js} +1 -1
  69. package/dashboard/dist/assets/{index-D9kwYWLX.js → index-C2DK8av-.js} +1 -1
  70. package/dashboard/dist/assets/{index-BFrbVqsm.js → index-CJknN9Jd.js} +1 -1
  71. package/dashboard/dist/assets/{index-c0crpe9Z.js → index-DojzoMqd.js} +1 -1
  72. package/dashboard/dist/assets/{info-B4MXmsNE.js → info-Torep-Ti.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-DuWCslzl.js → link-2-DE5Yu-z6.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-C2I3NZAO.js → loader-circle-BL8LxD9n.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-BQasvsUF.js → local-usage-transfer-8X1InWix.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-DbeNS6FT.js → modifiers.esm-D60-6UQa.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-C6Y4ktsc.js → monitor-smartphone-C-IFPjqk.js} +1 -1
  78. package/dashboard/dist/assets/{play-CwgcDJC2.js → play-n6uWrdH9.js} +1 -1
  79. package/dashboard/dist/assets/{plus-B796wsUO.js → plus-B2f6TaNj.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-Cw26RFDm.js → provider-display-DWqCEcsP.js} +1 -1
  81. package/dashboard/dist/assets/{react-d7Bk1-_K.js → react-B-zfi_j6.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-CDE0dNv_.js → safePolygon-C2PsvI-v.js} +1 -1
  83. package/dashboard/dist/assets/{search-DtKvsdRs.js → search-DZOaTLF9.js} +1 -1
  84. package/dashboard/dist/assets/{server-gAURHUyz.js → server-DIp4Qfsv.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-DFVdDTwh.js → shield-check-Cs2M8-iT.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-2rdPUrJI.js → skills-api-CoYy6Tov.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-DFlfwnJr.js → sortable.esm-B5_hjIfs.js} +1 -1
  88. package/dashboard/dist/assets/{store-BKPCk_de.js → store-BLXqtJzb.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-BtWFakvS.js → terminal-CEMKX3ba.js} +1 -1
  90. package/dashboard/dist/assets/{trash-bpr9zTAM.js → trash-DOWYaXjK.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-DMnGgLne.js → use-community-stats-BjRI1Bzz.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-BSBlJJHe.js → use-latest-request-guard-T7L5qBU6.js} +1 -1
  93. package/dashboard/dist/assets/use-limits-display-prefs-vNIrCPZA.js +1 -0
  94. package/dashboard/dist/assets/{use-native-settings-OthqiF6g.js → use-native-settings-Cg5Zb4q_.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list-DpAtr0R-.js → use-ordered-list-BF-wIvmG.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-B9K5mIx4.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-BRz9dz_2.js → use-transform-CbJ1IcNc.js} +1 -1
  98. package/dashboard/dist/assets/{use-usage-limits-CwmhudAX.js → use-usage-limits-Dgg3jpC4.js} +1 -1
  99. package/dashboard/dist/assets/{useCurrency-CAA-tQ27.js → useCurrency-3oBHWfSe.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-Cj2DPmwM.js → usePositioner-D0YrIW0A.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-Bf5-f7Hx.js → useTokenFormat-DZAHWE_R.js} +1 -1
  102. package/dashboard/dist/assets/{zap-B8Fg6PaC.js → zap-kLub6Jiv.js} +1 -1
  103. package/dashboard/dist/index.html +2 -2
  104. package/dashboard/dist/ip-check.html +2 -2
  105. package/dashboard/dist/leaderboard.html +2 -2
  106. package/dashboard/dist/share.html +2 -2
  107. package/package.json +1 -1
  108. package/src/commands/serve.js +3 -2
  109. package/src/lib/accept-encoding.js +59 -0
  110. package/src/lib/cursor-config.js +3 -1
  111. package/src/lib/deepseek-limits.js +280 -0
  112. package/src/lib/grok-limits.js +40 -1
  113. package/src/lib/local-api.js +229 -31
  114. package/src/lib/opencode-status.js +88 -0
  115. package/src/lib/pricing/index.js +182 -79
  116. package/src/lib/pricing/seed-snapshot.json +1 -1
  117. package/src/lib/provider-api-key-store.js +2 -0
  118. package/src/lib/provider-status.js +17 -5
  119. package/src/lib/session-analytics.js +194 -29
  120. package/src/lib/static-server.js +74 -2
  121. package/src/lib/usage-limits.js +535 -16
  122. package/src/lib/workbuddy-limits.js +106 -14
  123. package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +0 -1
  124. package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +0 -1
  125. package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +0 -6
  126. package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +0 -1
  127. package/dashboard/dist/assets/SessionsPage-CWS0iQ1L.js +0 -3
  128. package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +0 -45
  129. package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +0 -1
  130. package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +0 -1
  131. package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +0 -1
@@ -25,10 +25,10 @@ const { resolveWorkbuddyHome } = require("./rollout");
25
25
  const { UNAVAILABLE, readWorkbuddyIpcDiscovery, openWorkbuddyIpc } = require("./workbuddy-ipc");
26
26
 
27
27
  const WORKBUDDY_LIMITS_CACHE_FILE = "workbuddy-usage-limits-cache.json";
28
- // Windows without a usable reset_at — the pooled resource window by design,
29
- // plus a plan row whose cycle end never parsed — cannot be checked against a
30
- // quota rollover, so they are only served while the snapshot is young enough
31
- // to trust.
28
+ // Windows without a usable reset_at — the pooled resource window unless it
29
+ // carries a readable expiring_at, plus a plan row whose cycle end never
30
+ // parsed — cannot be checked against a quota rollover, so they are only
31
+ // served while the snapshot is young enough to trust.
32
32
  const WORKBUDDY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
33
33
 
34
34
  const PLAN_PRIORITY = Object.freeze({
@@ -195,6 +195,9 @@ function normalizePackageRow(row) {
195
195
  used,
196
196
  cycleEnd: firstString(row, ["CycleEndTime", "cycleEnd", "cycle_end"])
197
197
  || firstString(row, ["DeductionEndTime", "expireAt"]),
198
+ // Set once a grant has terminated (spent or revoked); active grants
199
+ // carry an empty string.
200
+ terminatedAt: firstString(row, ["ExpiredTime", "expiredTime"]),
198
201
  };
199
202
  }
200
203
 
@@ -406,6 +409,59 @@ function resourceWindow(pool) {
406
409
  };
407
410
  }
408
411
 
412
+ // Resource packages die individually: paid add-ons and reward grants each
413
+ // carry their own cycle end, so the pooled window can advertise when its
414
+ // credits start to lapse — "by <day>, <credits> expire" beats a bare date
415
+ // because a date alone cannot say how much is at stake. Grants are bucketed
416
+ // by local calendar day and only the earliest day that still holds credit is
417
+ // reported; later days belong in a per-grant breakdown, not the pooled bar.
418
+ // Rows that could be the subscription plan are not resource credit (the same
419
+ // rule collectResourceGroups applies), and a grant with no remaining credit,
420
+ // a past end, an unreadable end, or a termination stamp has nothing to lose.
421
+ function collectExpiringResourceCredits(rows, identity = {}, nowMs = Date.now()) {
422
+ const grants = new Map();
423
+ for (const row of rows) {
424
+ if (!row || isSelectablePlan(row, identity.code)) continue;
425
+ if (row.terminatedAt) continue;
426
+ const remain = row.remain
427
+ ?? (row.total !== null && row.used !== null ? Math.max(0, row.total - row.used) : null);
428
+ if (remain === null || remain <= 0) continue;
429
+ const endMs = Date.parse(normalizeResetAt(row.cycleEnd) || "");
430
+ if (!Number.isFinite(endMs) || endMs <= nowMs) continue;
431
+ // The summary and the paid/free pages list the same grant; one code with
432
+ // one end time is one grant, and the later listing wins the way
433
+ // mergePackageRows merges.
434
+ grants.set(`${row.code}\u0000${endMs}`, { endMs, remain });
435
+ }
436
+ let earliest = null;
437
+ for (const grant of grants.values()) {
438
+ const day = localDayNumber(grant.endMs);
439
+ if (!earliest || day < earliest.day) {
440
+ earliest = { day, endMs: grant.endMs, firstMs: grant.endMs, credits: grant.remain };
441
+ } else if (day === earliest.day) {
442
+ // Grants lapsing the same day merge; the moment to advertise is the
443
+ // last one, when the day's final grant dies, while the first one marks
444
+ // when the pool's numbers actually start to drift.
445
+ earliest.endMs = Math.max(earliest.endMs, grant.endMs);
446
+ earliest.firstMs = Math.min(earliest.firstMs, grant.endMs);
447
+ earliest.credits += grant.remain;
448
+ }
449
+ }
450
+ if (!earliest) return null;
451
+ return {
452
+ at: new Date(earliest.endMs).toISOString(),
453
+ firstAt: new Date(earliest.firstMs).toISOString(),
454
+ credits: earliest.credits,
455
+ };
456
+ }
457
+
458
+ // Year*10000 + month*100 + day, local time — an ordering key that survives
459
+ // unpadded string comparison and matches how the end times parse.
460
+ function localDayNumber(ms) {
461
+ const date = new Date(ms);
462
+ return date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();
463
+ }
464
+
409
465
  function englishPlanLabel(row, identity, variantId) {
410
466
  // An actual English subscription/package name takes precedence over the
411
467
  // client's edition fallback. Unknown localized tiers stay unnamed.
@@ -521,12 +577,22 @@ function workbuddyLimitsCachePath({ home } = {}) {
521
577
 
522
578
  // A window whose reset_at has already passed is stale data, not a usable
523
579
  // fallback — the quota has rolled over, so drop it. Undated windows (the
524
- // resource pool, or a plan row without a readable cycle end) cannot be
525
- // checked against a reset and are bounded by the snapshot-age TTL instead.
580
+ // resource pool without a readable expiring_at, or a plan row without a
581
+ // readable cycle end) cannot be checked against a reset and are bounded by
582
+ // the snapshot-age TTL instead.
526
583
  function workbuddyCachedWindow(window, { cachedAtMs, nowMs } = {}) {
527
584
  if (!window || typeof window !== "object") return null;
528
585
  const resetAtMs = Date.parse(window.reset_at || "");
529
586
  if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
587
+ // The resource pool has no reset but may carry its earliest grant expiry.
588
+ // expiring_first_at is the first moment the advertised credits start to
589
+ // lapse — from then on the pool's numbers no longer describe the account,
590
+ // so the snapshot is trusted exactly until then; expiring_at (the day's
591
+ // end) is only the display anchor. The same discipline as a reset date,
592
+ // not the undated TTL.
593
+ const expiringAtMs = Date.parse(window.expiring_first_at || "");
594
+ const anchorMs = Number.isFinite(expiringAtMs) ? expiringAtMs : Date.parse(window.expiring_at || "");
595
+ if (Number.isFinite(anchorMs)) return anchorMs > nowMs ? window : null;
530
596
  return nowMs - cachedAtMs <= WORKBUDDY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
531
597
  }
532
598
 
@@ -656,18 +722,27 @@ async function postBilling(fetchImpl, url, token, body = {}, signal) {
656
722
  return response.json();
657
723
  }
658
724
 
659
- async function fetchPlanQuota(post, variantId) {
725
+ async function fetchPlanQuota(post, variantId, nowMs) {
660
726
  const prefix = "/billing/meter/";
661
727
  const summary = await post(`${prefix}get-user-resource-summary`, {});
662
728
  if (!unwrapBillingBody(summary)) throw new Error("WorkBuddy quota request was rejected.");
663
729
  const identity = readSubscriptionIdentity(summary);
730
+ const summaryRows = collectPackageRows(summary);
664
731
  const planCodes = [...new Set([...Object.keys(PLAN_PRIORITY),
665
732
  ...(identity.code && !isRewardCode(identity.code) ? [identity.code] : [])])];
733
+ // Resource grants ride along in the same pages query. The summary carries
734
+ // their aggregated pool row without a cycle end, so the per-grant rows —
735
+ // where each grant's own CycleEndTime lives — only come back when their
736
+ // package codes are requested; a plan-code-only listing never sees them.
737
+ const resourceCodes = summaryRows
738
+ .filter((row) => !isSelectablePlan(row, identity.code))
739
+ .map((row) => row.code);
740
+ const packageCodes = [...new Set([...planCodes, ...resourceCodes])];
666
741
  const packageRows = await Promise.all(["paid", "free"].map(async (kind) => {
667
742
  const rows = [];
668
743
  for (let page = 1; page <= 10; page++) {
669
744
  const payload = await post(`${prefix}get-user-resource-${kind}-packages`, {
670
- PageNumber: page, PageSize: 100, PackageCodes: planCodes,
745
+ PageNumber: page, PageSize: 100, PackageCodes: packageCodes,
671
746
  Status: [0, 3], ...(kind === "paid" ? { NeedRenewInfo: true } : {}),
672
747
  });
673
748
  const data = unwrapBillingBody(payload);
@@ -678,14 +753,28 @@ async function fetchPlanQuota(post, variantId) {
678
753
  }
679
754
  throw new Error("WorkBuddy quota package list was incomplete.");
680
755
  }));
681
- const quota = selectWorkbuddyPlanQuota([
682
- ...collectPackageRows(summary), ...packageRows.flat(),
683
- ], identity, variantId);
756
+ const quota = selectWorkbuddyPlanQuota([...summaryRows, ...packageRows.flat()], identity, variantId);
757
+ const secondary = resourceWindow(collectResourceGroups(summary));
758
+ if (secondary) {
759
+ // The pooled bar cannot say which credits lapse first; the grant rows
760
+ // carry each grant's cycle end. The summary's own pool row has no cycle
761
+ // end and skips the expiry bucket, so grants are not double-counted. A
762
+ // pool without a window is dropped wholesale, so expiry has nothing to
763
+ // attach to.
764
+ const expiring = collectExpiringResourceCredits(
765
+ [...summaryRows, ...packageRows.flat()], identity, nowMs,
766
+ );
767
+ if (expiring) {
768
+ secondary.expiring_at = expiring.at;
769
+ secondary.expiring_first_at = expiring.firstAt;
770
+ secondary.expiring_credits = expiring.credits;
771
+ }
772
+ }
684
773
  return {
685
774
  configured: true, error: null,
686
775
  plan_label: quota?.plan_label || englishPlanLabel({ code: identity.code }, identity, variantId),
687
776
  primary_window: quota?.primary_window || null,
688
- secondary_window: resourceWindow(collectResourceGroups(summary)),
777
+ secondary_window: secondary,
689
778
  };
690
779
  }
691
780
 
@@ -724,7 +813,8 @@ async function fetchVariantLive(variant, variantId, { env, workbuddyHome, instal
724
813
  discovery, signal: opts.signal, timeoutMs: opts.timeoutMs, connectImpl: opts.connectImpl,
725
814
  });
726
815
  try {
727
- return { ...await fetchPlanQuota((apiPath, body) => client.postBilling(apiPath, body), variantId), source: "ipc" };
816
+ return { ...await fetchPlanQuota((apiPath, body) => client.postBilling(apiPath, body), variantId,
817
+ Number.isFinite(opts.nowMs) ? opts.nowMs : undefined), source: "ipc" };
728
818
  } finally {
729
819
  client.close();
730
820
  }
@@ -748,7 +838,8 @@ async function fetchVariantLive(variant, variantId, { env, workbuddyHome, instal
748
838
  throw error;
749
839
  }
750
840
  return { ...await fetchPlanQuota((apiPath, body) => postBilling(fetchImpl,
751
- `${origin}${apiPath}`, session.token, body, opts.signal), variantId), source: "api" };
841
+ `${origin}${apiPath}`, session.token, body, opts.signal), variantId,
842
+ Number.isFinite(opts.nowMs) ? opts.nowMs : undefined), source: "api" };
752
843
  }
753
844
 
754
845
  async function fetchVariant(variantId, opts = {}) {
@@ -817,6 +908,7 @@ module.exports = {
817
908
  selectWorkbuddyLimits,
818
909
  collectPackageRows,
819
910
  collectResourceGroups,
911
+ collectExpiringResourceCredits,
820
912
  resourceWindow,
821
913
  readSubscriptionIdentity,
822
914
  originAllowed,