quotacap 0.0.7 → 0.0.9
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 +1 -1
- package/dist/advisory/engine.js +6 -3
- package/dist/advisory/types.d.ts +4 -0
- package/dist/cli/index.js +7 -3
- package/dist/format/table.d.ts +1 -0
- package/dist/format/table.js +21 -0
- package/dist/http/server.js +3 -1
- package/dist/mcp/server.d.ts +12 -2
- package/dist/mcp/server.js +8 -18
- package/dist/src/advisory/engine.d.ts +1 -1
- package/dist/src/advisory/engine.js +6 -3
- package/dist/src/advisory/types.d.ts +4 -0
- package/dist/src/cli/index.js +7 -3
- package/dist/src/format/table.d.ts +1 -0
- package/dist/src/format/table.js +21 -0
- package/dist/src/http/server.js +3 -1
- package/dist/src/mcp/server.d.ts +12 -2
- package/dist/src/mcp/server.js +8 -18
- package/dist/src/store/quotas.d.ts +1 -0
- package/dist/src/store/quotas.js +24 -0
- package/dist/src/version.js +1 -1
- package/dist/src/webAssets.js +1 -1
- package/dist/src/webHtml.js +1 -1
- package/dist/store/quotas.d.ts +1 -0
- package/dist/store/quotas.js +24 -0
- package/dist/tests/advisory/engine.test.js +23 -0
- package/dist/tests/cli/cli.test.js +16 -0
- package/dist/tests/format/table.test.d.ts +1 -0
- package/dist/tests/format/table.test.js +23 -0
- package/dist/tests/mcp/quotas.test.d.ts +1 -0
- package/dist/tests/mcp/quotas.test.js +24 -0
- package/dist/tests/mcp/server.test.js +1 -19
- package/dist/tests/store/db.test.js +18 -1
- package/dist/version.js +1 -1
- package/dist/web/src/App.js +4 -2
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-G2N84rBQ.js → index-C7g0JA14.js} +8 -8
- package/web/dist/index.html +1 -1
package/dist/src/webHtml.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const webHtml = "<!doctype html><html><head><meta charset=\"utf-8\"><title>QuotaCap</title> <script type=\"module\" crossorigin src=\"/assets/index-
|
|
2
|
+
export const webHtml = "<!doctype html><html><head><meta charset=\"utf-8\"><title>QuotaCap</title> <script type=\"module\" crossorigin src=\"/assets/index-C7g0JA14.js\"></script>\n</head><body><div id=\"app\"></div></body></html>\n";
|
package/dist/store/quotas.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export declare function getLatestByProvider(db: any, provider: string): any;
|
|
|
3
3
|
export declare function getAllLatest(db: any): any;
|
|
4
4
|
export declare const getQuotas: typeof getAllLatest;
|
|
5
5
|
export declare function getSnapshots(db: any): any;
|
|
6
|
+
export declare function getBurnRates(db: any): Map<string, number>;
|
package/dist/store/quotas.js
CHANGED
|
@@ -26,3 +26,27 @@ export function getAllLatest(db) {
|
|
|
26
26
|
// alias for plan's getQuotas naming
|
|
27
27
|
export const getQuotas = getAllLatest;
|
|
28
28
|
export function getSnapshots(db) { return db.prepare(`SELECT * FROM snapshots ORDER BY day DESC`).all(); }
|
|
29
|
+
export function getBurnRates(db) {
|
|
30
|
+
const rows = db.prepare(`SELECT day, provider, used_pct FROM snapshots`).all();
|
|
31
|
+
const byProvider = new Map();
|
|
32
|
+
for (const r of rows) {
|
|
33
|
+
const pts = byProvider.get(r.provider) ?? [];
|
|
34
|
+
pts.push({ day: r.day, usedPct: r.used_pct });
|
|
35
|
+
byProvider.set(r.provider, pts);
|
|
36
|
+
}
|
|
37
|
+
const out = new Map();
|
|
38
|
+
for (const [provider, pts] of byProvider) {
|
|
39
|
+
if (pts.length < 2)
|
|
40
|
+
continue;
|
|
41
|
+
const sorted = [...pts].sort((a, b) => a.day.localeCompare(b.day));
|
|
42
|
+
const first = sorted[0];
|
|
43
|
+
const last = sorted[sorted.length - 1];
|
|
44
|
+
const days = (new Date(last.day).getTime() - new Date(first.day).getTime()) / 86400000;
|
|
45
|
+
if (days < 1)
|
|
46
|
+
continue;
|
|
47
|
+
const burn = (last.usedPct - first.usedPct) / days;
|
|
48
|
+
if (burn >= 0)
|
|
49
|
+
out.set(provider, burn);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
@@ -8,4 +8,27 @@ describe("advisory", () => {
|
|
|
8
8
|
expect(adv.wastePct).toBeGreaterThan(0);
|
|
9
9
|
expect(adv.urgency).toBe("burn now");
|
|
10
10
|
});
|
|
11
|
+
it("flags at risk when measured burn exhausts the quota before reset", () => {
|
|
12
|
+
const now = new Date("2026-08-28T06:00:00+10:00");
|
|
13
|
+
const q = { provider: "kimi", usedPct: 16, resetsAt: "2026-08-31T11:00:00+10:00" };
|
|
14
|
+
const adv = computeAdvisory(q, 40, now, true);
|
|
15
|
+
expect(adv.burnMeasured).toBe(true);
|
|
16
|
+
expect(adv.burnRate).toBe(40);
|
|
17
|
+
expect(adv.daysToExhaust).toBeCloseTo(2.1, 1);
|
|
18
|
+
expect(adv.status).toBe("at risk");
|
|
19
|
+
});
|
|
20
|
+
it("marks on track when burn stays under the ideal rate", () => {
|
|
21
|
+
const now = new Date("2026-08-28T06:00:00+10:00");
|
|
22
|
+
const q = { provider: "kimi", usedPct: 16, resetsAt: "2026-08-31T11:00:00+10:00" };
|
|
23
|
+
const adv = computeAdvisory(q, 20, now, true);
|
|
24
|
+
expect(adv.daysToExhaust).toBeCloseTo(4.2, 1);
|
|
25
|
+
expect(adv.status).toBe("on track");
|
|
26
|
+
});
|
|
27
|
+
it("reports unmeasured burn as a default", () => {
|
|
28
|
+
const now = new Date("2026-08-28T06:00:00+10:00");
|
|
29
|
+
const q = { provider: "kimi", usedPct: 16, resetsAt: "2026-08-31T11:00:00+10:00" };
|
|
30
|
+
const adv = computeAdvisory(q, 2, now, false);
|
|
31
|
+
expect(adv.burnMeasured).toBe(false);
|
|
32
|
+
expect(adv.burnRate).toBe(2);
|
|
33
|
+
});
|
|
11
34
|
});
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
4
7
|
const exec = promisify(execFile);
|
|
5
8
|
describe("cli", () => {
|
|
6
9
|
it("--help lists commands", async () => {
|
|
7
10
|
const { stdout } = await exec("node", ["dist/cli/index.js", "--help"]);
|
|
8
11
|
expect(stdout).toMatch(/status/);
|
|
9
12
|
});
|
|
13
|
+
it("status prints the shared quota table", async () => {
|
|
14
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "qc-cli-"));
|
|
15
|
+
const env = { ...process.env, HOME: home };
|
|
16
|
+
try {
|
|
17
|
+
await exec("node", ["dist/cli/index.js", "ingest", "--provider", "kimi", "--text", "Weekly limit 16% used resets in 3d"], { env });
|
|
18
|
+
const { stdout } = await exec("node", ["dist/cli/index.js", "status"], { env });
|
|
19
|
+
expect(stdout).toMatch(/\| Provider \| Used \| Remaining \| Resets \| Days left \|/);
|
|
20
|
+
expect(stdout).toMatch(/\| kimi \| 16% \| 84% \|/);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
}, 10000);
|
|
10
26
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderQuotasTable } from "../../src/format/table.js";
|
|
3
|
+
describe("format table", () => {
|
|
4
|
+
it("renders a markdown table with one row per provider", () => {
|
|
5
|
+
const quotas = [
|
|
6
|
+
{ provider: "kimi", usedPct: 16, resetsAt: "2026-09-01T09:02:00+10:00" },
|
|
7
|
+
{ provider: "claude", usedPct: 37, resetsAt: "2026-09-03T21:00:00+10:00" },
|
|
8
|
+
];
|
|
9
|
+
const advisories = [
|
|
10
|
+
{ provider: "kimi", wastePct: 78.0, daysLeft: 3.0, idealRate: 28.0, burnRate: 40, burnMeasured: true, status: "at risk" },
|
|
11
|
+
{ provider: "claude", wastePct: 52.9, daysLeft: 5.5, idealRate: 11.6, burnRate: 2, burnMeasured: false, status: "on track" },
|
|
12
|
+
];
|
|
13
|
+
const table = renderQuotasTable(quotas, advisories);
|
|
14
|
+
expect(table).toMatch(/\| Provider \| Used \| Remaining \| Resets \| Days left \| Ideal daily burn \| Burn rate \| Status \| Waste if unused \|/);
|
|
15
|
+
expect(table).toMatch(/\| kimi \| 16% \| 84% \|.*\| 3.0 \| 28%\/day \| 40.0%\/day \| ⚠️ \| 78% \|/);
|
|
16
|
+
expect(table).toMatch(/\| claude \| 37% \| 63% \|.*\| 5.5 \| 12%\/day \| collecting… \| ✅ \| 53% \|/);
|
|
17
|
+
});
|
|
18
|
+
it("renders quota-only rows with placeholders when no advisories exist", () => {
|
|
19
|
+
const quotas = [{ provider: "kimi", usedPct: 16, resetsAt: "2026-09-01T09:02:00+10:00" }];
|
|
20
|
+
const table = renderQuotasTable(quotas, []);
|
|
21
|
+
expect(table).toMatch(/\| kimi \| 16% \| 84% \|.*\| — \| — \| — \| — \| — \|/);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { handleTool } from "../../src/mcp/server.js";
|
|
3
|
+
import { buildApp } from "../../src/http/server.js";
|
|
4
|
+
import { openDb, migrate } from "../../src/store/db.js";
|
|
5
|
+
import { upsertQuota } from "../../src/store/quotas.js";
|
|
6
|
+
describe("mcp get_quotas", () => {
|
|
7
|
+
it("renders the shared table for quota status", async () => {
|
|
8
|
+
const db = openDb(":memory:");
|
|
9
|
+
migrate(db);
|
|
10
|
+
upsertQuota(db, { provider: "claude", plan: "max", usedPct: 40, resetsAt: "2026-09-03T21:00:00+10:00", periodStart: "2026-08-26T00:00:00Z", raw: "x", source: "cli", fetchedAt: new Date().toISOString() });
|
|
11
|
+
const app = buildApp(db);
|
|
12
|
+
const addr = await app.listen({ port: 0, host: "127.0.0.1" });
|
|
13
|
+
process.env.QUOTACAP_URL = addr;
|
|
14
|
+
try {
|
|
15
|
+
const res = await handleTool("get_quotas", {});
|
|
16
|
+
expect(res.content[0].text).toMatch(/\| Provider \| Used \| Remaining \| Resets \| Days left \|/);
|
|
17
|
+
expect(res.content[0].text).toMatch(/\| claude \| 40% \| 60% \|/);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
delete process.env.QUOTACAP_URL;
|
|
21
|
+
await app.close();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -1,25 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { tools
|
|
2
|
+
import { tools } from "../../src/mcp/server.js";
|
|
3
3
|
describe("mcp", () => {
|
|
4
4
|
it("exposes three tools", () => {
|
|
5
5
|
expect(tools.map(t => t.name)).toEqual(expect.arrayContaining(["get_quotas", "get_recommendation", "forecast"]));
|
|
6
6
|
});
|
|
7
|
-
it("renders a markdown table with one row per provider", () => {
|
|
8
|
-
const rec = {
|
|
9
|
-
use: "kimi",
|
|
10
|
-
reason: "78% waste in 3.0d",
|
|
11
|
-
alternatives: [
|
|
12
|
-
{ provider: "kimi", usedPct: 16, resetsAt: "2026-09-01T09:02:00+10:00" },
|
|
13
|
-
{ provider: "claude", usedPct: 37, resetsAt: "2026-09-03T21:00:00+10:00" },
|
|
14
|
-
],
|
|
15
|
-
advisories: [
|
|
16
|
-
{ provider: "kimi", wastePct: 78.0, daysLeft: 3.0, idealRate: 28.0 },
|
|
17
|
-
{ provider: "claude", wastePct: 52.9, daysLeft: 5.5, idealRate: 11.6 },
|
|
18
|
-
],
|
|
19
|
-
};
|
|
20
|
-
const table = recommendationTable(rec);
|
|
21
|
-
expect(table).toMatch(/\| Provider \| Used \| Remaining \| Resets \| Days left \| Ideal daily burn \| Waste if unused \|/);
|
|
22
|
-
expect(table).toMatch(/\| kimi \| 16% \| 84% \|.*\| 3.0 \| 28%\/day \| 78% \|/);
|
|
23
|
-
expect(table).toMatch(/\| claude \| 37% \| 63% \|.*\| 5.5 \| 12%\/day \| 53% \|/);
|
|
24
|
-
});
|
|
25
7
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { openDb, migrate } from "../../src/store/db.js";
|
|
3
|
-
import { upsertQuota, getLatestByProvider } from "../../src/store/quotas.js";
|
|
3
|
+
import { upsertQuota, getLatestByProvider, getBurnRates } from "../../src/store/quotas.js";
|
|
4
4
|
describe("store", () => {
|
|
5
5
|
it("migrates and upserts", async () => {
|
|
6
6
|
const db = openDb(":memory:");
|
|
@@ -10,4 +10,21 @@ describe("store", () => {
|
|
|
10
10
|
const got = getLatestByProvider(db, "claude");
|
|
11
11
|
expect(got?.usedPct).toBe(25);
|
|
12
12
|
});
|
|
13
|
+
it("measures burn rate from snapshot history", () => {
|
|
14
|
+
const db = openDb(":memory:");
|
|
15
|
+
migrate(db);
|
|
16
|
+
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES('2026-08-26','claude',35)`).run();
|
|
17
|
+
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES('2026-08-28','claude',40)`).run();
|
|
18
|
+
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES('2026-08-28','kimi',16)`).run();
|
|
19
|
+
const rates = getBurnRates(db);
|
|
20
|
+
expect(rates.get("claude")).toBeCloseTo(2.5, 5);
|
|
21
|
+
expect(rates.has("kimi")).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
it("skips a burn measurement across a reset (usage went down)", () => {
|
|
24
|
+
const db = openDb(":memory:");
|
|
25
|
+
migrate(db);
|
|
26
|
+
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES('2026-08-26','claude',40)`).run();
|
|
27
|
+
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES('2026-08-28','claude',16)`).run();
|
|
28
|
+
expect(getBurnRates(db).has("claude")).toBe(false);
|
|
29
|
+
});
|
|
13
30
|
});
|
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.9";
|
package/dist/web/src/App.js
CHANGED
|
@@ -42,19 +42,21 @@ function App() {
|
|
|
42
42
|
advisoriesByProvider.set(a.provider, a);
|
|
43
43
|
const hasStale = sorted.some((q) => q.stale ?? (q.fetchedAt ? Date.now() - new Date(q.fetchedAt).getTime() > 60 * 60 * 1000 : false));
|
|
44
44
|
const isDegraded = !!fetchError || (quotas.length === 0 && !rec);
|
|
45
|
-
return (_jsxs("div", { style: { fontFamily: "system-ui", padding: 16, maxWidth: 900, margin: "0 auto" }, children: [_jsx("h1", { style: { fontSize: 20, margin: "0 0 12px" }, children: "QuotaCap" }), _jsx(Banner, { rec: rec }), isDegraded && (_jsxs("div", { "data-testid": "error-banner", style: { background: "#fecaca", padding: 10, borderRadius: 8, marginBottom: 12, fontSize: 13 }, children: ["no fresh data", lastPollAt ? `, last ${new Date(lastPollAt).toLocaleString()}` : "", " \u2014 check network/daemon", fetchError ? `: ${fetchError}` : ""] })), hasStale && (_jsxs("div", { "data-testid": "stale-banner", style: { background: "#ffedd5", padding: 8, borderRadius: 8, marginBottom: 12, fontSize: 12 }, children: ["some quotas stale \u2014 ", _jsx("button", { onClick: () => fetch("/api/refresh", { method: "POST" }).then(() => location.reload()), style: { border: "1px solid #d1d5db", background: "#fff", borderRadius: 4, padding: "2px 6px", cursor: "pointer" }, children: "Refresh now" })] })), _jsxs("div", { "data-testid": "strip", style: { display: "flex", gap: 8, margin: "12px 0", overflowX: "auto", alignItems: "center" }, children: [_jsx("span", { style: { color: "#2563eb", fontWeight: 700 }, children: "NOW" }), sorted.map((q) => (_jsxs("span", { style: { border: "1px solid #d1d5db", borderRadius: 999, padding: "4px 10px", whiteSpace: "nowrap", background: "#fff" }, children: [q.provider, " ", formatReset(q.resetsAt)] }, q.provider))), sorted.length === 0 && _jsx("span", { style: { color: "#9ca3af" }, children: "no quotas yet" })] }), _jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 14 }, children: [_jsx("thead", { children: _jsxs("tr", { style: { textAlign: "left", borderBottom: "2px solid #e5e7eb" }, children: [_jsx("th", { style: { padding: "8px 6px" }, children: "Provider" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Quota" }), _jsx("th", { style: { padding: "8px 6px" }, children: "
|
|
45
|
+
return (_jsxs("div", { style: { fontFamily: "system-ui", padding: 16, maxWidth: 900, margin: "0 auto" }, children: [_jsx("h1", { style: { fontSize: 20, margin: "0 0 12px" }, children: "QuotaCap" }), _jsx(Banner, { rec: rec }), isDegraded && (_jsxs("div", { "data-testid": "error-banner", style: { background: "#fecaca", padding: 10, borderRadius: 8, marginBottom: 12, fontSize: 13 }, children: ["no fresh data", lastPollAt ? `, last ${new Date(lastPollAt).toLocaleString()}` : "", " \u2014 check network/daemon", fetchError ? `: ${fetchError}` : ""] })), hasStale && (_jsxs("div", { "data-testid": "stale-banner", style: { background: "#ffedd5", padding: 8, borderRadius: 8, marginBottom: 12, fontSize: 12 }, children: ["some quotas stale \u2014 ", _jsx("button", { onClick: () => fetch("/api/refresh", { method: "POST" }).then(() => location.reload()), style: { border: "1px solid #d1d5db", background: "#fff", borderRadius: 4, padding: "2px 6px", cursor: "pointer" }, children: "Refresh now" })] })), _jsxs("div", { "data-testid": "strip", style: { display: "flex", gap: 8, margin: "12px 0", overflowX: "auto", alignItems: "center" }, children: [_jsx("span", { style: { color: "#2563eb", fontWeight: 700 }, children: "NOW" }), sorted.map((q) => (_jsxs("span", { style: { border: "1px solid #d1d5db", borderRadius: 999, padding: "4px 10px", whiteSpace: "nowrap", background: "#fff" }, children: [q.provider, " ", formatReset(q.resetsAt)] }, q.provider))), sorted.length === 0 && _jsx("span", { style: { color: "#9ca3af" }, children: "no quotas yet" })] }), _jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 14 }, children: [_jsx("thead", { children: _jsxs("tr", { style: { textAlign: "left", borderBottom: "2px solid #e5e7eb" }, children: [_jsx("th", { style: { padding: "8px 6px" }, children: "Provider" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Quota" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Remaining" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Resets" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Days left" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Ideal" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Rate" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Status" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Waste" }), _jsx("th", { style: { padding: "8px 6px" }, children: "Advice" }), _jsx("th", { style: { padding: "8px 6px" } })] }) }), _jsxs("tbody", { children: [sorted.map((q) => {
|
|
46
46
|
const adv = advisoriesByProvider.get(q.provider);
|
|
47
47
|
const waste = adv?.wastePct ?? 0;
|
|
48
48
|
const ideal = adv?.idealRate;
|
|
49
49
|
const burn = adv?.burnRate;
|
|
50
|
+
const burnMeasured = adv?.burnMeasured;
|
|
50
51
|
const urgency = adv?.urgency ?? "";
|
|
52
|
+
const status = adv?.status ?? "on track";
|
|
51
53
|
const isExpanded = expanded === q.provider;
|
|
52
54
|
const isStale = q.stale ?? (q.fetchedAt ? Date.now() - new Date(q.fetchedAt).getTime() > 60 * 60 * 1000 : false);
|
|
53
55
|
return (_jsxs(React.Fragment, { children: [_jsxs("tr", { style: { background: waste > 30 ? "#fff7ed" : isStale ? "#fffbeb" : undefined, borderBottom: "1px solid #f3f4f6" }, children: [_jsxs("td", { style: { padding: "8px 6px", fontWeight: 600 }, children: [q.provider, " ", isStale && _jsx("span", { style: { background: "#f59e0b", color: "#fff", fontSize: 10, padding: "2px 4px", borderRadius: 4, marginLeft: 6 }, children: "stale" })] }), _jsx("td", { style: { padding: "8px 6px" }, children: _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("div", { style: { width: 80, height: 8, background: "#e5e7eb", borderRadius: 4, overflow: "hidden" }, children: _jsx("div", { style: {
|
|
54
56
|
width: `${Math.min(100, q.usedPct)}%`,
|
|
55
57
|
height: "100%",
|
|
56
58
|
background: waste > 30 ? "#f59e0b" : "#3b82f6",
|
|
57
|
-
} }) }), _jsxs("span", { children: [q.usedPct, "%"] })] }) }), _jsx("td", { style: { padding: "8px 6px" }, children:
|
|
59
|
+
} }) }), _jsxs("span", { children: [q.usedPct, "%"] })] }) }), _jsxs("td", { style: { padding: "8px 6px" }, children: [100 - q.usedPct, "%"] }), _jsx("td", { style: { padding: "8px 6px" }, children: formatReset(q.resetsAt) }), _jsx("td", { style: { padding: "8px 6px" }, children: adv?.daysLeft != null ? adv.daysLeft.toFixed(1) : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: ideal != null ? `${ideal.toFixed(1)}%/d` : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: burn != null ? (burnMeasured ? `${burn.toFixed(1)}%/d` : "collecting…") : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: status === "at risk" ? "⚠️ at risk" : "✅ on track" }), _jsx("td", { style: { padding: "8px 6px" }, children: adv?.wastePct != null ? `${Math.round(adv.wastePct)}%` : "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: urgency || "—" }), _jsx("td", { style: { padding: "8px 6px" }, children: _jsx("button", { "aria-label": `expand ${q.provider}`, onClick: () => setExpanded(isExpanded ? null : q.provider), style: { cursor: "pointer", border: "1px solid #d1d5db", background: "#fff", borderRadius: 4, padding: "2px 6px" }, children: isExpanded ? "▼" : "▶" }) })] }), isExpanded && (_jsx("tr", { children: _jsx("td", { colSpan: 6, style: { padding: 0 }, children: _jsxs("div", { style: { padding: 12, background: "#f9fafb", borderTop: "1px solid #e5e7eb" }, children: [_jsxs("div", { style: { position: "relative", height: 24, background: "#e5e7eb", borderRadius: 4, overflow: "hidden", marginBottom: 8 }, children: [_jsx("div", { style: {
|
|
58
60
|
position: "absolute",
|
|
59
61
|
left: "10%",
|
|
60
62
|
top: 0,
|