feat(qoder): show in Quota Tracker dashboard
Wire Qoder credits into the Quota Tracker card grid: - Add `qoder` to USAGE_SUPPORTED_PROVIDERS so the connection passes the isUsageEligible filter at /api/providers/client and shows up in providerOptions on the dashboard. - Reshape getQoderUsage so quota records (user, organization) live under `quotas` and scalar metadata (totalUsagePercentage, isQuotaExceeded, expiresAt) are siblings — the parser used to walk Object.entries(quotas) and would have rendered `totalUsagePercentage: 0.42` as a "0/0" row. - Surface Qoder's expiresAt as resetAt on each quota record so the card shows when credits reset. - Add a parser branch in ProviderLimits/utils.js: rename internal keys (user → "Personal", organization → "Organization"), drop empty org buckets so personal accounts don't render a misleading "0/0 Organization" row, and forward remaining/unit so the QuotaProgressBar can use them. - Add Qoder's brand color (#EC4899) to ProviderLimitCard's color map. 42 tests still pass; build clean.
This commit is contained in:
parent
69bc71cf11
commit
af7f6b1de2
4 changed files with 40 additions and 2 deletions
|
|
@ -1175,26 +1175,40 @@ async function getQoderUsage(accessToken, proxyOptions = null) {
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return { message: "Qoder connected. Usage response was not JSON." };
|
return { message: "Qoder connected. Usage response was not JSON." };
|
||||||
}
|
}
|
||||||
|
// Quota records live under `quotas`; scalar metadata
|
||||||
|
// (totalUsagePercentage, isQuotaExceeded, expiresAt) are surfaced as
|
||||||
|
// siblings so the dashboard parser doesn't try to render them as rows.
|
||||||
const userQuota = body.userQuota || {};
|
const userQuota = body.userQuota || {};
|
||||||
const orgQuota = body.orgResourcePackage || {};
|
const orgQuota = body.orgResourcePackage || {};
|
||||||
|
// Qoder publishes a single absolute reset timestamp (`expiresAt` in ms);
|
||||||
|
// surface it on every quota record as ISO so the table can render
|
||||||
|
// "resets at" alongside used/total.
|
||||||
|
const expiresAtMs = Number.isFinite(Number(body.expiresAt)) && Number(body.expiresAt) > 0
|
||||||
|
? Number(body.expiresAt)
|
||||||
|
: null;
|
||||||
|
const resetAt = expiresAtMs ? new Date(expiresAtMs).toISOString() : null;
|
||||||
const quotas = {
|
const quotas = {
|
||||||
user: {
|
user: {
|
||||||
total: Number(userQuota.total) || 0,
|
total: Number(userQuota.total) || 0,
|
||||||
used: Number(userQuota.used) || 0,
|
used: Number(userQuota.used) || 0,
|
||||||
remaining: Number(userQuota.remaining) || 0,
|
remaining: Number(userQuota.remaining) || 0,
|
||||||
unit: userQuota.unit || "credits",
|
unit: userQuota.unit || "credits",
|
||||||
|
resetAt,
|
||||||
},
|
},
|
||||||
organization: {
|
organization: {
|
||||||
total: Number(orgQuota.total) || 0,
|
total: Number(orgQuota.total) || 0,
|
||||||
used: Number(orgQuota.used) || 0,
|
used: Number(orgQuota.used) || 0,
|
||||||
remaining: Number(orgQuota.remaining) || 0,
|
remaining: Number(orgQuota.remaining) || 0,
|
||||||
unit: orgQuota.unit || "credits",
|
unit: orgQuota.unit || "credits",
|
||||||
|
resetAt,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
quotas,
|
||||||
totalUsagePercentage: Number(body.totalUsagePercentage) || 0,
|
totalUsagePercentage: Number(body.totalUsagePercentage) || 0,
|
||||||
isQuotaExceeded: !!body.isQuotaExceeded,
|
isQuotaExceeded: !!body.isQuotaExceeded,
|
||||||
expiresAt: Number(body.expiresAt) || null,
|
expiresAt: expiresAtMs,
|
||||||
};
|
};
|
||||||
return { quotas };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { message: `Qoder connected. Unable to fetch usage: ${error.message}` };
|
return { message: `Qoder connected. Unable to fetch usage: ${error.message}` };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export default function ProviderLimitCard({
|
||||||
antigravity: "#4285F4",
|
antigravity: "#4285F4",
|
||||||
codex: "#10A37F",
|
codex: "#10A37F",
|
||||||
kiro: "#FF9900",
|
kiro: "#FF9900",
|
||||||
|
qoder: "#EC4899",
|
||||||
claude: "#D97757",
|
claude: "#D97757",
|
||||||
};
|
};
|
||||||
return colors[provider?.toLowerCase()] || "#6B7280";
|
return colors[provider?.toLowerCase()] || "#6B7280";
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,28 @@ export function parseQuotaData(provider, data) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "qoder":
|
||||||
|
// Qoder ships a `user` quota and (optionally) an `organization`
|
||||||
|
// quota, both with same shape: {total, used, remaining, unit, resetAt}.
|
||||||
|
// Skip an organization bucket when its total is 0 — most personal
|
||||||
|
// Qoder accounts won't have one and rendering "0/0" is misleading.
|
||||||
|
if (data.quotas) {
|
||||||
|
Object.entries(data.quotas).forEach(([quotaType, quota]) => {
|
||||||
|
if (quotaType === "organization" && (!quota || (Number(quota.total) || 0) === 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
normalizedQuotas.push({
|
||||||
|
name: quotaType === "user" ? "Personal" : quotaType === "organization" ? "Organization" : quotaType,
|
||||||
|
used: quota.used || 0,
|
||||||
|
total: quota.total || 0,
|
||||||
|
remaining: quota.remaining,
|
||||||
|
unit: quota.unit,
|
||||||
|
resetAt: quota.resetAt || null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "claude":
|
case "claude":
|
||||||
if (data.message) {
|
if (data.message) {
|
||||||
// Handle error message case
|
// Handle error message case
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
|
||||||
"claude",
|
"claude",
|
||||||
"antigravity",
|
"antigravity",
|
||||||
"kiro",
|
"kiro",
|
||||||
|
"qoder",
|
||||||
"github",
|
"github",
|
||||||
"codex",
|
"codex",
|
||||||
"kimi-coding",
|
"kimi-coding",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue