quotacap 0.0.34 → 0.0.35

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 (68) hide show
  1. package/README.md +1 -1
  2. package/dist/advisory/engine.d.ts +8 -3
  3. package/dist/advisory/engine.js +80 -48
  4. package/dist/advisory/snapshot.js +37 -21
  5. package/dist/advisory/types.d.ts +28 -2
  6. package/dist/advisory/types.js +3 -0
  7. package/dist/catalog/agy.d.ts +4 -0
  8. package/dist/catalog/agy.js +59 -0
  9. package/dist/catalog/claude.d.ts +3 -0
  10. package/dist/catalog/claude.js +70 -0
  11. package/dist/catalog/codex.d.ts +3 -0
  12. package/dist/catalog/codex.js +55 -0
  13. package/dist/catalog/grok.d.ts +3 -0
  14. package/dist/catalog/grok.js +36 -0
  15. package/dist/catalog/index.d.ts +17 -0
  16. package/dist/catalog/index.js +152 -0
  17. package/dist/catalog/kimi.d.ts +3 -0
  18. package/dist/catalog/kimi.js +51 -0
  19. package/dist/catalog/muse.d.ts +4 -0
  20. package/dist/catalog/muse.js +67 -0
  21. package/dist/catalog/types.d.ts +21 -0
  22. package/dist/catalog/types.js +3 -0
  23. package/dist/cli/advise.js +20 -0
  24. package/dist/cli/clients.d.ts +3 -0
  25. package/dist/cli/clients.js +2 -0
  26. package/dist/cli/models.d.ts +3 -0
  27. package/dist/cli/models.js +134 -0
  28. package/dist/cli/snapshot-source.js +1 -0
  29. package/dist/config.d.ts +3 -0
  30. package/dist/config.js +2 -0
  31. package/dist/format/markdown.d.ts +1 -0
  32. package/dist/format/markdown.js +14 -0
  33. package/dist/format/rows.d.ts +1 -1
  34. package/dist/format/rows.js +24 -4
  35. package/dist/format/terminal.js +18 -1
  36. package/dist/http/server.d.ts +2 -0
  37. package/dist/http/server.js +100 -0
  38. package/dist/mcp/server.d.ts +13 -0
  39. package/dist/mcp/server.js +31 -3
  40. package/dist/runtime/poll.d.ts +13 -0
  41. package/dist/runtime/poll.js +34 -2
  42. package/dist/runtime/service.js +9 -0
  43. package/dist/store/catalogs.d.ts +10 -0
  44. package/dist/store/catalogs.js +64 -0
  45. package/dist/store/db.js +27 -1
  46. package/dist/store/quotas.d.ts +22 -0
  47. package/dist/store/quotas.js +93 -8
  48. package/dist/version.js +1 -1
  49. package/dist/web/src/components/ClosedWeeks.d.ts +16 -0
  50. package/dist/web/src/components/ClosedWeeks.js +102 -0
  51. package/dist/web/src/components/PaceBar.d.ts +17 -5
  52. package/dist/web/src/components/PaceBar.js +51 -8
  53. package/dist/web/src/components/PacingLegend.js +1 -0
  54. package/dist/web/src/components/ProviderCard.js +3 -1
  55. package/dist/web/src/components/ProviderDrawer.js +7 -1
  56. package/dist/web/src/components/ProviderRow.js +3 -1
  57. package/dist/web/src/components/Recommendation.js +12 -3
  58. package/dist/web/src/components/ResetRail.js +2 -0
  59. package/dist/web/src/state.d.ts +20 -1
  60. package/dist/web/src/state.js +1 -0
  61. package/dist/webAssets.js +1 -1
  62. package/dist/webHtml.js +1 -1
  63. package/package.json +1 -1
  64. package/web/dist/assets/index-Cm_QtC0G.js +12 -0
  65. package/web/dist/assets/index-DiOyUuQc.css +1 -0
  66. package/web/dist/index.html +2 -2
  67. package/web/dist/assets/index-CjNVp2Yx.css +0 -1
  68. package/web/dist/assets/index-irnos0UG.js +0 -12
@@ -26,6 +26,21 @@ function mapRow(row) {
26
26
  delete out.sessionPct;
27
27
  return out;
28
28
  }
29
+ function cycleBreakReason(prev, cur) {
30
+ const dropped = cur.usedPct < prev.usedPct;
31
+ const prevResets = prev.resetsAt ? new Date(prev.resetsAt).getTime() : NaN;
32
+ const boundaryRolled = !Number.isNaN(prevResets) && prevResets <= cur.t && prev.resetsAt !== cur.resetsAt;
33
+ if (dropped && boundaryRolled)
34
+ return "both";
35
+ if (dropped)
36
+ return "usage-drop";
37
+ if (boundaryRolled)
38
+ return "resets-at-rolled";
39
+ return null;
40
+ }
41
+ export function isWeeklyCycleBreak(prev, cur) {
42
+ return cycleBreakReason(prev, cur) !== null;
43
+ }
29
44
  export function upsertQuota(db, q) {
30
45
  if (Array.isArray(q)) {
31
46
  for (const item of q)
@@ -35,9 +50,52 @@ export function upsertQuota(db, q) {
35
50
  const credits = q.creditsUsd ?? q.credits_usd ?? null;
36
51
  const estimated = q.resetsAtEstimated ? 1 : null;
37
52
  const sessionPct = q.sessionPct ?? q.session_pct ?? null;
38
- 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);
39
- const day = new Date().toISOString().slice(0, 10);
40
- 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);
53
+ db.exec("BEGIN");
54
+ try {
55
+ const prev = getLatestByProvider(db, q.provider);
56
+ 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 });
58
+ if (reason) {
59
+ 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);
61
+ }
62
+ }
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);
64
+ 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);
66
+ db.exec("COMMIT");
67
+ }
68
+ catch (e) {
69
+ try {
70
+ db.exec("ROLLBACK");
71
+ }
72
+ catch { }
73
+ throw e;
74
+ }
75
+ }
76
+ export function getWindowCloses(db, provider, limit = 4) {
77
+ let rows = [];
78
+ try {
79
+ rows = db.prepare(`SELECT provider, plan, used_pct, leftover_pct, sampled_at, period_start, resets_at, resets_at_estimated, detected_at, reason FROM window_closes WHERE provider=? ORDER BY detected_at DESC, id DESC LIMIT ?`).all(provider, limit);
80
+ }
81
+ catch (e) {
82
+ // A pre-observability database read offline is never migrated, so the
83
+ // table can be absent. No table means no recorded closes.
84
+ if (!/no such table/i.test(String(e?.message ?? "")))
85
+ throw e;
86
+ }
87
+ return rows.map((r) => ({
88
+ provider: r.provider,
89
+ plan: r.plan ?? null,
90
+ usedPct: r.used_pct,
91
+ leftoverPct: r.leftover_pct,
92
+ sampledAt: r.sampled_at,
93
+ periodStart: r.period_start ?? null,
94
+ resetsAt: r.resets_at ?? null,
95
+ resetsAtEstimated: !!r.resets_at_estimated,
96
+ detectedAt: r.detected_at,
97
+ reason: r.reason,
98
+ }));
41
99
  }
42
100
  export function getLatestByProvider(db, provider) {
43
101
  const row = db.prepare(`SELECT * FROM quotas WHERE provider=? ORDER BY fetched_at DESC LIMIT 1`).get(provider);
@@ -60,11 +118,7 @@ export function getSnapshots(db) { return db.prepare(`SELECT * FROM snapshots OR
60
118
  function currentCycle(sorted) {
61
119
  let start = 0;
62
120
  for (let i = 1; i < sorted.length; i++) {
63
- const prev = sorted[i - 1];
64
- const cur = sorted[i];
65
- const prevResets = prev.resetsAt ? new Date(prev.resetsAt).getTime() : NaN;
66
- const boundaryRolled = !Number.isNaN(prevResets) && prevResets <= cur.t && prev.resetsAt !== cur.resetsAt;
67
- if (boundaryRolled || cur.usedPct < prev.usedPct)
121
+ if (isWeeklyCycleBreak(sorted[i - 1], sorted[i]))
68
122
  start = i;
69
123
  }
70
124
  return start === 0 ? sorted : sorted.slice(start);
@@ -97,3 +151,34 @@ export function getBurnRates(db, now = Date.now()) {
97
151
  }
98
152
  return out;
99
153
  }
154
+ export function getHistoryBaseline(db, limit = 4) {
155
+ let rows = [];
156
+ try {
157
+ rows = db.prepare(`SELECT provider, used_pct, resets_at_estimated FROM window_closes ORDER BY detected_at DESC, id DESC`).all();
158
+ }
159
+ catch (e) {
160
+ // A pre-observability database read offline is never migrated, so the
161
+ // table can be absent. No table means no recorded closes.
162
+ if (!/no such table/i.test(String(e?.message ?? "")))
163
+ throw e;
164
+ }
165
+ const byProvider = new Map();
166
+ for (const r of rows) {
167
+ const closes = byProvider.get(r.provider) ?? [];
168
+ closes.push({ usedPct: r.used_pct, verified: !r.resets_at_estimated });
169
+ byProvider.set(r.provider, closes);
170
+ }
171
+ const out = new Map();
172
+ for (const [provider, newestFirst] of byProvider) {
173
+ const pool = newestFirst.slice(0, limit);
174
+ if (!pool.length)
175
+ continue;
176
+ const verified = pool.filter((c) => c.verified);
177
+ const chosen = verified.length ? verified : pool;
178
+ const mean = chosen.reduce((sum, c) => sum + c.usedPct, 0) / chosen.length;
179
+ const rate = mean / 7;
180
+ if (Number.isFinite(rate))
181
+ out.set(provider, rate);
182
+ }
183
+ return out;
184
+ }
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.34";
2
+ export const VERSION = "0.0.35";
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import type { WindowCloseView } from "../state.js";
3
+ export declare function lastClose(provider: {
4
+ lastCloses?: WindowCloseView[];
5
+ }): WindowCloseView | null;
6
+ export declare function closeDateLabel(resetsAt: string | null): string | null;
7
+ export declare function closeIsStale(c: WindowCloseView): boolean;
8
+ export declare function closeSampleStamp(sampledAt: string): string | null;
9
+ export declare function closeResetStamp(resetsAt: string): string | null;
10
+ /** Four-cell strip of closed weekly windows, oldest left. Fill is used at
11
+ * close, the hatch is the stored leftover. One cell per real close, never a
12
+ * placeholder. Stale samples name their reading on the cell and under the
13
+ * strip. */
14
+ export declare function ClosedWeeks({ closes }: {
15
+ closes: WindowCloseView[];
16
+ }): React.JSX.Element | null;
@@ -0,0 +1,102 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { format } from "date-fns";
3
+ const STALE_EARLY_MS = 15 * 60 * 1000;
4
+ export function lastClose(provider) {
5
+ const closes = provider.lastCloses;
6
+ return Array.isArray(closes) && closes.length > 0 ? closes[0] : null;
7
+ }
8
+ export function closeDateLabel(resetsAt) {
9
+ if (!resetsAt)
10
+ return null;
11
+ const ms = Date.parse(resetsAt);
12
+ if (!Number.isFinite(ms))
13
+ return null;
14
+ return format(new Date(ms), "d MMM");
15
+ }
16
+ export function closeIsStale(c) {
17
+ if (!c.resetsAt)
18
+ return true;
19
+ const resetMs = Date.parse(c.resetsAt);
20
+ const sampledMs = Date.parse(c.sampledAt);
21
+ if (!Number.isFinite(resetMs) || !Number.isFinite(sampledMs))
22
+ return true;
23
+ return resetMs - sampledMs > STALE_EARLY_MS;
24
+ }
25
+ export function closeSampleStamp(sampledAt) {
26
+ const ms = Date.parse(sampledAt);
27
+ if (!Number.isFinite(ms))
28
+ return null;
29
+ return format(new Date(ms), "EEE d MMM H:mm");
30
+ }
31
+ export function closeResetStamp(resetsAt) {
32
+ const ms = Date.parse(resetsAt);
33
+ if (!Number.isFinite(ms))
34
+ return null;
35
+ return format(new Date(ms), "d MMM HH:mm");
36
+ }
37
+ /** One floored duration for both the compact cell line and the strip note. */
38
+ function earlyParts(ms) {
39
+ if (ms < 60_000)
40
+ return { n: Math.max(1, Math.floor(ms / 1000)), unit: "s" };
41
+ const minutes = Math.floor(ms / 60_000);
42
+ if (minutes < 60)
43
+ return { n: minutes, unit: "m" };
44
+ const hours = Math.floor(minutes / 60);
45
+ if (hours < 48)
46
+ return { n: hours, unit: "h" };
47
+ return { n: Math.floor(hours / 24), unit: "d" };
48
+ }
49
+ const EARLY_UNIT_WORDS = {
50
+ s: "second",
51
+ m: "minute",
52
+ h: "hour",
53
+ d: "day",
54
+ };
55
+ function earlyCompact(ms) {
56
+ const { n, unit } = earlyParts(ms);
57
+ return `${n}${unit}`;
58
+ }
59
+ function earlyWords(ms) {
60
+ const { n, unit } = earlyParts(ms);
61
+ return `${n} ${EARLY_UNIT_WORDS[unit]}${n === 1 ? "" : "s"}`;
62
+ }
63
+ /** Four-cell strip of closed weekly windows, oldest left. Fill is used at
64
+ * close, the hatch is the stored leftover. One cell per real close, never a
65
+ * placeholder. Stale samples name their reading on the cell and under the
66
+ * strip. */
67
+ export function ClosedWeeks({ closes }) {
68
+ if (!Array.isArray(closes) || closes.length === 0)
69
+ return null;
70
+ const cells = [...closes].reverse();
71
+ const labels = cells.map((c) => closeDateLabel(c.resetsAt));
72
+ return (_jsxs("section", { className: "dsec", "aria-label": "Closed weeks", children: [_jsx("h3", { children: "Closed weeks" }), _jsx("div", { className: "weeks", "data-testid": "closed-weeks", children: cells.map((c, i) => {
73
+ const used = Math.min(100, Math.max(0, c.usedPct));
74
+ const leftover = Math.min(100, Math.max(0, c.leftoverPct));
75
+ const stale = closeIsStale(c);
76
+ const sampled = closeSampleStamp(c.sampledAt);
77
+ const reset = c.resetsAt ? closeResetStamp(c.resetsAt) : null;
78
+ const earlyMs = c.resetsAt ? Date.parse(c.resetsAt) - Date.parse(c.sampledAt) : NaN;
79
+ const est = c.resetsAtEstimated ? " (est.)" : "";
80
+ const title = [
81
+ `${c.usedPct}% used`,
82
+ `${c.leftoverPct}% leftover`,
83
+ reset ? `reset ${reset}${est}` : "reset unknown",
84
+ stale && sampled ? `last reading ${sampled}` : null,
85
+ ]
86
+ .filter(Boolean)
87
+ .join(", ");
88
+ return (_jsxs("div", { className: "week", title: title, children: [_jsxs("div", { className: "track", role: "img", "aria-label": title, children: [_jsx("div", { className: "fill", style: { width: `${used}%` } }), leftover > 0 && (_jsx("div", { className: "band band-waste", style: { left: `${used}%`, width: `${leftover}%` } }))] }), _jsxs("div", { className: "lab", children: [_jsx("span", { children: labels[i] ?? "date unknown" }), _jsxs("b", { children: [c.leftoverPct, "% leftover", est] }), stale && Number.isFinite(earlyMs) && earlyMs > 0 && (_jsxs("span", { className: "stale-note", children: ["read ", earlyCompact(earlyMs), " early"] }))] })] }, `${c.detectedAt}-${c.sampledAt}`));
89
+ }) }), cells.map((c, i) => {
90
+ const resetMs = c.resetsAt ? Date.parse(c.resetsAt) : NaN;
91
+ const sampledMs = Date.parse(c.sampledAt);
92
+ const sampled = closeSampleStamp(c.sampledAt);
93
+ if (!closeIsStale(c) ||
94
+ !sampled ||
95
+ !Number.isFinite(resetMs) ||
96
+ !Number.isFinite(sampledMs) ||
97
+ resetMs <= sampledMs) {
98
+ return null;
99
+ }
100
+ return (_jsxs("p", { className: "cell-s stale-note", children: [labels[i] ?? "Date unknown", " leftover is from the ", sampled, " reading, ", earlyWords(resetMs - sampledMs), " ", "before that reset."] }, `note-${c.detectedAt}-${c.sampledAt}`));
101
+ })] }));
102
+ }
@@ -1,9 +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" | "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";
4
4
  /**
5
5
  * Pace-state badge from the mapping table. Exclusion always wins; otherwise
6
- * status "at risk" wins; otherwise urgency decides. No client thresholds.
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.
7
9
  */
8
10
  export declare function paceBadge(provider: ProviderView): PaceBadge;
9
11
  export interface PaceFigures {
@@ -12,11 +14,14 @@ export interface PaceFigures {
12
14
  /** Recent burn: last-24h rolling rate. Null unless paceSource is "recent". */
13
15
  recent: number | null;
14
16
  }
15
- /** Split the advisory into its two displayed paces. `burnRate` doubles as the
16
- * forecast input, so the recent figure only exists when it was measured. */
17
+ /** Split the advisory into its two displayed paces. The 24h figure is the
18
+ * measured `recentRate`, not the blended `burnRate` forecast input; an
19
+ * advisory from a daemon older than the blend falls back to `burnRate`,
20
+ * which was the raw measured rate then. */
17
21
  export declare function paceFigures(advisory: {
18
22
  avgPace?: number | null;
19
23
  burnRate: number | null;
24
+ recentRate?: number | null;
20
25
  paceSource: string;
21
26
  } | null | undefined): PaceFigures;
22
27
  export interface PaceCells {
@@ -29,6 +34,7 @@ export interface PaceCells {
29
34
  export declare function paceCells(advisory: {
30
35
  avgPace?: number | null;
31
36
  burnRate: number | null;
37
+ recentRate?: number | null;
32
38
  paceSource: string;
33
39
  } | null | undefined): PaceCells;
34
40
  /** Hover explanations shared by cards and rows. Plain words, no jargon. */
@@ -48,6 +54,7 @@ export interface PaceLine {
48
54
  export declare function paceLines(advisory: {
49
55
  avgPace?: number | null;
50
56
  burnRate: number | null;
57
+ recentRate?: number | null;
51
58
  paceSource: string;
52
59
  idealRate: number;
53
60
  } | null | undefined): PaceLine[];
@@ -89,12 +96,17 @@ export declare function barGeometry(provider: ProviderView, asOfMs: number): Bar
89
96
  export declare function badgeColor(badge: PaceBadge): string;
90
97
  export declare function fillToken(badge: PaceBadge): string;
91
98
  export declare function edgeToken(badge: PaceBadge): string;
92
- export declare function outKind(badge: PaceBadge): "behind" | "cap" | "ontrack" | "none";
99
+ export declare function outKind(badge: PaceBadge): "behind" | "watch" | "cap" | "ontrack" | "none";
93
100
  /**
94
101
  * Hatch fill for unused quota. Ink mix (not an alpha suffix on a custom
95
102
  * property) so the browser always paints a gradient.
96
103
  */
97
104
  export declare function hatchGradient(_badge?: PaceBadge): string;
105
+ /**
106
+ * Badge hover copy: red and amber explain the verdict; an estimated reset is
107
+ * confessed on the end. Other badges read their own label and need no title.
108
+ */
109
+ export declare function badgeTitle(badge: PaceBadge, estimated: boolean): string | undefined;
98
110
  export declare function Badge({ provider }: {
99
111
  provider: ProviderView;
100
112
  }): React.JSX.Element;
@@ -2,7 +2,9 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { ageDuration } from "../state.js";
3
3
  /**
4
4
  * Pace-state badge from the mapping table. Exclusion always wins; otherwise
5
- * status "at risk" wins; otherwise urgency decides. No client thresholds.
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.
6
8
  */
7
9
  export function paceBadge(provider) {
8
10
  if (provider.exclusionReason !== null)
@@ -12,8 +14,12 @@ export function paceBadge(provider) {
12
14
  return "Not reporting";
13
15
  if (advisory.status === "at risk")
14
16
  return "Cap risk";
17
+ if (advisory.status === "watch")
18
+ return "Watch";
15
19
  if (advisory.status === "unknown")
16
20
  return "Measuring";
21
+ if (advisory.aheadOfElapsed === true)
22
+ return "Ahead of pace";
17
23
  switch (advisory.urgency) {
18
24
  case "burn now":
19
25
  case "use soon":
@@ -25,13 +31,19 @@ export function paceBadge(provider) {
25
31
  return "On track";
26
32
  }
27
33
  }
28
- /** Split the advisory into its two displayed paces. `burnRate` doubles as the
29
- * forecast input, so the recent figure only exists when it was measured. */
34
+ /** Split the advisory into its two displayed paces. The 24h figure is the
35
+ * measured `recentRate`, not the blended `burnRate` forecast input; an
36
+ * advisory from a daemon older than the blend falls back to `burnRate`,
37
+ * which was the raw measured rate then. */
30
38
  export function paceFigures(advisory) {
31
39
  if (!advisory)
32
40
  return { avg: null, recent: null };
33
41
  const avg = advisory.avgPace ?? null;
34
- const recent = advisory.paceSource === "recent" ? advisory.burnRate : null;
42
+ const recent = advisory.recentRate !== undefined
43
+ ? advisory.recentRate
44
+ : advisory.paceSource === "recent"
45
+ ? advisory.burnRate
46
+ : null;
35
47
  return { avg, recent };
36
48
  }
37
49
  /** Card pace cells: average and 24h rate side by side, each "—" when unknown.
@@ -187,6 +199,8 @@ export function badgeColor(badge) {
187
199
  return "var(--warn)";
188
200
  case "Ahead of pace":
189
201
  return "var(--ahead)";
202
+ case "Watch":
203
+ return "var(--watch)";
190
204
  case "Cap risk":
191
205
  return "var(--danger)";
192
206
  case "Measuring":
@@ -203,6 +217,8 @@ export function fillToken(badge) {
203
217
  return "var(--fill-behind)";
204
218
  case "Ahead of pace":
205
219
  return "var(--fill-ahead)";
220
+ case "Watch":
221
+ return "var(--fill-watch)";
206
222
  case "Cap risk":
207
223
  return "var(--fill-cap)";
208
224
  case "Measuring":
@@ -219,6 +235,8 @@ export function edgeToken(badge) {
219
235
  return "var(--edge-behind)";
220
236
  case "Ahead of pace":
221
237
  return "var(--edge-ahead)";
238
+ case "Watch":
239
+ return "var(--edge-watch)";
222
240
  case "Cap risk":
223
241
  return "var(--edge-cap)";
224
242
  case "Measuring":
@@ -231,6 +249,8 @@ export function outKind(badge) {
231
249
  switch (badge) {
232
250
  case "Behind pace":
233
251
  return "behind";
252
+ case "Watch":
253
+ return "watch";
234
254
  case "Cap risk":
235
255
  return "cap";
236
256
  case "On track":
@@ -246,6 +266,22 @@ export function outKind(badge) {
246
266
  export function hatchGradient(_badge) {
247
267
  return "repeating-linear-gradient(45deg, color-mix(in oklch, var(--ink) 20%, transparent) 0 1.5px, transparent 1.5px 7px)";
248
268
  }
269
+ /**
270
+ * Badge hover copy: red and amber explain the verdict; an estimated reset is
271
+ * confessed on the end. Other badges read their own label and need no title.
272
+ */
273
+ export function badgeTitle(badge, estimated) {
274
+ let title;
275
+ if (badge === "Cap risk") {
276
+ title = "Burning faster than the window allows and ahead of elapsed time";
277
+ }
278
+ else if (badge === "Watch") {
279
+ title = "Pace points at the cap, but position or the reset clock is uncertain";
280
+ }
281
+ if (title && estimated)
282
+ title += " (estimated reset)";
283
+ return title;
284
+ }
249
285
  export function Badge({ provider }) {
250
286
  const badge = paceBadge(provider);
251
287
  const detail = exclusionDetail(provider.exclusionReason);
@@ -255,10 +291,12 @@ export function Badge({ provider }) {
255
291
  ? "pace-ontrack"
256
292
  : badge === "Ahead of pace"
257
293
  ? "pace-ahead"
258
- : badge === "Cap risk"
259
- ? "pace-cap"
260
- : "pace-out"; // Not reporting and Measuring share the neutral style
261
- return (_jsxs("span", { "data-testid": "pace-badge", className: `pace ${badgeClass}`, children: [badge, detail ? ` · ${detail}` : ""] }));
294
+ : badge === "Watch"
295
+ ? "pace-watch"
296
+ : badge === "Cap risk"
297
+ ? "pace-cap"
298
+ : "pace-out"; // Not reporting and Measuring share the neutral style
299
+ return (_jsxs("span", { "data-testid": "pace-badge", className: `pace ${badgeClass}`, title: badgeTitle(badge, provider.quota?.resetsAtEstimated === true), children: [badge, detail ? ` · ${detail}` : ""] }));
262
300
  }
263
301
  /**
264
302
  * Two-tier pacing rail: used fill, elapsed tick, projected-unused hatch.
@@ -316,6 +354,11 @@ export function forecastLine(provider) {
316
354
  ? `Cap risk · exhausts in ${advisory.daysToExhaust.toFixed(1)}d at current pace`
317
355
  : "Cap risk at current pace";
318
356
  }
357
+ if (advisory.status === "watch") {
358
+ return advisory.daysToExhaust !== null && Number.isFinite(advisory.daysToExhaust)
359
+ ? `Watch · exhausts in ${advisory.daysToExhaust.toFixed(1)}d at current pace`
360
+ : "Watch at current pace";
361
+ }
319
362
  if (advisory.wastePct !== null) {
320
363
  return `${Math.round(advisory.wastePct)}% waste in ${advisory.daysLeft.toFixed(1)}d`;
321
364
  }
@@ -5,6 +5,7 @@ const STATES = [
5
5
  { label: "Behind pace", cls: "pace-behind", meaning: "Burning slow; risk of unused quota waste" },
6
6
  { label: "Ahead of pace", cls: "pace-ahead", meaning: "Burning faster than time; monitor usage" },
7
7
  { label: "Cap risk", cls: "pace-cap", meaning: "Will hit cap before reset window closes" },
8
+ { label: "Watch", cls: "pace-watch", meaning: "Pace points at the cap, but position or the reset clock is uncertain" },
8
9
  { label: "Measuring", cls: "pace-out", meaning: "Not enough data yet; pace appears after 6h" },
9
10
  { label: "Not reporting", cls: "pace-out", meaning: "No usable reading; excluded from ranking" },
10
11
  ];
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  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
+ import { lastClose } from "./ClosedWeeks.js";
5
6
  function resetsDate(provider) {
6
7
  if (!provider.quota)
7
8
  return null;
@@ -19,10 +20,11 @@ export function ProviderCard({ provider, recommended, asOf, onSelect, }) {
19
20
  const resetsIn = quota ? timeLeft(quota.resetsAt, asOfMs) : null;
20
21
  const name = provider.displayName;
21
22
  const plan = displayPlan(quota?.plan);
23
+ const last = lastClose(provider);
22
24
  return (_jsxs("article", { "data-testid": `provider-card-${provider.id}`, role: "button", tabIndex: 0, "aria-label": `${name} details`, onClick: () => onSelect(provider.id), onKeyDown: (e) => {
23
25
  if (e.key === "Enter" || e.key === " ") {
24
26
  e.preventDefault();
25
27
  onSelect(provider.id);
26
28
  }
27
- }, className: `pcard ${recommended ? "is-recommended is-top-pick" : ""}`, children: [_jsxs("div", { className: "pcard-head", children: [_jsx("span", { className: "picon", style: providerTint(provider.id), "aria-hidden": "true", children: _jsx(ProviderIcon, { id: provider.id, title: name }) }), _jsxs("div", { className: "who", children: [_jsxs("b", { children: [name, recommended && (_jsx("span", { className: "rec-icon", title: "Recommended for next session", role: "img", "aria-label": "recommended", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.3", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }) }))] }), 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: [_jsx("span", { children: resetsDate(provider) ?? resetCountdown(provider, asOfMs) }), _jsx("span", { className: "rowbtn", "aria-hidden": "true", children: "Inspect" })] })] }));
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" })] })] }));
28
30
  }
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { ageDuration, displayPlan, providerSubtitle, shortWindowLabel } from "../state.js";
4
4
  import { Badge, PaceBar, barGeometry, isEstimated, paceBadge, paceFigures, resetClock, timeLeft, } from "./PaceBar.js";
5
5
  import { ProviderIcon, providerTint } from "./ProviderIcon.js";
6
+ import { ClosedWeeks } from "./ClosedWeeks.js";
6
7
  import { renameProvider } from "../api.js";
7
8
  export function exclusionWords(reason) {
8
9
  switch (reason) {
@@ -120,6 +121,8 @@ export function pacingStatusToken(provider) {
120
121
  return "ontrack";
121
122
  case "Ahead of pace":
122
123
  return "ahead";
124
+ case "Watch":
125
+ return "watch";
123
126
  case "Cap risk":
124
127
  return "cap";
125
128
  case "Measuring":
@@ -181,6 +184,9 @@ export function rankingCopy(provider, recommendation) {
181
184
  else if (advisory?.status === "at risk") {
182
185
  decision = "Forecast to hit the cap before reset; flagged to ease off.";
183
186
  }
187
+ else if (advisory?.status === "watch") {
188
+ decision = "Near the cap — position or reset clock uncertain.";
189
+ }
184
190
  else if (advisory?.urgency === "slow down") {
185
191
  decision = "Burning faster than the window; ease off.";
186
192
  }
@@ -362,7 +368,7 @@ export function ProviderDrawer({ provider, asOf, recommendation, onClose, onRena
362
368
  width: `${Math.min(100, Math.max(0, model.sessionPct))}%`,
363
369
  background: "var(--fill-ontrack)",
364
370
  ["--fill-edge"]: "var(--edge-ontrack)",
365
- } }) })] })), _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: [
371
+ } }) })] })), _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: [
366
372
  provider.quota?.plan && provider.quota.plan !== "unknown" ? provider.quota.plan : null,
367
373
  resetStamp(provider) ? `resets ${resetStamp(provider)}` : null,
368
374
  ]
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { displayPlan, shortWindowLabel } from "../state.js";
3
3
  import { Badge, PaceBar, forecastLine, paceLines, resetCellLines, } from "./PaceBar.js";
4
+ import { lastClose } from "./ClosedWeeks.js";
4
5
  export function ProviderRow({ provider, recommended, asOf, onSelect, }) {
5
6
  const quota = provider.quota;
6
7
  const advisory = provider.advisory;
@@ -10,7 +11,8 @@ export function ProviderRow({ provider, recommended, asOf, onSelect, }) {
10
11
  const plan = displayPlan(quota?.plan);
11
12
  const shortWindow = shortWindowLabel(provider.id);
12
13
  const reset = resetCellLines(provider, asOfMs);
13
- 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 })] }), _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) => {
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) => {
14
16
  e.stopPropagation();
15
17
  onSelect(provider.id);
16
18
  }, children: "Inspect" }), _jsx("span", { className: "sr-only", children: forecastLine(provider) })] }));
@@ -56,9 +56,18 @@ export function Recommendation({ recommendation, providers, asOf, onSelectProvid
56
56
  const { useMore, easeOff } = lanesFor(recommendation.advisories, providers);
57
57
  const asOfMs = Date.parse(asOf);
58
58
  const measuring = recommendation.use !== "none" && recommendation.wastePct === null;
59
- const isClear = recommendation.use === "none" && useMore.length === 0 && easeOff.length === 0;
59
+ // Lane membership keys off urgency, and both risk tiers can carry urgency
60
+ // "on track" (burn inside 1.4x ideal), so a board with any at-risk or Watch
61
+ // provider is never "clear" even when both lanes are empty.
62
+ const hasRiskOrWatch = recommendation.advisories.some((a) => a.status === "at risk" || a.status === "watch");
63
+ const isClear = recommendation.use === "none" && useMore.length === 0 && easeOff.length === 0 && !hasRiskOrWatch;
60
64
  const tracked = providers.filter((p) => p.enabled && p.id !== "manual").length;
61
- const easeTint = easeOff.some((a) => a.status === "at risk") ? "tint-cap" : "tint-ahead";
65
+ const easeTint = easeOff.some((a) => a.status === "watch")
66
+ ? "tint-watch"
67
+ : easeOff.some((a) => a.status === "at risk")
68
+ ? "tint-cap"
69
+ : "tint-ahead";
70
+ const easeLane = easeTint === "tint-cap" ? "l-cap" : easeTint === "tint-watch" ? "l-watch" : "l-ahead";
62
71
  const pick = providers.find((p) => p.id === recommendation.use);
63
72
  const pickLeft = pick?.quota ? timeLeft(pick.quota.resetsAt, asOfMs) : null;
64
73
  const openAdvice = () => {
@@ -72,5 +81,5 @@ export function Recommendation({ recommendation, providers, asOf, onSelectProvid
72
81
  background: "var(--ahead-soft)",
73
82
  color: "var(--ahead)",
74
83
  borderColor: "var(--ahead)",
75
- }, children: "measuring pace" }))] }), recommendation.use !== "none" && (_jsxs("button", { type: "button", className: "btn", "data-testid": "view-advice", title: "Open recommended provider", onClick: openAdvice, children: ["View advice", _jsx("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M5 12h14M12 5l7 7-7 7" }) })] }))] }), (useMore.length > 0 || easeOff.length > 0) && (_jsxs("div", { className: "recB", children: [_jsx(LaneSection, { testId: "lane-use-more", label: "Use more", tintClass: "tint-behind", laneClass: "l-behind", icon: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.6", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M12 20V5M6 11l6-6 6 6" }) }), members: useMore, providers: providers, asOfMs: asOfMs, onSelectProvider: onSelectProvider }), _jsx(LaneSection, { testId: "lane-ease-off", label: "Ease off", tintClass: easeTint, laneClass: easeTint === "tint-cap" ? "l-cap" : "l-ahead", icon: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.6", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M12 4v15M6 13l6 6 6-6" }) }), members: easeOff, providers: providers, asOfMs: asOfMs, onSelectProvider: onSelectProvider })] }))] })] }));
84
+ }, children: "measuring pace" }))] }), recommendation.use !== "none" && (_jsxs("button", { type: "button", className: "btn", "data-testid": "view-advice", title: "Open recommended provider", onClick: openAdvice, children: ["View advice", _jsx("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M5 12h14M12 5l7 7-7 7" }) })] }))] }), (useMore.length > 0 || easeOff.length > 0) && (_jsxs("div", { className: "recB", children: [_jsx(LaneSection, { testId: "lane-use-more", label: "Use more", tintClass: "tint-behind", laneClass: "l-behind", icon: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.6", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M12 20V5M6 11l6-6 6 6" }) }), members: useMore, providers: providers, asOfMs: asOfMs, onSelectProvider: onSelectProvider }), _jsx(LaneSection, { testId: "lane-ease-off", label: "Ease off", tintClass: easeTint, laneClass: easeLane, icon: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.6", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M12 4v15M6 13l6 6 6-6" }) }), members: easeOff, providers: providers, asOfMs: asOfMs, onSelectProvider: onSelectProvider })] }))] })] }));
76
85
  }
@@ -134,6 +134,8 @@ function pinPace(provider) {
134
134
  return { cls: "pace-ontrack", color: "var(--good)" };
135
135
  case "Ahead of pace":
136
136
  return { cls: "pace-ahead", color: "var(--ahead)" };
137
+ case "Watch":
138
+ return { cls: "pace-watch", color: "var(--watch)" };
137
139
  case "Cap risk":
138
140
  return { cls: "pace-cap", color: "var(--danger)" };
139
141
  default: