(data)
| 178 | } |
| 179 | |
| 180 | function normalizeUserStatus(data) { |
| 181 | const ps = data?.userStatus?.planStatus || {}; |
| 182 | const plan = ps.planInfo || {}; |
| 183 | |
| 184 | // Legacy values come in hundredths; divide by 100 for display. |
| 185 | const legacyDiv = (n) => (typeof n === 'number' ? n / 100 : null); |
| 186 | |
| 187 | // Unix timestamps may be numeric or string depending on server version. |
| 188 | const asUnix = (v) => { |
| 189 | if (v == null) return null; |
| 190 | if (typeof v === 'number') return v; |
| 191 | const n = parseInt(v, 10); |
| 192 | return Number.isFinite(n) ? n : null; |
| 193 | }; |
| 194 | |
| 195 | const out = { |
| 196 | planName: plan.planName || 'Unknown', |
| 197 | dailyPercent: typeof ps.dailyQuotaRemainingPercent === 'number' ? ps.dailyQuotaRemainingPercent : null, |
| 198 | weeklyPercent: typeof ps.weeklyQuotaRemainingPercent === 'number' ? ps.weeklyQuotaRemainingPercent : null, |
| 199 | dailyResetAt: asUnix(ps.dailyQuotaResetAtUnix), |
| 200 | weeklyResetAt: asUnix(ps.weeklyQuotaResetAtUnix), |
| 201 | overageBalance: typeof ps.overageBalanceMicros === 'number' ? ps.overageBalanceMicros / 1_000_000 : null, |
| 202 | prompt: { |
| 203 | limit: legacyDiv(plan.monthlyPromptCredits), |
| 204 | used: legacyDiv(ps.usedPromptCredits), |
| 205 | remaining: legacyDiv(ps.availablePromptCredits), |
| 206 | }, |
| 207 | flex: { |
| 208 | limit: legacyDiv(plan.monthlyFlexCreditPurchaseAmount), |
| 209 | used: legacyDiv(ps.usedFlexCredits), |
| 210 | remaining: legacyDiv(ps.availableFlexCredits), |
| 211 | }, |
| 212 | planStart: ps.planStart || null, |
| 213 | planEnd: ps.planEnd || null, |
| 214 | // Preserve the untouched response so downstream caching (model catalog) |
| 215 | // can inspect fields we haven't normalized yet. |
| 216 | raw: data, |
| 217 | fetchedAt: Date.now(), |
| 218 | }; |
| 219 | |
| 220 | // Derive a single display-friendly percent: prefer daily remaining; otherwise |
| 221 | // compute from prompt credits; otherwise null. |
| 222 | if (out.dailyPercent != null) { |
| 223 | out.percent = out.dailyPercent; |
| 224 | } else if (out.prompt.limit && out.prompt.remaining != null) { |
| 225 | out.percent = (out.prompt.remaining / out.prompt.limit) * 100; |
| 226 | } else { |
| 227 | out.percent = null; |
| 228 | } |
| 229 | |
| 230 | return out; |
| 231 | } |
| 232 | |
| 233 | // ─── Dynamic model catalog ──────────────────────────────── |
| 234 |
no test coverage detected