quotacap 0.0.54 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advisory/engine.d.ts +5 -2
- package/dist/advisory/engine.js +28 -10
- package/dist/advisory/snapshot.js +8 -2
- package/dist/advisory/types.d.ts +2 -0
- package/dist/config.js +1 -1
- package/dist/store/quotas.d.ts +4 -0
- package/dist/store/quotas.js +52 -0
- package/dist/version.js +1 -1
- package/dist/web/src/components/BurnDown.d.ts +1 -0
- package/dist/web/src/components/BurnDown.js +65 -27
- package/dist/web/src/components/PaceBar.js +5 -1
- package/dist/web/src/components/ProviderDrawer.js +15 -6
- package/dist/web/src/components/settings/DaemonPanel.js +1 -1
- package/dist/web/src/state.d.ts +1 -0
- package/dist/web/src/state.js +1 -1
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-Bc6C7q-U.js +13 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-C8-Ig-3l.js +0 -13
|
@@ -9,10 +9,13 @@ import type { Advisory, RecommendationBasis, BindingWindow } from "./types.js";
|
|
|
9
9
|
* when less than MIN_PACE_SPAN_DAYS has elapsed (annualizing a smaller
|
|
10
10
|
* denominator fabricates verdicts).
|
|
11
11
|
*/
|
|
12
|
-
export declare function averagePace(q: Quota, now?: Date): number | null;
|
|
12
|
+
export declare function averagePace(q: Quota, now?: Date, exhaustedAt?: string | null): number | null;
|
|
13
13
|
export declare function blendForecast(recent: number | null, baseline: number | null): number | null;
|
|
14
14
|
export declare function baselineFor(historyAvg: number | null, q: Quota, now: Date): number | null;
|
|
15
|
-
export declare function computeAdvisory(q: Quota, recent: number | null, baseline: number | null, now?: Date
|
|
15
|
+
export declare function computeAdvisory(q: Quota, recent: number | null, baseline: number | null, now?: Date, exhaustedTimestamps?: string | {
|
|
16
|
+
weekly?: string | null;
|
|
17
|
+
monthly?: string | null;
|
|
18
|
+
} | null): Advisory;
|
|
16
19
|
export declare function recommend(quotas: Quota[], _task: string, paceByProvider?: Map<string, {
|
|
17
20
|
recent: number | null;
|
|
18
21
|
baseline: number | null;
|
package/dist/advisory/engine.js
CHANGED
|
@@ -9,16 +9,22 @@ const DAY_MS = 86400000;
|
|
|
9
9
|
* when less than MIN_PACE_SPAN_DAYS has elapsed (annualizing a smaller
|
|
10
10
|
* denominator fabricates verdicts).
|
|
11
11
|
*/
|
|
12
|
-
export function averagePace(q, now = new Date()) {
|
|
12
|
+
export function averagePace(q, now = new Date(), exhaustedAt) {
|
|
13
13
|
if (!q.periodStart)
|
|
14
14
|
return null;
|
|
15
15
|
const start = new Date(q.periodStart).getTime();
|
|
16
16
|
if (Number.isNaN(start))
|
|
17
17
|
return null;
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const endMs = exhaustedAt && q.weeklyPct >= 100
|
|
19
|
+
? new Date(exhaustedAt).getTime()
|
|
20
|
+
: now.getTime();
|
|
21
|
+
if (Number.isNaN(endMs))
|
|
20
22
|
return null;
|
|
21
|
-
const
|
|
23
|
+
const elapsedDays = (endMs - start) / DAY_MS;
|
|
24
|
+
const effectiveDays = q.weeklyPct >= 100 ? Math.max(MIN_PACE_SPAN_DAYS, elapsedDays) : elapsedDays;
|
|
25
|
+
if (effectiveDays < MIN_PACE_SPAN_DAYS)
|
|
26
|
+
return null;
|
|
27
|
+
const pace = q.weeklyPct / effectiveDays;
|
|
22
28
|
if (!Number.isFinite(pace))
|
|
23
29
|
return null;
|
|
24
30
|
return Math.max(0, pace);
|
|
@@ -57,15 +63,15 @@ function elapsedPctOf(q, now) {
|
|
|
57
63
|
return null;
|
|
58
64
|
return Math.min(100, Math.max(0, (elapsed / span) * 100));
|
|
59
65
|
}
|
|
60
|
-
export function computeAdvisory(q, recent, baseline, now = new Date()) {
|
|
66
|
+
export function computeAdvisory(q, recent, baseline, now = new Date(), exhaustedTimestamps) {
|
|
61
67
|
const resets = new Date(q.resetsAt);
|
|
62
68
|
const daysLeft = Math.max(0.1, (resets.getTime() - now.getTime()) / DAY_MS);
|
|
63
69
|
const remaining = 100 - q.weeklyPct;
|
|
64
70
|
const idealRate = remaining / daysLeft;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
71
|
+
const weeklyExhaustedAt = typeof exhaustedTimestamps === "string"
|
|
72
|
+
? exhaustedTimestamps
|
|
73
|
+
: exhaustedTimestamps?.weekly ?? (q.weeklyPct >= 100 ? (q.fetchedAt ?? null) : null);
|
|
74
|
+
const avgPace = averagePace(q, now, weeklyExhaustedAt);
|
|
69
75
|
const burnRate = blendForecast(recent, baseline);
|
|
70
76
|
// Above every return: the unknown-pace path below is exactly what pool 2
|
|
71
77
|
// ranks, so a binding computed later would leave it on weekly numbers.
|
|
@@ -83,6 +89,16 @@ export function computeAdvisory(q, recent, baseline, now = new Date()) {
|
|
|
83
89
|
bindingDaysLeft = monthlyDaysLeft;
|
|
84
90
|
}
|
|
85
91
|
}
|
|
92
|
+
const monthlyExhaustedAt = typeof exhaustedTimestamps === "object" && exhaustedTimestamps !== null
|
|
93
|
+
? exhaustedTimestamps.monthly ?? (bindingRemaining <= 0 ? (q.fetchedAt ?? null) : null)
|
|
94
|
+
: null;
|
|
95
|
+
const monthlyExhausted = bindingWindow === "monthly" && bindingRemaining <= 0;
|
|
96
|
+
const exhausted = remaining <= 0;
|
|
97
|
+
const exhaustedAt = monthlyExhausted
|
|
98
|
+
? monthlyExhaustedAt
|
|
99
|
+
: exhausted
|
|
100
|
+
? weeklyExhaustedAt
|
|
101
|
+
: null;
|
|
86
102
|
// paceSource describes recent availability; the blend's composition is
|
|
87
103
|
// carried by recentRate and baselineRate.
|
|
88
104
|
const paceSource = recent !== null ? "recent" : burnRate !== null ? "window-average" : "unknown";
|
|
@@ -107,6 +123,7 @@ export function computeAdvisory(q, recent, baseline, now = new Date()) {
|
|
|
107
123
|
bindingWindow,
|
|
108
124
|
bindingRemaining,
|
|
109
125
|
bindingDaysLeft,
|
|
126
|
+
exhaustedAt,
|
|
110
127
|
};
|
|
111
128
|
}
|
|
112
129
|
if (exhausted) {
|
|
@@ -129,6 +146,7 @@ export function computeAdvisory(q, recent, baseline, now = new Date()) {
|
|
|
129
146
|
bindingWindow,
|
|
130
147
|
bindingRemaining,
|
|
131
148
|
bindingDaysLeft,
|
|
149
|
+
exhaustedAt,
|
|
132
150
|
};
|
|
133
151
|
}
|
|
134
152
|
// Red needs a deadband margin and a cumulatively-ahead position on a
|
|
@@ -159,7 +177,7 @@ export function computeAdvisory(q, recent, baseline, now = new Date()) {
|
|
|
159
177
|
urgency = "save";
|
|
160
178
|
if (estimated && urgency === "burn now")
|
|
161
179
|
urgency = "use soon";
|
|
162
|
-
return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, recentRate: recent, baselineRate: baseline, aheadOfElapsed, avgPace, burnMeasured, paceSource, daysToExhaust, status, wastePct, urgency, bindingWindow, bindingRemaining, bindingDaysLeft };
|
|
180
|
+
return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, recentRate: recent, baselineRate: baseline, aheadOfElapsed, avgPace, burnMeasured, paceSource, daysToExhaust, status, wastePct, urgency, bindingWindow, bindingRemaining, bindingDaysLeft, exhaustedAt };
|
|
163
181
|
}
|
|
164
182
|
/**
|
|
165
183
|
* Known-waste sort key. `wastePct` is a flow (budget forecast to go unspent
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAllLatest, getBurnRates, getHistoryBaseline, getWindowCloses } from "../store/quotas.js";
|
|
1
|
+
import { getAllLatest, getBurnRates, getExhaustedTimestamps, getHistoryBaseline, getWindowCloses } from "../store/quotas.js";
|
|
2
2
|
import { getAttempts } from "../store/attempts.js";
|
|
3
3
|
import { getCatalogs } from "../store/catalogs.js";
|
|
4
4
|
import { emptyCatalog } from "../catalog/types.js";
|
|
@@ -27,6 +27,7 @@ function normalizeAdvisory(adv) {
|
|
|
27
27
|
bindingDaysLeft: Number.isFinite(adv.bindingDaysLeft)
|
|
28
28
|
? adv.bindingDaysLeft
|
|
29
29
|
: Number.isFinite(adv.daysLeft) ? adv.daysLeft : 0,
|
|
30
|
+
exhaustedAt: typeof adv.exhaustedAt === "string" ? adv.exhaustedAt : null,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
export function buildSnapshot(db, opts) {
|
|
@@ -49,6 +50,7 @@ export function buildSnapshot(db, opts) {
|
|
|
49
50
|
}
|
|
50
51
|
const unionIds = Array.from(new Set([...opts.enabledProviders, ...storedIds])).sort((a, b) => a.localeCompare(b));
|
|
51
52
|
const burnRates = getBurnRates(db, asOfMs);
|
|
53
|
+
const exhaustedTimestamps = getExhaustedTimestamps(db, asOfMs);
|
|
52
54
|
const history = getHistoryBaseline(db);
|
|
53
55
|
const catalogMap = getCatalogs(db);
|
|
54
56
|
const providerSnapshots = [];
|
|
@@ -128,7 +130,11 @@ export function buildSnapshot(db, opts) {
|
|
|
128
130
|
const recent = burnRates.get(id) ?? null;
|
|
129
131
|
const baseline = baselineFor(history.get(id) ?? null, quota, now);
|
|
130
132
|
pace = { recent, baseline };
|
|
131
|
-
|
|
133
|
+
const exhausted = {
|
|
134
|
+
weekly: exhaustedTimestamps.weekly.get(id) ?? null,
|
|
135
|
+
monthly: exhaustedTimestamps.monthly.get(id) ?? null,
|
|
136
|
+
};
|
|
137
|
+
rawAdvisory = computeAdvisory(quota, recent, baseline, now, exhausted);
|
|
132
138
|
}
|
|
133
139
|
const advisory = normalizeAdvisory(rawAdvisory);
|
|
134
140
|
// Controlled evidence vocabulary
|
package/dist/advisory/types.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface Advisory {
|
|
|
46
46
|
bindingRemaining: number;
|
|
47
47
|
/** Days until the binding window resets. Equals `daysLeft` when weekly binds. */
|
|
48
48
|
bindingDaysLeft: number;
|
|
49
|
+
/** ISO timestamp when the binding quota reached 100% in this window, or null if not exhausted. */
|
|
50
|
+
exhaustedAt?: string | null;
|
|
49
51
|
}
|
|
50
52
|
export interface Recommendation {
|
|
51
53
|
use: string;
|
package/dist/config.js
CHANGED
|
@@ -32,7 +32,7 @@ export const ProviderDisplayNameSchema = z
|
|
|
32
32
|
.transform((val) => val.trim());
|
|
33
33
|
const ConfigSchema = z.object({
|
|
34
34
|
port: z.number().default(8787),
|
|
35
|
-
pollMinutes: z.number().default(
|
|
35
|
+
pollMinutes: z.number().default(10),
|
|
36
36
|
catalogTtlHours: z.number().finite().positive().default(6),
|
|
37
37
|
enabledProviders: z.array(z.string()).default(["claude", "codex", "kimi", "grok", "agy", "muse"]),
|
|
38
38
|
knownProviders: z.array(z.string()).default(["claude", "codex", "kimi", "grok", "agy", "muse"]),
|
package/dist/store/quotas.d.ts
CHANGED
|
@@ -25,4 +25,8 @@ export declare function getAllLatest(db: any): any;
|
|
|
25
25
|
export declare const getQuotas: typeof getAllLatest;
|
|
26
26
|
export declare function getSnapshots(db: any): any;
|
|
27
27
|
export declare function getBurnRates(db: any, now?: number): Map<string, number>;
|
|
28
|
+
export declare function getExhaustedTimestamps(db: any, now?: number): {
|
|
29
|
+
weekly: Map<string, string>;
|
|
30
|
+
monthly: Map<string, string>;
|
|
31
|
+
};
|
|
28
32
|
export declare function getHistoryBaseline(db: any, limit?: number): Map<string, number>;
|
package/dist/store/quotas.js
CHANGED
|
@@ -239,6 +239,58 @@ export function getBurnRates(db, now = Date.now()) {
|
|
|
239
239
|
}
|
|
240
240
|
return out;
|
|
241
241
|
}
|
|
242
|
+
export function getExhaustedTimestamps(db, now = Date.now()) {
|
|
243
|
+
let rows = [];
|
|
244
|
+
try {
|
|
245
|
+
rows = db
|
|
246
|
+
.prepare(`SELECT provider, used_pct, fetched_at, resets_at, monthly_pct, monthly_status FROM quotas`)
|
|
247
|
+
.all();
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
if (!/no such column/i.test(String(e?.message ?? "")))
|
|
251
|
+
throw e;
|
|
252
|
+
rows = db
|
|
253
|
+
.prepare(`SELECT provider, used_pct, fetched_at, resets_at FROM quotas`)
|
|
254
|
+
.all();
|
|
255
|
+
}
|
|
256
|
+
const byProvider = new Map();
|
|
257
|
+
for (const r of rows) {
|
|
258
|
+
const t = new Date(r.fetched_at).getTime();
|
|
259
|
+
if (Number.isNaN(t) || t > now)
|
|
260
|
+
continue;
|
|
261
|
+
const pts = byProvider.get(r.provider) ?? [];
|
|
262
|
+
pts.push({
|
|
263
|
+
usedPct: r.used_pct,
|
|
264
|
+
t,
|
|
265
|
+
resetsAt: r.resets_at,
|
|
266
|
+
monthlyPct: r.monthly_pct,
|
|
267
|
+
monthlyStatus: r.monthly_status,
|
|
268
|
+
});
|
|
269
|
+
byProvider.set(r.provider, pts);
|
|
270
|
+
}
|
|
271
|
+
const weekly = new Map();
|
|
272
|
+
const monthly = new Map();
|
|
273
|
+
for (const [provider, all] of byProvider) {
|
|
274
|
+
const sorted = currentCycle(all.sort((a, b) => a.t - b.t));
|
|
275
|
+
if (sorted.length === 0)
|
|
276
|
+
continue;
|
|
277
|
+
const latest = sorted[sorted.length - 1];
|
|
278
|
+
if (latest.usedPct >= 100) {
|
|
279
|
+
const first = sorted.find((p) => p.usedPct >= 100);
|
|
280
|
+
if (first)
|
|
281
|
+
weekly.set(provider, new Date(first.t).toISOString());
|
|
282
|
+
}
|
|
283
|
+
const latestMonthlyExhausted = latest.monthlyStatus === "exhausted" ||
|
|
284
|
+
(typeof latest.monthlyPct === "number" && latest.monthlyPct >= 100);
|
|
285
|
+
if (latestMonthlyExhausted) {
|
|
286
|
+
const firstMonthly = sorted.find((p) => p.monthlyStatus === "exhausted" ||
|
|
287
|
+
(typeof p.monthlyPct === "number" && p.monthlyPct >= 100));
|
|
288
|
+
if (firstMonthly)
|
|
289
|
+
monthly.set(provider, new Date(firstMonthly.t).toISOString());
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { weekly, monthly };
|
|
293
|
+
}
|
|
242
294
|
export function getHistoryBaseline(db, limit = 4) {
|
|
243
295
|
let rows = [];
|
|
244
296
|
try {
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.55";
|
|
@@ -52,38 +52,60 @@ export function burnGeometry(provider, asOfMs) {
|
|
|
52
52
|
const { usedPct, elapsedPct } = barGeometry(provider, asOfMs);
|
|
53
53
|
if (elapsedPct === null || !Number.isFinite(elapsedPct))
|
|
54
54
|
return null;
|
|
55
|
+
let exhaustedElapsedPct = null;
|
|
56
|
+
const isExhausted = usedPct >= 100 ||
|
|
57
|
+
(monthly && advisory.bindingRemaining <= 0) ||
|
|
58
|
+
advisory.remaining <= 0;
|
|
59
|
+
if (isExhausted) {
|
|
60
|
+
if (advisory.exhaustedAt) {
|
|
61
|
+
const exMs = Date.parse(advisory.exhaustedAt);
|
|
62
|
+
const startIso = monthly ? provider.quota.periodStart : quotaWindowStartIso(provider);
|
|
63
|
+
const resetIso = monthly ? provider.quota.monthlyResetsAt : quotaResetsAt(provider);
|
|
64
|
+
const startMs = startIso ? Date.parse(startIso) : NaN;
|
|
65
|
+
const resetMs = resetIso ? Date.parse(resetIso) : NaN;
|
|
66
|
+
if (Number.isFinite(exMs) &&
|
|
67
|
+
Number.isFinite(startMs) &&
|
|
68
|
+
Number.isFinite(resetMs) &&
|
|
69
|
+
resetMs > startMs) {
|
|
70
|
+
exhaustedElapsedPct = Math.min(elapsedPct, Math.max(0, ((exMs - startMs) / (resetMs - startMs)) * 100));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (exhaustedElapsedPct === null) {
|
|
74
|
+
exhaustedElapsedPct = elapsedPct;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
55
77
|
let projXPct;
|
|
56
78
|
let projYPct;
|
|
57
79
|
let exhaustsBeforeReset = false;
|
|
58
|
-
if (
|
|
80
|
+
if (isExhausted) {
|
|
81
|
+
exhaustsBeforeReset = true;
|
|
82
|
+
projYPct = 100;
|
|
83
|
+
projXPct = 100;
|
|
84
|
+
}
|
|
85
|
+
else if (daysToExhaust !== null &&
|
|
59
86
|
Number.isFinite(daysToExhaust) &&
|
|
60
87
|
daysToExhaust < daysLeft) {
|
|
61
88
|
exhaustsBeforeReset = true;
|
|
62
89
|
projYPct = 100;
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
const startIso = quotaWindowStartIso(provider);
|
|
91
|
+
const resetIso = quotaResetsAt(provider);
|
|
92
|
+
const startMs = startIso ? Date.parse(startIso) : NaN;
|
|
93
|
+
const resetMs = resetIso ? Date.parse(resetIso) : NaN;
|
|
94
|
+
if (Number.isFinite(startMs) &&
|
|
95
|
+
Number.isFinite(resetMs) &&
|
|
96
|
+
resetMs > startMs &&
|
|
97
|
+
Number.isFinite(asOfMs)) {
|
|
98
|
+
const totalDays = (resetMs - startMs) / 86_400_000;
|
|
99
|
+
const elapsedDays = (asOfMs - startMs) / 86_400_000;
|
|
100
|
+
projXPct = ((elapsedDays + daysToExhaust) / totalDays) * 100;
|
|
101
|
+
}
|
|
102
|
+
else if (elapsedPct < 100 && daysLeft > 0) {
|
|
103
|
+
projXPct = elapsedPct + (daysToExhaust * (100 - elapsedPct)) / daysLeft;
|
|
65
104
|
}
|
|
66
105
|
else {
|
|
67
|
-
|
|
68
|
-
const resetIso = quotaResetsAt(provider);
|
|
69
|
-
const startMs = startIso ? Date.parse(startIso) : NaN;
|
|
70
|
-
const resetMs = resetIso ? Date.parse(resetIso) : NaN;
|
|
71
|
-
if (Number.isFinite(startMs) &&
|
|
72
|
-
Number.isFinite(resetMs) &&
|
|
73
|
-
resetMs > startMs &&
|
|
74
|
-
Number.isFinite(asOfMs)) {
|
|
75
|
-
const totalDays = (resetMs - startMs) / 86_400_000;
|
|
76
|
-
const elapsedDays = (asOfMs - startMs) / 86_400_000;
|
|
77
|
-
projXPct = ((elapsedDays + daysToExhaust) / totalDays) * 100;
|
|
78
|
-
}
|
|
79
|
-
else if (elapsedPct < 100 && daysLeft > 0) {
|
|
80
|
-
projXPct = elapsedPct + (daysToExhaust * (100 - elapsedPct)) / daysLeft;
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
projXPct = 100;
|
|
84
|
-
}
|
|
85
|
-
projXPct = Math.min(100, Math.max(elapsedPct, projXPct));
|
|
106
|
+
projXPct = 100;
|
|
86
107
|
}
|
|
108
|
+
projXPct = Math.min(100, Math.max(elapsedPct, projXPct));
|
|
87
109
|
}
|
|
88
110
|
else {
|
|
89
111
|
projXPct = 100;
|
|
@@ -99,12 +121,17 @@ export function burnGeometry(provider, asOfMs) {
|
|
|
99
121
|
projYPct,
|
|
100
122
|
exhaustsBeforeReset,
|
|
101
123
|
tone: burnTone(provider),
|
|
124
|
+
exhaustedElapsedPct,
|
|
102
125
|
};
|
|
103
126
|
}
|
|
104
127
|
export function burnAriaLabel(provider, geo) {
|
|
105
128
|
const used = Math.round(geo.usedPct);
|
|
106
129
|
const elapsed = Math.round(geo.elapsedPct);
|
|
107
130
|
const window = `${quotaWindowName(provider).toLowerCase()} quota window`;
|
|
131
|
+
if (geo.exhaustedElapsedPct !== null && geo.usedPct >= 100) {
|
|
132
|
+
const exElapsed = Math.round(geo.exhaustedElapsedPct);
|
|
133
|
+
return `Burn-down: 100 percent used at ${exElapsed} percent elapsed, currently ${elapsed} percent elapsed (${window})`;
|
|
134
|
+
}
|
|
108
135
|
if (geo.exhaustsBeforeReset) {
|
|
109
136
|
return `Burn-down: ${used} percent used at ${elapsed} percent elapsed, forecast exhausts the quota before reset (${window})`;
|
|
110
137
|
}
|
|
@@ -126,10 +153,17 @@ function fallbackBasis(provider, asOfMs) {
|
|
|
126
153
|
return PACE_TIPS.recent;
|
|
127
154
|
const startIso = quotaWindowStartIso(provider);
|
|
128
155
|
const start = startIso ? Date.parse(startIso) : NaN;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
const isExhausted = (provider.quota && provider.quota.weeklyPct >= 100) ||
|
|
157
|
+
(advisory.bindingRemaining <= 0);
|
|
158
|
+
const endMs = isExhausted && advisory.exhaustedAt ? Date.parse(advisory.exhaustedAt) : asOfMs;
|
|
159
|
+
if (Number.isFinite(start) && Number.isFinite(endMs) && endMs > start) {
|
|
160
|
+
const days = (endMs - start) / 86_400_000;
|
|
161
|
+
if (days >= 0.1) {
|
|
162
|
+
if (isExhausted && advisory.exhaustedAt) {
|
|
163
|
+
return `averaged over ${days.toFixed(1)} days to reach 100%`;
|
|
164
|
+
}
|
|
132
165
|
return `averaged over ${days.toFixed(1)} days of this window`;
|
|
166
|
+
}
|
|
133
167
|
}
|
|
134
168
|
if (provider.evidence.includes("measured") || advisory.burnMeasured)
|
|
135
169
|
return "Measured";
|
|
@@ -167,9 +201,13 @@ export function BurnDown({ provider, asOf, basis, }) {
|
|
|
167
201
|
const tone = geo.tone;
|
|
168
202
|
const nowX = sx(geo.elapsedPct);
|
|
169
203
|
const nowY = sy(geo.usedPct);
|
|
204
|
+
const isExhausted = geo.exhaustedElapsedPct !== null && geo.usedPct >= 100;
|
|
205
|
+
const exhaustX = isExhausted ? sx(geo.exhaustedElapsedPct) : nowX;
|
|
206
|
+
const dotX = isExhausted ? exhaustX : nowX;
|
|
207
|
+
const dotY = isExhausted ? PLOT.top : nowY;
|
|
170
208
|
const projX = sx(geo.projXPct);
|
|
171
209
|
const projY = sy(geo.projYPct);
|
|
172
|
-
const showProjDot = Math.hypot(projX - nowX, projY - nowY) >= 3;
|
|
210
|
+
const showProjDot = !isExhausted && Math.hypot(projX - nowX, projY - nowY) >= 3;
|
|
173
211
|
const label = burnAriaLabel(provider, geo);
|
|
174
212
|
const windowName = quotaWindowName(provider);
|
|
175
213
|
const elapsedLabel = `${Math.round(geo.elapsedPct)}% elapsed`;
|
|
@@ -179,5 +217,5 @@ export function BurnDown({ provider, asOf, basis, }) {
|
|
|
179
217
|
const idealPct = geo.elapsedPct > 52 ? 35 : 66;
|
|
180
218
|
const idealX = sx(idealPct);
|
|
181
219
|
const idealY = sy(idealPct) - 5;
|
|
182
|
-
return (_jsxs("section", { className: "dsec", "aria-label": "Pace", "data-testid": "burn-down-section", children: [_jsxs("div", { className: "burn-head", children: [_jsx("h3", { children: "Pace" }), _jsxs("span", { className: "burn-win", children: [windowName, " quota window"] })] }), _jsxs("svg", { className: "burn", viewBox: "0 0 400 184", role: "img", "aria-label": label, "data-testid": "burn-down", children: [_jsx("text", { x: 11, y: PLOT_MID_Y, className: "axis-y", transform: `rotate(-90 11 ${PLOT_MID_Y})`, children: "Quota used (%)" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.bottom, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT_MID_Y, x2: PLOT.right, y2: PLOT_MID_Y, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT.top, x2: PLOT.right, y2: PLOT.top, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.left, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("line", { x1: PLOT_MID_X, y1: PLOT.bottom, x2: PLOT_MID_X, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("line", { x1: PLOT.right, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("text", { x: 40, y: PLOT.bottom + 3.5, className: "ylab", children: "0%" }), _jsx("text", { x: 40, y: PLOT_MID_Y + 3.5, className: "ylab", children: "50%" }), _jsx("text", { x: 40, y: PLOT.top + 3.5, className: "ylab", children: "100%" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.top, className: "ideal" }), _jsx("text", { x: n(idealX), y: n(idealY), className: "ideal-lab", textAnchor: "middle", transform: `rotate(-18.5 ${n(idealX)} ${n(idealY)})`, children: "ideal pace" }), _jsx("line", { x1: n(nowX), y1: PLOT.top - 8, x2: n(nowX), y2: PLOT.bottom, className: "now" }), _jsxs("text", { x: n(nowTextX), y: 12, className: "now-lab", textAnchor: "middle", children: ["now \u00B7 ", elapsedLabel] }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: n(nowX), y2: n(nowY), className: `actual ${tone}`, "data-testid": "burn-actual" }), _jsx("line", { x1: n(nowX), y1: n(nowY), x2: n(projX), y2: n(projY), className: `proj ${tone}`, "data-testid": "burn-proj", "data-proj-x": n(geo.projXPct), "data-proj-y": n(geo.projYPct) }), _jsx("circle", { cx: n(
|
|
220
|
+
return (_jsxs("section", { className: "dsec", "aria-label": "Pace", "data-testid": "burn-down-section", children: [_jsxs("div", { className: "burn-head", children: [_jsx("h3", { children: "Pace" }), _jsxs("span", { className: "burn-win", children: [windowName, " quota window"] })] }), _jsxs("svg", { className: "burn", viewBox: "0 0 400 184", role: "img", "aria-label": label, "data-testid": "burn-down", children: [_jsx("text", { x: 11, y: PLOT_MID_Y, className: "axis-y", transform: `rotate(-90 11 ${PLOT_MID_Y})`, children: "Quota used (%)" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.bottom, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT_MID_Y, x2: PLOT.right, y2: PLOT_MID_Y, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT.top, x2: PLOT.right, y2: PLOT.top, className: "grid" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.left, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("line", { x1: PLOT_MID_X, y1: PLOT.bottom, x2: PLOT_MID_X, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("line", { x1: PLOT.right, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.bottom + 4, className: "grid-tick" }), _jsx("text", { x: 40, y: PLOT.bottom + 3.5, className: "ylab", children: "0%" }), _jsx("text", { x: 40, y: PLOT_MID_Y + 3.5, className: "ylab", children: "50%" }), _jsx("text", { x: 40, y: PLOT.top + 3.5, className: "ylab", children: "100%" }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: PLOT.right, y2: PLOT.top, className: "ideal" }), _jsx("text", { x: n(idealX), y: n(idealY), className: "ideal-lab", textAnchor: "middle", transform: `rotate(-18.5 ${n(idealX)} ${n(idealY)})`, children: "ideal pace" }), _jsx("line", { x1: n(nowX), y1: PLOT.top - 8, x2: n(nowX), y2: PLOT.bottom, className: "now" }), _jsxs("text", { x: n(nowTextX), y: 12, className: "now-lab", textAnchor: "middle", children: ["now \u00B7 ", elapsedLabel] }), _jsx("line", { x1: PLOT.left, y1: PLOT.bottom, x2: n(isExhausted ? exhaustX : nowX), y2: n(isExhausted ? PLOT.top : nowY), className: `actual ${tone}`, "data-testid": "burn-actual" }), isExhausted && nowX > exhaustX && (_jsx("line", { x1: n(exhaustX), y1: PLOT.top, x2: n(nowX), y2: PLOT.top, className: `actual ${tone}`, "data-testid": "burn-exhausted-plateau" })), _jsx("line", { x1: n(nowX), y1: n(isExhausted ? PLOT.top : nowY), x2: n(projX), y2: n(projY), className: `proj ${tone}`, "data-testid": "burn-proj", "data-proj-x": n(geo.projXPct), "data-proj-y": n(geo.projYPct) }), _jsx("circle", { cx: n(dotX), cy: n(dotY), r: 4.5, className: `dot ${tone}`, "data-testid": "burn-dot" }), showProjDot && (_jsx("circle", { cx: n(projX), cy: n(projY), r: 3.5, className: `dot ${tone} hollow` })), _jsx("text", { x: PLOT.left, y: 160, className: "xlab", children: "start \u00B7 0%" }), _jsx("text", { x: PLOT_MID_X, y: 160, className: "xlab mid", children: "50%" }), _jsx("text", { x: PLOT.right, y: 160, className: "xlab end", children: "quota reset \u00B7 100%" }), _jsx("text", { x: PLOT_MID_X, y: 178, className: "axis-x", children: "Time elapsed in window (%)" })] }), _jsxs("p", { className: "burn-legend", children: [_jsx("span", { className: "legend-label", children: "Legend" }), _jsxs("span", { className: "legend-item", children: [_jsx("span", { className: `burn-swatch-dot ${tone}`, "aria-hidden": "true" }), isExhausted ? "Dot is quota exhausted" : "Dot is now"] }), _jsxs("span", { className: "legend-item", children: [_jsx("span", { className: `burn-swatch-proj ${tone}`, "aria-hidden": "true" }), "Dashed is forecast"] })] }), _jsx(PaceStats, { provider: provider, asOf: asOf }), _jsx("p", { className: "pacecap", children: _jsx("span", { className: "basis", children: basisLine }) })] }));
|
|
183
221
|
}
|
|
@@ -140,7 +140,11 @@ export function quotaAvgPace(provider, asOfMs) {
|
|
|
140
140
|
const startMs = Date.parse(startIso);
|
|
141
141
|
if (!Number.isFinite(startMs) || !Number.isFinite(asOfMs))
|
|
142
142
|
return null;
|
|
143
|
-
const
|
|
143
|
+
const isExhausted = provider.advisory.bindingRemaining <= 0 || used >= 100;
|
|
144
|
+
const endMs = isExhausted && provider.advisory.exhaustedAt
|
|
145
|
+
? Date.parse(provider.advisory.exhaustedAt)
|
|
146
|
+
: asOfMs;
|
|
147
|
+
const elapsedDays = (endMs - startMs) / 86_400_000;
|
|
144
148
|
if (elapsedDays < 0.25)
|
|
145
149
|
return null;
|
|
146
150
|
const pace = used / elapsedDays;
|
|
@@ -116,10 +116,17 @@ export function paceBasis(provider, asOfMs) {
|
|
|
116
116
|
return "No current reading";
|
|
117
117
|
const startIso = quotaWindowStartIso(provider);
|
|
118
118
|
const start = startIso ? Date.parse(startIso) : NaN;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const isExhausted = (provider.quota && provider.quota.weeklyPct >= 100) ||
|
|
120
|
+
(advisory.bindingRemaining <= 0);
|
|
121
|
+
const endMs = isExhausted && advisory.exhaustedAt ? Date.parse(advisory.exhaustedAt) : asOfMs;
|
|
122
|
+
if (Number.isFinite(start) && Number.isFinite(endMs) && endMs > start) {
|
|
123
|
+
const days = (endMs - start) / 86_400_000;
|
|
124
|
+
if (days >= 0.1) {
|
|
125
|
+
if (isExhausted && advisory.exhaustedAt) {
|
|
126
|
+
return `averaged over ${days.toFixed(1)} days to reach 100%`;
|
|
127
|
+
}
|
|
122
128
|
return `averaged over ${days.toFixed(1)} days of this window`;
|
|
129
|
+
}
|
|
123
130
|
}
|
|
124
131
|
if (provider.evidence.includes("measured") || advisory.burnMeasured)
|
|
125
132
|
return "Measured";
|
|
@@ -312,9 +319,11 @@ export function verdictFor(provider, asOfMs) {
|
|
|
312
319
|
return { tone: "use-more", label: "↑ Use more", detail };
|
|
313
320
|
}
|
|
314
321
|
if (lane === "ease-off" || advisory.status === "at risk" || advisory.status === "watch") {
|
|
315
|
-
const detail = advisory.
|
|
316
|
-
?
|
|
317
|
-
:
|
|
322
|
+
const detail = advisory.remaining <= 0 || paceBadge(provider) === "Quota exhausted"
|
|
323
|
+
? "quota fully used"
|
|
324
|
+
: advisory.daysToExhaust !== null && Number.isFinite(advisory.daysToExhaust)
|
|
325
|
+
? `exhausts in ${daysToDuration(advisory.daysToExhaust)} at current pace`
|
|
326
|
+
: paceBadge(provider);
|
|
318
327
|
return { tone: "ease-off", label: "↓ Ease off", detail };
|
|
319
328
|
}
|
|
320
329
|
return { tone: "neutral", label: "• On track", detail: forecastLine(provider) };
|
|
@@ -14,5 +14,5 @@ function servingPort() {
|
|
|
14
14
|
*/
|
|
15
15
|
export function DaemonPanel() {
|
|
16
16
|
const port = servingPort();
|
|
17
|
-
return (_jsxs("div", { children: [_jsx("h3", { className: "settings-h3", children: "Local Daemon Process" }), _jsx("p", { className: "settings-desc", children: "Background process managing provider adapters and SQLite store." }), _jsxs("dl", { className: "dkv", children: [_jsx("dt", { children: "HTTP Service" }), _jsx("dd", { children: _jsxs("code", { children: ["127.0.0.1:", port ?? "8787"] }) }), _jsx("dt", { children: "Polling Cadence" }), _jsx("dd", { children: "Every 15 minutes (default)" }), _jsx("dt", { children: "Database File" }), _jsx("dd", { children: _jsx("code", { children: "~/.quotacap/quotacap.db" }) })] }), _jsxs("p", { className: "settings-note", children: ["Change the port and polling cadence in ", _jsx("code", { children: "~/.quotacap/config.json" }), " (", _jsx("code", { children: "port" }), ",", " ", _jsx("code", { children: "pollMinutes" }), "; defaults 8787 and
|
|
17
|
+
return (_jsxs("div", { children: [_jsx("h3", { className: "settings-h3", children: "Local Daemon Process" }), _jsx("p", { className: "settings-desc", children: "Background process managing provider adapters and SQLite store." }), _jsxs("dl", { className: "dkv", children: [_jsx("dt", { children: "HTTP Service" }), _jsx("dd", { children: _jsxs("code", { children: ["127.0.0.1:", port ?? "8787"] }) }), _jsx("dt", { children: "Polling Cadence" }), _jsx("dd", { children: "Every 15 minutes (default)" }), _jsx("dt", { children: "Database File" }), _jsx("dd", { children: _jsx("code", { children: "~/.quotacap/quotacap.db" }) })] }), _jsxs("p", { className: "settings-note", children: ["Change the port and polling cadence in ", _jsx("code", { children: "~/.quotacap/config.json" }), " (", _jsx("code", { children: "port" }), ",", " ", _jsx("code", { children: "pollMinutes" }), "; defaults 8787 and 10), then restart the daemon. These controls stay read-only until the server offers a settings API."] })] }));
|
|
18
18
|
}
|
package/dist/web/src/state.d.ts
CHANGED
package/dist/web/src/state.js
CHANGED
|
@@ -96,7 +96,7 @@ export function ageLabel(asOf, nowMs = Date.now()) {
|
|
|
96
96
|
return `read ${Math.floor(hours / 24)}d ago`;
|
|
97
97
|
}
|
|
98
98
|
/** Default poll interval; matches `pollMinutes` in `src/config.ts`. */
|
|
99
|
-
export const DEFAULT_POLL_INTERVAL_MS =
|
|
99
|
+
export const DEFAULT_POLL_INTERVAL_MS = 10 * 60 * 1000;
|
|
100
100
|
/** Health-strip copy for the next scheduled poll. */
|
|
101
101
|
export function nextRefreshLabel(lastCompletedPollAt, polling, nowMs = Date.now(), intervalMs = DEFAULT_POLL_INTERVAL_MS) {
|
|
102
102
|
if (polling === "in-progress")
|