quotacap 0.0.53 → 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/format/rows.d.ts +1 -1
- package/dist/format/rows.js +6 -0
- package/dist/format/terminal.js +1 -0
- 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 +66 -27
- package/dist/web/src/components/PaceBar.d.ts +1 -1
- package/dist/web/src/components/PaceBar.js +23 -3
- package/dist/web/src/components/PacingLegend.js +1 -0
- package/dist/web/src/components/ProviderDrawer.js +15 -6
- package/dist/web/src/components/ResetRail.js +1 -0
- 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-DHHYR0xZ.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/format/rows.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderSnapshot } from "../advisory/types.js";
|
|
2
|
-
export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Watch" | "Measuring" | "Not reporting" | "Monthly exhausted";
|
|
2
|
+
export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Watch" | "Measuring" | "Not reporting" | "Monthly exhausted" | "Quota exhausted";
|
|
3
3
|
export type RailCell = "used" | "tick" | "unused";
|
|
4
4
|
export type SortKey = "recommended" | "reset-asc" | "reset-desc" | "used-desc" | "used-asc";
|
|
5
5
|
export type CompactSuffix = "" | "!" | "~" | "?";
|
package/dist/format/rows.js
CHANGED
|
@@ -171,6 +171,12 @@ export function stateWord(ps) {
|
|
|
171
171
|
// spent until the month resets.
|
|
172
172
|
if (adv.bindingWindow === "monthly" && adv.bindingRemaining === 0)
|
|
173
173
|
return "Monthly exhausted";
|
|
174
|
+
const u = bindingUsedPct(ps);
|
|
175
|
+
const isExhausted = (typeof adv.bindingRemaining === "number" && adv.bindingRemaining <= 0) ||
|
|
176
|
+
(typeof adv.remaining === "number" && adv.remaining <= 0) ||
|
|
177
|
+
(typeof u === "number" && u >= 100);
|
|
178
|
+
if (isExhausted)
|
|
179
|
+
return "Quota exhausted";
|
|
174
180
|
if (adv.status === "at risk")
|
|
175
181
|
return "Cap risk";
|
|
176
182
|
if (adv.status === "watch")
|
package/dist/format/terminal.js
CHANGED
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";
|
|
@@ -5,6 +5,7 @@ import { PACE_TIPS, PaceStats, barGeometry, isMonthlyBound, paceBadge, paceFigur
|
|
|
5
5
|
export function burnTone(provider) {
|
|
6
6
|
const badge = paceBadge(provider);
|
|
7
7
|
switch (badge) {
|
|
8
|
+
case "Quota exhausted":
|
|
8
9
|
case "Cap risk":
|
|
9
10
|
case "Monthly exhausted":
|
|
10
11
|
case "Watch":
|
|
@@ -51,38 +52,60 @@ export function burnGeometry(provider, asOfMs) {
|
|
|
51
52
|
const { usedPct, elapsedPct } = barGeometry(provider, asOfMs);
|
|
52
53
|
if (elapsedPct === null || !Number.isFinite(elapsedPct))
|
|
53
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
|
+
}
|
|
54
77
|
let projXPct;
|
|
55
78
|
let projYPct;
|
|
56
79
|
let exhaustsBeforeReset = false;
|
|
57
|
-
if (
|
|
80
|
+
if (isExhausted) {
|
|
81
|
+
exhaustsBeforeReset = true;
|
|
82
|
+
projYPct = 100;
|
|
83
|
+
projXPct = 100;
|
|
84
|
+
}
|
|
85
|
+
else if (daysToExhaust !== null &&
|
|
58
86
|
Number.isFinite(daysToExhaust) &&
|
|
59
87
|
daysToExhaust < daysLeft) {
|
|
60
88
|
exhaustsBeforeReset = true;
|
|
61
89
|
projYPct = 100;
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
64
104
|
}
|
|
65
105
|
else {
|
|
66
|
-
|
|
67
|
-
const resetIso = quotaResetsAt(provider);
|
|
68
|
-
const startMs = startIso ? Date.parse(startIso) : NaN;
|
|
69
|
-
const resetMs = resetIso ? Date.parse(resetIso) : NaN;
|
|
70
|
-
if (Number.isFinite(startMs) &&
|
|
71
|
-
Number.isFinite(resetMs) &&
|
|
72
|
-
resetMs > startMs &&
|
|
73
|
-
Number.isFinite(asOfMs)) {
|
|
74
|
-
const totalDays = (resetMs - startMs) / 86_400_000;
|
|
75
|
-
const elapsedDays = (asOfMs - startMs) / 86_400_000;
|
|
76
|
-
projXPct = ((elapsedDays + daysToExhaust) / totalDays) * 100;
|
|
77
|
-
}
|
|
78
|
-
else if (elapsedPct < 100 && daysLeft > 0) {
|
|
79
|
-
projXPct = elapsedPct + (daysToExhaust * (100 - elapsedPct)) / daysLeft;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
projXPct = 100;
|
|
83
|
-
}
|
|
84
|
-
projXPct = Math.min(100, Math.max(elapsedPct, projXPct));
|
|
106
|
+
projXPct = 100;
|
|
85
107
|
}
|
|
108
|
+
projXPct = Math.min(100, Math.max(elapsedPct, projXPct));
|
|
86
109
|
}
|
|
87
110
|
else {
|
|
88
111
|
projXPct = 100;
|
|
@@ -98,12 +121,17 @@ export function burnGeometry(provider, asOfMs) {
|
|
|
98
121
|
projYPct,
|
|
99
122
|
exhaustsBeforeReset,
|
|
100
123
|
tone: burnTone(provider),
|
|
124
|
+
exhaustedElapsedPct,
|
|
101
125
|
};
|
|
102
126
|
}
|
|
103
127
|
export function burnAriaLabel(provider, geo) {
|
|
104
128
|
const used = Math.round(geo.usedPct);
|
|
105
129
|
const elapsed = Math.round(geo.elapsedPct);
|
|
106
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
|
+
}
|
|
107
135
|
if (geo.exhaustsBeforeReset) {
|
|
108
136
|
return `Burn-down: ${used} percent used at ${elapsed} percent elapsed, forecast exhausts the quota before reset (${window})`;
|
|
109
137
|
}
|
|
@@ -125,10 +153,17 @@ function fallbackBasis(provider, asOfMs) {
|
|
|
125
153
|
return PACE_TIPS.recent;
|
|
126
154
|
const startIso = quotaWindowStartIso(provider);
|
|
127
155
|
const start = startIso ? Date.parse(startIso) : NaN;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
165
|
return `averaged over ${days.toFixed(1)} days of this window`;
|
|
166
|
+
}
|
|
132
167
|
}
|
|
133
168
|
if (provider.evidence.includes("measured") || advisory.burnMeasured)
|
|
134
169
|
return "Measured";
|
|
@@ -166,9 +201,13 @@ export function BurnDown({ provider, asOf, basis, }) {
|
|
|
166
201
|
const tone = geo.tone;
|
|
167
202
|
const nowX = sx(geo.elapsedPct);
|
|
168
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;
|
|
169
208
|
const projX = sx(geo.projXPct);
|
|
170
209
|
const projY = sy(geo.projYPct);
|
|
171
|
-
const showProjDot = Math.hypot(projX - nowX, projY - nowY) >= 3;
|
|
210
|
+
const showProjDot = !isExhausted && Math.hypot(projX - nowX, projY - nowY) >= 3;
|
|
172
211
|
const label = burnAriaLabel(provider, geo);
|
|
173
212
|
const windowName = quotaWindowName(provider);
|
|
174
213
|
const elapsedLabel = `${Math.round(geo.elapsedPct)}% elapsed`;
|
|
@@ -178,5 +217,5 @@ export function BurnDown({ provider, asOf, basis, }) {
|
|
|
178
217
|
const idealPct = geo.elapsedPct > 52 ? 35 : 66;
|
|
179
218
|
const idealX = sx(idealPct);
|
|
180
219
|
const idealY = sy(idealPct) - 5;
|
|
181
|
-
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 }) })] }));
|
|
182
221
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
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" | "Monthly exhausted";
|
|
3
|
+
export type PaceBadge = "Not reporting" | "Cap risk" | "Watch" | "Behind pace" | "Ahead of pace" | "On track" | "Measuring" | "Monthly exhausted" | "Quota exhausted";
|
|
4
4
|
/**
|
|
5
5
|
* Pace-state badge from the mapping table. Exclusion always wins; a blocked
|
|
6
6
|
* month next; otherwise status "at risk" wins, then "watch", then "unknown";
|
|
@@ -14,6 +14,12 @@ export function paceBadge(provider) {
|
|
|
14
14
|
return "Not reporting";
|
|
15
15
|
if (advisory.bindingWindow === "monthly" && advisory.bindingRemaining === 0)
|
|
16
16
|
return "Monthly exhausted";
|
|
17
|
+
const quotaUsed = quotaUsedPct(provider);
|
|
18
|
+
const isExhausted = (typeof advisory.bindingRemaining === "number" && advisory.bindingRemaining <= 0) ||
|
|
19
|
+
(typeof advisory.remaining === "number" && advisory.remaining <= 0) ||
|
|
20
|
+
(quotaUsed !== null && quotaUsed >= 100);
|
|
21
|
+
if (isExhausted)
|
|
22
|
+
return "Quota exhausted";
|
|
17
23
|
if (advisory.status === "at risk")
|
|
18
24
|
return "Cap risk";
|
|
19
25
|
if (advisory.status === "watch")
|
|
@@ -134,7 +140,11 @@ export function quotaAvgPace(provider, asOfMs) {
|
|
|
134
140
|
const startMs = Date.parse(startIso);
|
|
135
141
|
if (!Number.isFinite(startMs) || !Number.isFinite(asOfMs))
|
|
136
142
|
return null;
|
|
137
|
-
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;
|
|
138
148
|
if (elapsedDays < 0.25)
|
|
139
149
|
return null;
|
|
140
150
|
const pace = used / elapsedDays;
|
|
@@ -399,6 +409,7 @@ export function badgeColor(badge) {
|
|
|
399
409
|
return "var(--ahead)";
|
|
400
410
|
case "Watch":
|
|
401
411
|
return "var(--watch)";
|
|
412
|
+
case "Quota exhausted":
|
|
402
413
|
case "Monthly exhausted":
|
|
403
414
|
case "Cap risk":
|
|
404
415
|
return "var(--danger)";
|
|
@@ -418,6 +429,7 @@ export function fillToken(badge) {
|
|
|
418
429
|
return "var(--fill-ahead)";
|
|
419
430
|
case "Watch":
|
|
420
431
|
return "var(--fill-watch)";
|
|
432
|
+
case "Quota exhausted":
|
|
421
433
|
case "Monthly exhausted":
|
|
422
434
|
case "Cap risk":
|
|
423
435
|
return "var(--fill-cap)";
|
|
@@ -437,6 +449,7 @@ export function edgeToken(badge) {
|
|
|
437
449
|
return "var(--edge-ahead)";
|
|
438
450
|
case "Watch":
|
|
439
451
|
return "var(--edge-watch)";
|
|
452
|
+
case "Quota exhausted":
|
|
440
453
|
case "Monthly exhausted":
|
|
441
454
|
case "Cap risk":
|
|
442
455
|
return "var(--edge-cap)";
|
|
@@ -452,6 +465,7 @@ export function outKind(badge) {
|
|
|
452
465
|
return "behind";
|
|
453
466
|
case "Watch":
|
|
454
467
|
return "watch";
|
|
468
|
+
case "Quota exhausted":
|
|
455
469
|
case "Monthly exhausted":
|
|
456
470
|
case "Cap risk":
|
|
457
471
|
return "cap";
|
|
@@ -474,7 +488,13 @@ export function hatchGradient(_badge) {
|
|
|
474
488
|
*/
|
|
475
489
|
export function badgeTitle(badge, estimated) {
|
|
476
490
|
let title;
|
|
477
|
-
if (badge === "
|
|
491
|
+
if (badge === "Quota exhausted") {
|
|
492
|
+
title = "Quota fully used until reset";
|
|
493
|
+
}
|
|
494
|
+
else if (badge === "Monthly exhausted") {
|
|
495
|
+
title = "Monthly quota fully used until reset";
|
|
496
|
+
}
|
|
497
|
+
else if (badge === "Cap risk") {
|
|
478
498
|
title = "Burning faster than the window allows and ahead of elapsed time";
|
|
479
499
|
}
|
|
480
500
|
else if (badge === "Watch") {
|
|
@@ -495,7 +515,7 @@ export function Badge({ provider, compact = false, }) {
|
|
|
495
515
|
? "pace-ahead"
|
|
496
516
|
: badge === "Watch"
|
|
497
517
|
? "pace-watch"
|
|
498
|
-
: badge === "Cap risk" || badge === "Monthly exhausted"
|
|
518
|
+
: badge === "Cap risk" || badge === "Monthly exhausted" || badge === "Quota exhausted"
|
|
499
519
|
? "pace-cap"
|
|
500
520
|
: "pace-out"; // Not reporting and Measuring share the neutral style
|
|
501
521
|
const title = compact && detail
|
|
@@ -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: "Quota exhausted", cls: "pace-cap", meaning: "Quota is 100% consumed until next reset" },
|
|
8
9
|
{ label: "Watch", cls: "pace-watch", meaning: "Pace points at the cap, but position or the reset clock is uncertain" },
|
|
9
10
|
{ label: "Measuring", cls: "pace-out", meaning: "Not enough data yet; pace appears after 6h" },
|
|
10
11
|
{ label: "Not reporting", cls: "pace-out", meaning: "No usable reading; excluded from ranking" },
|
|
@@ -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) };
|
|
@@ -239,6 +239,7 @@ function pinPace(provider) {
|
|
|
239
239
|
return { cls: "pace-ahead", color: "var(--ahead)" };
|
|
240
240
|
case "Watch":
|
|
241
241
|
return { cls: "pace-watch", color: "var(--watch)" };
|
|
242
|
+
case "Quota exhausted":
|
|
242
243
|
case "Monthly exhausted":
|
|
243
244
|
case "Cap risk":
|
|
244
245
|
return { cls: "pace-cap", color: "var(--danger)" };
|
|
@@ -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")
|