From af7f6b1de259bd61384c62108479436c9dc09d5b Mon Sep 17 00:00:00 2001 From: Simon Shi Date: Sat, 23 May 2026 22:50:53 +0900 Subject: [PATCH] feat(qoder): show in Quota Tracker dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- open-sse/services/usage.js | 18 +++++++++++++-- .../ProviderLimits/ProviderLimitCard.js | 1 + .../usage/components/ProviderLimits/utils.js | 22 +++++++++++++++++++ src/shared/constants/providers.js | 1 + 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index e5955072..277472b6 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -1175,26 +1175,40 @@ async function getQoderUsage(accessToken, proxyOptions = null) { if (!body) { 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 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 = { user: { total: Number(userQuota.total) || 0, used: Number(userQuota.used) || 0, remaining: Number(userQuota.remaining) || 0, unit: userQuota.unit || "credits", + resetAt, }, organization: { total: Number(orgQuota.total) || 0, used: Number(orgQuota.used) || 0, remaining: Number(orgQuota.remaining) || 0, unit: orgQuota.unit || "credits", + resetAt, }, + }; + return { + quotas, totalUsagePercentage: Number(body.totalUsagePercentage) || 0, isQuotaExceeded: !!body.isQuotaExceeded, - expiresAt: Number(body.expiresAt) || null, + expiresAt: expiresAtMs, }; - return { quotas }; } catch (error) { return { message: `Qoder connected. Unable to fetch usage: ${error.message}` }; } diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js index e0186998..eb145263 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js @@ -44,6 +44,7 @@ export default function ProviderLimitCard({ antigravity: "#4285F4", codex: "#10A37F", kiro: "#FF9900", + qoder: "#EC4899", claude: "#D97757", }; return colors[provider?.toLowerCase()] || "#6B7280"; diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js index 8d30b987..0e916d4d 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js @@ -160,6 +160,28 @@ export function parseQuotaData(provider, data) { } 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": if (data.message) { // Handle error message case diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js index ea07fb4a..fcecc39a 100644 --- a/src/shared/constants/providers.js +++ b/src/shared/constants/providers.js @@ -267,6 +267,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [ "claude", "antigravity", "kiro", + "qoder", "github", "codex", "kimi-coding",