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.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/agy.js +2 -2
  3. package/dist/adapters/claude.js +1 -1
  4. package/dist/adapters/codex.js +2 -2
  5. package/dist/adapters/grok.js +1 -1
  6. package/dist/adapters/kimi.js +2 -2
  7. package/dist/adapters/manual.js +2 -2
  8. package/dist/adapters/muse.js +2 -2
  9. package/dist/adapters/opencode-go.js +30 -2
  10. package/dist/adapters/types.d.ts +17 -2
  11. package/dist/advisory/engine.d.ts +4 -2
  12. package/dist/advisory/engine.js +55 -14
  13. package/dist/advisory/snapshot.js +11 -3
  14. package/dist/advisory/types.d.ts +11 -3
  15. package/dist/cli/models.js +1 -1
  16. package/dist/diagnostics/failure.js +9 -0
  17. package/dist/format/rows.d.ts +6 -1
  18. package/dist/format/rows.js +27 -4
  19. package/dist/format/terminal.js +5 -4
  20. package/dist/http/server.js +3 -3
  21. package/dist/runtime/poll.js +1 -1
  22. package/dist/store/db.js +12 -1
  23. package/dist/store/quotas.js +33 -8
  24. package/dist/version.js +1 -1
  25. package/dist/web/src/components/AdviceDrawer.js +10 -5
  26. package/dist/web/src/components/PaceBar.d.ts +9 -5
  27. package/dist/web/src/components/PaceBar.js +32 -10
  28. package/dist/web/src/components/ProviderCard.js +4 -2
  29. package/dist/web/src/components/ProviderDrawer.d.ts +7 -1
  30. package/dist/web/src/components/ProviderDrawer.js +46 -9
  31. package/dist/web/src/components/ProviderRow.js +4 -4
  32. package/dist/web/src/components/Recommendation.d.ts +5 -2
  33. package/dist/web/src/components/Recommendation.js +8 -4
  34. package/dist/web/src/components/ResetRail.js +3 -2
  35. package/dist/web/src/components/SubscriptionList.d.ts +2 -1
  36. package/dist/web/src/components/SubscriptionList.js +8 -5
  37. package/dist/web/src/components/WindowsRow.d.ts +30 -0
  38. package/dist/web/src/components/WindowsRow.js +68 -0
  39. package/dist/web/src/state.d.ts +12 -0
  40. package/dist/webAssets.js +1 -1
  41. package/dist/webHtml.js +1 -1
  42. package/package.json +1 -1
  43. package/web/dist/assets/index-CsOiHZyD.js +12 -0
  44. package/web/dist/assets/{index-NtT9XTrm.css → index-Dk6l5h27.css} +1 -1
  45. package/web/dist/index.html +2 -2
  46. 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))
@@ -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
- usedPct: row.used_pct ?? row.usedPct,
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
- sessionPct: row.session_pct ?? row.sessionPct ?? undefined,
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.sessionPct == null)
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
- const sessionPct = q.sessionPct ?? q.session_pct ?? null;
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.usedPct, t: Date.parse(prev.fetchedAt), resetsAt: prev.resetsAt ?? null }, { usedPct: q.usedPct, t: Date.parse(q.fetchedAt), resetsAt: q.resetsAt ?? null });
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.usedPct, Math.max(0, 100 - prev.usedPct), prev.fetchedAt, prev.id, prev.periodStart ?? null, prev.resetsAt ?? null, prev.resetsAtEstimated ? 1 : null, q.fetchedAt, reason);
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(?,?,?,?,?,?,?,?,?,?)`).run(q.provider, q.plan, q.usedPct, q.resetsAt, q.periodStart, q.source, q.fetchedAt, credits, estimated, sessionPct);
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, q.usedPct);
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.39";
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
- const waste = recommendation.wastePct !== null
54
- ? Math.round(recommendation.wastePct)
55
- : pick?.advisory?.wastePct !== null && pick?.advisory?.wastePct !== undefined
56
- ? Math.round(pick.advisory.wastePct)
57
- : null;
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; otherwise
6
- * status "at risk" wins, then "watch", then "unknown"; on-track rows ahead of
7
- * cumulative elapsed time read Ahead of pace; otherwise urgency decides. No
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; otherwise
5
- * status "at risk" wins, then "watch", then "unknown"; on-track rows ahead of
6
- * cumulative elapsed time read Ahead of pace; otherwise urgency decides. No
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.usedPct)) : 0;
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
- const showHatch = wastePct !== null && wastePct !== undefined && Number.isFinite(wastePct) && wastePct >= 3;
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.usedPct}% used, ${badge}`
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 = outKind(badge);
316
- 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.usedPct, "% 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: {
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
- if (e.key === "Enter" || e.key === " ") {
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
- sessionPct?: number;
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?.sessionPct !== undefined && provider.quota.sessionPct !== null) {
73
- model.sessionPct = provider.quota.sessionPct;
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.usedPct}% of quota used.${stamp ? ` Resets ${stamp}.` : ""}`
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?.usedPct ?? 100}% used.${stamp ? ` Resets ${stamp}.` : ""}`;
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?.usedPct ?? null,
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" }), _jsx("div", { className: "pcard-topline", children: _jsx(Badge, { provider: provider }) }), 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.sessionPct !== undefined && model.sessionPct !== null && Number.isFinite(model.sessionPct) && (_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.sessionPct, "% used"] }) }), _jsx("div", { className: "track", role: "img", "aria-label": `${model.sessionPct} percent of ${(shortWindowLabel(provider.id) ?? "5h limit").toLowerCase()} used`, children: _jsx("div", { className: "fill", style: {
371
- width: `${Math.min(100, Math.max(0, model.sessionPct))}%`,
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
- } }) })] })), _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: [
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, shortWindowLabel } from "../state.js";
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 }), quota?.sessionPct !== undefined && quota.sessionPct !== null && (_jsxs("span", { className: "src", children: [shortWindow ?? "5h Limit", " \u00B7 ", quota.sessionPct, "% used"] }))] })] }), _jsx("div", { "data-label": "Used vs elapsed", children: quota ? _jsx(PaceBar, { provider: provider, asOf: asOf }) : "—" }), _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) => {
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 keeps the
6
- * provider out of both lanes; "on track" belongs to neither lane.
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 keeps the
7
- * provider out of both lanes; "on track" belongs to neither lane.
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.usedPct, "%"] }), " used", left ? (_jsxs(_Fragment, { children: [" · ", _jsx("b", { children: left }), " left"] })) : null] })) : null })] }, m.provider));
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.usedPct}% · ` : "", provider ? resetCountdown(provider, asOfMs) : ""] })] }) }, id));
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.usedPct}% used` : "no readings", _jsx("span", { className: "pt-sep", children: "\u00B7" }), left ? `${left} left` : when, pin.estimated ? " (est.)" : ""] })] }, pin.id));
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";