oc-go-usage-display 1.0.2 → 1.1.0
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/README.md +30 -1
- package/bin/lib.js +47 -1
- package/bin/oc-go-usage-display-remove.js +33 -0
- package/bin/oc-go-usage-display-status.js +19 -7
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -133
- package/dist/index.js.map +1 -1
- package/dist/shared.d.ts +28 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +152 -0
- package/dist/shared.js.map +1 -0
- package/dist/tui.d.ts +17 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +26 -124
- package/dist/tui.js.map +1 -1
- package/package.json +4 -2
- package/src/index.ts +34 -148
- package/src/shared.ts +179 -0
- package/src/tui.tsx +42 -137
package/src/index.ts
CHANGED
|
@@ -32,8 +32,23 @@
|
|
|
32
32
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
33
33
|
import { tool } from "@opencode-ai/plugin";
|
|
34
34
|
import * as fs from "node:fs";
|
|
35
|
-
import * as os from "node:os";
|
|
36
35
|
import * as path from "node:path";
|
|
36
|
+
import {
|
|
37
|
+
CONFIG_DIR,
|
|
38
|
+
extractSnapshotFromApiPayload,
|
|
39
|
+
extractWindow,
|
|
40
|
+
formatResetDuration,
|
|
41
|
+
isRecord,
|
|
42
|
+
mockSnapshot,
|
|
43
|
+
readAuthJsonApiKey,
|
|
44
|
+
toFiniteNumber,
|
|
45
|
+
toNonEmptyString,
|
|
46
|
+
unavailableSnapshot,
|
|
47
|
+
} from "./shared.js";
|
|
48
|
+
import type { UsageSnapshot, UsageWindow } from "./shared.js";
|
|
49
|
+
|
|
50
|
+
// Re-exported so `dist/index.js` keeps the helper surface used by tests.
|
|
51
|
+
export { formatResetDuration };
|
|
37
52
|
|
|
38
53
|
// ---------------------------------------------------------------------------
|
|
39
54
|
// Constants
|
|
@@ -43,82 +58,25 @@ const API_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
|
43
58
|
const CACHE_TTL_MS = 60_000;
|
|
44
59
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
45
60
|
|
|
46
|
-
const CONFIG_DIR = path.join(os.homedir(), ".config", "opencode");
|
|
47
61
|
const FILE_CONFIG_PATH = path.join(CONFIG_DIR, "oc-go-usage-display.json");
|
|
48
62
|
const DISK_CACHE_PATH = path.join(CONFIG_DIR, "oc-go-usage-display-cache.json");
|
|
49
63
|
|
|
50
|
-
function dataShareAuthPath(): string {
|
|
51
|
-
const xdgDataHome = toNonEmptyString(process.env.XDG_DATA_HOME);
|
|
52
|
-
if (xdgDataHome) return path.join(xdgDataHome, "opencode", "auth.json");
|
|
53
|
-
return path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function authJsonPaths(): string[] {
|
|
57
|
-
return [dataShareAuthPath(), path.join(CONFIG_DIR, "auth.json")];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
64
|
// ---------------------------------------------------------------------------
|
|
61
65
|
// Trusted types (parsed at the boundary, trusted internally)
|
|
62
66
|
// ---------------------------------------------------------------------------
|
|
63
67
|
|
|
64
|
-
type UsageWindow = {
|
|
65
|
-
percent: number;
|
|
66
|
-
resetInSec: number | null;
|
|
67
|
-
status: string | null;
|
|
68
|
-
resetText: string | null;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
type UsageSnapshot = {
|
|
72
|
-
rolling: UsageWindow | null;
|
|
73
|
-
weekly: UsageWindow | null;
|
|
74
|
-
monthly: UsageWindow | null;
|
|
75
|
-
source: "api" | "scrape" | "mock" | "unavailable";
|
|
76
|
-
fetchedAt: number;
|
|
77
|
-
apiUnavailable?: boolean;
|
|
78
|
-
apiError?: string;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
68
|
type Credentials =
|
|
82
69
|
| { kind: "apiKey"; apiKey: string }
|
|
83
70
|
| { kind: "cookie"; workspaceId: string; authCookie: string }
|
|
84
71
|
| { kind: "mock" }
|
|
85
72
|
| { kind: "none" };
|
|
86
73
|
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Small pure helpers
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
92
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function toFiniteNumber(value: unknown): number | null {
|
|
96
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function toNonEmptyString(value: unknown): string | null {
|
|
101
|
-
if (typeof value !== "string") return null;
|
|
102
|
-
const trimmed = value.trim();
|
|
103
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function formatResetDuration(totalSec: number | null): string | null {
|
|
107
|
-
if (totalSec === null || !Number.isFinite(totalSec) || totalSec < 0) return null;
|
|
108
|
-
const sec = Math.floor(totalSec);
|
|
109
|
-
const hours = Math.floor(sec / 3600);
|
|
110
|
-
const minutes = Math.floor((sec % 3600) / 60);
|
|
111
|
-
if (hours > 0) return `${hours}h${minutes}m`;
|
|
112
|
-
if (minutes > 0) return `${minutes}m`;
|
|
113
|
-
return `${sec}s`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
74
|
function formatWindow(window: UsageWindow | null): string {
|
|
117
75
|
if (!window) return "n/a";
|
|
118
76
|
return `${window.percent}%`;
|
|
119
77
|
}
|
|
120
78
|
|
|
121
|
-
function formatCompactLine(snapshot: UsageSnapshot): string {
|
|
79
|
+
export function formatCompactLine(snapshot: UsageSnapshot): string {
|
|
122
80
|
if (snapshot.apiUnavailable || (!snapshot.rolling && !snapshot.weekly && !snapshot.monthly)) {
|
|
123
81
|
const reason = snapshot.apiError ?? "unknown error";
|
|
124
82
|
return `Go n/a (${reason})`;
|
|
@@ -158,31 +116,6 @@ function readFileConfig(): { workspaceId: string | null; authCookie: string | nu
|
|
|
158
116
|
};
|
|
159
117
|
}
|
|
160
118
|
|
|
161
|
-
function readAuthJsonApiKey(): string | null {
|
|
162
|
-
for (const authPath of authJsonPaths()) {
|
|
163
|
-
let raw: string;
|
|
164
|
-
try {
|
|
165
|
-
raw = fs.readFileSync(authPath, "utf8");
|
|
166
|
-
} catch {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
let parsed: unknown;
|
|
170
|
-
try {
|
|
171
|
-
parsed = JSON.parse(raw);
|
|
172
|
-
} catch {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (!isRecord(parsed)) continue;
|
|
176
|
-
const goEntry = parsed["opencode-go"];
|
|
177
|
-
const fallbackEntry = parsed["opencode"];
|
|
178
|
-
const goKey = isRecord(goEntry) ? toNonEmptyString(goEntry.key) : null;
|
|
179
|
-
if (goKey) return goKey;
|
|
180
|
-
const fallbackKey = isRecord(fallbackEntry) ? toNonEmptyString(fallbackEntry.key) : null;
|
|
181
|
-
if (fallbackKey) return fallbackKey;
|
|
182
|
-
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
119
|
function resolveCredentials(): Credentials {
|
|
187
120
|
if (process.env.OPENCODE_GO_MOCK === "1") return { kind: "mock" };
|
|
188
121
|
|
|
@@ -229,6 +162,13 @@ function readDiskCache(now: number): UsageSnapshot | null {
|
|
|
229
162
|
if (!isRecord(parsed.snapshot)) return null;
|
|
230
163
|
const snapshot = parsed.snapshot;
|
|
231
164
|
if (!("rolling" in snapshot && "weekly" in snapshot && "monthly" in snapshot)) return null;
|
|
165
|
+
// Validate cached windows instead of blindly trusting the shape; a corrupt
|
|
166
|
+
// entry is dropped so the next fetch repopulates the cache.
|
|
167
|
+
for (const key of ["rolling", "weekly", "monthly"] as const) {
|
|
168
|
+
const cachedWindow = snapshot[key];
|
|
169
|
+
if (cachedWindow === null) continue;
|
|
170
|
+
if (extractWindow(cachedWindow) === null) return null;
|
|
171
|
+
}
|
|
232
172
|
return snapshot as UsageSnapshot;
|
|
233
173
|
}
|
|
234
174
|
|
|
@@ -249,18 +189,6 @@ function writeDiskCache(snapshot: UsageSnapshot): void {
|
|
|
249
189
|
// Fetch helpers
|
|
250
190
|
// ---------------------------------------------------------------------------
|
|
251
191
|
|
|
252
|
-
function unavailableSnapshot(error: string, source: UsageSnapshot["source"] = "unavailable"): UsageSnapshot {
|
|
253
|
-
return {
|
|
254
|
-
rolling: null,
|
|
255
|
-
weekly: null,
|
|
256
|
-
monthly: null,
|
|
257
|
-
source,
|
|
258
|
-
fetchedAt: Date.now(),
|
|
259
|
-
apiUnavailable: true,
|
|
260
|
-
apiError: error,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
192
|
async function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
|
|
265
193
|
const controller = new AbortController();
|
|
266
194
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
@@ -271,47 +199,8 @@ async function fetchWithTimeout(url: string, init: RequestInit): Promise<Respons
|
|
|
271
199
|
}
|
|
272
200
|
}
|
|
273
201
|
|
|
274
|
-
// --- API-key path: tolerant JSON parsing (shape may
|
|
275
|
-
|
|
276
|
-
function extractWindow(candidate: unknown): UsageWindow | null {
|
|
277
|
-
if (!isRecord(candidate)) return null;
|
|
278
|
-
const percent = toFiniteNumber(
|
|
279
|
-
candidate.percent ??
|
|
280
|
-
candidate.usagePercent ??
|
|
281
|
-
candidate.usedPercent ??
|
|
282
|
-
candidate.value ??
|
|
283
|
-
candidate.usage,
|
|
284
|
-
);
|
|
285
|
-
if (percent === null) return null;
|
|
286
|
-
const resetInSec = toFiniteNumber(candidate.resetInSec ?? candidate.resetInSeconds ?? null);
|
|
287
|
-
const status = toNonEmptyString(candidate.status);
|
|
288
|
-
const resetText = toNonEmptyString(candidate.resetText ?? candidate.reset ?? null);
|
|
289
|
-
return { percent: Math.round(percent), resetInSec, status, resetText };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function extractSnapshotFromApiPayload(payload: unknown): UsageSnapshot | null {
|
|
293
|
-
if (!isRecord(payload)) return null;
|
|
294
|
-
const containers: unknown[] = [payload];
|
|
295
|
-
for (const key of ["usage", "data", "go"]) {
|
|
296
|
-
if (isRecord(payload[key])) containers.push(payload[key]);
|
|
297
|
-
}
|
|
298
|
-
for (const container of containers) {
|
|
299
|
-
if (!isRecord(container)) continue;
|
|
300
|
-
const rolling = extractWindow(container.rolling ?? container.rollingUsage ?? container["5h"]);
|
|
301
|
-
const weekly = extractWindow(container.weekly ?? container.weeklyUsage ?? container["7d"]);
|
|
302
|
-
const monthly = extractWindow(container.monthly ?? container.monthlyUsage ?? container["30d"]);
|
|
303
|
-
if (rolling || weekly || monthly) {
|
|
304
|
-
return {
|
|
305
|
-
rolling,
|
|
306
|
-
weekly,
|
|
307
|
-
monthly,
|
|
308
|
-
source: "api",
|
|
309
|
-
fetchedAt: Date.now(),
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
202
|
+
// --- API-key path: tolerant JSON parsing lives in `./shared.js` (shape may
|
|
203
|
+
// evolve); scrape helpers below stay server-local. ---
|
|
315
204
|
|
|
316
205
|
async function fetchViaApiKey(apiKey: string): Promise<UsageSnapshot> {
|
|
317
206
|
let response: Response;
|
|
@@ -464,7 +353,7 @@ async function fetchViaCookie(workspaceId: string, authCookie: string): Promise<
|
|
|
464
353
|
redirect: "follow",
|
|
465
354
|
headers: {
|
|
466
355
|
Cookie: `auth=${authCookie}`,
|
|
467
|
-
"User-Agent": "oc-go-usage-display-plugin
|
|
356
|
+
"User-Agent": "oc-go-usage-display-plugin",
|
|
468
357
|
Accept: "text/html,application/xhtml+xml",
|
|
469
358
|
},
|
|
470
359
|
});
|
|
@@ -497,22 +386,19 @@ async function fetchViaCookie(workspaceId: string, authCookie: string): Promise<
|
|
|
497
386
|
};
|
|
498
387
|
}
|
|
499
388
|
|
|
500
|
-
function mockSnapshot(): UsageSnapshot {
|
|
501
|
-
return {
|
|
502
|
-
rolling: { percent: 42, resetInSec: 7543, status: "active", resetText: null },
|
|
503
|
-
weekly: { percent: 15, resetInSec: null, status: "active", resetText: null },
|
|
504
|
-
monthly: { percent: 61, resetInSec: null, status: "active", resetText: null },
|
|
505
|
-
source: "mock",
|
|
506
|
-
fetchedAt: Date.now(),
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
389
|
// ---------------------------------------------------------------------------
|
|
511
390
|
// Snapshot entry point (cached, never throws, never logs secrets)
|
|
512
391
|
// ---------------------------------------------------------------------------
|
|
513
392
|
|
|
514
393
|
async function getUsageSnapshot(): Promise<UsageSnapshot> {
|
|
515
394
|
const now = Date.now();
|
|
395
|
+
// Mock bypasses cache for determinism: a stale disk/memory entry must
|
|
396
|
+
// never shadow the deterministic mock snapshot during tests.
|
|
397
|
+
if (process.env.OPENCODE_GO_MOCK === "1") {
|
|
398
|
+
const snapshot = mockSnapshot();
|
|
399
|
+
memoryCache = { at: now, snapshot };
|
|
400
|
+
return snapshot;
|
|
401
|
+
}
|
|
516
402
|
if (memoryCache && isFresh(memoryCache.at, now)) return memoryCache.snapshot;
|
|
517
403
|
const diskCached = readDiskCache(now);
|
|
518
404
|
if (diskCached) {
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Shared pure helpers for the server (`src/index.ts`) and TUI (`src/tui.tsx`)
|
|
2
|
+
// targets: auth.json resolution, tolerant API-payload parsing, snapshot
|
|
3
|
+
// builders, and small parsing/formatting primitives.
|
|
4
|
+
//
|
|
5
|
+
// Single source of truth for the duplicated logic so both surfaces parse the
|
|
6
|
+
// usage API identically. Secrets are never logged here. Behavior must stay
|
|
7
|
+
// identical for both consumers (import via `./shared.js` under NodeNext).
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".config", "opencode");
|
|
18
|
+
|
|
19
|
+
export function dataShareAuthPath(): string {
|
|
20
|
+
const xdgDataHome = toNonEmptyString(process.env.XDG_DATA_HOME);
|
|
21
|
+
if (xdgDataHome) return path.join(xdgDataHome, "opencode", "auth.json");
|
|
22
|
+
return path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function authJsonPaths(): string[] {
|
|
26
|
+
return [dataShareAuthPath(), path.join(CONFIG_DIR, "auth.json")];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Trusted types (parsed at the boundary, trusted internally)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export type UsageWindow = {
|
|
34
|
+
percent: number;
|
|
35
|
+
resetInSec: number | null;
|
|
36
|
+
status: string | null;
|
|
37
|
+
resetText: string | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type UsageSnapshot = {
|
|
41
|
+
rolling: UsageWindow | null;
|
|
42
|
+
weekly: UsageWindow | null;
|
|
43
|
+
monthly: UsageWindow | null;
|
|
44
|
+
source: "api" | "scrape" | "mock" | "unavailable";
|
|
45
|
+
fetchedAt: number;
|
|
46
|
+
apiUnavailable?: boolean;
|
|
47
|
+
apiError?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Small pure helpers
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function toFiniteNumber(value: unknown): number | null {
|
|
59
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function toNonEmptyString(value: unknown): string | null {
|
|
64
|
+
if (typeof value !== "string") return null;
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatResetDuration(totalSec: number | null): string | null {
|
|
70
|
+
if (totalSec === null || !Number.isFinite(totalSec) || totalSec < 0) return null;
|
|
71
|
+
const sec = Math.floor(totalSec);
|
|
72
|
+
const hours = Math.floor(sec / 3600);
|
|
73
|
+
const minutes = Math.floor((sec % 3600) / 60);
|
|
74
|
+
if (hours > 0) return `${hours}h${minutes}m`;
|
|
75
|
+
if (minutes > 0) return `${minutes}m`;
|
|
76
|
+
return `${sec}s`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Credentials boundary (auth.json; secrets never logged)
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export function readAuthJsonApiKey(): string | null {
|
|
84
|
+
for (const authPath of authJsonPaths()) {
|
|
85
|
+
let raw: string;
|
|
86
|
+
try {
|
|
87
|
+
raw = fs.readFileSync(authPath, "utf8");
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
let parsed: unknown;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(raw);
|
|
94
|
+
} catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!isRecord(parsed)) continue;
|
|
98
|
+
const goEntry = parsed["opencode-go"];
|
|
99
|
+
const fallbackEntry = parsed["opencode"];
|
|
100
|
+
const goKey = isRecord(goEntry) ? toNonEmptyString(goEntry.key) : null;
|
|
101
|
+
if (goKey) return goKey;
|
|
102
|
+
const fallbackKey = isRecord(fallbackEntry) ? toNonEmptyString(fallbackEntry.key) : null;
|
|
103
|
+
if (fallbackKey) return fallbackKey;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// API payload boundary (tolerant JSON parsing; shape may evolve)
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
export function extractWindow(candidate: unknown): UsageWindow | null {
|
|
113
|
+
if (!isRecord(candidate)) return null;
|
|
114
|
+
const percent = toFiniteNumber(
|
|
115
|
+
candidate.percent ??
|
|
116
|
+
candidate.usagePercent ??
|
|
117
|
+
candidate.usedPercent ??
|
|
118
|
+
candidate.value ??
|
|
119
|
+
candidate.usage,
|
|
120
|
+
);
|
|
121
|
+
if (percent === null) return null;
|
|
122
|
+
const resetInSec = toFiniteNumber(candidate.resetInSec ?? candidate.resetInSeconds ?? null);
|
|
123
|
+
const status = toNonEmptyString(candidate.status);
|
|
124
|
+
const resetText = toNonEmptyString(candidate.resetText ?? candidate.reset ?? null);
|
|
125
|
+
return { percent: Math.round(percent), resetInSec, status, resetText };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function extractSnapshotFromApiPayload(payload: unknown): UsageSnapshot | null {
|
|
129
|
+
if (!isRecord(payload)) return null;
|
|
130
|
+
const containers: unknown[] = [payload];
|
|
131
|
+
for (const key of ["usage", "data", "go"]) {
|
|
132
|
+
if (isRecord(payload[key])) containers.push(payload[key]);
|
|
133
|
+
}
|
|
134
|
+
for (const container of containers) {
|
|
135
|
+
if (!isRecord(container)) continue;
|
|
136
|
+
const rolling = extractWindow(container.rolling ?? container.rollingUsage ?? container["5h"]);
|
|
137
|
+
const weekly = extractWindow(container.weekly ?? container.weeklyUsage ?? container["7d"]);
|
|
138
|
+
const monthly = extractWindow(container.monthly ?? container.monthlyUsage ?? container["30d"]);
|
|
139
|
+
if (rolling !== null || weekly !== null || monthly !== null) {
|
|
140
|
+
return {
|
|
141
|
+
rolling,
|
|
142
|
+
weekly,
|
|
143
|
+
monthly,
|
|
144
|
+
source: "api",
|
|
145
|
+
fetchedAt: Date.now(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Snapshot builders
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
export function mockSnapshot(): UsageSnapshot {
|
|
157
|
+
return {
|
|
158
|
+
rolling: { percent: 42, resetInSec: 7543, status: "active", resetText: null },
|
|
159
|
+
weekly: { percent: 15, resetInSec: null, status: "active", resetText: null },
|
|
160
|
+
monthly: { percent: 61, resetInSec: null, status: "active", resetText: null },
|
|
161
|
+
source: "mock",
|
|
162
|
+
fetchedAt: Date.now(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function unavailableSnapshot(
|
|
167
|
+
error: string,
|
|
168
|
+
source: UsageSnapshot["source"] = "unavailable",
|
|
169
|
+
): UsageSnapshot {
|
|
170
|
+
return {
|
|
171
|
+
rolling: null,
|
|
172
|
+
weekly: null,
|
|
173
|
+
monthly: null,
|
|
174
|
+
source,
|
|
175
|
+
fetchedAt: Date.now(),
|
|
176
|
+
apiUnavailable: true,
|
|
177
|
+
apiError: error,
|
|
178
|
+
};
|
|
179
|
+
}
|