diff --git a/internal/lua/plugins/ccquota.lua b/internal/lua/plugins/ccquota.lua index b37cd11..f9ecb8e 100644 --- a/internal/lua/plugins/ccquota.lua +++ b/internal/lua/plugins/ccquota.lua @@ -46,6 +46,26 @@ local REQUIRED_HEADERS = { -- key store). The kernel deliberately does NOT let a plugin read the config, so -- this is the only channel — which also means the key never sits in Lua state -- or in the persisted plugin file. +-- Monthly credit caps per planId. +-- +-- The credits endpoint reports what is LEFT but never the cap, so a percentage +-- is not derivable from it alone. The caps below come from the vendor's own +-- pricing page: every plan costs $10/month and grants a different credit pool +-- ($10 Go, $70 GOAT, $70 Pro, $70 Max). totalMonthlyCredits + monthlyCredits +-- also sums to the cap, so this table is a CROSS-CHECK on the plan, not the only +-- source — if the two disagree, PLAN_CAPS_WAIVED shows it instead of silently +-- displaying a wrong percentage. +-- +-- A planId absent from this table renders the balance with no percentage and no +-- bar. Guessing a cap for an unknown plan would be worse than showing none. +local PLAN_CAPS = { + ["individual-go"] = 10, + ["individual-goat"] = 70, + ["individual-pro"] = 70, + ["individual-max"] = 70, + ["team-goat"] = 70, +} + local ACCOUNTS = {} -- ---------------------------------------------------------------- helpers @@ -125,6 +145,11 @@ local function collectAccount(acct) report.purchased = tonum(c.purchasedCredits) report.free = tonum(c.freeCredits) report.total = report.monthly + report.purchased + report.free + -- belowThreshold is the vendor's own "running low" flag, distinct from + -- whether any balance remains: an account can hold $3 and still be below + -- their configured threshold. + report.below_threshold = c.belowThreshold == true + report.threshold = tonum(c.creditThreshold) report.limited = wl.limited == true report.exceeded = wl.exceeded report.fiveHour = wl.fiveHour @@ -137,7 +162,11 @@ local function collectAccount(acct) report.failures[#report.failures + 1] = "subscriptions: " .. tostring(serr) elseif type(subs) == "table" and type(subs.data) == "table" then report.plan_status = subs.data.status - report.plan_price = subs.data.metadata and nil + report.plan_id = subs.data.planId + report.plan_cap = PLAN_CAPS[tostring(subs.data.planId or "")] + report.period_start = subs.data.currentPeriodStart + report.period_end = subs.data.currentPeriodEnd + report.cancel_at_period_end = subs.data.cancelAtPeriodEnd == true local md = subs.data.metadata if type(md) == "table" then report.plan_name = md.plan or md.name or md.tier @@ -152,6 +181,23 @@ local function collectAccount(acct) report.spent = tonum(usage.totalCost) report.tokens = tonum(usage.totalTokens) report.success_rate = tonum(usage.successRate) + -- usage/summary counts over the BILLING PERIOD, so this pairs with the + -- monthly allowance rather than with the rolling 5h/weekly windows. + report.monthly_used = tonum(usage.totalMonthlyCredits) + report.period_basis = usage.periodBasis + end + + -- Cross-check the configured cap against what the vendor spent plus what is + -- left. A mismatch means PLAN_CAPS drifted from the real plan (a price change, + -- or a planId this table does not know) — surfaced rather than papered over, + -- because a confidently wrong "99% used" is the worst possible reading here. + if report.plan_cap and report.monthly_used and report.monthly_used > 0 then + local implied = report.monthly_used + report.total + local drift = math.abs(implied - report.plan_cap) + if drift > (report.plan_cap * 0.02 + 0.5) then + report.cap_mismatch = true + report.implied_cap = implied + end end -- An account whose every endpoint failed is an auth problem, not a data @@ -213,6 +259,8 @@ PLUGIN.ui = { .ccq-meta{display:flex;gap:14px;flex-wrap:wrap;font-size:11.5px;opacity:.7;margin-top:10px} .ccq-fail{font-size:11.5px;color:#b91c1c;margin-top:8px;word-break:break-word} .ccq-empty{opacity:.6;font-size:13px;padding:8px 0} +.ccq-win-nodat{opacity:.5} +.ccq-warn-line{font-size:11px;color:#b45309;margin-top:3px}