quotacap 0.0.50 → 0.0.52

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 (69) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/agy.js +3 -0
  3. package/dist/adapters/claude.js +3 -0
  4. package/dist/adapters/codex.js +4 -2
  5. package/dist/adapters/kimi.js +14 -0
  6. package/dist/adapters/manual.js +4 -0
  7. package/dist/adapters/muse.js +19 -5
  8. package/dist/adapters/opencode-go.d.ts +12 -2
  9. package/dist/adapters/opencode-go.js +156 -31
  10. package/dist/adapters/types.d.ts +2 -0
  11. package/dist/advisory/snapshot.d.ts +1 -0
  12. package/dist/advisory/snapshot.js +7 -1
  13. package/dist/advisory/types.d.ts +3 -0
  14. package/dist/cli/models.js +5 -4
  15. package/dist/format/rows.d.ts +30 -1
  16. package/dist/format/rows.js +109 -24
  17. package/dist/format/terminal.js +5 -3
  18. package/dist/http/server.d.ts +3 -0
  19. package/dist/http/server.js +7 -3
  20. package/dist/runtime/service.js +14 -3
  21. package/dist/service/macos.d.ts +5 -0
  22. package/dist/service/macos.js +6 -2
  23. package/dist/store/db.js +2 -1
  24. package/dist/store/quotas.d.ts +1 -1
  25. package/dist/store/quotas.js +65 -2
  26. package/dist/version.js +1 -1
  27. package/dist/web/src/App.js +10 -4
  28. package/dist/web/src/components/AdviceDrawer.d.ts +2 -1
  29. package/dist/web/src/components/AdviceDrawer.js +27 -16
  30. package/dist/web/src/components/BurnDown.d.ts +37 -0
  31. package/dist/web/src/components/BurnDown.js +182 -0
  32. package/dist/web/src/components/ClosedWeeks.d.ts +6 -5
  33. package/dist/web/src/components/ClosedWeeks.js +25 -37
  34. package/dist/web/src/components/CopyButton.d.ts +4 -0
  35. package/dist/web/src/components/CopyButton.js +27 -0
  36. package/dist/web/src/components/ErrorBoundary.d.ts +37 -0
  37. package/dist/web/src/components/ErrorBoundary.js +32 -0
  38. package/dist/web/src/components/FaultBanner.d.ts +3 -2
  39. package/dist/web/src/components/FaultBanner.js +7 -4
  40. package/dist/web/src/components/OpencodeGoBanner.d.ts +3 -1
  41. package/dist/web/src/components/OpencodeGoBanner.js +16 -10
  42. package/dist/web/src/components/PaceBar.d.ts +80 -3
  43. package/dist/web/src/components/PaceBar.js +225 -34
  44. package/dist/web/src/components/PacingLegend.js +1 -1
  45. package/dist/web/src/components/ProviderCard.js +6 -9
  46. package/dist/web/src/components/ProviderDrawer.d.ts +56 -10
  47. package/dist/web/src/components/ProviderDrawer.js +263 -138
  48. package/dist/web/src/components/ProviderRow.js +3 -3
  49. package/dist/web/src/components/Recommendation.js +11 -7
  50. package/dist/web/src/components/ResetRail.js +13 -9
  51. package/dist/web/src/components/SubscriptionList.js +11 -4
  52. package/dist/web/src/components/WindowsRow.d.ts +13 -3
  53. package/dist/web/src/components/WindowsRow.js +27 -5
  54. package/dist/web/src/components/settings/CliIntegrationPanel.js +7 -45
  55. package/dist/web/src/components/settings/DaemonPanel.js +1 -28
  56. package/dist/web/src/components/settings/ProvidersPanel.d.ts +2 -2
  57. package/dist/web/src/components/settings/ProvidersPanel.js +10 -24
  58. package/dist/web/src/components/settings/SettingsDrawer.d.ts +1 -2
  59. package/dist/web/src/components/settings/SettingsDrawer.js +2 -2
  60. package/dist/web/src/state.d.ts +6 -1
  61. package/dist/web/src/state.js +19 -10
  62. package/dist/webAssets.js +1 -1
  63. package/dist/webHtml.js +1 -1
  64. package/package.json +1 -1
  65. package/web/dist/assets/index-Co33DTl5.css +1 -0
  66. package/web/dist/assets/index-DqK6QVaw.js +13 -0
  67. package/web/dist/index.html +2 -2
  68. package/web/dist/assets/index-BSQW0C_t.css +0 -1
  69. package/web/dist/assets/index-Dq9ZvTUe.js +0 -13
@@ -1,13 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from "react";
3
- export const OPENCODE_GO_CONSENT_COPY = [
4
- "• What is read: API key OpenCode already stored when you ran `opencode auth login -p opencode-go` (`~/.local/share/opencode/auth.json`, `opencode-go` → `opencode` fallback).",
5
- "• Read frequency: in-memory, read-only once per poll (every 15 minutes by default).",
6
- "• Request destination: sent only as `Authorization: Bearer` to `https://opencode.ai/zen/go/v1/usage` to fetch `rolling/weekly/monthly` `percent` + `resetsAt`.",
7
- "• Never-list: never used to make chat or model requests, never written or rotated, never stored in the `quotacap` DB, never returned by the API or MCP, and never logged (errors show `[redacted]`).",
8
- "• Revocation instructions: revoke at `https://opencode.ai/auth` or `opencode providers logout opencode-go` and run `quotacap providers disable opencode-go` to stop polling.",
9
- "• Optional environment override: if you prefer no file access, set `OPENCODE_API_KEY` instead.",
10
- ].join("\n");
3
+ export function openCodeGoConsentCopy(loginCommand) {
4
+ const loginClause = loginCommand
5
+ ? `when you ran \`${loginCommand}\``
6
+ : "with the command for your CLI version: v2 `opencode auth login opencode` or v1 `opencode console login`";
7
+ return [
8
+ `• What is read: Bearer [REDACTED] already stored ${loginClause} (v2 \`~/.local/share/opencode/opencode.db\` active credential, else legacy \`~/.local/share/opencode/auth.json\`; \`opencode-go\` then \`opencode\`).`,
9
+ "• Read frequency: in-memory, read-only once per poll (every 15 minutes by default).",
10
+ "• Request destination: sent only as `Authorization: Bearer` to `https://opencode.ai/zen/go/v1/usage` to fetch `rolling/weekly/monthly` `percent` + `resetsAt`.",
11
+ "• Never-list: never used to make chat or model requests, never written or rotated, never stored in the `quotacap` DB, never returned by the API or MCP, and never logged (errors show `[redacted]`).",
12
+ "• Revocation instructions: revoke at `https://opencode.ai/auth` or `opencode providers logout opencode-go` and run `quotacap providers disable opencode-go` to stop polling.",
13
+ "• Optional environment override: if you prefer no file access, set `OPENCODE_API_KEY` instead.",
14
+ ].join("\n");
15
+ }
16
+ export const OPENCODE_GO_CONSENT_COPY = openCodeGoConsentCopy();
11
17
  export function showOpenCodeGoBanner(runtime, providers, suppressed) {
12
18
  const detected = runtime?.detectedProviders?.includes("opencode-go") ?? false;
13
19
  const row = providers.find((p) => p.id === "opencode-go");
@@ -16,7 +22,7 @@ export function showOpenCodeGoBanner(runtime, providers, suppressed) {
16
22
  export function OpencodeGoBanner({ onEnable }) {
17
23
  return (_jsxs("div", { "data-testid": "opencode-go-banner", role: "status", className: "alert", children: [_jsxs("span", { children: [_jsx("strong", { children: "OpenCode Go detected." }), " Enable to track 5h/weekly usage? The API key stays with OpenCode until you enable tracking."] }), _jsx("button", { type: "button", className: "linkbtn", onClick: onEnable, style: { flex: "none", marginLeft: "auto" }, children: "Enable OpenCode Go" })] }));
18
24
  }
19
- export function ConsentModal({ onConfirm, onCancel, busy, }) {
25
+ export function ConsentModal({ onConfirm, onCancel, busy, loginCommand, }) {
20
26
  useEffect(() => {
21
27
  const onKey = (e) => {
22
28
  if (e.key === "Escape")
@@ -39,5 +45,5 @@ export function ConsentModal({ onConfirm, onCancel, busy, }) {
39
45
  padding: "var(--s4) var(--s5)",
40
46
  maxWidth: 560,
41
47
  margin: "var(--s4)",
42
- }, children: [_jsx("h2", { id: "opencode-go-consent-title", style: { fontSize: "var(--t-4)", margin: "0 0 var(--s3)" }, children: "Enable OpenCode Go?" }), _jsx("p", { style: { fontSize: "var(--t-2)", color: "var(--ink-soft)", margin: "0 0 var(--s4)", whiteSpace: "pre-line" }, children: OPENCODE_GO_CONSENT_COPY }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: "var(--s2)" }, children: [_jsx("button", { type: "button", className: "btn btn-quiet", onClick: onCancel, disabled: busy, children: "Cancel" }), _jsx("button", { type: "button", className: "btn btn-primary", onClick: onConfirm, disabled: busy, children: busy ? "Enabling…" : "Enable" })] })] }) }));
48
+ }, children: [_jsx("h2", { id: "opencode-go-consent-title", style: { fontSize: "var(--t-4)", margin: "0 0 var(--s3)" }, children: "Enable OpenCode Go?" }), _jsx("p", { style: { fontSize: "var(--t-2)", color: "var(--ink-soft)", margin: "0 0 var(--s4)", whiteSpace: "pre-line" }, children: openCodeGoConsentCopy(loginCommand) }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: "var(--s2)" }, children: [_jsx("button", { type: "button", className: "btn btn-quiet", onClick: onCancel, disabled: busy, children: "Cancel" }), _jsx("button", { type: "button", className: "btn btn-primary", onClick: onConfirm, disabled: busy, children: busy ? "Enabling…" : "Enable" })] })] }) }));
43
49
  }
@@ -8,6 +8,47 @@ export type PaceBadge = "Not reporting" | "Cap risk" | "Watch" | "Behind pace" |
8
8
  * otherwise urgency decides. No client thresholds.
9
9
  */
10
10
  export declare function paceBadge(provider: ProviderView): PaceBadge;
11
+ /**
12
+ * Quota-window resolution. The advisory `bindingWindow` names the window
13
+ * that actually rations the account (weekly for most providers, monthly
14
+ * for OpenCode Go / Kimi when the month is scarcer). Every standalone
15
+ * reset, pace figure, and main bar reads this window; the other windows
16
+ * stay visible only inside Limits / the windows row as rate-limit detail.
17
+ * Weekly-bound providers are untouched: every helper falls back to the
18
+ * weekly fields.
19
+ */
20
+ export declare function isMonthlyBound(provider: ProviderView): boolean;
21
+ /** Reset instant of the quota window, or null without a quota reading. */
22
+ export declare function quotaResetsAt(provider: ProviderView): string | null;
23
+ /** Used percent of the quota window. Null without a quota reading. */
24
+ export declare function quotaUsedPct(provider: ProviderView): number | null;
25
+ /** Display name of the quota window ("Monthly" / "Weekly"). */
26
+ export declare function quotaWindowName(provider: ProviderView): "Monthly" | "Weekly";
27
+ /**
28
+ * Start of the monthly window. Providers report only the monthly reset,
29
+ * so the start is one calendar month before it (same inference the
30
+ * drawer Limits bars use).
31
+ */
32
+ export declare function monthlyWindowStartIso(resetsAt: string): string | null;
33
+ /** Start instant of the quota window. Null when unusable. */
34
+ export declare function quotaWindowStartIso(provider: ProviderView): string | null;
35
+ /** Elapsed share (0..100) of the quota window. Null when either stamp is unusable. */
36
+ export declare function quotaElapsedPct(provider: ProviderView, asOfMs: number): number | null;
37
+ /** Even pace that lands on 100% at the quota reset: binding remaining ÷ binding days left. */
38
+ export declare function quotaIdealRate(advisory: {
39
+ idealRate: number;
40
+ bindingWindow?: string;
41
+ bindingRemaining?: number;
42
+ bindingDaysLeft?: number;
43
+ }): number;
44
+ /**
45
+ * Window-average pace of the quota window. Weekly reads the server
46
+ * `avgPace`; monthly divides the monthly used percent by the elapsed
47
+ * monthly days. Null under a 6h span, mirroring the server minimum.
48
+ */
49
+ export declare function quotaAvgPace(provider: ProviderView, asOfMs: number): number | null;
50
+ /** Estimated-reset flag for the quota window; monthly resets are reported, never estimated. */
51
+ export declare function quotaResetEstimated(provider: ProviderView): boolean;
11
52
  export interface PaceFigures {
12
53
  /** Window average: used % ÷ days elapsed. Null before the window start is known. */
13
54
  avg: number | null;
@@ -37,6 +78,16 @@ export declare function paceCells(advisory: {
37
78
  recentRate?: number | null;
38
79
  paceSource: string;
39
80
  } | null | undefined): PaceCells;
81
+ /**
82
+ * Four-cell pace grid shared by the provider card and the drawer Pace
83
+ * section, so both surfaces present the same figures the same way:
84
+ * average pace, 24h pace, ideal pace, and time until reset. Unknown
85
+ * figures read "—" rather than disappearing.
86
+ */
87
+ export declare function PaceStats({ provider, asOf }: {
88
+ provider: ProviderView;
89
+ asOf: string;
90
+ }): React.JSX.Element;
40
91
  /** Hover explanations shared by cards and rows. Plain words, no jargon. */
41
92
  export declare const PACE_TIPS: {
42
93
  readonly avg: "Window average — quota used ÷ days elapsed. Your overall pace this window.";
@@ -57,7 +108,10 @@ export declare function paceLines(advisory: {
57
108
  recentRate?: number | null;
58
109
  paceSource: string;
59
110
  idealRate: number;
60
- } | null | undefined): PaceLine[];
111
+ bindingWindow?: string;
112
+ bindingRemaining?: number;
113
+ bindingDaysLeft?: number;
114
+ } | null | undefined, provider?: ProviderView, asOfMs?: number): PaceLine[];
61
115
  /** Plain-words reason shown beside "Not reporting". Null when the badge says it all. */
62
116
  export declare function exclusionDetail(reason: ExclusionReason): string | null;
63
117
  export declare function isEstimated(provider: ProviderView): boolean;
@@ -74,6 +128,14 @@ export declare function resetDateClock(resetsAt: string, locale?: string): strin
74
128
  export declare function resetDay(iso: string | undefined): string;
75
129
  /** "6d 12h" style time left from server timestamps. Null when unparseable. */
76
130
  export declare function timeLeft(resetsAt: string, asOfMs: number): string | null;
131
+ /**
132
+ * Days-to-duration formatter mirroring timeLeft: under 1h `Xm`, under 48h
133
+ * `Xh Ym`, at 48h+ `Xd Xh`. Hours in the days branch round to the nearest
134
+ * hour so 2.4d reads "2d 10h".
135
+ */
136
+ export declare function daysToDuration(days: number): string;
137
+ export declare const formatDurationDays: typeof daysToDuration;
138
+ export declare const durationFromDays: typeof daysToDuration;
77
139
  /**
78
140
  * Reset rendering for a row. Estimated resets carry "(est.)"; reset-passed
79
141
  * rows await a fresh window; invalid rows carry a row-level note.
@@ -100,7 +162,7 @@ export interface BarGeometry {
100
162
  usedPct: number;
101
163
  elapsedPct: number | null;
102
164
  }
103
- /** Bar geometry from server timestamps and percents. Presentation only. */
165
+ /** Bar geometry of the quota window from server timestamps and percents. Presentation only. */
104
166
  export declare function barGeometry(provider: ProviderView, asOfMs: number): BarGeometry;
105
167
  export declare function badgeColor(badge: PaceBadge): string;
106
168
  export declare function fillToken(badge: PaceBadge): string;
@@ -116,9 +178,24 @@ export declare function hatchGradient(_badge?: PaceBadge): string;
116
178
  * confessed on the end. Other badges read their own label and need no title.
117
179
  */
118
180
  export declare function badgeTitle(badge: PaceBadge, estimated: boolean): string | undefined;
119
- export declare function Badge({ provider }: {
181
+ export declare function Badge({ provider, compact, }: {
120
182
  provider: ProviderView;
183
+ /** Compact drops the exclusion detail from the label into the tooltip. */
184
+ compact?: boolean;
121
185
  }): React.JSX.Element;
186
+ export type PaceHeadlineKind = "behind" | "watch" | "cap" | "ontrack" | "none";
187
+ export interface PaceHeadline {
188
+ text: string;
189
+ kind: PaceHeadlineKind;
190
+ /** True when the projected-unused hatch belongs on the bar. */
191
+ hatch: boolean;
192
+ }
193
+ /**
194
+ * Headline forecast shared by the card bar and the drawer quota row:
195
+ * "expires unused" wording when a waste hatch applies, otherwise the
196
+ * one-line forecast. Null when there is nothing to forecast.
197
+ */
198
+ export declare function paceHeadline(provider: ProviderView): PaceHeadline | null;
122
199
  /**
123
200
  * Two-tier pacing rail: used fill, elapsed tick, projected-unused hatch.
124
201
  * Unknown pace shows fill and tick but no hatch and no numeric rate.
@@ -33,6 +33,123 @@ export function paceBadge(provider) {
33
33
  return "On track";
34
34
  }
35
35
  }
36
+ /**
37
+ * Quota-window resolution. The advisory `bindingWindow` names the window
38
+ * that actually rations the account (weekly for most providers, monthly
39
+ * for OpenCode Go / Kimi when the month is scarcer). Every standalone
40
+ * reset, pace figure, and main bar reads this window; the other windows
41
+ * stay visible only inside Limits / the windows row as rate-limit detail.
42
+ * Weekly-bound providers are untouched: every helper falls back to the
43
+ * weekly fields.
44
+ */
45
+ export function isMonthlyBound(provider) {
46
+ return (provider.advisory?.bindingWindow === "monthly" &&
47
+ !!provider.quota?.monthlyResetsAt &&
48
+ typeof provider.quota?.monthlyPct === "number");
49
+ }
50
+ /** Reset instant of the quota window, or null without a quota reading. */
51
+ export function quotaResetsAt(provider) {
52
+ if (!provider.quota)
53
+ return null;
54
+ if (isMonthlyBound(provider))
55
+ return provider.quota.monthlyResetsAt ?? null;
56
+ return provider.quota.resetsAt ?? null;
57
+ }
58
+ /** Used percent of the quota window. Null without a quota reading. */
59
+ export function quotaUsedPct(provider) {
60
+ if (!provider.quota)
61
+ return null;
62
+ if (isMonthlyBound(provider))
63
+ return provider.quota.monthlyPct ?? null;
64
+ return provider.quota.weeklyPct ?? null;
65
+ }
66
+ /** Display name of the quota window ("Monthly" / "Weekly"). */
67
+ export function quotaWindowName(provider) {
68
+ return isMonthlyBound(provider) ? "Monthly" : "Weekly";
69
+ }
70
+ /**
71
+ * Start of the monthly window. Providers report only the monthly reset,
72
+ * so the start is one calendar month before it (same inference the
73
+ * drawer Limits bars use).
74
+ */
75
+ export function monthlyWindowStartIso(resetsAt) {
76
+ const resetMs = Date.parse(resetsAt);
77
+ if (!Number.isFinite(resetMs))
78
+ return null;
79
+ const start = new Date(resetMs);
80
+ start.setMonth(start.getMonth() - 1);
81
+ return start.toISOString();
82
+ }
83
+ /** Start instant of the quota window. Null when unusable. */
84
+ export function quotaWindowStartIso(provider) {
85
+ if (!provider.quota)
86
+ return null;
87
+ if (isMonthlyBound(provider)) {
88
+ return provider.quota.monthlyResetsAt
89
+ ? monthlyWindowStartIso(provider.quota.monthlyResetsAt)
90
+ : null;
91
+ }
92
+ return provider.quota.periodStart ?? null;
93
+ }
94
+ /** Elapsed share (0..100) of the quota window. Null when either stamp is unusable. */
95
+ export function quotaElapsedPct(provider, asOfMs) {
96
+ const startIso = quotaWindowStartIso(provider);
97
+ const resetIso = quotaResetsAt(provider);
98
+ if (!startIso || !resetIso)
99
+ return null;
100
+ const startMs = Date.parse(startIso);
101
+ const resetMs = Date.parse(resetIso);
102
+ if (!Number.isFinite(startMs) || !Number.isFinite(resetMs) || resetMs <= startMs)
103
+ return null;
104
+ if (!Number.isFinite(asOfMs))
105
+ return null;
106
+ return Math.min(100, Math.max(0, ((asOfMs - startMs) / (resetMs - startMs)) * 100));
107
+ }
108
+ /** Even pace that lands on 100% at the quota reset: binding remaining ÷ binding days left. */
109
+ export function quotaIdealRate(advisory) {
110
+ if (advisory.bindingWindow === "monthly" &&
111
+ typeof advisory.bindingRemaining === "number" &&
112
+ typeof advisory.bindingDaysLeft === "number" &&
113
+ advisory.bindingDaysLeft > 0) {
114
+ return advisory.bindingRemaining / advisory.bindingDaysLeft;
115
+ }
116
+ return advisory.idealRate;
117
+ }
118
+ /**
119
+ * Window-average pace of the quota window. Weekly reads the server
120
+ * `avgPace`; monthly divides the monthly used percent by the elapsed
121
+ * monthly days. Null under a 6h span, mirroring the server minimum.
122
+ */
123
+ export function quotaAvgPace(provider, asOfMs) {
124
+ if (!provider.quota || !provider.advisory)
125
+ return null;
126
+ if (!isMonthlyBound(provider))
127
+ return provider.advisory.avgPace ?? null;
128
+ const used = provider.quota.monthlyPct;
129
+ if (typeof used !== "number" || !Number.isFinite(used))
130
+ return null;
131
+ const startIso = quotaWindowStartIso(provider);
132
+ if (!startIso)
133
+ return null;
134
+ const startMs = Date.parse(startIso);
135
+ if (!Number.isFinite(startMs) || !Number.isFinite(asOfMs))
136
+ return null;
137
+ const elapsedDays = (asOfMs - startMs) / 86_400_000;
138
+ if (elapsedDays < 0.25)
139
+ return null;
140
+ const pace = used / elapsedDays;
141
+ return Number.isFinite(pace) ? Math.max(0, pace) : null;
142
+ }
143
+ /** Estimated-reset flag for the quota window; monthly resets are reported, never estimated. */
144
+ export function quotaResetEstimated(provider) {
145
+ if (isMonthlyBound(provider))
146
+ return false;
147
+ if (provider.quota?.resetsAtEstimated === true)
148
+ return true;
149
+ return Array.isArray(provider.evidence)
150
+ ? provider.evidence.includes("estimated-reset")
151
+ : false;
152
+ }
36
153
  /** Split the advisory into its two displayed paces. The 24h figure is the
37
154
  * measured `recentRate`, not the blended `burnRate` forecast input; an
38
155
  * advisory from a daemon older than the blend falls back to `burnRate`,
@@ -58,6 +175,26 @@ export function paceCells(advisory) {
58
175
  recent: recent !== null ? `${recent.toFixed(1)}%/day` : "—",
59
176
  };
60
177
  }
178
+ /**
179
+ * Four-cell pace grid shared by the provider card and the drawer Pace
180
+ * section, so both surfaces present the same figures the same way:
181
+ * average pace, 24h pace, ideal pace, and time until reset. Unknown
182
+ * figures read "—" rather than disappearing.
183
+ */
184
+ export function PaceStats({ provider, asOf }) {
185
+ const asOfMs = Date.parse(asOf);
186
+ const advisory = provider.advisory;
187
+ const monthly = isMonthlyBound(provider);
188
+ // Pace follows the quota window: average and ideal are quota figures,
189
+ // and the 24h rate exists only for the weekly window (monthly history
190
+ // is not tracked), so a monthly-bound grid reads "—" there.
191
+ const avg = quotaAvgPace(provider, asOfMs);
192
+ const pace = paceCells(provider.advisory);
193
+ const ideal = advisory ? `${quotaIdealRate(advisory).toFixed(1)}%/day` : "—";
194
+ const resetIso = quotaResetsAt(provider);
195
+ const resetsIn = resetIso ? timeLeft(resetIso, asOfMs) : null;
196
+ return (_jsxs("div", { className: "pstats", children: [_jsxs("div", { title: PACE_TIPS.avg, children: [_jsx("span", { className: "l", children: "Avg pace" }), _jsx("span", { className: "v", children: avg !== null ? `${avg.toFixed(1)}%/day` : "—" })] }), _jsxs("div", { title: PACE_TIPS.recent, children: [_jsx("span", { className: "l", children: "24h pace" }), _jsx("span", { className: "v", children: monthly ? "—" : pace.recent })] }), _jsxs("div", { title: PACE_TIPS.ideal, children: [_jsx("span", { className: "l", children: "Ideal pace" }), _jsx("span", { className: "v", children: ideal })] }), _jsxs("div", { children: [_jsx("span", { className: "l", children: "Resets in" }), _jsx("span", { className: "v", children: resetsIn ? `${resetsIn}${quotaResetEstimated(provider) ? " (est.)" : ""}` : resetCountdown(provider, asOfMs) })] })] }));
197
+ }
61
198
  /** Hover explanations shared by cards and rows. Plain words, no jargon. */
62
199
  export const PACE_TIPS = {
63
200
  avg: "Window average — quota used ÷ days elapsed. Your overall pace this window.",
@@ -67,16 +204,21 @@ export const PACE_TIPS = {
67
204
  /** Table-row pace stack: one labeled line per known pace plus the needed
68
205
  * rate. Both paces always show when known (no collapsing) so rows read
69
206
  * uniformly. Empty when no advisory is present. */
70
- export function paceLines(advisory) {
207
+ export function paceLines(advisory, provider, asOfMs) {
71
208
  if (!advisory)
72
209
  return [];
73
- const { avg, recent } = paceFigures(advisory);
210
+ // Quota-window pace: a monthly-bound row shows the monthly average and
211
+ // ideal; the 24h rate is weekly-only and drops out.
212
+ const monthly = provider ? isMonthlyBound(provider) : advisory.bindingWindow === "monthly";
213
+ const avg = provider && asOfMs !== undefined ? quotaAvgPace(provider, asOfMs) : (paceFigures(advisory).avg);
214
+ const { recent } = paceFigures(advisory);
215
+ const ideal = quotaIdealRate(advisory) ?? advisory.idealRate;
74
216
  const lines = [];
75
217
  if (avg !== null)
76
218
  lines.push({ label: "Avg", value: `${avg.toFixed(1)}%/day`, tip: PACE_TIPS.avg });
77
- if (recent !== null)
219
+ if (!monthly && recent !== null)
78
220
  lines.push({ label: "24h", value: `${recent.toFixed(1)}%/day`, tip: PACE_TIPS.recent });
79
- lines.push({ label: "Ideal", value: `${advisory.idealRate.toFixed(1)}%/day`, tip: PACE_TIPS.ideal });
221
+ lines.push({ label: "Ideal", value: `${ideal.toFixed(1)}%/day`, tip: PACE_TIPS.ideal });
80
222
  return lines;
81
223
  }
82
224
  /** Plain-words reason shown beside "Not reporting". Null when the badge says it all. */
@@ -155,6 +297,33 @@ export function timeLeft(resetsAt, asOfMs) {
155
297
  return `${hours}h ${minutes}m`;
156
298
  return `${Math.max(1, minutes)}m`;
157
299
  }
300
+ /**
301
+ * Days-to-duration formatter mirroring timeLeft: under 1h `Xm`, under 48h
302
+ * `Xh Ym`, at 48h+ `Xd Xh`. Hours in the days branch round to the nearest
303
+ * hour so 2.4d reads "2d 10h".
304
+ */
305
+ export function daysToDuration(days) {
306
+ if (!Number.isFinite(days) || days <= 0)
307
+ return "0m";
308
+ const totalMinutes = Math.round(days * 24 * 60);
309
+ if (totalMinutes < 60)
310
+ return `${Math.max(1, totalMinutes)}m`;
311
+ if (totalMinutes < 48 * 60) {
312
+ const hours = Math.floor(totalMinutes / 60);
313
+ const minutes = totalMinutes % 60;
314
+ return `${hours}h ${minutes}m`;
315
+ }
316
+ let d = Math.floor(totalMinutes / (24 * 60));
317
+ const remainderMinutes = totalMinutes - d * 24 * 60;
318
+ let h = Math.round(remainderMinutes / 60);
319
+ if (h === 24) {
320
+ d += 1;
321
+ h = 0;
322
+ }
323
+ return `${d}d ${h}h`;
324
+ }
325
+ export const formatDurationDays = daysToDuration;
326
+ export const durationFromDays = daysToDuration;
158
327
  /**
159
328
  * Reset rendering for a row. Estimated resets carry "(est.)"; reset-passed
160
329
  * rows await a fresh window; invalid rows carry a row-level note.
@@ -164,14 +333,17 @@ export function resetCountdown(provider, asOfMs) {
164
333
  return "awaiting fresh window";
165
334
  if (!provider.quota)
166
335
  return "no readings";
167
- const left = timeLeft(provider.quota.resetsAt, asOfMs);
336
+ const resetIso = quotaResetsAt(provider);
337
+ if (!resetIso)
338
+ return "no readings";
339
+ const left = timeLeft(resetIso, asOfMs);
168
340
  if (left === null) {
169
- const resetMs = Date.parse(provider.quota.resetsAt);
341
+ const resetMs = Date.parse(resetIso);
170
342
  if (Number.isFinite(resetMs) && resetMs <= asOfMs)
171
343
  return "awaiting fresh window";
172
344
  return "reset unknown";
173
345
  }
174
- const estimated = isEstimated(provider) ? " (est.)" : "";
346
+ const estimated = quotaResetEstimated(provider) ? " (est.)" : "";
175
347
  return `in ${left}${estimated}`;
176
348
  }
177
349
  /**
@@ -184,13 +356,18 @@ export function resetCellLines(provider, asOfMs) {
184
356
  return { top: "awaiting fresh window", bottom: null };
185
357
  if (!provider.quota)
186
358
  return { top: "no readings", bottom: null };
187
- const clock = resetClock(provider.quota.resetsAt);
188
- const left = timeLeft(provider.quota.resetsAt, asOfMs);
189
- const estimated = isEstimated(provider) ? " (est.)" : "";
359
+ const resetIso = quotaResetsAt(provider);
360
+ if (!resetIso)
361
+ return { top: "no readings", bottom: null };
362
+ // A monthly quota reset sits weeks out: it carries the calendar date,
363
+ // where the weekly clock's weekday alone would read as this week.
364
+ const clock = isMonthlyBound(provider) ? resetDateClock(resetIso) : resetClock(resetIso);
365
+ const left = timeLeft(resetIso, asOfMs);
366
+ const estimated = quotaResetEstimated(provider) ? " (est.)" : "";
190
367
  if (clock && left)
191
368
  return { top: `${clock}${estimated}`, bottom: `in ${left}` };
192
369
  if (left === null) {
193
- const resetMs = Date.parse(provider.quota.resetsAt);
370
+ const resetMs = Date.parse(resetIso);
194
371
  if (Number.isFinite(resetMs) && resetMs <= asOfMs)
195
372
  return { top: "awaiting fresh window", bottom: null };
196
373
  return { top: "reset unknown", bottom: null };
@@ -205,17 +382,11 @@ export function resetCell(provider, asOfMs) {
205
382
  const { top, bottom } = resetCellLines(provider, asOfMs);
206
383
  return bottom ? `${top} · ${bottom}` : top;
207
384
  }
208
- /** Bar geometry from server timestamps and percents. Presentation only. */
385
+ /** Bar geometry of the quota window from server timestamps and percents. Presentation only. */
209
386
  export function barGeometry(provider, asOfMs) {
210
- const usedPct = provider.quota ? Math.min(100, Math.max(0, provider.quota.weeklyPct)) : 0;
211
- let elapsedPct = null;
212
- if (provider.quota) {
213
- const startMs = Date.parse(provider.quota.periodStart);
214
- const resetMs = Date.parse(provider.quota.resetsAt);
215
- if (Number.isFinite(startMs) && Number.isFinite(resetMs) && resetMs > startMs) {
216
- elapsedPct = Math.min(1, Math.max(0, (asOfMs - startMs) / (resetMs - startMs))) * 100;
217
- }
218
- }
387
+ const usedRaw = quotaUsedPct(provider);
388
+ const usedPct = usedRaw !== null ? Math.min(100, Math.max(0, usedRaw)) : 0;
389
+ const elapsedPct = quotaElapsedPct(provider, asOfMs);
219
390
  return { usedPct, elapsedPct };
220
391
  }
221
392
  export function badgeColor(badge) {
@@ -313,7 +484,7 @@ export function badgeTitle(badge, estimated) {
313
484
  title += " (estimated reset)";
314
485
  return title;
315
486
  }
316
- export function Badge({ provider }) {
487
+ export function Badge({ provider, compact = false, }) {
317
488
  const badge = paceBadge(provider);
318
489
  const detail = exclusionDetail(provider.exclusionReason);
319
490
  const badgeClass = badge === "Behind pace"
@@ -327,7 +498,30 @@ export function Badge({ provider }) {
327
498
  : badge === "Cap risk" || badge === "Monthly exhausted"
328
499
  ? "pace-cap"
329
500
  : "pace-out"; // Not reporting and Measuring share the neutral style
330
- return (_jsxs("span", { "data-testid": "pace-badge", className: `pace ${badgeClass}`, title: badgeTitle(badge, provider.quota?.resetsAtEstimated === true), children: [badge, detail ? ` · ${detail}` : ""] }));
501
+ const title = compact && detail
502
+ ? `${badge} · ${detail}`
503
+ : badgeTitle(badge, quotaResetEstimated(provider));
504
+ return (_jsxs("span", { "data-testid": "pace-badge", className: `pace ${badgeClass}`, title: title, children: [badge, !compact && detail ? ` · ${detail}` : ""] }));
505
+ }
506
+ /**
507
+ * Headline forecast shared by the card bar and the drawer quota row:
508
+ * "expires unused" wording when a waste hatch applies, otherwise the
509
+ * one-line forecast. Null when there is nothing to forecast.
510
+ */
511
+ export function paceHeadline(provider) {
512
+ const badge = paceBadge(provider);
513
+ const monthlyBinds = provider.advisory?.bindingWindow === "monthly";
514
+ const wastePct = provider.advisory?.wastePct;
515
+ const hatch = !monthlyBinds && wastePct !== null && wastePct !== undefined && Number.isFinite(wastePct) && wastePct >= 3;
516
+ const text = hatch && badge === "Behind pace" ? `${Math.round(wastePct)}% expires unused` : forecastLine(provider);
517
+ if (!text)
518
+ return null;
519
+ const kind = monthlyBinds
520
+ ? provider.advisory.bindingRemaining === 0
521
+ ? "cap"
522
+ : "behind"
523
+ : outKind(badge);
524
+ return { text, kind, hatch };
331
525
  }
332
526
  /**
333
527
  * Two-tier pacing rail: used fill, elapsed tick, projected-unused hatch.
@@ -337,18 +531,15 @@ export function PaceBar({ provider, asOf, showHeadline = true, }) {
337
531
  const badge = paceBadge(provider);
338
532
  const asOfMs = Date.parse(asOf);
339
533
  const { usedPct, elapsedPct } = barGeometry(provider, asOfMs);
340
- const monthlyBinds = provider.advisory?.bindingWindow === "monthly";
341
534
  const wastePct = provider.advisory?.wastePct;
535
+ const headline = paceHeadline(provider);
342
536
  // A weekly-sized hatch under a month-bound card restates issue 109.
343
- const showHatch = !monthlyBinds && wastePct !== null && wastePct !== undefined && Number.isFinite(wastePct) && wastePct >= 3;
537
+ const showHatch = headline?.hatch ?? false;
538
+ const quotaUsed = quotaUsedPct(provider);
344
539
  const label = provider.quota
345
- ? `${provider.id}: ${provider.quota.weeklyPct}% used, ${badge}`
540
+ ? `${provider.id}: ${quotaUsed ?? provider.quota.weeklyPct}% used, ${badge}`
346
541
  : `${provider.id}: no readings, ${badge}`;
347
- const forecast = showHatch && badge === "Behind pace" ? `${Math.round(wastePct)}% expires unused` : forecastLine(provider);
348
- const kind = monthlyBinds
349
- ? provider.advisory.bindingRemaining === 0 ? "cap" : "behind"
350
- : outKind(badge);
351
- return (_jsxs("div", { "data-testid": "pace-bar", role: "img", "aria-label": label, className: "trackwrap", children: [showHeadline && provider.quota && (_jsxs("div", { className: "tline", children: [_jsxs("span", { className: "used", children: [provider.quota.weeklyPct, "% used"] }), forecast && kind !== "none" ? (_jsxs("span", { className: `out out-${kind}`, children: ["\u2192 ", forecast] })) : forecast ? (_jsxs("span", { className: "out out-none", children: ["\u2192 ", forecast] })) : null] })), _jsxs("div", { className: "track", children: [_jsx("div", { "aria-hidden": "true", className: "fill", style: {
542
+ return (_jsxs("div", { "data-testid": "pace-bar", role: "img", "aria-label": label, className: "trackwrap", children: [showHeadline && provider.quota && (_jsxs("div", { className: "tline", children: [_jsxs("span", { className: "used", children: [quotaUsed ?? provider.quota.weeklyPct, "% used"] }), headline && headline.kind !== "none" ? (_jsxs("span", { className: `out out-${headline.kind}`, children: ["\u2192 ", headline.text] })) : headline ? (_jsxs("span", { className: "out out-none", children: ["\u2192 ", headline.text] })) : null] })), _jsxs("div", { className: "track", children: [_jsx("div", { "aria-hidden": "true", className: "fill", style: {
352
543
  width: `${usedPct}%`,
353
544
  background: fillToken(badge),
354
545
  ["--fill-edge"]: edgeToken(badge),
@@ -392,16 +583,16 @@ export function forecastLine(provider) {
392
583
  }
393
584
  if (advisory.status === "at risk") {
394
585
  return advisory.daysToExhaust !== null && Number.isFinite(advisory.daysToExhaust)
395
- ? `Cap risk · exhausts in ${advisory.daysToExhaust.toFixed(1)}d at current pace`
586
+ ? `Cap risk · exhausts in ${daysToDuration(advisory.daysToExhaust)} at current pace`
396
587
  : "Cap risk at current pace";
397
588
  }
398
589
  if (advisory.status === "watch") {
399
590
  return advisory.daysToExhaust !== null && Number.isFinite(advisory.daysToExhaust)
400
- ? `Watch · exhausts in ${advisory.daysToExhaust.toFixed(1)}d at current pace`
591
+ ? `Watch · exhausts in ${daysToDuration(advisory.daysToExhaust)} at current pace`
401
592
  : "Watch at current pace";
402
593
  }
403
594
  if (advisory.wastePct !== null) {
404
- return `${Math.round(advisory.wastePct)}% waste in ${advisory.daysLeft.toFixed(1)}d`;
595
+ return `${Math.round(advisory.wastePct)}% waste in ${daysToDuration(advisory.daysLeft)}`;
405
596
  }
406
597
  return "On track.";
407
598
  }
@@ -30,7 +30,7 @@ export function PacingLegend() {
30
30
  document.removeEventListener("keydown", onKey);
31
31
  };
32
32
  }, [open]);
33
- return (_jsxs("div", { className: "legend-wrap", ref: rootRef, children: [_jsx("button", { type: "button", className: "legend-btn", "aria-expanded": open, "aria-controls": "pacing-legend", onClick: () => setOpen((v) => !v), children: "Pacing Legend \u25BE" }), open && (_jsxs("div", { id: "pacing-legend", className: "legend-popover", role: "dialog", "aria-label": "Pacing legend", children: [_jsxs("div", { className: "legend-head", children: [_jsx("b", { children: "Pacing Scale Legend" }), _jsx("button", { type: "button", className: "legend-close", "aria-label": "Close pacing legend", onClick: () => setOpen(false), children: "\u00D7" })] }), _jsx("p", { className: "legend-intro", children: "Pacing measures quota burn against elapsed window time:" }), _jsx("div", { className: "legend-rows", children: STATES.map((s) => (_jsxs("div", { className: "legend-row", children: [_jsx("span", { className: `pace ${s.cls}`, children: s.label }), _jsx("span", { children: s.meaning })] }, s.label))) }), _jsxs("div", { className: "legend-foot", children: [_jsx("code", { children: "\u2588" }), " allowance used \u00B7 ", _jsx("code", { children: "\u2502" }), " time elapsed \u00B7 ", _jsx("code", { children: "\u2591" }), " projected waste"] }), _jsx("div", { className: "legend-foot", children: "Avg = used \u00F7 days elapsed \u00B7 24h = burn over the last day \u00B7 Ideal = remaining \u00F7 days left" })] }))] }));
33
+ return (_jsxs("div", { className: "legend-wrap", ref: rootRef, children: [_jsx("button", { type: "button", className: "legend-btn", "aria-expanded": open, "aria-controls": "pacing-legend", onClick: () => setOpen((v) => !v), children: "Pacing Legend \u25BE" }), open && (_jsxs("div", { id: "pacing-legend", className: "legend-popover", role: "dialog", "aria-label": "Pacing legend", children: [_jsxs("div", { className: "legend-head", children: [_jsx("b", { children: "Pacing Scale Legend" }), _jsx("button", { type: "button", className: "legend-close", "aria-label": "Close pacing legend", onClick: () => setOpen(false), children: "\u00D7" })] }), _jsx("p", { className: "legend-intro", children: "Pacing measures quota burn against elapsed window time:" }), _jsx("div", { className: "legend-rows", children: STATES.map((s) => (_jsxs("div", { className: "legend-row", children: [_jsx("span", { className: `pace ${s.cls}`, children: s.label }), _jsx("span", { children: s.meaning })] }, s.label))) }), _jsxs("div", { className: "legend-foot", children: [_jsx("code", { children: "\u2588" }), " quota used \u00B7 ", _jsx("code", { children: "\u2502" }), " time elapsed \u00B7 ", _jsx("code", { children: "\u2591" }), " projected waste"] }), _jsx("div", { className: "legend-foot", children: "Avg = used \u00F7 days elapsed \u00B7 24h = burn over the last day \u00B7 Ideal = remaining \u00F7 days left" })] }))] }));
34
34
  }
35
35
  function servingHost() {
36
36
  try {
@@ -1,24 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ageDuration } from "../state.js";
3
- import { Badge, PACE_TIPS, PaceBar, isEstimated, paceCells, resetClock, resetCountdown, timeLeft, } from "./PaceBar.js";
3
+ import { Badge, PaceBar, PaceStats, isMonthlyBound, quotaResetEstimated, quotaResetsAt, resetClock, resetCountdown, resetDateClock, } from "./PaceBar.js";
4
4
  import { ProviderIcon, providerTint } from "./ProviderIcon.js";
5
5
  import { lastClose } from "./ClosedWeeks.js";
6
6
  import { WindowsRow } from "./WindowsRow.js";
7
7
  function resetsDate(provider) {
8
- if (!provider.quota)
8
+ const resetIso = quotaResetsAt(provider);
9
+ if (!resetIso)
9
10
  return null;
10
- const clock = resetClock(provider.quota.resetsAt);
11
+ const clock = isMonthlyBound(provider) ? resetDateClock(resetIso) : resetClock(resetIso);
11
12
  if (!clock)
12
13
  return null;
13
- return `Resets ${clock}${isEstimated(provider) ? " (est.)" : ""}`;
14
+ return `Resets ${clock}${quotaResetEstimated(provider) ? " (est.)" : ""}`;
14
15
  }
15
16
  export function ProviderCard({ provider, recommended, asOf, onSelect, }) {
16
- const quota = provider.quota;
17
- const advisory = provider.advisory;
18
- const pace = paceCells(advisory);
19
17
  const asOfMs = Date.parse(asOf);
20
18
  const updated = ageDuration(provider.ageMs);
21
- const resetsIn = quota ? timeLeft(quota.resetsAt, asOfMs) : null;
22
19
  const name = provider.displayName;
23
20
  const last = lastClose(provider);
24
21
  return (_jsxs("article", { "data-testid": `provider-card-${provider.id}`, role: "button", tabIndex: 0, "aria-label": `${name} details`, onClick: () => onSelect(provider.id), onKeyDown: (e) => {
@@ -27,5 +24,5 @@ export function ProviderCard({ provider, recommended, asOf, onSelect, }) {
27
24
  e.preventDefault();
28
25
  onSelect(provider.id);
29
26
  }
30
- }, className: `pcard ${recommended ? "is-recommended is-top-pick" : ""}`, children: [_jsxs("div", { className: "pcard-head", children: [_jsx("span", { className: "picon", style: providerTint(provider.id), "aria-hidden": "true", children: _jsx(ProviderIcon, { id: provider.id, title: name }) }), _jsxs("div", { className: "who", children: [_jsxs("b", { children: [name, recommended && (_jsx("span", { className: "rec-icon", title: "Recommended for next session", role: "img", "aria-label": "recommended", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.3", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }) }))] }), updated && _jsxs("span", { className: "src", children: ["Updated ", updated, " ago"] })] }), _jsx(Badge, { provider: provider })] }), _jsxs("div", { className: "pcard-body", children: [_jsx(PaceBar, { provider: provider, asOf: asOf }), _jsx(WindowsRow, { provider: provider, asOf: asOf, variant: "card" }), _jsxs("div", { className: "pstats", children: [_jsxs("div", { title: PACE_TIPS.avg, children: [_jsx("span", { className: "l", children: "Avg pace" }), _jsx("span", { className: "v", children: pace.avg })] }), _jsxs("div", { title: PACE_TIPS.recent, children: [_jsx("span", { className: "l", children: "24h pace" }), _jsx("span", { className: "v", children: pace.recent })] }), _jsxs("div", { title: PACE_TIPS.ideal, children: [_jsx("span", { className: "l", children: "Ideal pace" }), _jsx("span", { className: "v", children: advisory ? `${advisory.idealRate.toFixed(1)}%/day` : "—" })] }), _jsxs("div", { children: [_jsx("span", { className: "l", children: "Resets in" }), _jsx("span", { className: "v", children: resetsIn ? `${resetsIn}${isEstimated(provider) ? " (est.)" : ""}` : resetCountdown(provider, asOfMs) })] })] })] }), _jsxs("div", { className: "pcard-foot", children: [_jsxs("div", { className: "foot-meta", children: [_jsx("span", { children: resetsDate(provider) ?? resetCountdown(provider, asOfMs) }), last && (_jsxs("span", { className: "last-week", children: ["Last week ", last.leftoverPct, "% leftover", last.resetsAtEstimated ? " (est.)" : ""] }))] }), _jsx("span", { className: "rowbtn", "aria-hidden": "true", children: "Inspect" })] })] }));
27
+ }, className: `pcard ${recommended ? "is-recommended is-top-pick" : ""}`, children: [_jsxs("div", { className: "pcard-head", children: [_jsx("span", { className: "picon", style: providerTint(provider.id), "aria-hidden": "true", children: _jsx(ProviderIcon, { id: provider.id, title: name }) }), _jsxs("div", { className: "who", children: [_jsxs("b", { children: [name, recommended && (_jsx("span", { className: "rec-icon", title: "Recommended for next session", role: "img", "aria-label": "recommended", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.3", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }) }))] }), updated && _jsxs("span", { className: "src", children: ["Updated ", updated, " ago"] })] }), _jsx(Badge, { provider: provider })] }), _jsxs("div", { className: "pcard-body", children: [_jsx(PaceBar, { provider: provider, asOf: asOf }), _jsx(WindowsRow, { provider: provider, asOf: asOf, variant: "card" }), _jsx(PaceStats, { provider: provider, asOf: asOf })] }), _jsxs("div", { className: "pcard-foot", children: [_jsxs("div", { className: "foot-meta", children: [_jsx("span", { children: resetsDate(provider) ?? resetCountdown(provider, asOfMs) }), last && (_jsxs("span", { className: "last-week", children: ["Last ", isMonthlyBound(provider) ? "month" : "week", " ", last.leftoverPct, "% leftover", last.resetsAtEstimated ? " (est.)" : ""] }))] }), _jsx("span", { className: "rowbtn", "aria-hidden": "true", children: "Inspect" })] })] }));
31
28
  }