export { parseQuotaData } from "./quotaParsing"; const PROVIDER_PLAN_FALLBACKS = new Set([ "claude code", "kimi coding", "kiro", "amazon q", "openai codex", "codex", "github copilot", ]); const QUOTA_LABEL_MAP: Record = { chat: "Chat", completions: "Completions", premium_interactions: "Premium", session: "Session", weekly: "Weekly", code_review: "Code Review", code_review_weekly: "Code Review Weekly", gpt_5_3_codex_spark_session: "GPT-5.3-Codex-Spark", gpt_5_3_codex_spark_weekly: "GPT-5.3-Codex-Spark Weekly", agentic_request: "Agentic", agentic_request_freetrial: "Agentic (Trial)", credits: "AI Credits", models: "Models", mcp_monthly: "Monthly", "search-prime": "Web Search", "web-reader": "Web Reader", zread: "Zread", "5 Hours Quota": "5 Hours", "Weekly Quota": "Weekly", "Monthly Tools": "Monthly Tools", tokens: "Tokens", time_limit: "Time Limit", }; function toRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; } function isClaudeOrganizationTypeLabel(value: string) { return /^default_claude(?:_ai)?$/i.test(value.trim()); } function normalizePlanCandidate(value: unknown) { if (typeof value !== "string") return null; const trimmed = value.trim(); if (!trimmed) return null; if (trimmed.toLowerCase() === "unknown") return null; if (PROVIDER_PLAN_FALLBACKS.has(trimmed.toLowerCase())) return null; if (isClaudeOrganizationTypeLabel(trimmed)) return null; return trimmed; } function escapeRegExpToken(token: string): string { return token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** Match tier tokens as whole words (avoids MINIMAX → Max, APPROVE → Pro, etc.). */ function hasTierToken(upper: string, token: string): boolean { const escaped = escapeRegExpToken(token.toUpperCase()); const pattern = new RegExp(`(?:^|[^A-Z])${escaped}(?:[^A-Z]|$)`); return pattern.test(upper); } function toTitleCaseWords(value: string) { return value .split(/[\s_-]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } export function formatQuotaLabel(name: string) { const trimmed = typeof name === "string" ? name.trim() : ""; if (!trimmed) return ""; const mapped = QUOTA_LABEL_MAP[trimmed]; if (mapped) return mapped; if (/^session\s*\(\d+[hm]\)$/i.test(trimmed)) { return "Session"; } if (/^weekly\s*\(\d+d\)$/i.test(trimmed)) { return "Weekly"; } const weeklyModelMatch = trimmed.match(/^weekly\s+(.+?)\s*\(\d+d\)$/i); if (weeklyModelMatch) { return `Weekly ${toTitleCaseWords(weeklyModelMatch[1])}`; } return trimmed; } /** * Format ISO date string to countdown format (inspired by vscode-antigravity-cockpit) * @param {string|Date} date - ISO date string or Date object * @returns {string} Formatted countdown (e.g., "2d 5h 30m", "4h 40m", "15m") or "-" */ export function formatResetTime(date) { if (!date) return "-"; try { const resetDate = typeof date === "string" ? new Date(date) : date; const now = new Date(); const diffMs = (resetDate as any) - (now as any); if (diffMs <= 0) return "-"; const totalMinutes = Math.ceil(diffMs / (1000 * 60)); // < 60 minutes: show only minutes if (totalMinutes < 60) { return `${totalMinutes}m`; } const totalHours = Math.floor(totalMinutes / 60); const remainingMinutes = totalMinutes % 60; // < 24 hours: show hours and minutes if (totalHours < 24) { return `${totalHours}h ${remainingMinutes}m`; } // >= 24 hours: show days, hours, and minutes const days = Math.floor(totalHours / 24); const remainingHours = totalHours % 24; return `${days}d ${remainingHours}h ${remainingMinutes}m`; } catch (error) { return "-"; } } /** * Get Tailwind color class based on percentage * @param {number} percentage - Remaining percentage (0-100) * @returns {string} Color name: "green" | "yellow" | "red" */ export function getStatusColor(percentage) { if (percentage > 70) return "green"; if (percentage >= 30) return "yellow"; return "red"; // 0-29% including 0% (out of quota) - show red } /** * Get status emoji based on percentage * @param {number} percentage - Remaining percentage (0-100) * @returns {string} Emoji: "🟢" | "🟡" | "🔴" */ export function getStatusEmoji(percentage) { if (percentage > 70) return "🟢"; if (percentage >= 30) return "🟡"; return "🔴"; // 0-29% including 0% (out of quota) - show red } /** * Calculate remaining percentage * @param {number} used - Used amount * @param {number} total - Total amount * @returns {number} Remaining percentage (0-100) */ export function calculatePercentage(used, total) { if (!total || total === 0) return 0; if (!used || used < 0) return 100; if (used >= total) return 0; return Math.round(((total - used) / total) * 100); } /** * Resolve the best available plan label using live usage first, then persisted * provider-specific connection metadata. */ export function resolvePlanValue(plan, providerSpecificData) { const psd = toRecord(providerSpecificData); const livePlan = normalizePlanCandidate(plan); const persistedCandidates = [ psd.workspacePlanType, psd.plan, psd.subscriptionTier, psd.subscription, psd.tier, psd.accountTier, // Claude OAuth bootstrap: rate_limit_tier has the Max 5x/20x multiplier. psd.organizationRateLimitTier, psd.rateLimitTier, psd.organizationType, ]; if (livePlan && normalizePlanTier(livePlan).key !== "free") { return livePlan; } for (const candidate of persistedCandidates) { const normalized = normalizePlanCandidate(candidate); if (normalized) return normalized; } return livePlan || null; } function unknownPlanTier(raw: string | null = null) { return { key: "unknown", label: "Unknown", variant: "default", rank: 0, raw }; } function formatUnknownPlanLabel(raw: string) { return raw .toLowerCase() .split(/[\s_-]+/) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function matchClaudePlanTier(raw: string, upper: string) { const match = upper.match(/(?:DEFAULT_)?CLAUDE_(MAX|PRO|TEAM|ENTERPRISE|FREE)(?:_(\d+X))?/); if (!match) return null; const multiplier = match[2] ? ` ${match[2].toLowerCase()}` : ""; const tiers = { MAX: { key: "ultra", label: `Max${multiplier}`, variant: "success", rank: 4, raw }, PRO: { key: "pro", label: "Pro", variant: "success", rank: 3, raw }, TEAM: { key: "team", label: "Team", variant: "info", rank: 6, raw }, ENTERPRISE: { key: "enterprise", label: "Enterprise", variant: "info", rank: 7, raw }, FREE: { key: "free", label: "Free", variant: "default", rank: 1, raw }, }; return tiers[match[1]]; } function matchKeywordPlanTier(raw: string, upper: string) { if (upper.includes("PRO+") || upper.includes("PRO PLUS") || upper.includes("PROPLUS")) return { key: "plus", label: "Pro+", variant: "success", rank: 4, raw }; if (upper.includes("ENTERPRISE") || upper.includes("CORP") || upper.includes("ORG")) return { key: "enterprise", label: "Enterprise", variant: "info", rank: 7, raw }; if (upper.includes("TEAM") || upper.includes("CHATGPTTEAM")) return { key: "team", label: "Team", variant: "info", rank: 6, raw }; if (upper.includes("BUSINESS") || upper.includes("STANDARD") || upper.includes("BIZ")) return { key: "business", label: "Business", variant: "warning", rank: 5, raw }; if (upper.includes("STUDENT")) return { key: "pro", label: "Student", variant: "success", rank: 3, raw }; if (upper.includes("ULTRA")) return { key: "ultra", label: "Ultra", variant: "success", rank: 4, raw }; return null; } function matchTokenPlanTier(raw: string, upper: string) { if (hasTierToken(upper, "MAX")) return { key: "ultra", label: "Max", variant: "success", rank: 4, raw }; if (hasTierToken(upper, "PRO") || hasTierToken(upper, "PREMIUM")) return { key: "pro", label: "Pro", variant: "success", rank: 3, raw }; if (hasTierToken(upper, "STARTER")) return { key: "lite", label: "Starter", variant: "primary", rank: 2, raw }; if (hasTierToken(upper, "LITE") || hasTierToken(upper, "LIGHT")) return { key: "lite", label: "Lite", variant: "primary", rank: 2, raw }; if (hasTierToken(upper, "PLUS") || hasTierToken(upper, "PAID")) return { key: "plus", label: "Plus", variant: "success", rank: 2, raw }; return null; } function matchFreePlanTier(raw: string, upper: string) { return upper.includes("FREE") || upper.includes("BASIC") || upper.includes("TRIAL") || upper.includes("LEGACY") ? { key: "free", label: "Free", variant: "default", rank: 1, raw } : null; } /** * Normalize provider-specific plan labels into a shared tier taxonomy. * Supported tiers: enterprise, business, team, ultra, pro, plus, lite, free, unknown. */ export function normalizePlanTier(plan) { const raw = typeof plan === "string" ? plan.trim() : ""; if (!raw) return unknownPlanTier(null); const upper = raw.toUpperCase(); // Provider names that are not real plan tiers — treat as unknown if (PROVIDER_PLAN_FALLBACKS.has(raw.toLowerCase())) return unknownPlanTier(raw); // Match Anthropic bootstrap strings (claude_max, default_claude_max_20x, etc.) // before the generic PRO/TEAM checks so underscored values don't fall through. const matched = matchClaudePlanTier(raw, upper) || matchKeywordPlanTier(raw, upper) || matchTokenPlanTier(raw, upper) || matchFreePlanTier(raw, upper); return matched || { ...unknownPlanTier(raw), label: formatUnknownPlanLabel(raw) || "Unknown" }; } // === Card Grid Helpers (T7) ================================================= export const STATUS_EMOJI = { critical: "🔴", alert: "🟡", ok: "🟢", empty: "⚪", } as const; export type CardStatus = keyof typeof STATUS_EMOJI; const QUOTA_BAR_GREEN_THRESHOLD = 50; const QUOTA_BAR_YELLOW_THRESHOLD = 20; function quotaRemainingPercent(q: any): number { return getQuotaRemainingPercentage(q); } function quotaStatus(q: any): "critical" | "alert" | "ok" { const pct = quotaRemainingPercent(q); if (pct <= QUOTA_BAR_YELLOW_THRESHOLD) return "critical"; if (pct <= QUOTA_BAR_GREEN_THRESHOLD) return "alert"; return "ok"; } export function worstStatus(quotas: any[] | undefined): CardStatus { if (!quotas || quotas.length === 0) return "empty"; let worst: "ok" | "alert" = "ok"; for (const q of quotas) { const s = quotaStatus(q); if (s === "critical") return "critical"; if (s === "alert" && worst === "ok") worst = "alert"; } return worst; } const STATUS_ORDER: Record<"critical" | "alert" | "ok", number> = { critical: 0, alert: 1, ok: 2, }; export function topQuotas(quotas: any[], n = 3): any[] { return [...quotas.filter(Boolean)] .sort((a, b) => { const sa = STATUS_ORDER[quotaStatus(a)]; const sb = STATUS_ORDER[quotaStatus(b)]; if (sa !== sb) return sa - sb; return quotaRemainingPercent(a) - quotaRemainingPercent(b); }) .slice(0, n); } export function getQuotaRemainingPercentage(q: any): number { if (q?.unlimited) return 100; if (q?.remainingPercentage !== undefined) return Number(q.remainingPercentage); return calculatePercentage(q?.used, q?.total); } export function isPercentageOnlyQuota(q: any): boolean { return q?.isPercentageOnly === true || q?.fractionReported === true; } export function shouldShowQuotaUsageCount(q: any): boolean { const total = Number(q?.total || 0); return total > 0 && q?.unlimited !== true && !isPercentageOnlyQuota(q); } export function getBarColor(remainingPercentage: number): { bar: string; text: string; bg: string; } { if (remainingPercentage > QUOTA_BAR_GREEN_THRESHOLD) { return { bar: "#22c55e", text: "#22c55e", bg: "rgba(34,197,94,0.12)" }; } if (remainingPercentage > QUOTA_BAR_YELLOW_THRESHOLD) { return { bar: "#eab308", text: "#eab308", bg: "rgba(234,179,8,0.12)" }; } return { bar: "#ef4444", text: "#ef4444", bg: "rgba(239,68,68,0.12)" }; } export function formatCountdown(resetAt: string | null | undefined): string | null { if (!resetAt) return null; try { const diff = new Date(resetAt).getTime() - Date.now(); if (diff <= 0) return null; const h = Math.floor(diff / 3_600_000); const m = Math.floor((diff % 3_600_000) / 60_000); if (h >= 24) { const d = Math.floor(h / 24); return `${d}d ${h % 24}h ${m}m`; } return `${h}h ${m}m`; } catch { return null; } } export function getNextResetSummary(quotas: any[] | undefined): string | null { if (!quotas || quotas.length === 0) return null; const now = Date.now(); let soonest = Number.POSITIVE_INFINITY; let soonestIso: string | null = null; for (const q of quotas) { if (!q?.resetAt) continue; const ts = new Date(q.resetAt).getTime(); if (!Number.isFinite(ts) || ts <= now) continue; if (ts < soonest) { soonest = ts; soonestIso = typeof q.resetAt === "string" ? q.resetAt : new Date(ts).toISOString(); } } return soonestIso ? formatCountdown(soonestIso) : null; } function addQuotaModelIdVariants(out: Set, provider: string, modelId: string) { const raw = modelId.trim().toLowerCase(); const providerId = provider.trim().toLowerCase(); if (!raw) return; out.add(raw); if (!providerId) return; const prefix = `${providerId}/`; if (raw.startsWith(prefix)) { const stripped = raw.slice(prefix.length); if (stripped) out.add(stripped); } else { out.add(`${providerId}/${raw}`); } } export function collectHiddenQuotaModelIds(provider: string, payload: unknown): string[] { const hidden = new Set(); const data = toRecord(payload); const collect = (entries: unknown) => { if (!Array.isArray(entries)) return; for (const entry of entries) { const record = toRecord(entry); if (record.isHidden !== true && record.isDeleted !== true) continue; if (typeof record.id === "string") addQuotaModelIdVariants(hidden, provider, record.id); } }; collect(data.models); collect(data.modelCompatOverrides); return Array.from(hidden); } export function filterHiddenModelQuotas( provider: string, quotas: any[] | undefined, hiddenModelIds: string[] | undefined ): any[] { if (!Array.isArray(quotas)) return []; if (!hiddenModelIds || hiddenModelIds.length === 0) return quotas; const hidden = new Set( hiddenModelIds.map((id) => id.trim().toLowerCase()).filter((id) => id.length > 0) ); if (hidden.size === 0) return quotas; return quotas.filter((quota) => { if (!quota || quota.isCredits) return true; const modelId = typeof quota.modelKey === "string" ? quota.modelKey : typeof quota.modelId === "string" ? quota.modelId : ""; if (!modelId) return true; const candidates = new Set(); addQuotaModelIdVariants(candidates, provider, modelId); return !Array.from(candidates).some((candidate) => hidden.has(candidate)); }); } // --- Provider dropdown filter (PR #769 port) ----------------------------- // Pure helpers extracted from so the filter+dropdown logic // can be exercised by unit tests without rendering React. Keep them free of // browser-only globals so Node's native test runner can import them directly. /** * Returns true when `connection` should be visible under the selected * `providerFilter`. The sentinel `"all"` matches every connection; any other * value must equal the connection's `provider` key exactly. Connections with a * missing/non-string provider are filtered out when a specific provider is * selected (defensive — the live route only emits string provider keys). */ export function matchesProviderFilter( connection: { provider?: unknown } | null | undefined, providerFilter: string ): boolean { if (!providerFilter || providerFilter === "all") return true; if (!connection || typeof connection.provider !== "string") return false; return connection.provider === providerFilter; } /** * Distinct provider keys present in `connections`, optionally sorted with the * supplied `compare` function (defaults to `String.prototype.localeCompare` so * tests get deterministic output without depending on the i18n-aware * `compareTr` helper). Empty / non-string provider values are skipped. */ export function buildProviderOptions( connections: ReadonlyArray<{ provider?: unknown }>, compare: (a: string, b: string) => number = (a, b) => a.localeCompare(b) ): string[] { const seen = new Set(); for (const conn of connections) { if (conn && typeof conn.provider === "string" && conn.provider) { seen.add(conn.provider); } } return Array.from(seen).sort(compare); }