src/dashboard/quota-burn-down.js
Rama master · réplica de lectura del repositorio original.
/**
* P0-1 quota burn-down aggregate reader.
*
* Four GET-only billing sources are read through the shared cache
* (`quota`, `status`, `limits`, `daily`) and normalized into the card payload.
* `status` and `quota` use the same cache keys as the billing-health card, so a
* cold dashboard open fetches each upstream document once.
*/
import {
TTL_MS,
cacheKey,
httpStatusForKind,
readCached,
readDevinJson,
resolveAccount,
resolveOrgId,
sourceSummary,
worstError,
ReadError,
} from './devin-read.js';
import { normalizeUsage } from './usage.js';
export async function readQuotaBurnDown({
id,
// Org-scoped aggregators already resolved the org and hold the exact token
// that must be used (e.g. a synthetic env-token candidate). Passing both
// avoids a second membership read and keeps the cache key on that account.
account: providedAccount = null,
orgId: providedOrgId = '',
force = false,
fetchImpl = null,
now = Date.now(),
} = {}) {
const account = providedAccount || resolveAccount(id);
if (!account.ok) return { ok: false, error: account.error, httpStatus: account.status };
let orgId = providedOrgId;
try {
if (!orgId) orgId = await resolveOrgId({ token: account.token, fetchImpl, force });
} catch (err) {
const kind = err instanceof ReadError ? err.kind : 'upstream';
return {
ok: false,
accountId: account.accountId,
accountRef: account.accountRef,
error: kind,
httpStatus: err?.status || httpStatusForKind(kind),
};
}
const read = (name, path, ttl) => readCached(
cacheKey({ accountId: account.accountId, token: account.token, name }),
ttl,
() => readDevinJson({ token: account.token, orgId, path, fetchImpl }),
{ force },
);
const [quota, status, limits, daily] = await Promise.all([
read('quota', `/${orgId}/billing/quota/usage`, TTL_MS.quota),
read('status', `/${orgId}/billing/status`, TTL_MS.status),
read('limits', `/${orgId}/billing/usage/limits`, TTL_MS.limits),
read('daily-all', `/${orgId}/billing/usage/daily-usage?cycle=current&view=all`, TTL_MS.daily),
]);
const results = { quota, status, limits, daily };
const sources = Object.fromEntries(
Object.entries(results).map(([name, result]) => [name, sourceSummary(result)]),
);
const failures = Object.values(results).filter((result) => !result.ok);
if (failures.length === Object.keys(results).length) {
const kind = worstError(failures.map((result) => result.error));
return {
ok: false,
accountId: account.accountId,
accountRef: account.accountRef,
error: kind,
httpStatus: httpStatusForKind(kind),
sources,
};
}
const value = normalizeUsage({
quota: quota.value,
status: status.value,
limits: limits.value,
dailyUsage: daily.value,
now,
});
return {
ok: true,
accountId: account.accountId,
accountRef: account.accountRef,
cached: Object.values(results).every((result) => result.ok && result.cached && !result.stale),
stale: Object.values(results).some((result) => result.stale),
partial: failures.length > 0,
...value,
sources,
};
}