quotacap 0.0.39 → 0.0.40
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 +1 -1
- package/dist/adapters/agy.js +2 -2
- package/dist/adapters/claude.js +1 -1
- package/dist/adapters/codex.js +2 -2
- package/dist/adapters/grok.js +1 -1
- package/dist/adapters/kimi.js +2 -2
- package/dist/adapters/manual.js +2 -2
- package/dist/adapters/muse.js +2 -2
- package/dist/adapters/opencode-go.js +30 -2
- package/dist/adapters/types.d.ts +17 -2
- package/dist/advisory/engine.d.ts +4 -2
- package/dist/advisory/engine.js +55 -14
- package/dist/advisory/snapshot.js +11 -3
- package/dist/advisory/types.d.ts +11 -3
- package/dist/cli/models.js +1 -1
- package/dist/diagnostics/failure.js +9 -0
- package/dist/format/rows.d.ts +6 -1
- package/dist/format/rows.js +27 -4
- package/dist/format/terminal.js +5 -4
- package/dist/http/server.js +3 -3
- package/dist/runtime/poll.js +1 -1
- package/dist/store/db.js +12 -1
- package/dist/store/quotas.js +33 -8
- package/dist/version.js +1 -1
- package/dist/web/src/components/AdviceDrawer.js +10 -5
- package/dist/web/src/components/PaceBar.d.ts +9 -5
- package/dist/web/src/components/PaceBar.js +32 -10
- package/dist/web/src/components/ProviderCard.js +4 -2
- package/dist/web/src/components/ProviderDrawer.d.ts +7 -1
- package/dist/web/src/components/ProviderDrawer.js +46 -9
- package/dist/web/src/components/ProviderRow.js +4 -4
- package/dist/web/src/components/Recommendation.d.ts +5 -2
- package/dist/web/src/components/Recommendation.js +8 -4
- package/dist/web/src/components/ResetRail.js +3 -2
- package/dist/web/src/components/SubscriptionList.d.ts +2 -1
- package/dist/web/src/components/SubscriptionList.js +8 -5
- package/dist/web/src/components/WindowsRow.d.ts +30 -0
- package/dist/web/src/components/WindowsRow.js +68 -0
- package/dist/web/src/state.d.ts +12 -0
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-CsOiHZyD.js +12 -0
- package/web/dist/assets/{index-NtT9XTrm.css → index-Dk6l5h27.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CD8jaN87.js +0 -12
package/dist/store/db.js
CHANGED
|
@@ -36,7 +36,7 @@ export function openDb(p) {
|
|
|
36
36
|
return db;
|
|
37
37
|
}
|
|
38
38
|
export function migrate(db) {
|
|
39
|
-
db.exec(`CREATE TABLE IF NOT EXISTS quotas(id INTEGER PRIMARY KEY, provider TEXT, plan TEXT, used_pct REAL, resets_at TEXT, period_start TEXT, source TEXT, fetched_at TEXT, credits_usd REAL, resets_at_estimated INTEGER, session_pct REAL);
|
|
39
|
+
db.exec(`CREATE TABLE IF NOT EXISTS quotas(id INTEGER PRIMARY KEY, provider TEXT, plan TEXT, used_pct REAL, resets_at TEXT, period_start TEXT, source TEXT, fetched_at TEXT, credits_usd REAL, resets_at_estimated INTEGER, session_pct REAL, monthly_pct REAL, monthly_resets_at TEXT, monthly_status TEXT, monthly_kind TEXT);
|
|
40
40
|
CREATE TABLE IF NOT EXISTS snapshots(day TEXT, provider TEXT, used_pct REAL, burn_rate REAL, ideal_rate REAL, PRIMARY KEY(day, provider));
|
|
41
41
|
CREATE TABLE IF NOT EXISTS adapter_attempts(provider TEXT PRIMARY KEY, attempted_at TEXT NOT NULL, completed_at TEXT, succeeded_at TEXT, success INTEGER NOT NULL, failure_category TEXT, diagnostic_code TEXT, summary TEXT, action TEXT, error_detail TEXT);
|
|
42
42
|
CREATE INDEX IF NOT EXISTS idx_quotas_provider ON quotas(provider);
|
|
@@ -97,6 +97,17 @@ export function migrate(db) {
|
|
|
97
97
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_quotas_provider ON quotas(provider)`);
|
|
98
98
|
}
|
|
99
99
|
catch { }
|
|
100
|
+
// After both migration branches, so a rebuilt legacy table gains these too.
|
|
101
|
+
const quotaCols = new Set(db.prepare("PRAGMA table_info(quotas)").all().map((c) => c.name));
|
|
102
|
+
for (const [column, type] of [
|
|
103
|
+
["monthly_pct", "REAL"],
|
|
104
|
+
["monthly_resets_at", "TEXT"],
|
|
105
|
+
["monthly_status", "TEXT"],
|
|
106
|
+
["monthly_kind", "TEXT"],
|
|
107
|
+
]) {
|
|
108
|
+
if (!quotaCols.has(column))
|
|
109
|
+
db.exec(`ALTER TABLE quotas ADD COLUMN ${column} ${type}`);
|
|
110
|
+
}
|
|
100
111
|
const names = new Set(db.prepare("PRAGMA table_info(adapter_attempts)").all().map((c) => c.name));
|
|
101
112
|
for (const column of ["diagnostic_code", "summary", "action", "error_detail"]) {
|
|
102
113
|
if (!names.has(column))
|
package/dist/store/quotas.js
CHANGED
|
@@ -2,15 +2,19 @@ import { MIN_PACE_SPAN_DAYS } from "../advisory/types.js";
|
|
|
2
2
|
function mapRow(row) {
|
|
3
3
|
if (!row)
|
|
4
4
|
return row;
|
|
5
|
+
const weeklyPct = row.used_pct ?? row.weeklyPct ?? row.usedPct;
|
|
6
|
+
const fiveHourPct = row.session_pct ?? row.fiveHourPct ?? row.sessionPct ?? undefined;
|
|
5
7
|
const out = {
|
|
6
8
|
...row,
|
|
7
|
-
|
|
9
|
+
weeklyPct,
|
|
10
|
+
usedPct: weeklyPct,
|
|
8
11
|
resetsAt: row.resets_at ?? row.resetsAt,
|
|
9
12
|
periodStart: row.period_start ?? row.periodStart,
|
|
10
13
|
fetchedAt: row.fetched_at ?? row.fetchedAt,
|
|
11
14
|
creditsUsd: row.credits_usd ?? row.creditsUsd,
|
|
12
15
|
resetsAtEstimated: !!(row.resets_at_estimated ?? row.resetsAtEstimated),
|
|
13
|
-
|
|
16
|
+
fiveHourPct,
|
|
17
|
+
sessionPct: fiveHourPct,
|
|
14
18
|
};
|
|
15
19
|
delete out.used_pct;
|
|
16
20
|
delete out.resets_at;
|
|
@@ -22,8 +26,26 @@ function mapRow(row) {
|
|
|
22
26
|
delete out.raw;
|
|
23
27
|
if (!out.resetsAtEstimated)
|
|
24
28
|
delete out.resetsAtEstimated;
|
|
25
|
-
if (out.
|
|
29
|
+
if (out.fiveHourPct == null) {
|
|
30
|
+
delete out.fiveHourPct;
|
|
26
31
|
delete out.sessionPct;
|
|
32
|
+
}
|
|
33
|
+
// A junk monthly percent means no usable monthly reading. It never
|
|
34
|
+
// invalidates the provider; only a bad weekly percent does (snapshot.ts).
|
|
35
|
+
const m = row.monthly_pct;
|
|
36
|
+
const monthlyValid = typeof m === "number" && Number.isFinite(m) && m >= 0 && m <= 100;
|
|
37
|
+
out.monthlyPct = monthlyValid ? m : undefined;
|
|
38
|
+
out.monthlyResetsAt = row.monthly_resets_at ?? undefined;
|
|
39
|
+
out.monthlyStatus = row.monthly_status ?? undefined;
|
|
40
|
+
out.monthlyKind = row.monthly_kind ?? undefined;
|
|
41
|
+
delete out.monthly_pct;
|
|
42
|
+
delete out.monthly_resets_at;
|
|
43
|
+
delete out.monthly_status;
|
|
44
|
+
delete out.monthly_kind;
|
|
45
|
+
for (const k of ["monthlyPct", "monthlyResetsAt", "monthlyStatus", "monthlyKind"]) {
|
|
46
|
+
if (!monthlyValid || out[k] == null)
|
|
47
|
+
delete out[k];
|
|
48
|
+
}
|
|
27
49
|
return out;
|
|
28
50
|
}
|
|
29
51
|
function cycleBreakReason(prev, cur) {
|
|
@@ -49,20 +71,23 @@ export function upsertQuota(db, q) {
|
|
|
49
71
|
}
|
|
50
72
|
const credits = q.creditsUsd ?? q.credits_usd ?? null;
|
|
51
73
|
const estimated = q.resetsAtEstimated ? 1 : null;
|
|
52
|
-
|
|
74
|
+
// Either spelling binds; an explicit null stays null (SQLite binds it),
|
|
75
|
+
// while a missing field stays undefined and fails the bind as before.
|
|
76
|
+
const weeklyPct = q.weeklyPct !== undefined ? q.weeklyPct : q.usedPct;
|
|
77
|
+
const fiveHourPct = q.fiveHourPct ?? q.sessionPct ?? q.session_pct ?? null;
|
|
53
78
|
db.exec("BEGIN");
|
|
54
79
|
try {
|
|
55
80
|
const prev = getLatestByProvider(db, q.provider);
|
|
56
81
|
if (prev) {
|
|
57
|
-
const reason = cycleBreakReason({ usedPct: prev.
|
|
82
|
+
const reason = cycleBreakReason({ usedPct: prev.weeklyPct, t: Date.parse(prev.fetchedAt), resetsAt: prev.resetsAt ?? null }, { usedPct: weeklyPct, t: Date.parse(q.fetchedAt), resetsAt: q.resetsAt ?? null });
|
|
58
83
|
if (reason) {
|
|
59
84
|
db.prepare(`INSERT OR IGNORE INTO window_closes(provider, plan, used_pct, leftover_pct, sampled_at, sampled_quota_id, period_start, resets_at, resets_at_estimated, detected_at, reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)`)
|
|
60
|
-
.run(prev.provider, prev.plan ?? null, prev.
|
|
85
|
+
.run(prev.provider, prev.plan ?? null, prev.weeklyPct, Math.max(0, 100 - prev.weeklyPct), prev.fetchedAt, prev.id, prev.periodStart ?? null, prev.resetsAt ?? null, prev.resetsAtEstimated ? 1 : null, q.fetchedAt, reason);
|
|
61
86
|
}
|
|
62
87
|
}
|
|
63
|
-
db.prepare(`INSERT INTO quotas(provider, plan, used_pct, resets_at, period_start, source, fetched_at, credits_usd, resets_at_estimated, session_pct) VALUES(
|
|
88
|
+
db.prepare(`INSERT INTO quotas(provider, plan, used_pct, resets_at, period_start, source, fetched_at, credits_usd, resets_at_estimated, session_pct, monthly_pct, monthly_resets_at, monthly_status, monthly_kind) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(q.provider, q.plan, weeklyPct, q.resetsAt, q.periodStart, q.source, q.fetchedAt, credits, estimated, fiveHourPct, q.monthlyPct ?? null, q.monthlyResetsAt ?? null, q.monthlyStatus ?? null, q.monthlyKind ?? null);
|
|
64
89
|
const day = new Date().toISOString().slice(0, 10);
|
|
65
|
-
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES(?,?,?) ON CONFLICT(day, provider) DO UPDATE SET used_pct=excluded.used_pct`).run(day, q.provider,
|
|
90
|
+
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES(?,?,?) ON CONFLICT(day, provider) DO UPDATE SET used_pct=excluded.used_pct`).run(day, q.provider, weeklyPct);
|
|
66
91
|
db.exec("COMMIT");
|
|
67
92
|
}
|
|
68
93
|
catch (e) {
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.40";
|
|
@@ -50,11 +50,16 @@ export function AdviceDrawer({ open, recommendation, providers, asOf, onClose, }
|
|
|
50
50
|
const remaining = pick?.advisory?.remaining !== null && pick?.advisory?.remaining !== undefined
|
|
51
51
|
? Math.round(pick.advisory.remaining)
|
|
52
52
|
: null;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
// A monthly-bound pick's leftover is unusable until the month resets, so
|
|
54
|
+
// the weekly expiry sentence would restate issue 109; show the reason.
|
|
55
|
+
const monthlyBound = recommendation.bindingWindow === "monthly";
|
|
56
|
+
const waste = monthlyBound
|
|
57
|
+
? null
|
|
58
|
+
: recommendation.wastePct !== null
|
|
59
|
+
? Math.round(recommendation.wastePct)
|
|
60
|
+
: pick?.advisory?.wastePct !== null && pick?.advisory?.wastePct !== undefined
|
|
61
|
+
? Math.round(pick.advisory.wastePct)
|
|
62
|
+
: null;
|
|
58
63
|
const left = pick?.quota ? timeLeft(pick.quota.resetsAt, asOfMs) : null;
|
|
59
64
|
const basis = pick?.advisory?.paceSource === "window-average" ? "window-average pace" : "current 24h pace";
|
|
60
65
|
const othersMore = useMore.filter((a) => a.provider !== pickId);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ExclusionReason, ProviderView } from "../state.js";
|
|
3
|
-
export type PaceBadge = "Not reporting" | "Cap risk" | "Watch" | "Behind pace" | "Ahead of pace" | "On track" | "Measuring";
|
|
3
|
+
export type PaceBadge = "Not reporting" | "Cap risk" | "Watch" | "Behind pace" | "Ahead of pace" | "On track" | "Measuring" | "Monthly exhausted";
|
|
4
4
|
/**
|
|
5
|
-
* Pace-state badge from the mapping table. Exclusion always wins;
|
|
6
|
-
* status "at risk" wins, then "watch", then "unknown";
|
|
7
|
-
* cumulative elapsed time read Ahead of pace;
|
|
8
|
-
* client thresholds.
|
|
5
|
+
* Pace-state badge from the mapping table. Exclusion always wins; a blocked
|
|
6
|
+
* month next; otherwise status "at risk" wins, then "watch", then "unknown";
|
|
7
|
+
* on-track rows ahead of cumulative elapsed time read Ahead of pace;
|
|
8
|
+
* otherwise urgency decides. No client thresholds.
|
|
9
9
|
*/
|
|
10
10
|
export declare function paceBadge(provider: ProviderView): PaceBadge;
|
|
11
11
|
export interface PaceFigures {
|
|
@@ -63,6 +63,10 @@ export declare function exclusionDetail(reason: ExclusionReason): string | null;
|
|
|
63
63
|
export declare function isEstimated(provider: ProviderView): boolean;
|
|
64
64
|
/** Weekday plus 24h clock, e.g. "Thu 21:00". Null when unparseable. */
|
|
65
65
|
export declare function resetClock(resetsAt: string, locale?: string): string | null;
|
|
66
|
+
/** Short weekday in the locked English copy, e.g. "Tue". Mirrors weekdayOf in
|
|
67
|
+
* src/format/rows.ts, including its pinned en-US locale, so the CLI and the
|
|
68
|
+
* dashboard word the same sentence identically on any machine. */
|
|
69
|
+
export declare function resetWeekday(iso: string | undefined): string;
|
|
66
70
|
/** "6d 12h" style time left from server timestamps. Null when unparseable. */
|
|
67
71
|
export declare function timeLeft(resetsAt: string, asOfMs: number): string | null;
|
|
68
72
|
/**
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { ageDuration } from "../state.js";
|
|
3
3
|
/**
|
|
4
|
-
* Pace-state badge from the mapping table. Exclusion always wins;
|
|
5
|
-
* status "at risk" wins, then "watch", then "unknown";
|
|
6
|
-
* cumulative elapsed time read Ahead of pace;
|
|
7
|
-
* client thresholds.
|
|
4
|
+
* Pace-state badge from the mapping table. Exclusion always wins; a blocked
|
|
5
|
+
* month next; otherwise status "at risk" wins, then "watch", then "unknown";
|
|
6
|
+
* on-track rows ahead of cumulative elapsed time read Ahead of pace;
|
|
7
|
+
* otherwise urgency decides. No client thresholds.
|
|
8
8
|
*/
|
|
9
9
|
export function paceBadge(provider) {
|
|
10
10
|
if (provider.exclusionReason !== null)
|
|
@@ -12,6 +12,8 @@ export function paceBadge(provider) {
|
|
|
12
12
|
const advisory = provider.advisory;
|
|
13
13
|
if (!advisory)
|
|
14
14
|
return "Not reporting";
|
|
15
|
+
if (advisory.bindingWindow === "monthly" && advisory.bindingRemaining === 0)
|
|
16
|
+
return "Monthly exhausted";
|
|
15
17
|
if (advisory.status === "at risk")
|
|
16
18
|
return "Cap risk";
|
|
17
19
|
if (advisory.status === "watch")
|
|
@@ -110,6 +112,12 @@ export function resetClock(resetsAt, locale) {
|
|
|
110
112
|
});
|
|
111
113
|
return `${day} ${time}`;
|
|
112
114
|
}
|
|
115
|
+
/** Short weekday in the locked English copy, e.g. "Tue". Mirrors weekdayOf in
|
|
116
|
+
* src/format/rows.ts, including its pinned en-US locale, so the CLI and the
|
|
117
|
+
* dashboard word the same sentence identically on any machine. */
|
|
118
|
+
export function resetWeekday(iso) {
|
|
119
|
+
return new Date(iso ?? "").toLocaleDateString("en-US", { weekday: "short" });
|
|
120
|
+
}
|
|
113
121
|
/** "6d 12h" style time left from server timestamps. Null when unparseable. */
|
|
114
122
|
export function timeLeft(resetsAt, asOfMs) {
|
|
115
123
|
const resetMs = Date.parse(resetsAt);
|
|
@@ -180,7 +188,7 @@ export function resetCell(provider, asOfMs) {
|
|
|
180
188
|
}
|
|
181
189
|
/** Bar geometry from server timestamps and percents. Presentation only. */
|
|
182
190
|
export function barGeometry(provider, asOfMs) {
|
|
183
|
-
const usedPct = provider.quota ? Math.min(100, Math.max(0, provider.quota.
|
|
191
|
+
const usedPct = provider.quota ? Math.min(100, Math.max(0, provider.quota.weeklyPct)) : 0;
|
|
184
192
|
let elapsedPct = null;
|
|
185
193
|
if (provider.quota) {
|
|
186
194
|
const startMs = Date.parse(provider.quota.periodStart);
|
|
@@ -201,6 +209,7 @@ export function badgeColor(badge) {
|
|
|
201
209
|
return "var(--ahead)";
|
|
202
210
|
case "Watch":
|
|
203
211
|
return "var(--watch)";
|
|
212
|
+
case "Monthly exhausted":
|
|
204
213
|
case "Cap risk":
|
|
205
214
|
return "var(--danger)";
|
|
206
215
|
case "Measuring":
|
|
@@ -219,6 +228,7 @@ export function fillToken(badge) {
|
|
|
219
228
|
return "var(--fill-ahead)";
|
|
220
229
|
case "Watch":
|
|
221
230
|
return "var(--fill-watch)";
|
|
231
|
+
case "Monthly exhausted":
|
|
222
232
|
case "Cap risk":
|
|
223
233
|
return "var(--fill-cap)";
|
|
224
234
|
case "Measuring":
|
|
@@ -237,6 +247,7 @@ export function edgeToken(badge) {
|
|
|
237
247
|
return "var(--edge-ahead)";
|
|
238
248
|
case "Watch":
|
|
239
249
|
return "var(--edge-watch)";
|
|
250
|
+
case "Monthly exhausted":
|
|
240
251
|
case "Cap risk":
|
|
241
252
|
return "var(--edge-cap)";
|
|
242
253
|
case "Measuring":
|
|
@@ -251,6 +262,7 @@ export function outKind(badge) {
|
|
|
251
262
|
return "behind";
|
|
252
263
|
case "Watch":
|
|
253
264
|
return "watch";
|
|
265
|
+
case "Monthly exhausted":
|
|
254
266
|
case "Cap risk":
|
|
255
267
|
return "cap";
|
|
256
268
|
case "On track":
|
|
@@ -293,7 +305,7 @@ export function Badge({ provider }) {
|
|
|
293
305
|
? "pace-ahead"
|
|
294
306
|
: badge === "Watch"
|
|
295
307
|
? "pace-watch"
|
|
296
|
-
: badge === "Cap risk"
|
|
308
|
+
: badge === "Cap risk" || badge === "Monthly exhausted"
|
|
297
309
|
? "pace-cap"
|
|
298
310
|
: "pace-out"; // Not reporting and Measuring share the neutral style
|
|
299
311
|
return (_jsxs("span", { "data-testid": "pace-badge", className: `pace ${badgeClass}`, title: badgeTitle(badge, provider.quota?.resetsAtEstimated === true), children: [badge, detail ? ` · ${detail}` : ""] }));
|
|
@@ -306,14 +318,18 @@ export function PaceBar({ provider, asOf, showHeadline = true, }) {
|
|
|
306
318
|
const badge = paceBadge(provider);
|
|
307
319
|
const asOfMs = Date.parse(asOf);
|
|
308
320
|
const { usedPct, elapsedPct } = barGeometry(provider, asOfMs);
|
|
321
|
+
const monthlyBinds = provider.advisory?.bindingWindow === "monthly";
|
|
309
322
|
const wastePct = provider.advisory?.wastePct;
|
|
310
|
-
|
|
323
|
+
// A weekly-sized hatch under a month-bound card restates issue 109.
|
|
324
|
+
const showHatch = !monthlyBinds && wastePct !== null && wastePct !== undefined && Number.isFinite(wastePct) && wastePct >= 3;
|
|
311
325
|
const label = provider.quota
|
|
312
|
-
? `${provider.id}: ${provider.quota.
|
|
326
|
+
? `${provider.id}: ${provider.quota.weeklyPct}% used, ${badge}`
|
|
313
327
|
: `${provider.id}: no readings, ${badge}`;
|
|
314
328
|
const forecast = showHatch && badge === "Behind pace" ? `${Math.round(wastePct)}% expires unused` : forecastLine(provider);
|
|
315
|
-
const kind =
|
|
316
|
-
|
|
329
|
+
const kind = monthlyBinds
|
|
330
|
+
? provider.advisory.bindingRemaining === 0 ? "cap" : "behind"
|
|
331
|
+
: outKind(badge);
|
|
332
|
+
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: {
|
|
317
333
|
width: `${usedPct}%`,
|
|
318
334
|
background: fillToken(badge),
|
|
319
335
|
["--fill-edge"]: edgeToken(badge),
|
|
@@ -343,6 +359,12 @@ export function forecastLine(provider) {
|
|
|
343
359
|
const advisory = provider.advisory;
|
|
344
360
|
if (!advisory)
|
|
345
361
|
return null;
|
|
362
|
+
if (advisory.bindingWindow === "monthly" && advisory.bindingRemaining <= 0) {
|
|
363
|
+
return `unused until ${resetWeekday(provider.quota.monthlyResetsAt)}`;
|
|
364
|
+
}
|
|
365
|
+
if (advisory.bindingWindow === "monthly") {
|
|
366
|
+
return `${Math.round(advisory.bindingRemaining)}% of month left`;
|
|
367
|
+
}
|
|
346
368
|
if (advisory.remaining <= 0) {
|
|
347
369
|
return "Exhausted · quota fully used";
|
|
348
370
|
}
|
|
@@ -3,6 +3,7 @@ import { ageDuration, displayPlan } from "../state.js";
|
|
|
3
3
|
import { Badge, PACE_TIPS, PaceBar, isEstimated, paceCells, resetClock, resetCountdown, timeLeft, } from "./PaceBar.js";
|
|
4
4
|
import { ProviderIcon, providerTint } from "./ProviderIcon.js";
|
|
5
5
|
import { lastClose } from "./ClosedWeeks.js";
|
|
6
|
+
import { WindowsRow } from "./WindowsRow.js";
|
|
6
7
|
function resetsDate(provider) {
|
|
7
8
|
if (!provider.quota)
|
|
8
9
|
return null;
|
|
@@ -22,9 +23,10 @@ export function ProviderCard({ provider, recommended, asOf, onSelect, }) {
|
|
|
22
23
|
const plan = displayPlan(quota?.plan);
|
|
23
24
|
const last = lastClose(provider);
|
|
24
25
|
return (_jsxs("article", { "data-testid": `provider-card-${provider.id}`, role: "button", tabIndex: 0, "aria-label": `${name} details`, onClick: () => onSelect(provider.id), onKeyDown: (e) => {
|
|
25
|
-
|
|
26
|
+
// Space or Enter aimed at the windows chevron must not also open the drawer.
|
|
27
|
+
if ((e.key === "Enter" || e.key === " ") && e.target === e.currentTarget) {
|
|
26
28
|
e.preventDefault();
|
|
27
29
|
onSelect(provider.id);
|
|
28
30
|
}
|
|
29
|
-
}, 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" }) }) }))] }), plan && _jsx("span", { className: "pl", children: plan }), 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 }), _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" })] })] }));
|
|
31
|
+
}, 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" }) }) }))] }), plan && _jsx("span", { className: "pl", children: plan }), 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" })] })] }));
|
|
30
32
|
}
|
|
@@ -24,7 +24,11 @@ export interface DrawerModel {
|
|
|
24
24
|
evidence: string[];
|
|
25
25
|
exclusionReason: ExclusionReason;
|
|
26
26
|
exclusionWords: string | null;
|
|
27
|
-
|
|
27
|
+
fiveHourPct?: number;
|
|
28
|
+
monthlyPct?: number;
|
|
29
|
+
monthlyResetsAt?: string;
|
|
30
|
+
monthlyStatus?: "ok" | "exhausted";
|
|
31
|
+
monthlyKind?: "included";
|
|
28
32
|
/** Type-level guard: a session reset is never synthesized, so this stays absent. */
|
|
29
33
|
sessionReset?: undefined;
|
|
30
34
|
}
|
|
@@ -38,6 +42,8 @@ export declare function credentialLabel(provider: ProviderView): string;
|
|
|
38
42
|
export declare function pacingStatusToken(provider: ProviderView): string;
|
|
39
43
|
export declare function resetStamp(provider: ProviderView): string | null;
|
|
40
44
|
export declare function resetUnderline(provider: ProviderView, asOfMs: number): string | null;
|
|
45
|
+
/** Monthly section meta line. Null when the provider has no included monthly window. */
|
|
46
|
+
export declare function monthlyMeta(provider: ProviderView): string | null;
|
|
41
47
|
export declare function rankingCopy(provider: ProviderView, recommendation: RecommendationView | null): {
|
|
42
48
|
known: string;
|
|
43
49
|
pace: string;
|
|
@@ -69,8 +69,18 @@ export function drawerModel(provider) {
|
|
|
69
69
|
exclusionReason: provider.exclusionReason,
|
|
70
70
|
exclusionWords: exclusionWords(provider.exclusionReason),
|
|
71
71
|
};
|
|
72
|
-
if (provider.quota?.
|
|
73
|
-
model.
|
|
72
|
+
if (provider.quota?.fiveHourPct !== undefined && provider.quota.fiveHourPct !== null) {
|
|
73
|
+
model.fiveHourPct = provider.quota.fiveHourPct;
|
|
74
|
+
}
|
|
75
|
+
const q = provider.quota;
|
|
76
|
+
if (q?.monthlyKind === "included") {
|
|
77
|
+
model.monthlyKind = q.monthlyKind;
|
|
78
|
+
if (q.monthlyPct !== undefined)
|
|
79
|
+
model.monthlyPct = q.monthlyPct;
|
|
80
|
+
if (q.monthlyResetsAt !== undefined)
|
|
81
|
+
model.monthlyResetsAt = q.monthlyResetsAt;
|
|
82
|
+
if (q.monthlyStatus !== undefined)
|
|
83
|
+
model.monthlyStatus = q.monthlyStatus;
|
|
74
84
|
}
|
|
75
85
|
return model;
|
|
76
86
|
}
|
|
@@ -126,6 +136,8 @@ export function pacingStatusToken(provider) {
|
|
|
126
136
|
return "ahead";
|
|
127
137
|
case "Watch":
|
|
128
138
|
return "watch";
|
|
139
|
+
case "Monthly exhausted":
|
|
140
|
+
return "monthly-exhausted";
|
|
129
141
|
case "Cap risk":
|
|
130
142
|
return "cap";
|
|
131
143
|
case "Measuring":
|
|
@@ -149,20 +161,36 @@ export function resetUnderline(provider, asOfMs) {
|
|
|
149
161
|
const left = timeLeft(provider.quota.resetsAt, asOfMs);
|
|
150
162
|
return left ? `Resets ${stamp} · in ${left}` : `Resets ${stamp}`;
|
|
151
163
|
}
|
|
164
|
+
/** Monthly section meta line. Null when the provider has no included monthly window. */
|
|
165
|
+
export function monthlyMeta(provider) {
|
|
166
|
+
const q = provider.quota;
|
|
167
|
+
if (q?.monthlyKind !== "included" || !q.monthlyResetsAt)
|
|
168
|
+
return null;
|
|
169
|
+
const clock = resetClock(q.monthlyResetsAt);
|
|
170
|
+
const base = clock ? `Resets ${clock}` : "Reset unknown";
|
|
171
|
+
const adv = provider.advisory;
|
|
172
|
+
if (q.monthlyStatus === "exhausted" || (adv?.bindingWindow === "monthly" && adv.bindingRemaining === 0)) {
|
|
173
|
+
return `${base} · weekly leftover unused until then`;
|
|
174
|
+
}
|
|
175
|
+
if (adv?.bindingWindow === "monthly") {
|
|
176
|
+
return `${base} · ${Math.round(adv.bindingRemaining)}% of month left`;
|
|
177
|
+
}
|
|
178
|
+
return base;
|
|
179
|
+
}
|
|
152
180
|
export function rankingCopy(provider, recommendation) {
|
|
153
181
|
const name = provider.displayName;
|
|
154
182
|
const quota = provider.quota;
|
|
155
183
|
const advisory = provider.advisory;
|
|
156
184
|
const stamp = resetStamp(provider);
|
|
157
185
|
const known = quota
|
|
158
|
-
? `${name} reports ${quota.
|
|
186
|
+
? `${name} reports ${quota.weeklyPct}% of quota used.${stamp ? ` Resets ${stamp}.` : ""}`
|
|
159
187
|
: "No reading yet.";
|
|
160
188
|
let pace;
|
|
161
189
|
if (!advisory || advisory.paceSource === "unknown" || advisory.burnRate === null) {
|
|
162
190
|
pace = modelFailure(provider) ?? "No current pace.";
|
|
163
191
|
}
|
|
164
192
|
else if (advisory.remaining <= 0) {
|
|
165
|
-
pace = `Exhausted at ${quota?.
|
|
193
|
+
pace = `Exhausted at ${quota?.weeklyPct ?? 100}% used.${stamp ? ` Resets ${stamp}.` : ""}`;
|
|
166
194
|
}
|
|
167
195
|
else {
|
|
168
196
|
const { avg, recent } = paceFigures(advisory);
|
|
@@ -211,9 +239,13 @@ export function compactSnapshot(provider, asOfMs) {
|
|
|
211
239
|
return {
|
|
212
240
|
provider: provider.id,
|
|
213
241
|
plan: quota?.plan ? quota.plan.split(" · ")[0] : null,
|
|
214
|
-
usedPct: quota?.
|
|
242
|
+
usedPct: quota?.weeklyPct ?? null,
|
|
215
243
|
elapsedPct: elapsedPct !== null ? Math.round(elapsedPct) : null,
|
|
216
244
|
resetsAt: quota?.resetsAt ?? null,
|
|
245
|
+
monthlyPct: quota?.monthlyPct ?? null,
|
|
246
|
+
monthlyResetsAt: quota?.monthlyResetsAt ?? null,
|
|
247
|
+
monthlyStatus: quota?.monthlyStatus ?? null,
|
|
248
|
+
bindingWindow: provider.advisory?.bindingWindow ?? null,
|
|
217
249
|
source: quota?.source ?? null,
|
|
218
250
|
pacingStatus: pacingStatusToken(provider),
|
|
219
251
|
reporting: provider.reporting,
|
|
@@ -309,6 +341,7 @@ export function ProviderDrawer({ provider, asOf, recommendation, onClose, onRena
|
|
|
309
341
|
const recordJson = JSON.stringify(record, null, 2);
|
|
310
342
|
const planBits = [displayPlan(provider.quota?.plan), sourceLabel(provider.quota?.source)].filter((b) => b && b !== "unknown");
|
|
311
343
|
const sub = planBits.join(" · ");
|
|
344
|
+
const monthlyLine = monthlyMeta(provider);
|
|
312
345
|
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "drawer-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "provider-drawer", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, className: "drawer is-open", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "drawer-head", children: [_jsx("span", { className: "picon", style: providerTint(provider.id), children: _jsx(ProviderIcon, { id: provider.id, title: model.name }) }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsxs("h2", { id: titleId, onClick: !isEditing ? () => setIsEditing(true) : undefined, title: !isEditing ? "Click to rename" : undefined, style: isEditing
|
|
313
346
|
? {
|
|
314
347
|
position: "absolute",
|
|
@@ -363,15 +396,19 @@ export function ProviderDrawer({ provider, asOf, recommendation, onClose, onRena
|
|
|
363
396
|
setIsEditing(false);
|
|
364
397
|
setNameDraft(provider.displayName);
|
|
365
398
|
setRenameError(null);
|
|
366
|
-
}, className: "btn btn-quiet btn-sm", style: { flex: "none" }, children: "Cancel" })] }), renameError && (_jsx("span", { style: { fontSize: "var(--t-2)", color: "var(--danger)" }, children: renameError }))] })), provider.builtinName && provider.displayName !== provider.builtinName && (_jsx("div", { className: "sub", style: { marginTop: "2px" }, "data-testid": "provider-builtin-name", children: `Built-in: ${provider.builtinName}` })), _jsx("span", { className: "sub", children: providerSubtitle(provider) ?? sub })] }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close provider details", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) })] }), provider.description && (_jsx("p", { style: {
|
|
399
|
+
}, className: "btn btn-quiet btn-sm", style: { flex: "none" }, children: "Cancel" })] }), renameError && (_jsx("span", { style: { fontSize: "var(--t-2)", color: "var(--danger)" }, children: renameError }))] })), provider.builtinName && provider.displayName !== provider.builtinName && (_jsx("div", { className: "sub", style: { marginTop: "2px" }, "data-testid": "provider-builtin-name", children: `Built-in: ${provider.builtinName}` })), _jsx("span", { className: "sub", children: providerSubtitle(provider) ?? sub })] }), _jsx(Badge, { provider: provider }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close provider details", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) })] }), provider.description && (_jsx("p", { style: {
|
|
367
400
|
margin: "0 var(--s4) var(--s3)",
|
|
368
401
|
fontSize: "var(--t-2)",
|
|
369
402
|
color: "var(--ink-soft)",
|
|
370
|
-
}, children: provider.description })), _jsxs("section", { className: "dsec", "aria-label": "Weekly limit", children: [_jsx("h3", { children: "Weekly limit" }),
|
|
371
|
-
width: `${Math.min(100, Math.max(0, model.
|
|
403
|
+
}, children: provider.description })), _jsxs("section", { className: "dsec", "aria-label": "Weekly limit", children: [_jsx("h3", { children: "Weekly limit" }), provider.quota ? (_jsxs(_Fragment, { children: [_jsx(PaceBar, { provider: provider, asOf: asOf }), underline && _jsx("div", { className: "pcard-underline", children: underline })] })) : (_jsx("p", { className: "cell-s", children: "No readings yet." }))] }), model.fiveHourPct !== undefined && model.fiveHourPct !== null && Number.isFinite(model.fiveHourPct) && (_jsxs("section", { className: "dsec", "aria-label": shortWindowLabel(provider.id) ?? "5h Limit", children: [_jsx("h3", { children: shortWindowLabel(provider.id) ?? "5h Limit" }), _jsx("div", { className: "pcard-topline", children: _jsxs("span", { className: "used", children: [model.fiveHourPct, "% used"] }) }), _jsx("div", { className: "track", role: "img", "aria-label": `${model.fiveHourPct} percent of ${(shortWindowLabel(provider.id) ?? "5h limit").toLowerCase()} used`, children: _jsx("div", { className: "fill", style: {
|
|
404
|
+
width: `${Math.min(100, Math.max(0, model.fiveHourPct))}%`,
|
|
372
405
|
background: "var(--fill-ontrack)",
|
|
373
406
|
["--fill-edge"]: "var(--edge-ontrack)",
|
|
374
|
-
} }) })] })),
|
|
407
|
+
} }) })] })), model.monthlyKind === "included" && model.monthlyPct !== undefined && Number.isFinite(model.monthlyPct) && (_jsxs("section", { className: "dsec", "aria-label": "Monthly limit", "data-testid": "drawer-monthly", children: [_jsx("h3", { children: "Monthly limit" }), _jsx("div", { className: "pcard-topline", children: _jsxs("span", { className: "used", children: [model.monthlyPct, "% used"] }) }), _jsx("div", { className: "track", role: "img", "aria-label": `${model.monthlyPct} percent of monthly limit used`, children: _jsx("div", { className: "fill", style: {
|
|
408
|
+
width: `${Math.min(100, Math.max(0, model.monthlyPct))}%`,
|
|
409
|
+
background: model.monthlyPct >= 80 ? "var(--fill-cap)" : "var(--fill-ontrack)",
|
|
410
|
+
["--fill-edge"]: model.monthlyPct >= 80 ? "var(--edge-cap)" : "var(--edge-ontrack)",
|
|
411
|
+
} }) }), monthlyLine && _jsx("div", { className: "pcard-underline", children: monthlyLine })] })), _jsx(ClosedWeeks, { closes: provider.lastCloses }), _jsxs("section", { className: "dsec", "aria-label": "Adapter and provenance", children: [_jsx("h3", { children: "Adapter and provenance" }), _jsxs("dl", { className: "dkv", children: [_jsx("dt", { children: "Read by" }), _jsx("dd", { children: sourceLabel(provider.quota?.source) }), _jsx("dt", { children: "Credential" }), _jsx("dd", { children: credentialLabel(provider) }), _jsx("dt", { children: "Window" }), _jsx("dd", { children: [
|
|
375
412
|
provider.quota?.plan && provider.quota.plan !== "unknown" ? provider.quota.plan : null,
|
|
376
413
|
resetStamp(provider) ? `resets ${resetStamp(provider)}` : null,
|
|
377
414
|
]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { displayPlan
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { displayPlan } from "../state.js";
|
|
3
3
|
import { Badge, PaceBar, forecastLine, paceLines, resetCellLines, } from "./PaceBar.js";
|
|
4
4
|
import { lastClose } from "./ClosedWeeks.js";
|
|
5
|
+
import { WindowsRow } from "./WindowsRow.js";
|
|
5
6
|
export function ProviderRow({ provider, recommended, asOf, onSelect, }) {
|
|
6
7
|
const quota = provider.quota;
|
|
7
8
|
const advisory = provider.advisory;
|
|
@@ -9,10 +10,9 @@ export function ProviderRow({ provider, recommended, asOf, onSelect, }) {
|
|
|
9
10
|
const lines = paceLines(advisory);
|
|
10
11
|
const name = provider.displayName;
|
|
11
12
|
const plan = displayPlan(quota?.plan);
|
|
12
|
-
const shortWindow = shortWindowLabel(provider.id);
|
|
13
13
|
const reset = resetCellLines(provider, asOfMs);
|
|
14
14
|
const last = lastClose(provider);
|
|
15
|
-
return (_jsxs("div", { "data-testid": `provider-row-${provider.id}`, className: `lrow ${recommended ? "is-recommended is-top-pick" : ""}`, onClick: () => onSelect(provider.id), children: [_jsxs("div", { className: "prov", "data-label": "Provider", children: [_jsx("span", { className: "dot", style: { background: `var(--p-${provider.id.split(":")[0]}, var(--surface-raised))` }, "aria-hidden": "true" }), _jsxs("span", { children: [_jsxs("span", { className: "nm", 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" }) }) }))] }), plan && _jsx("span", { className: "pl", children: plan })
|
|
15
|
+
return (_jsxs("div", { "data-testid": `provider-row-${provider.id}`, className: `lrow ${recommended ? "is-recommended is-top-pick" : ""}`, onClick: () => onSelect(provider.id), children: [_jsxs("div", { className: "prov", "data-label": "Provider", children: [_jsx("span", { className: "dot", style: { background: `var(--p-${provider.id.split(":")[0]}, var(--surface-raised))` }, "aria-hidden": "true" }), _jsxs("span", { children: [_jsxs("span", { className: "nm", 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" }) }) }))] }), plan && _jsx("span", { className: "pl", children: plan })] })] }), _jsx("div", { "data-label": "Used vs elapsed", children: quota ? (_jsxs(_Fragment, { children: [_jsx(PaceBar, { provider: provider, asOf: asOf }), _jsx(WindowsRow, { provider: provider, asOf: asOf, variant: "row" })] })) : "—" }), _jsxs("div", { "data-label": "Reset", className: "reset-cell", children: [_jsx("span", { className: "cell-b", children: reset.top }), reset.bottom && _jsx("span", { className: "cell-s", children: reset.bottom }), last && (_jsxs("span", { className: "cell-s", children: ["last week ", last.leftoverPct, "% leftover", last.resetsAtEstimated ? " (est.)" : ""] }))] }), _jsxs("div", { "data-label": "Pace", children: [_jsx(Badge, { provider: provider }), lines.length > 0 && (_jsx("div", { style: { marginTop: 2 }, children: lines.map((l) => (_jsxs("div", { className: "cell-s paceline", title: l.tip, children: [_jsx("span", { children: l.label }), _jsx("span", { className: "cell-b", children: l.value })] }, l.label))) }))] }), _jsx("button", { type: "button", className: "rowbtn", onClick: (e) => {
|
|
16
16
|
e.stopPropagation();
|
|
17
17
|
onSelect(provider.id);
|
|
18
18
|
}, children: "Inspect" }), _jsx("span", { className: "sr-only", children: forecastLine(provider) })] }));
|
|
@@ -2,11 +2,14 @@ import React from "react";
|
|
|
2
2
|
import type { AdvisoryView, ExclusionReason, ProviderView, RecommendationView, Urgency } from "../state.js";
|
|
3
3
|
export type Lane = "use-more" | "ease-off";
|
|
4
4
|
/**
|
|
5
|
-
* Lane membership from server urgency only. Any exclusionReason
|
|
6
|
-
* provider out of both lanes; "on track"
|
|
5
|
+
* Lane membership from server urgency only. Any exclusionReason, or an
|
|
6
|
+
* exhausted binding month, keeps the provider out of both lanes; "on track"
|
|
7
|
+
* belongs to neither lane.
|
|
7
8
|
*/
|
|
8
9
|
export declare function laneOf(advisory: {
|
|
9
10
|
urgency: Urgency;
|
|
11
|
+
bindingWindow?: "weekly" | "monthly";
|
|
12
|
+
bindingRemaining?: number;
|
|
10
13
|
}, exclusionReason: ExclusionReason): Lane | null;
|
|
11
14
|
export interface LaneMembership {
|
|
12
15
|
useMore: AdvisoryView[];
|
|
@@ -3,12 +3,16 @@ import { ageLabel } from "../state.js";
|
|
|
3
3
|
import { timeLeft } from "./PaceBar.js";
|
|
4
4
|
import { ProviderIcon, providerTint } from "./ProviderIcon.js";
|
|
5
5
|
/**
|
|
6
|
-
* Lane membership from server urgency only. Any exclusionReason
|
|
7
|
-
* provider out of both lanes; "on track"
|
|
6
|
+
* Lane membership from server urgency only. Any exclusionReason, or an
|
|
7
|
+
* exhausted binding month, keeps the provider out of both lanes; "on track"
|
|
8
|
+
* belongs to neither lane.
|
|
8
9
|
*/
|
|
9
10
|
export function laneOf(advisory, exclusionReason) {
|
|
10
11
|
if (exclusionReason !== null)
|
|
11
12
|
return null;
|
|
13
|
+
// A blocked month vetoes weekly leftover: no lane, exactly like an exclusion.
|
|
14
|
+
if (advisory.bindingWindow === "monthly" && advisory.bindingRemaining === 0)
|
|
15
|
+
return null;
|
|
12
16
|
switch (advisory.urgency) {
|
|
13
17
|
case "burn now":
|
|
14
18
|
case "use soon":
|
|
@@ -45,7 +49,7 @@ function LaneSection({ testId, label, tintClass, laneClass, icon, members, provi
|
|
|
45
49
|
const quota = byId.get(m.provider)?.quota;
|
|
46
50
|
const left = quota ? timeLeft(quota.resetsAt, asOfMs) : null;
|
|
47
51
|
const name = byId.get(m.provider)?.displayName ?? m.provider;
|
|
48
|
-
return (_jsxs("div", { className: "rsub", children: [_jsx("span", { className: "sq", style: providerTint(m.provider), "aria-hidden": "true", children: _jsx(ProviderIcon, { id: m.provider, title: name, size: 13 }) }), _jsx("div", { className: "rnm-group", children: _jsx("button", { type: "button", className: "rnm", onClick: () => onSelectProvider?.(m.provider), "aria-label": `View ${name} details`, children: name }) }), _jsx("span", { className: "rnum", children: quota ? (_jsxs(_Fragment, { children: [_jsxs("b", { children: [quota.
|
|
52
|
+
return (_jsxs("div", { className: "rsub", children: [_jsx("span", { className: "sq", style: providerTint(m.provider), "aria-hidden": "true", children: _jsx(ProviderIcon, { id: m.provider, title: name, size: 13 }) }), _jsx("div", { className: "rnm-group", children: _jsx("button", { type: "button", className: "rnm", onClick: () => onSelectProvider?.(m.provider), "aria-label": `View ${name} details`, children: name }) }), _jsx("span", { className: "rnum", children: quota ? (_jsxs(_Fragment, { children: [_jsxs("b", { children: [quota.weeklyPct, "%"] }), " used", left ? (_jsxs(_Fragment, { children: [" · ", _jsx("b", { children: left }), " left"] })) : null] })) : null })] }, m.provider));
|
|
49
53
|
}) })] }));
|
|
50
54
|
}
|
|
51
55
|
/**
|
|
@@ -77,7 +81,7 @@ export function Recommendation({ recommendation, providers, asOf, onSelectProvid
|
|
|
77
81
|
if (isClear) {
|
|
78
82
|
return (_jsxs("section", { "aria-label": "Quota advice and pacing", children: [head, _jsxs("section", { "data-testid": "recommendation", "aria-label": "Recommendation", className: "recwrap is-clear", role: "status", children: [_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" }), _jsx("path", { d: "M8 12.5l2.8 2.8L16 10" })] }), _jsxs("div", { children: [_jsx("b", { children: "No subscription needs priority" }), _jsx("span", { "data-testid": "rec-prose", children: recommendation.reason })] })] })] }));
|
|
79
83
|
}
|
|
80
|
-
return (_jsxs("section", { "aria-label": "Quota advice and pacing", children: [head, _jsxs("section", { "data-testid": "recommendation", "aria-label": "Recommendation", className: "recwrap", role: "status", children: [_jsxs("div", { className: "rec-top", children: [_jsxs("div", { className: "rec-lead", children: [_jsxs("span", { className: "rec-badge", children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", 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" }) }), "Recommendation"] }), recommendation.use === "none" ? (_jsx("p", { className: "rec-advice-prose", "data-testid": "rec-prose", children: recommendation.reason })) : (_jsxs("p", { className: "rec-advice-prose", "data-testid": "rec-prose", children: ["Switch to ", _jsx("strong", { children: pick?.displayName ?? recommendation.use }), " next", recommendation.wastePct !== null ? (_jsxs(_Fragment, { children: [" — ", _jsxs("strong", { children: [Math.round(recommendation.wastePct), "%"] }), " unused quota forecast", pickLeft ? _jsxs(_Fragment, { children: [" to expire in ", _jsx("strong", { children: pickLeft })] }) : null, "."] })) : (_jsxs(_Fragment, { children: [" \u2014 ", recommendation.reason] }))] })), measuring && (_jsx("span", { className: "rec-badge", "data-testid": "rec-measuring", style: {
|
|
84
|
+
return (_jsxs("section", { "aria-label": "Quota advice and pacing", children: [head, _jsxs("section", { "data-testid": "recommendation", "aria-label": "Recommendation", className: "recwrap", role: "status", children: [_jsxs("div", { className: "rec-top", children: [_jsxs("div", { className: "rec-lead", children: [_jsxs("span", { className: "rec-badge", children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", 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" }) }), "Recommendation"] }), recommendation.use === "none" ? (_jsx("p", { className: "rec-advice-prose", "data-testid": "rec-prose", children: recommendation.reason })) : (_jsxs("p", { className: "rec-advice-prose", "data-testid": "rec-prose", children: ["Switch to ", _jsx("strong", { children: pick?.displayName ?? recommendation.use }), " next", recommendation.wastePct !== null && recommendation.bindingWindow !== "monthly" ? (_jsxs(_Fragment, { children: [" — ", _jsxs("strong", { children: [Math.round(recommendation.wastePct), "%"] }), " unused quota forecast", pickLeft ? _jsxs(_Fragment, { children: [" to expire in ", _jsx("strong", { children: pickLeft })] }) : null, "."] })) : (_jsxs(_Fragment, { children: [" \u2014 ", recommendation.reason] }))] })), measuring && (_jsx("span", { className: "rec-badge", "data-testid": "rec-measuring", style: {
|
|
81
85
|
background: "var(--ahead-soft)",
|
|
82
86
|
color: "var(--ahead)",
|
|
83
87
|
borderColor: "var(--ahead)",
|
|
@@ -112,7 +112,7 @@ function ClusterPopover({ cluster, providers, asOfMs, onSelect, onClose, }) {
|
|
|
112
112
|
return (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => {
|
|
113
113
|
onSelect(id);
|
|
114
114
|
onClose();
|
|
115
|
-
}, children: [_jsx("b", { children: name }), _jsxs("span", { children: [provider?.quota ? `${provider.quota.
|
|
115
|
+
}, children: [_jsx("b", { children: name }), _jsxs("span", { children: [provider?.quota ? `${provider.quota.weeklyPct}% · ` : "", provider ? resetCountdown(provider, asOfMs) : ""] })] }) }, id));
|
|
116
116
|
}) }), _jsx("button", { type: "button", className: "btn btn-sm btn-quiet", style: { marginTop: "var(--s2)", width: "100%" }, onClick: onClose, children: "Close" })] }));
|
|
117
117
|
}
|
|
118
118
|
function tzAbbrev() {
|
|
@@ -136,6 +136,7 @@ function pinPace(provider) {
|
|
|
136
136
|
return { cls: "pace-ahead", color: "var(--ahead)" };
|
|
137
137
|
case "Watch":
|
|
138
138
|
return { cls: "pace-watch", color: "var(--watch)" };
|
|
139
|
+
case "Monthly exhausted":
|
|
139
140
|
case "Cap risk":
|
|
140
141
|
return { cls: "pace-cap", color: "var(--danger)" };
|
|
141
142
|
default:
|
|
@@ -177,7 +178,7 @@ export function ResetRail({ providers, asOf, onSelectProvider, }) {
|
|
|
177
178
|
return (_jsxs("button", { "data-testid": "pin", type: "button", className: `pin ${pin.band} ${tierClass} ${alignClass}`, style: {
|
|
178
179
|
left: `${pin.positionPct}%`,
|
|
179
180
|
"--dot-color": paceColor,
|
|
180
|
-
}, "aria-label": `${name}: ${pace}, resets ${when}`, onClick: () => onSelectProvider(pin.id), children: [pin.band === "above" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: "pin-dot" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-dot" }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when })] })), _jsxs("span", { className: "pin-tooltip", role: "tooltip", children: [_jsx("b", { children: name }), _jsx("span", { className: "pt-sep", children: "\u00B7" }), provider?.quota ? `${provider.quota.
|
|
181
|
+
}, "aria-label": `${name}: ${pace}, resets ${when}`, onClick: () => onSelectProvider(pin.id), children: [pin.band === "above" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: "pin-dot" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-dot" }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when })] })), _jsxs("span", { className: "pin-tooltip", role: "tooltip", children: [_jsx("b", { children: name }), _jsx("span", { className: "pt-sep", children: "\u00B7" }), provider?.quota ? `${provider.quota.weeklyPct}% used` : "no readings", _jsx("span", { className: "pt-sep", children: "\u00B7" }), left ? `${left} left` : when, pin.estimated ? " (est.)" : ""] })] }, pin.id));
|
|
181
182
|
}), placement.clusters.map((cluster, index) => {
|
|
182
183
|
const alignClass = cluster.positionPct < 12 ? "pin-start" : cluster.positionPct > 88 ? "pin-end" : "";
|
|
183
184
|
const tierClass = cluster.tier > 0 ? `tier${cluster.tier + 1}` : "tier1";
|