quotacap 0.0.9 → 0.0.11
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/cli/index.js +15 -5
- package/dist/config.js +2 -2
- package/dist/daemon.d.ts +7 -0
- package/dist/daemon.js +45 -5
- package/dist/format/table.d.ts +1 -0
- package/dist/format/table.js +14 -3
- package/dist/src/cli/index.js +15 -5
- package/dist/src/config.js +2 -2
- package/dist/src/daemon.d.ts +7 -0
- package/dist/src/daemon.js +45 -5
- package/dist/src/format/table.d.ts +1 -0
- package/dist/src/format/table.js +14 -3
- package/dist/src/store/quotas.d.ts +1 -1
- package/dist/src/store/quotas.js +15 -11
- 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 -1
- package/dist/store/quotas.js +15 -11
- package/dist/tests/daemon.test.d.ts +1 -0
- package/dist/tests/daemon.test.js +62 -0
- package/dist/tests/format/table.test.js +9 -2
- package/dist/tests/store/db.test.js +19 -8
- package/dist/version.js +1 -1
- package/dist/web/src/App.js +11 -2
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-C7g0JA14.js → index-BBN0J6GO.js} +1 -1
- package/web/dist/index.html +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -62,6 +62,12 @@ program.command("web").option("--port <n>").action(async (o) => {
|
|
|
62
62
|
ensureDbDir();
|
|
63
63
|
const db = openDb(getDbPath());
|
|
64
64
|
migrate(db);
|
|
65
|
+
const { isDaemonRunning, startDaemon } = await import("../daemon.js");
|
|
66
|
+
if (!isDaemonRunning()) {
|
|
67
|
+
const started = await startDaemon();
|
|
68
|
+
if (!started.alreadyRunning)
|
|
69
|
+
console.log("daemon started (auto)");
|
|
70
|
+
}
|
|
65
71
|
const app = buildApp(db);
|
|
66
72
|
const port = o.port ? parseInt(o.port) : (await readConfig()).port;
|
|
67
73
|
await app.listen({ port, host: "127.0.0.1" });
|
|
@@ -75,14 +81,18 @@ program.command("init").action(async () => {
|
|
|
75
81
|
});
|
|
76
82
|
program.command("daemon").option("--foreground", "keep foreground (default: true)").action(async (o) => {
|
|
77
83
|
const { startDaemon } = await import("../daemon.js");
|
|
78
|
-
const
|
|
84
|
+
const started = await startDaemon();
|
|
85
|
+
if (started.alreadyRunning) {
|
|
86
|
+
console.log(`daemon already running (pid ${started.alreadyRunning})`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
79
89
|
console.log("QuotaCap daemon started" + (o.foreground !== false ? " (foreground)" : ""));
|
|
80
90
|
// keep alive until SIGINT/SIGTERM — timer is ref'd so event loop stays alive
|
|
81
|
-
process.on("SIGINT", () => {
|
|
82
|
-
process.on("SIGTERM", () => {
|
|
91
|
+
process.on("SIGINT", () => { started.stop(); process.exit(0); });
|
|
92
|
+
process.on("SIGTERM", () => { started.stop(); process.exit(0); });
|
|
83
93
|
// explicitly keep process alive if interval was somehow unref'd elsewhere
|
|
84
|
-
if (timer.ref)
|
|
85
|
-
timer.ref();
|
|
94
|
+
if (started.timer.ref)
|
|
95
|
+
started.timer.ref();
|
|
86
96
|
});
|
|
87
97
|
program.command("mcp").description("start MCP server (stdio over HTTP)").action(async () => {
|
|
88
98
|
const mod = await import("../mcp/server.js");
|
package/dist/config.js
CHANGED
|
@@ -5,12 +5,12 @@ import { z } from "zod";
|
|
|
5
5
|
export function getConfigPath(p) {
|
|
6
6
|
if (p)
|
|
7
7
|
return p;
|
|
8
|
-
return path.join(os.homedir(), ".quotacap", "config.json");
|
|
8
|
+
return path.join(process.env.QUOTACAP_HOME ?? os.homedir(), ".quotacap", "config.json");
|
|
9
9
|
}
|
|
10
10
|
export function getDbPath(p) {
|
|
11
11
|
if (p)
|
|
12
12
|
return p;
|
|
13
|
-
return path.join(os.homedir(), ".quotacap", "quotacap.db");
|
|
13
|
+
return path.join(process.env.QUOTACAP_HOME ?? os.homedir(), ".quotacap", "quotacap.db");
|
|
14
14
|
}
|
|
15
15
|
const ConfigSchema = z.object({
|
|
16
16
|
port: z.number().default(8787),
|
package/dist/daemon.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function isDaemonRunning(file?: string): boolean;
|
|
1
2
|
export declare function pollOnce(db: any, enabled: string[]): Promise<({
|
|
2
3
|
provider: string;
|
|
3
4
|
status: "fulfilled";
|
|
@@ -15,8 +16,14 @@ export declare function pollOnce(db: any, enabled: string[]): Promise<({
|
|
|
15
16
|
value?: undefined;
|
|
16
17
|
})[]>;
|
|
17
18
|
export declare function startDaemon(): Promise<{
|
|
19
|
+
alreadyRunning: string;
|
|
20
|
+
db: any;
|
|
21
|
+
timer: any;
|
|
22
|
+
stop: () => void;
|
|
23
|
+
} | {
|
|
18
24
|
db: any;
|
|
19
25
|
timer: NodeJS.Timeout;
|
|
26
|
+
alreadyRunning: string | undefined;
|
|
20
27
|
stop: () => void;
|
|
21
28
|
}>;
|
|
22
29
|
export declare const start: typeof startDaemon;
|
package/dist/daemon.js
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { pollAll } from "./adapters/index.js";
|
|
2
2
|
import { upsertQuota } from "./store/quotas.js";
|
|
3
|
+
import { getDbPath, readConfig } from "./config.js";
|
|
4
|
+
import { openDb, migrate } from "./store/db.js";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
function pidFile() {
|
|
9
|
+
return path.join(path.dirname(getDbPath()), "daemon.pid");
|
|
10
|
+
}
|
|
11
|
+
// True when the pidfile names a live process that still runs quotacap — the
|
|
12
|
+
// command check keeps a recycled pid from being mistaken for a running daemon
|
|
13
|
+
// and matches both the compiled binary and `node dist/cli/index.js`.
|
|
14
|
+
export function isDaemonRunning(file = pidFile()) {
|
|
15
|
+
try {
|
|
16
|
+
const pid = parseInt(fs.readFileSync(file, "utf8").trim(), 10);
|
|
17
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" }).trim();
|
|
26
|
+
return cmd.includes("quotacap") || cmd.includes("cli/index.js");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
3
32
|
export async function pollOnce(db, enabled) {
|
|
4
33
|
const results = await pollAll(enabled);
|
|
5
34
|
for (const r of results) {
|
|
@@ -12,19 +41,30 @@ export async function pollOnce(db, enabled) {
|
|
|
12
41
|
return results;
|
|
13
42
|
}
|
|
14
43
|
export async function startDaemon() {
|
|
15
|
-
|
|
16
|
-
|
|
44
|
+
if (isDaemonRunning()) {
|
|
45
|
+
const existing = fs.readFileSync(pidFile(), "utf8").trim();
|
|
46
|
+
return { alreadyRunning: existing, db: null, timer: null, stop: () => { } };
|
|
47
|
+
}
|
|
48
|
+
const dbDir = path.dirname(getDbPath());
|
|
49
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
17
50
|
const db = openDb(getDbPath());
|
|
18
51
|
migrate(db);
|
|
52
|
+
fs.mkdirSync(path.dirname(pidFile()), { recursive: true });
|
|
53
|
+
fs.writeFileSync(pidFile(), String(process.pid));
|
|
19
54
|
const cfg = await readConfig();
|
|
20
55
|
const intervalMs = (cfg.pollMinutes ?? 15) * 60 * 1000;
|
|
21
56
|
// initial poll (fire-and-forget, don't block start)
|
|
22
57
|
pollOnce(db, cfg.enabledProviders).catch(e => console.warn("[quotacap] initial poll failed", e?.message ?? String(e)));
|
|
23
58
|
const jitter = Math.floor(Math.random() * 5000);
|
|
24
59
|
const timer = setInterval(() => { pollOnce(db, cfg.enabledProviders).catch(e => console.warn("[quotacap] interval poll failed", e?.message ?? String(e))); }, intervalMs + jitter);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
const stop = () => {
|
|
61
|
+
clearInterval(timer);
|
|
62
|
+
try {
|
|
63
|
+
fs.rmSync(pidFile(), { force: true });
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
};
|
|
67
|
+
return { db, timer, alreadyRunning: undefined, stop };
|
|
28
68
|
}
|
|
29
69
|
// alias for plan's daemon.start() naming
|
|
30
70
|
export const start = startDaemon;
|
package/dist/format/table.d.ts
CHANGED
package/dist/format/table.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
// The one table every surface renders: MCP tools, CLI status, web dashboard.
|
|
2
2
|
// Quotas carry used/resets; advisories add days-left, burn and waste analysis.
|
|
3
3
|
// Without advisories the analysis columns render as placeholders.
|
|
4
|
+
export function formatResetDate(iso) {
|
|
5
|
+
return new Date(iso).toLocaleString(undefined, {
|
|
6
|
+
day: "numeric",
|
|
7
|
+
month: "short",
|
|
8
|
+
year: "numeric",
|
|
9
|
+
hour: "numeric",
|
|
10
|
+
minute: "2-digit",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
4
13
|
export function renderQuotasTable(quotas, advisories = []) {
|
|
5
14
|
const rows = quotas.map((q) => {
|
|
6
15
|
const a = advisories.find((x) => x.provider === q.provider);
|
|
7
16
|
const used = Math.round(q.usedPct ?? 0);
|
|
8
|
-
const resets = q.resetsAt ?
|
|
17
|
+
const resets = q.resetsAt ? formatResetDate(q.resetsAt) : "—";
|
|
9
18
|
const daysLeft = a?.daysLeft != null ? a.daysLeft.toFixed(1) : "—";
|
|
10
19
|
const ideal = a?.idealRate != null ? `${Math.round(a.idealRate)}%/day` : "—";
|
|
11
|
-
const burn = a?.burnMeasured ? `${a.burnRate.toFixed(1)}%/day` :
|
|
12
|
-
|
|
20
|
+
const burn = a?.burnMeasured ? `${a.burnRate.toFixed(1)}%/day` : "—";
|
|
21
|
+
// Text-presentation glyphs (no emoji variation selector): single cell
|
|
22
|
+
// wide, so markdown column alignment survives terminal renderers.
|
|
23
|
+
const icon = a?.status === "at risk" ? "⚠" : a != null ? "✔" : "—";
|
|
13
24
|
const waste = a?.wastePct != null ? `${Math.round(a.wastePct)}%` : "—";
|
|
14
25
|
return `| ${q.provider} | ${used}% | ${100 - used}% | ${resets} | ${daysLeft} | ${ideal} | ${burn} | ${icon} | ${waste} |`;
|
|
15
26
|
});
|
package/dist/src/cli/index.js
CHANGED
|
@@ -62,6 +62,12 @@ program.command("web").option("--port <n>").action(async (o) => {
|
|
|
62
62
|
ensureDbDir();
|
|
63
63
|
const db = openDb(getDbPath());
|
|
64
64
|
migrate(db);
|
|
65
|
+
const { isDaemonRunning, startDaemon } = await import("../daemon.js");
|
|
66
|
+
if (!isDaemonRunning()) {
|
|
67
|
+
const started = await startDaemon();
|
|
68
|
+
if (!started.alreadyRunning)
|
|
69
|
+
console.log("daemon started (auto)");
|
|
70
|
+
}
|
|
65
71
|
const app = buildApp(db);
|
|
66
72
|
const port = o.port ? parseInt(o.port) : (await readConfig()).port;
|
|
67
73
|
await app.listen({ port, host: "127.0.0.1" });
|
|
@@ -75,14 +81,18 @@ program.command("init").action(async () => {
|
|
|
75
81
|
});
|
|
76
82
|
program.command("daemon").option("--foreground", "keep foreground (default: true)").action(async (o) => {
|
|
77
83
|
const { startDaemon } = await import("../daemon.js");
|
|
78
|
-
const
|
|
84
|
+
const started = await startDaemon();
|
|
85
|
+
if (started.alreadyRunning) {
|
|
86
|
+
console.log(`daemon already running (pid ${started.alreadyRunning})`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
79
89
|
console.log("QuotaCap daemon started" + (o.foreground !== false ? " (foreground)" : ""));
|
|
80
90
|
// keep alive until SIGINT/SIGTERM — timer is ref'd so event loop stays alive
|
|
81
|
-
process.on("SIGINT", () => {
|
|
82
|
-
process.on("SIGTERM", () => {
|
|
91
|
+
process.on("SIGINT", () => { started.stop(); process.exit(0); });
|
|
92
|
+
process.on("SIGTERM", () => { started.stop(); process.exit(0); });
|
|
83
93
|
// explicitly keep process alive if interval was somehow unref'd elsewhere
|
|
84
|
-
if (timer.ref)
|
|
85
|
-
timer.ref();
|
|
94
|
+
if (started.timer.ref)
|
|
95
|
+
started.timer.ref();
|
|
86
96
|
});
|
|
87
97
|
program.command("mcp").description("start MCP server (stdio over HTTP)").action(async () => {
|
|
88
98
|
const mod = await import("../mcp/server.js");
|
package/dist/src/config.js
CHANGED
|
@@ -5,12 +5,12 @@ import { z } from "zod";
|
|
|
5
5
|
export function getConfigPath(p) {
|
|
6
6
|
if (p)
|
|
7
7
|
return p;
|
|
8
|
-
return path.join(os.homedir(), ".quotacap", "config.json");
|
|
8
|
+
return path.join(process.env.QUOTACAP_HOME ?? os.homedir(), ".quotacap", "config.json");
|
|
9
9
|
}
|
|
10
10
|
export function getDbPath(p) {
|
|
11
11
|
if (p)
|
|
12
12
|
return p;
|
|
13
|
-
return path.join(os.homedir(), ".quotacap", "quotacap.db");
|
|
13
|
+
return path.join(process.env.QUOTACAP_HOME ?? os.homedir(), ".quotacap", "quotacap.db");
|
|
14
14
|
}
|
|
15
15
|
const ConfigSchema = z.object({
|
|
16
16
|
port: z.number().default(8787),
|
package/dist/src/daemon.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function isDaemonRunning(file?: string): boolean;
|
|
1
2
|
export declare function pollOnce(db: any, enabled: string[]): Promise<({
|
|
2
3
|
provider: string;
|
|
3
4
|
status: "fulfilled";
|
|
@@ -15,8 +16,14 @@ export declare function pollOnce(db: any, enabled: string[]): Promise<({
|
|
|
15
16
|
value?: undefined;
|
|
16
17
|
})[]>;
|
|
17
18
|
export declare function startDaemon(): Promise<{
|
|
19
|
+
alreadyRunning: string;
|
|
20
|
+
db: any;
|
|
21
|
+
timer: any;
|
|
22
|
+
stop: () => void;
|
|
23
|
+
} | {
|
|
18
24
|
db: any;
|
|
19
25
|
timer: NodeJS.Timeout;
|
|
26
|
+
alreadyRunning: string | undefined;
|
|
20
27
|
stop: () => void;
|
|
21
28
|
}>;
|
|
22
29
|
export declare const start: typeof startDaemon;
|
package/dist/src/daemon.js
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { pollAll } from "./adapters/index.js";
|
|
2
2
|
import { upsertQuota } from "./store/quotas.js";
|
|
3
|
+
import { getDbPath, readConfig } from "./config.js";
|
|
4
|
+
import { openDb, migrate } from "./store/db.js";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
function pidFile() {
|
|
9
|
+
return path.join(path.dirname(getDbPath()), "daemon.pid");
|
|
10
|
+
}
|
|
11
|
+
// True when the pidfile names a live process that still runs quotacap — the
|
|
12
|
+
// command check keeps a recycled pid from being mistaken for a running daemon
|
|
13
|
+
// and matches both the compiled binary and `node dist/cli/index.js`.
|
|
14
|
+
export function isDaemonRunning(file = pidFile()) {
|
|
15
|
+
try {
|
|
16
|
+
const pid = parseInt(fs.readFileSync(file, "utf8").trim(), 10);
|
|
17
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" }).trim();
|
|
26
|
+
return cmd.includes("quotacap") || cmd.includes("cli/index.js");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
3
32
|
export async function pollOnce(db, enabled) {
|
|
4
33
|
const results = await pollAll(enabled);
|
|
5
34
|
for (const r of results) {
|
|
@@ -12,19 +41,30 @@ export async function pollOnce(db, enabled) {
|
|
|
12
41
|
return results;
|
|
13
42
|
}
|
|
14
43
|
export async function startDaemon() {
|
|
15
|
-
|
|
16
|
-
|
|
44
|
+
if (isDaemonRunning()) {
|
|
45
|
+
const existing = fs.readFileSync(pidFile(), "utf8").trim();
|
|
46
|
+
return { alreadyRunning: existing, db: null, timer: null, stop: () => { } };
|
|
47
|
+
}
|
|
48
|
+
const dbDir = path.dirname(getDbPath());
|
|
49
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
17
50
|
const db = openDb(getDbPath());
|
|
18
51
|
migrate(db);
|
|
52
|
+
fs.mkdirSync(path.dirname(pidFile()), { recursive: true });
|
|
53
|
+
fs.writeFileSync(pidFile(), String(process.pid));
|
|
19
54
|
const cfg = await readConfig();
|
|
20
55
|
const intervalMs = (cfg.pollMinutes ?? 15) * 60 * 1000;
|
|
21
56
|
// initial poll (fire-and-forget, don't block start)
|
|
22
57
|
pollOnce(db, cfg.enabledProviders).catch(e => console.warn("[quotacap] initial poll failed", e?.message ?? String(e)));
|
|
23
58
|
const jitter = Math.floor(Math.random() * 5000);
|
|
24
59
|
const timer = setInterval(() => { pollOnce(db, cfg.enabledProviders).catch(e => console.warn("[quotacap] interval poll failed", e?.message ?? String(e))); }, intervalMs + jitter);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
const stop = () => {
|
|
61
|
+
clearInterval(timer);
|
|
62
|
+
try {
|
|
63
|
+
fs.rmSync(pidFile(), { force: true });
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
};
|
|
67
|
+
return { db, timer, alreadyRunning: undefined, stop };
|
|
28
68
|
}
|
|
29
69
|
// alias for plan's daemon.start() naming
|
|
30
70
|
export const start = startDaemon;
|
package/dist/src/format/table.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
// The one table every surface renders: MCP tools, CLI status, web dashboard.
|
|
2
2
|
// Quotas carry used/resets; advisories add days-left, burn and waste analysis.
|
|
3
3
|
// Without advisories the analysis columns render as placeholders.
|
|
4
|
+
export function formatResetDate(iso) {
|
|
5
|
+
return new Date(iso).toLocaleString(undefined, {
|
|
6
|
+
day: "numeric",
|
|
7
|
+
month: "short",
|
|
8
|
+
year: "numeric",
|
|
9
|
+
hour: "numeric",
|
|
10
|
+
minute: "2-digit",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
4
13
|
export function renderQuotasTable(quotas, advisories = []) {
|
|
5
14
|
const rows = quotas.map((q) => {
|
|
6
15
|
const a = advisories.find((x) => x.provider === q.provider);
|
|
7
16
|
const used = Math.round(q.usedPct ?? 0);
|
|
8
|
-
const resets = q.resetsAt ?
|
|
17
|
+
const resets = q.resetsAt ? formatResetDate(q.resetsAt) : "—";
|
|
9
18
|
const daysLeft = a?.daysLeft != null ? a.daysLeft.toFixed(1) : "—";
|
|
10
19
|
const ideal = a?.idealRate != null ? `${Math.round(a.idealRate)}%/day` : "—";
|
|
11
|
-
const burn = a?.burnMeasured ? `${a.burnRate.toFixed(1)}%/day` :
|
|
12
|
-
|
|
20
|
+
const burn = a?.burnMeasured ? `${a.burnRate.toFixed(1)}%/day` : "—";
|
|
21
|
+
// Text-presentation glyphs (no emoji variation selector): single cell
|
|
22
|
+
// wide, so markdown column alignment survives terminal renderers.
|
|
23
|
+
const icon = a?.status === "at risk" ? "⚠" : a != null ? "✔" : "—";
|
|
13
24
|
const waste = a?.wastePct != null ? `${Math.round(a.wastePct)}%` : "—";
|
|
14
25
|
return `| ${q.provider} | ${used}% | ${100 - used}% | ${resets} | ${daysLeft} | ${ideal} | ${burn} | ${icon} | ${waste} |`;
|
|
15
26
|
});
|
|
@@ -3,4 +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>;
|
|
6
|
+
export declare function getBurnRates(db: any, now?: number): Map<string, number>;
|
package/dist/src/store/quotas.js
CHANGED
|
@@ -26,25 +26,29 @@ 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
|
-
|
|
29
|
+
export function getBurnRates(db, now = Date.now()) {
|
|
30
|
+
// Burn is the used-pct delta over a real rolling window of poll history
|
|
31
|
+
// (up to 24h), so calendar-day boundaries and poll timing cannot skew it.
|
|
32
|
+
const rows = db.prepare(`SELECT provider, used_pct, fetched_at FROM quotas`).all();
|
|
31
33
|
const byProvider = new Map();
|
|
32
34
|
for (const r of rows) {
|
|
35
|
+
const t = new Date(r.fetched_at).getTime();
|
|
36
|
+
if (Number.isNaN(t))
|
|
37
|
+
continue;
|
|
33
38
|
const pts = byProvider.get(r.provider) ?? [];
|
|
34
|
-
pts.push({
|
|
39
|
+
pts.push({ usedPct: r.used_pct, t });
|
|
35
40
|
byProvider.set(r.provider, pts);
|
|
36
41
|
}
|
|
37
42
|
const out = new Map();
|
|
38
43
|
for (const [provider, pts] of byProvider) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (days < 1)
|
|
44
|
+
const sorted = pts.sort((a, b) => a.t - b.t);
|
|
45
|
+
const latest = sorted[sorted.length - 1];
|
|
46
|
+
const cutoff = latest.t - 86400000;
|
|
47
|
+
const windowStart = sorted.find((p) => p.t >= cutoff) ?? sorted[0];
|
|
48
|
+
const days = (latest.t - windowStart.t) / 86400000;
|
|
49
|
+
if (sorted.length < 2 || days < 1 / 24)
|
|
46
50
|
continue;
|
|
47
|
-
const burn = (
|
|
51
|
+
const burn = (latest.usedPct - windowStart.usedPct) / days;
|
|
48
52
|
if (burn >= 0)
|
|
49
53
|
out.set(provider, burn);
|
|
50
54
|
}
|
package/dist/src/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.11";
|