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.
- package/README.md +2 -2
- package/dist/adapters/agy.js +3 -0
- package/dist/adapters/claude.js +3 -0
- package/dist/adapters/codex.js +4 -2
- package/dist/adapters/kimi.js +14 -0
- package/dist/adapters/manual.js +4 -0
- package/dist/adapters/muse.js +19 -5
- package/dist/adapters/opencode-go.d.ts +12 -2
- package/dist/adapters/opencode-go.js +156 -31
- package/dist/adapters/types.d.ts +2 -0
- package/dist/advisory/snapshot.d.ts +1 -0
- package/dist/advisory/snapshot.js +7 -1
- package/dist/advisory/types.d.ts +3 -0
- package/dist/cli/models.js +5 -4
- package/dist/format/rows.d.ts +30 -1
- package/dist/format/rows.js +109 -24
- package/dist/format/terminal.js +5 -3
- package/dist/http/server.d.ts +3 -0
- package/dist/http/server.js +7 -3
- package/dist/runtime/service.js +14 -3
- package/dist/service/macos.d.ts +5 -0
- package/dist/service/macos.js +6 -2
- package/dist/store/db.js +2 -1
- package/dist/store/quotas.d.ts +1 -1
- package/dist/store/quotas.js +65 -2
- package/dist/version.js +1 -1
- package/dist/web/src/App.js +10 -4
- package/dist/web/src/components/AdviceDrawer.d.ts +2 -1
- package/dist/web/src/components/AdviceDrawer.js +27 -16
- package/dist/web/src/components/BurnDown.d.ts +37 -0
- package/dist/web/src/components/BurnDown.js +182 -0
- package/dist/web/src/components/ClosedWeeks.d.ts +6 -5
- package/dist/web/src/components/ClosedWeeks.js +25 -37
- package/dist/web/src/components/CopyButton.d.ts +4 -0
- package/dist/web/src/components/CopyButton.js +27 -0
- package/dist/web/src/components/ErrorBoundary.d.ts +37 -0
- package/dist/web/src/components/ErrorBoundary.js +32 -0
- package/dist/web/src/components/FaultBanner.d.ts +3 -2
- package/dist/web/src/components/FaultBanner.js +7 -4
- package/dist/web/src/components/OpencodeGoBanner.d.ts +3 -1
- package/dist/web/src/components/OpencodeGoBanner.js +16 -10
- package/dist/web/src/components/PaceBar.d.ts +80 -3
- package/dist/web/src/components/PaceBar.js +225 -34
- package/dist/web/src/components/PacingLegend.js +1 -1
- package/dist/web/src/components/ProviderCard.js +6 -9
- package/dist/web/src/components/ProviderDrawer.d.ts +56 -10
- package/dist/web/src/components/ProviderDrawer.js +263 -138
- package/dist/web/src/components/ProviderRow.js +3 -3
- package/dist/web/src/components/Recommendation.js +11 -7
- package/dist/web/src/components/ResetRail.js +13 -9
- package/dist/web/src/components/SubscriptionList.js +11 -4
- package/dist/web/src/components/WindowsRow.d.ts +13 -3
- package/dist/web/src/components/WindowsRow.js +27 -5
- package/dist/web/src/components/settings/CliIntegrationPanel.js +7 -45
- package/dist/web/src/components/settings/DaemonPanel.js +1 -28
- package/dist/web/src/components/settings/ProvidersPanel.d.ts +2 -2
- package/dist/web/src/components/settings/ProvidersPanel.js +10 -24
- package/dist/web/src/components/settings/SettingsDrawer.d.ts +1 -2
- package/dist/web/src/components/settings/SettingsDrawer.js +2 -2
- package/dist/web/src/state.d.ts +6 -1
- package/dist/web/src/state.js +19 -10
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-Co33DTl5.css +1 -0
- package/web/dist/assets/index-DqK6QVaw.js +13 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BSQW0C_t.css +0 -1
- 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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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: `${
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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" }), "
|
|
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,
|
|
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
|
-
|
|
8
|
+
const resetIso = quotaResetsAt(provider);
|
|
9
|
+
if (!resetIso)
|
|
9
10
|
return null;
|
|
10
|
-
const clock =
|
|
11
|
+
const clock = isMonthlyBound(provider) ? resetDateClock(resetIso) : resetClock(resetIso);
|
|
11
12
|
if (!clock)
|
|
12
13
|
return null;
|
|
13
|
-
return `Resets ${clock}${
|
|
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" }),
|
|
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
|
}
|