fix(admin): simplify Grok usage UI for free 24h and paid windows

Show free accounts only the rolling 24h soft-gate bar; paid accounts show
7d/30d and prepaid money. Drop the always-visible manual probe chip from the
usage cell in favor of scheduled recovery and usage load.
This commit is contained in:
IanShaw027
2026-08-08 08:48:38 +08:00
parent 8399a30417
commit e1d6600eb2
3 changed files with 123 additions and 682 deletions
@@ -113,8 +113,6 @@
<!-- No data yet -->
<div v-else class="space-y-1">
<div class="text-xs text-gray-400">-</div>
<!-- Always allow on-demand upstream quota probe, even before passive headers exist. -->
<GrokQuotaProbeCell :account="account" />
</div>
</template>
@@ -356,120 +354,72 @@
</div>
<div v-else-if="isForbidden" class="space-y-1">
<span class="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
{{ grokEntitlementLabel || t('admin.accounts.forbidden') }}
{{ usageInfo?.grok_entitlement_status || t('admin.accounts.forbidden') }}
</span>
</div>
<div v-else-if="usageInfo" class="space-y-1">
<div v-if="grokEntitlementLabel" class="mb-0.5">
<span class="inline-block rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
{{ grokEntitlementLabel }}
</span>
</div>
<div v-if="showGrokStandaloneLocalStats && grokLocalUsage" class="mb-0.5 flex items-center">
<div class="flex items-center gap-1.5 text-[9px] text-gray-500 dark:text-gray-400">
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800">
{{ formatWindowRequests(grokLocalUsage) }} req
</span>
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800">
{{ formatWindowTokens(grokLocalUsage) }}
</span>
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800" :title="t('usage.accountBilled')">
A ${{ formatWindowCost(grokLocalUsage) }}
</span>
<!-- Free: only rolling 24h soft-gate bar. Paid: 7d + 30d + prepaid money. -->
<template v-if="grokIsFree">
<UsageProgressBar
v-if="grokFreeTokenBar"
label="24h"
:title="t('admin.accounts.usageWindow.grokFreeQuota24hHint', { limit: formatCompactNumber(grokFreeTokenBar.limit) })"
:utilization="grokFreeTokenBar.utilization"
:show-now-when-idle="true"
color="emerald"
/>
<div v-else-if="grokQuotaUnknown" class="text-[10px] text-gray-500 dark:text-gray-400">
{{ grokQuotaUnknownLabel }}
</div>
</template>
<template v-else>
<UsageProgressBar
v-if="grokWeeklyBillingBar"
label="7d"
:utilization="grokWeeklyBillingBar.utilization"
:resets-at="grokWeeklyBillingBar.resetsAt"
:show-now-when-idle="true"
color="indigo"
/>
<UsageProgressBar
v-if="grokMonthlyBillingBar"
label="30d"
:utilization="grokMonthlyBillingBar.utilization"
:resets-at="grokMonthlyBillingBar.resetsAt"
:show-now-when-idle="true"
color="indigo"
/>
<div
v-if="grokPrepaidMoneyLine"
class="flex flex-wrap items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400"
>
<span
v-if="grokLocalUsage.user_cost != null"
class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800"
:title="t('usage.userBilled')"
class="rounded bg-emerald-50 px-1 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
:title="t('admin.accounts.usageWindow.grokPrepaid')"
>
U ${{ formatWindowUserCost(grokLocalUsage) }}
{{ t('admin.accounts.usageWindow.grokPrepaid') }} ${{ grokPrepaidMoneyLine.prepaid }}
</span>
<span :title="t('admin.accounts.usageWindow.grokMonthlyLimit')">
{{ t('admin.accounts.usageWindow.grokUsed') }}
{{ grokPrepaidMoneyLine.used }}/{{ grokPrepaidMoneyLine.limit }}
</span>
</div>
<div v-if="grokQuotaUnknown" class="text-[10px] text-gray-500 dark:text-gray-400">
{{ grokQuotaUnknownLabel }}
</div>
</template>
<div v-if="usageInfo.error" class="truncate text-xs text-amber-600 dark:text-amber-400 max-w-[200px]" :title="usageInfo.error">
{{ usageErrorLabel }}
</div>
<UsageProgressBar
v-if="grokWeeklyBillingBar"
label="7d"
:utilization="grokWeeklyBillingBar.utilization"
:resets-at="grokWeeklyBillingBar.resetsAt"
:show-now-when-idle="true"
color="indigo"
/>
<UsageProgressBar
v-if="grokMonthlyBillingBar"
label="30d"
:utilization="grokMonthlyBillingBar.utilization"
:resets-at="grokMonthlyBillingBar.resetsAt"
:show-now-when-idle="true"
color="indigo"
/>
<div
v-if="grokBillingMoneySummary"
class="flex flex-wrap items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400"
>
<span
v-if="grokBillingMoneySummary.prepaid != null"
class="rounded bg-emerald-50 px-1 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
:title="t('admin.accounts.usageWindow.grokPrepaid')"
>
{{ t('admin.accounts.usageWindow.grokPrepaid') }} ${{ grokBillingMoneySummary.prepaid }}
</span>
<span :title="t('admin.accounts.usageWindow.grokMonthlyLimit')">
{{ t('admin.accounts.usageWindow.grokUsed') }}
{{ grokBillingMoneySummary.used }}/{{ grokBillingMoneySummary.limit }}
</span>
<span
v-if="grokBillingMoneySummary.usedPercent != null"
class="rounded bg-gray-100 px-1 py-0.5 dark:bg-gray-800"
>
{{ grokBillingMoneySummary.usedPercent }}%
</span>
<span
v-if="grokBillingMoneySummary.showOverage"
class="rounded bg-amber-50 px-1 py-0.5 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
:title="t('admin.accounts.usageWindow.grokOverage')"
>
{{ t('admin.accounts.usageWindow.grokOverageShort') }}
{{ grokBillingMoneySummary.onDemandUsed }}/{{ grokBillingMoneySummary.onDemandCap }}
</span>
</div>
<UsageProgressBar
v-if="!grokWeeklyBillingBar && !grokIsFree && grokRequestQuotaBar"
:label="t('admin.accounts.usageWindow.grokRequests')"
:utilization="grokRequestQuotaBar.utilization"
:resets-at="grokRequestQuotaBar.resetsAt"
:remaining-capacity="true"
color="indigo"
/>
<UsageProgressBar
v-if="!grokWeeklyBillingBar && !grokIsFree && grokTokenQuotaBar"
:label="t('admin.accounts.usageWindow.grokTokens')"
:utilization="grokTokenQuotaBar.utilization"
:resets-at="grokTokenQuotaBar.resetsAt"
:remaining-capacity="true"
color="emerald"
/>
<UsageProgressBar
v-if="grokFreeTokenBar"
label="24h"
:title="t('admin.accounts.usageWindow.grokFreeQuota24hHint', { limit: formatCompactNumber(grokFreeTokenBar.limit) })"
:utilization="grokFreeTokenBar.utilization"
:show-now-when-idle="true"
color="emerald"
/>
<div v-if="grokRetryAfterLabel" class="text-[10px] text-amber-600 dark:text-amber-400">
{{ t('admin.accounts.usageWindow.grokRetryAfter', { time: grokRetryAfterLabel }) }}
</div>
<div v-if="grokQuotaUnknown" class="text-[10px] text-gray-500 dark:text-gray-400">
{{ grokQuotaUnknownLabel }}
</div>
<div v-else-if="usageInfo.error" class="truncate text-xs text-amber-600 dark:text-amber-400 max-w-[200px]" :title="usageInfo.error">
{{ usageErrorLabel }}
</div>
<div v-if="grokQuotaStatusLine" class="text-[10px] text-gray-500 dark:text-gray-400">
{{ grokQuotaStatusLine }}
</div>
<GrokQuotaProbeCell :account="account" @probed="handleGrokProbed" />
<GrokQuotaProbeCell :account="account" compact @probed="handleGrokProbed" />
</div>
<div v-else class="space-y-1">
<div class="text-xs text-gray-400">-</div>
<GrokQuotaProbeCell :account="account" compact @probed="handleGrokProbed" />
</div>
<div v-else class="text-xs text-gray-400">-</div>
</template>
<!-- Gemini platform: show quota + local usage window -->
@@ -667,11 +617,10 @@
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { adminAPI } from '@/api/admin'
import type { GrokQuotaProbeResult } from '@/api/admin/grok'
import type { Account, AccountUsageInfo, GeminiCredentials, WindowStats } from '@/types'
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
import { enqueueUsageRequest } from '@/utils/usageLoadQueue'
import { formatCompactNumber, formatRelativeTime } from '@/utils/format'
import { formatCompactNumber } from '@/utils/format'
import UsageProgressBar from './UsageProgressBar.vue'
import AccountQuotaInfo from './AccountQuotaInfo.vue'
import OpenAIQuotaResetCell from './OpenAIQuotaResetCell.vue'
@@ -1124,17 +1073,6 @@ interface GrokQuotaBarInfo {
resetsAt: string | null
}
const makeGrokQuotaBar = (quota?: { limit?: number | null; remaining?: number | null; reset_at?: string | null } | null): GrokQuotaBarInfo | null => {
if (!quota || quota.limit == null || quota.remaining == null || quota.limit <= 0) return null
const remaining = Math.min(quota.limit, Math.max(0, quota.remaining))
return {
utilization: (remaining / quota.limit) * 100,
resetsAt: quota.reset_at || null
}
}
const grokRequestQuotaBar = computed(() => makeGrokQuotaBar(usageInfo.value?.grok_request_quota))
const grokTokenQuotaBar = computed(() => makeGrokQuotaBar(usageInfo.value?.grok_token_quota))
const grokBilling = computed(() => usageInfo.value?.grok_billing || null)
const grokWeeklyBillingBar = computed((): GrokQuotaBarInfo | null => {
const billing = grokBilling.value
@@ -1177,49 +1115,30 @@ const formatGrokMoney = (value?: number | null) => {
if (value >= 10) return value.toFixed(1)
return value.toFixed(2)
}
// Absolute money: prefer prepaid/monthly_used fields; fall back to cents/100.
const grokBillingMoneySummary = computed(() => {
// Prepaid money line for paid Grok: show when prepaid_balance is present.
// Monthly used/limit numbers are optional context; primary progress is the 30d bar.
const grokPrepaidMoneyLine = computed(() => {
const billing = grokBilling.value
if (!billing) return null
const prepaid =
billing.prepaid_balance != null
? billing.prepaid_balance
: null
const prepaid = billing.prepaid_balance
// "只针对预付": only render when prepaid field exists (including $0.00).
if (prepaid == null || !Number.isFinite(prepaid)) return null
const used =
billing.monthly_used != null
? billing.monthly_used
: billing.used_cents != null
? billing.used_cents / 100
: null
: 0
const limit =
billing.monthly_limit != null
? billing.monthly_limit
: billing.monthly_limit_cents != null
? billing.monthly_limit_cents / 100
: null
const onDemandUsed = billing.on_demand_used ?? null
const onDemandCap = billing.on_demand_cap ?? null
const hasAny =
(prepaid ?? 0) > 0 ||
(used ?? 0) > 0 ||
(limit ?? 0) > 0 ||
(onDemandCap ?? 0) > 0 ||
(onDemandUsed ?? 0) > 0
if (!hasAny) return null
let usedPercent: number | null = null
if (billing.used_percent != null && Number.isFinite(billing.used_percent)) {
usedPercent = Math.round(Math.min(100, Math.max(0, billing.used_percent)))
} else if (limit != null && limit > 0 && used != null) {
usedPercent = Math.round(Math.min(100, Math.max(0, (used / limit) * 100)))
}
: 0
return {
prepaid: prepaid != null ? formatGrokMoney(prepaid) : null,
used: formatGrokMoney(used ?? 0),
limit: formatGrokMoney(limit ?? 0),
onDemandUsed: formatGrokMoney(onDemandUsed ?? 0),
onDemandCap: formatGrokMoney(onDemandCap ?? 0),
showOverage: (onDemandCap ?? 0) > 0 || (onDemandUsed ?? 0) > 0,
usedPercent
prepaid: formatGrokMoney(prepaid),
used: formatGrokMoney(used),
limit: formatGrokMoney(limit)
}
})
const grokPlanLabelIsFree = (value: string) => value.includes('free') || value.includes('basic')
@@ -1247,18 +1166,6 @@ const grokIsFree = computed(() => {
return billing != null
})
const grokFreeQuotaUsage = computed(() => usageInfo.value?.grok_local_usage_24h || null)
const showGrokStandaloneLocalStats = computed(() => {
// Official 7d/30d bars already embed window_stats — avoid duplicate local row.
return !usageInfo.value?.seven_day && !usageInfo.value?.thirty_day
})
const grokLocalUsage = computed(() => {
if (grokIsFree.value) return grokFreeQuotaUsage.value
return props.todayStats ||
usageInfo.value?.grok_local_usage ||
usageInfo.value?.grok_local_usage_7d ||
usageInfo.value?.grok_local_usage_monthly ||
null
})
const grokFreeTokenBar = computed(() => {
if (!grokIsFree.value || !grokFreeQuotaUsage.value) return null
const limit = usageInfo.value?.grok_free_token_limit
@@ -1268,14 +1175,10 @@ const grokFreeTokenBar = computed(() => {
})
const grokQuotaUnknown = computed(() => {
if (props.account.platform !== 'grok') return false
if (
grokBilling.value ||
grokBillingMoneySummary.value ||
grokMonthlyBillingBar.value ||
grokFreeTokenBar.value ||
grokRequestQuotaBar.value ||
grokTokenQuotaBar.value
) {
if (grokIsFree.value) {
return !grokFreeTokenBar.value
}
if (grokWeeklyBillingBar.value || grokMonthlyBillingBar.value || grokPrepaidMoneyLine.value) {
return false
}
return usageInfo.value?.grok_quota_snapshot_state !== 'observed'
@@ -1285,33 +1188,6 @@ const grokQuotaUnknownLabel = computed(() => {
? t('admin.accounts.usageWindow.grokNoHeaders')
: t('admin.accounts.usageWindow.grokUnknown')
})
const grokQuotaStatusLine = computed(() => {
if (props.account.platform !== 'grok') return null
const parts: string[] = []
const status = usageInfo.value?.grok_last_status_code
if (status) {
parts.push(t('admin.accounts.usageWindow.grokLastStatus', { status }))
}
if (usageInfo.value?.grok_last_quota_probe_at) {
parts.push(
t('admin.accounts.usageWindow.grokLastProbe', {
time: formatRelativeTime(usageInfo.value.grok_last_quota_probe_at)
})
)
}
if (usageInfo.value?.grok_last_headers_seen_at) {
parts.push(
t('admin.accounts.usageWindow.grokLastHeadersSeen', {
time: formatRelativeTime(usageInfo.value.grok_last_headers_seen_at)
})
)
}
return parts.length > 0 ? parts.join(' | ') : null
})
const grokEntitlementLabel = computed(() => {
const status = (usageInfo.value?.grok_entitlement_status || '').trim()
return status || null
})
const grokRetryAfterLabel = computed(() => {
const seconds = usageInfo.value?.grok_retry_after_seconds
if (seconds == null || seconds <= 0) return null
@@ -1320,11 +1196,6 @@ const grokRetryAfterLabel = computed(() => {
return `${minutes}m`
})
const formatWindowRequests = (stats: WindowStats) => formatCompactNumber(stats.requests, { allowBillions: false })
const formatWindowTokens = (stats: WindowStats) => formatCompactNumber(stats.tokens)
const formatWindowCost = (stats: WindowStats) => stats.cost.toFixed(2)
const formatWindowUserCost = (stats: WindowStats) => (stats.user_cost ?? 0).toFixed(2)
// 账户类型显示标签
const antigravityTierLabel = computed(() => {
switch (antigravityTier.value) {
@@ -1447,9 +1318,9 @@ const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?
error.value = null
try {
const fetchFn = () => options?.source
? adminAPI.accounts.getUsage(props.account.id, options.source)
: adminAPI.accounts.getUsage(props.account.id)
const fetchFn = () => options?.source
? adminAPI.accounts.getUsage(props.account.id, options.source, options.bypassCache === true)
: adminAPI.accounts.getUsage(props.account.id)
const result = await enqueueUsageRequest(props.account, fetchFn)
if (!unmounted.value) {
usageInfo.value = result
@@ -1526,54 +1397,10 @@ const loadActiveUsage = async () => {
}
}
const handleGrokProbed = (result: GrokQuotaProbeResult) => {
const current = usageInfo.value
if (!current) return
const snapshot = result.snapshot
const statusCode = snapshot?.status_code ?? result.status_code
const hasActiveProbeSnapshot = snapshot != null && (
result.source === 'active_probe' ||
result.source === 'hybrid_probe' ||
snapshot.observation_source === 'active_probe'
)
const probeSucceeded = hasActiveProbeSnapshot &&
statusCode != null && statusCode >= 200 && statusCode < 300
const snapshotEntitlement = snapshot?.entitlement_status?.trim()
const currentEntitlement = current.grok_entitlement_status?.trim()
const entitlementStatus = snapshotEntitlement || (
probeSucceeded && currentEntitlement?.toLowerCase() === 'forbidden'
? undefined
: current.grok_entitlement_status
)
const merged: AccountUsageInfo = {
...current,
grok_billing: result.billing ?? current.grok_billing,
grok_local_usage_24h: result.local_usage_24h ?? current.grok_local_usage_24h,
grok_local_usage_7d: result.local_usage_7d ?? current.grok_local_usage_7d,
grok_local_usage_monthly: result.local_usage_monthly ?? current.grok_local_usage_monthly,
grok_request_quota: snapshot?.requests ?? current.grok_request_quota,
grok_token_quota: snapshot?.tokens ?? current.grok_token_quota,
grok_retry_after_seconds: snapshot?.retry_after_seconds ?? current.grok_retry_after_seconds,
grok_entitlement_status: entitlementStatus,
grok_quota_snapshot_state: result.billing
? 'billing_observed'
: snapshot?.headers_observed
? 'observed'
: current.grok_quota_snapshot_state,
grok_last_quota_probe_at: result.billing?.fetched_at ?? snapshot?.last_probe_at ?? current.grok_last_quota_probe_at,
grok_last_headers_seen_at: snapshot?.last_headers_seen_at ?? current.grok_last_headers_seen_at,
grok_last_status_code: result.status_code ?? snapshot?.status_code ?? current.grok_last_status_code,
is_forbidden: probeSucceeded ? false : current.is_forbidden,
forbidden_reason: probeSucceeded ? undefined : current.forbidden_reason,
forbidden_type: probeSucceeded ? undefined : current.forbidden_type,
validation_url: probeSucceeded ? undefined : current.validation_url,
needs_verify: probeSucceeded ? false : current.needs_verify,
is_banned: probeSucceeded ? false : current.is_banned,
error: result.billing || snapshot ? undefined : current.error,
error_code: result.billing || snapshot ? undefined : current.error_code
}
usageInfo.value = merged
_usageCache.set(props.account.id, { data: merged, ts: Date.now() })
// The probe persists upstream quota state; refresh this cell so its compact
// bars and entitlement status reflect the newly observed snapshot.
const handleGrokProbed = async () => {
await loadUsage({ source: 'active', bypassCache: true })
}
// ===== API Key quota progress bars =====
@@ -24,18 +24,13 @@
</svg>
{{ t('admin.accounts.usageWindow.grokProbe') }}
</button>
<button
type="button"
class="inline-flex cursor-not-allowed items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] font-medium text-gray-400 opacity-70 dark:text-gray-500"
disabled
:title="t('admin.accounts.usageWindow.grokResetUnsupportedTooltip')"
>
{{ t('admin.accounts.usageWindow.grokResetUnsupported') }}
</button>
</div>
<div v-if="summary" class="text-[10px] text-gray-600 dark:text-gray-300">
<!-- Compact mode: parent already shows 7d/30d/prepaid or 24h — only surface errors. -->
<div
v-if="!compact && summary"
class="text-[10px] text-gray-600 dark:text-gray-300"
>
{{ summary }}
</div>
<div v-if="error" class="truncate text-[10px] text-red-600 dark:text-red-400" :title="error">
@@ -48,12 +43,17 @@
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { adminAPI } from '@/api/admin'
import type { GrokQuotaProbeResult, GrokQuotaWindow } from '@/api/admin/grok'
import type { GrokQuotaProbeResult } from '@/api/admin/grok'
import type { Account } from '@/types'
const props = defineProps<{
account: Account
}>()
const props = withDefaults(
defineProps<{
account: Account
/** When true, only show the probe button (+ errors). No duplicate weekly summary. */
compact?: boolean
}>(),
{ compact: false }
)
const emit = defineEmits<{ probed: [result: GrokQuotaProbeResult] }>()
@@ -79,42 +79,16 @@ const extractErrorMessage = (e: unknown): string => {
)
}
const formatWindow = (label: string, window?: GrokQuotaWindow | null): string | null => {
if (!window || window.limit == null || window.remaining == null) return null
return `${label} ${window.remaining}/${window.limit}`
}
const retryAfterLabel = computed(() => {
const seconds = data.value?.snapshot?.retry_after_seconds
if (seconds == null || seconds <= 0) return null
if (seconds < 60) return `${seconds}s`
return `${Math.ceil(seconds / 60)}m`
})
const summary = computed(() => {
const snapshot = data.value?.snapshot
if (!data.value) return ''
if (props.compact || !data.value) return ''
// Non-compact fallback (rarely used): brief weekly percent if present.
const billing = data.value.billing
const parts: Array<string | null> = []
if (billing?.period_type?.toLowerCase() === 'weekly' && billing.usage_percent != null) {
parts.push(t('admin.accounts.usageWindow.grokWeeklyUsage', {
return t('admin.accounts.usageWindow.grokWeeklyUsage', {
percent: Math.round(Math.min(100, Math.max(0, billing.usage_percent)))
}))
})
}
if (snapshot) {
parts.push(
formatWindow(t('admin.accounts.usageWindow.grokRequests'), snapshot.requests),
formatWindow(t('admin.accounts.usageWindow.grokTokens'), snapshot.tokens)
)
}
if (retryAfterLabel.value) {
parts.push(t('admin.accounts.usageWindow.grokRetryAfter', { time: retryAfterLabel.value }))
}
if (snapshot?.entitlement_status) {
parts.push(snapshot.entitlement_status)
}
const visibleParts = parts.filter((part): part is string => Boolean(part))
return visibleParts.length > 0 ? visibleParts.join(' | ') : t('admin.accounts.usageWindow.grokNoHeaders')
return ''
})
const truncatedError = computed(() => {
@@ -651,7 +651,7 @@ describe('AccountUsageCell', () => {
expect(badges.some(node => node.attributes('title') === 'usage.userBilled')).toBe(true)
})
it('Grok OAuth 会展示本地 user billed 用量并把耗尽配额显示为 0% 剩余', async () => {
it('Grok OAuth compact UI drops local chips and header quota bars', async () => {
getUsage.mockResolvedValue({
grok_local_usage: {
requests: 4,
@@ -670,12 +670,7 @@ describe('AccountUsageCell', () => {
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 3861,
platform: 'grok',
type: 'oauth',
extra: {}
})
account: makeAccount({ id: 3861, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
@@ -683,66 +678,49 @@ describe('AccountUsageCell', () => {
props: ['label', 'utilization', 'resetsAt', 'color'],
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ resetsAt }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(getUsage).toHaveBeenCalledWith(3861)
expect(wrapper.text()).toContain('4 req')
expect(wrapper.text()).toContain('1.2K')
expect(wrapper.text()).toContain('A $0.12')
expect(wrapper.text()).toContain('U $0.34')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|0|2026-07-09T16:00:00Z')
const badges = wrapper.findAll('span[title]')
expect(badges.some(node => node.attributes('title') === 'usage.accountBilled')).toBe(true)
expect(badges.some(node => node.attributes('title') === 'usage.userBilled')).toBe(true)
expect(wrapper.text()).not.toContain('4 req')
expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokRequests|')
})
it('Grok OAuth 配额条按剩余容量显示 100% 满格和 25% 低量', async () => {
it('Grok paid monthly limits show 30d bar without free 24h', async () => {
getUsage.mockResolvedValue({
grok_request_quota: {
limit: 100,
remaining: 100,
reset_at: '2026-07-09T16:00:00Z'
grok_billing: {
period_type: 'weekly',
usage_percent: null,
used_percent: 12,
monthly_limit_cents: 25_000,
used_cents: 3_000,
plan: ''
},
grok_token_quota: {
limit: 1000,
remaining: 250,
reset_at: '2026-07-09T16:00:00Z'
},
grok_quota_snapshot_state: 'observed'
grok_entitlement_status: 'free',
grok_token_quota: { limit: 1_000, remaining: 250 }
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 4073,
platform: 'grok',
type: 'oauth',
extra: {}
})
account: makeAccount({ id: 4402, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
UsageProgressBar: {
props: ['label', 'utilization', 'resetsAt', 'color', 'remainingCapacity'],
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ remainingCapacity }}</div>'
props: ['label', 'utilization'],
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|100|true')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokTokens|25|true')
expect(wrapper.text()).toContain('30d|')
expect(wrapper.text()).not.toContain('24h|')
})
it('Grok OAuth uses the official weekly billing percentage when available', async () => {
@@ -775,7 +753,6 @@ describe('AccountUsageCell', () => {
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ resetsAt }}|{{ remainingCapacity }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
@@ -789,11 +766,11 @@ describe('AccountUsageCell', () => {
})
it.each([
{ tokens: 0, expected: 0, compact: '0' },
{ tokens: 500_000, expected: 50, compact: '500.0K' },
{ tokens: 1_000_000, expected: 100, compact: '1.0M' },
{ tokens: 1_100_000, expected: 100, compact: '1.1M' }
])('Grok Free derives its 1M quota from local tokens: $tokens -> $expected%', async ({ tokens, expected, compact }) => {
{ tokens: 0, expected: 0 },
{ tokens: 500_000, expected: 50 },
{ tokens: 1_000_000, expected: 100 },
{ tokens: 1_100_000, expected: 100 }
])('Grok Free derives its 1M quota from local tokens: $tokens -> $expected%', async ({ tokens, expected }) => {
getUsage.mockResolvedValue({
grok_free_token_limit: 1_000_000,
grok_billing: {
@@ -823,7 +800,6 @@ describe('AccountUsageCell', () => {
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
@@ -831,10 +807,10 @@ describe('AccountUsageCell', () => {
await flushPromises()
expect(wrapper.text()).toContain(`24h|${expected}`)
expect(wrapper.findAll('span').filter((node) => node.text() === compact)).toHaveLength(1)
expect(wrapper.findAll('.usage-bar')).toHaveLength(1)
expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokRequests|')
expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokTokens|')
expect(wrapper.text()).not.toContain('7d|')
})
it('Grok Free uses rolling 24h usage instead of today-only usage', async () => {
@@ -872,7 +848,6 @@ describe('AccountUsageCell', () => {
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ title }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
@@ -880,7 +855,6 @@ describe('AccountUsageCell', () => {
await flushPromises()
expect(wrapper.text()).toContain('24h|75|admin.accounts.usageWindow.grokFreeQuota24hHint')
expect(wrapper.text()).toContain('750.0K')
expect(wrapper.text()).not.toContain('7d|')
expect(wrapper.text()).not.toContain('200.0K')
expect(wrapper.text()).not.toContain('250.0K')
@@ -917,7 +891,6 @@ describe('AccountUsageCell', () => {
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
@@ -930,85 +903,6 @@ describe('AccountUsageCell', () => {
expect(wrapper.text()).not.toContain('250.0K')
})
it('Grok paid plans are not mistaken for Free when weekly usage is temporarily missing', async () => {
getUsage.mockResolvedValue({
grok_billing: {
period_type: 'weekly',
usage_percent: null,
plan: 'SuperGrok Heavy'
},
grok_entitlement_status: 'free',
grok_local_usage: {
requests: 2,
tokens: 2_000_000,
cost: 1,
standard_cost: 1
},
grok_token_quota: { limit: 1_000, remaining: 250 }
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({ id: 4401, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
UsageProgressBar: {
props: ['label', 'utilization'],
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
await flushPromises()
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokTokens|25')
expect(wrapper.text()).not.toContain('2M|')
})
it('Grok custom paid monthly limits override stale Free entitlement', async () => {
getUsage.mockResolvedValue({
grok_billing: {
period_type: 'weekly',
usage_percent: null,
monthly_limit_cents: 25_000,
plan: ''
},
grok_entitlement_status: 'free',
grok_local_usage: {
requests: 2,
tokens: 2_000_000,
cost: 1,
standard_cost: 1
},
grok_token_quota: { limit: 1_000, remaining: 250 }
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({ id: 4402, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
UsageProgressBar: {
props: ['label', 'utilization'],
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
await flushPromises()
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokTokens|25')
expect(wrapper.text()).not.toContain('2M|')
})
it('Grok credential Free tier keeps the 1M fallback when billing is unavailable', async () => {
getUsage.mockResolvedValue({
grok_free_token_limit: 1_000_000,
@@ -1032,7 +926,6 @@ describe('AccountUsageCell', () => {
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
@@ -1042,257 +935,6 @@ describe('AccountUsageCell', () => {
expect(wrapper.text()).toContain('24h|100')
})
it('Grok paid manual probes keep the weekly/local summary when 24h usage is returned', async () => {
getUsage.mockResolvedValue({
grok_quota_snapshot_state: 'no_headers',
error: 'stale error',
error_code: 'quota_unknown'
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({ id: 4501, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
UsageProgressBar: {
props: ['label', 'utilization', 'resetsAt'],
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ resetsAt }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: {
emits: ['probed'],
template: `<button class="probe" @click="$emit('probed', {
source: 'hybrid_probe',
billing: { period_type: 'weekly', usage_percent: 42, period_end: '2026-07-17T00:00:00Z' },
snapshot: {
headers_observed: true,
updated_at: '2026-07-13T00:00:00Z',
entitlement_status: 'ACTIVE',
requests: { limit: 100, remaining: 20 }
},
local_usage_24h: { requests: 3, tokens: 750000, cost: 0.75, standard_cost: 0.75, user_cost: 0.25 },
local_usage_7d: { requests: 4, tokens: 1000000, cost: 1, standard_cost: 1, user_cost: 0.5 },
local_usage_monthly: { requests: 7, tokens: 1500000, cost: 2, standard_cost: 2, user_cost: 1 },
status_code: 200,
headers_observed: true,
reset_supported: false,
fetched_at: 1
})">probe</button>`
}
}
}
})
await flushPromises()
await wrapper.get('.probe').trigger('click')
expect(wrapper.text()).toContain('7d|42|2026-07-17T00:00:00Z')
expect(wrapper.text()).toContain('1.0M')
expect(wrapper.text()).not.toContain('750.0K')
expect(wrapper.text()).toContain('ACTIVE')
expect(wrapper.text()).not.toContain('stale error')
})
it('Grok successful probes immediately clear stale forbidden state', async () => {
getUsage.mockResolvedValue({
is_forbidden: true,
forbidden_reason: 'stale forbidden response',
forbidden_type: 'validation',
validation_url: 'https://example.com/verify',
needs_verify: true,
is_banned: true,
grok_entitlement_status: 'forbidden',
grok_quota_snapshot_state: 'no_headers',
error: 'stale forbidden response',
error_code: 'forbidden'
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({ id: 4503, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
await flushPromises()
expect(wrapper.text()).toContain('forbidden')
const setupState = wrapper.vm.$.setupState as {
handleGrokProbed: (result: Record<string, unknown>) => void
usageInfo: Record<string, unknown> | null
}
setupState.handleGrokProbed({
source: 'active_probe',
snapshot: {
headers_observed: false,
updated_at: '2026-07-18T00:00:00Z',
status_code: 200
},
status_code: 200,
headers_observed: false,
reset_supported: false,
fetched_at: 1
})
await wrapper.vm.$nextTick()
expect(setupState.usageInfo).toMatchObject({
is_forbidden: false,
needs_verify: false,
is_banned: false,
grok_last_status_code: 200
})
expect(setupState.usageInfo?.forbidden_reason).toBeUndefined()
expect(setupState.usageInfo?.forbidden_type).toBeUndefined()
expect(setupState.usageInfo?.validation_url).toBeUndefined()
expect(setupState.usageInfo?.grok_entitlement_status).toBeUndefined()
expect(wrapper.text()).not.toContain('admin.accounts.forbidden')
})
it('Grok successful probes preserve the entitlement reported by the latest snapshot', async () => {
getUsage.mockResolvedValue({
is_forbidden: true,
grok_entitlement_status: 'forbidden'
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({ id: 4504, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
await flushPromises()
const setupState = wrapper.vm.$.setupState as {
handleGrokProbed: (result: Record<string, unknown>) => void
usageInfo: Record<string, unknown> | null
}
setupState.handleGrokProbed({
source: 'active_probe',
snapshot: {
headers_observed: true,
updated_at: '2026-07-18T00:00:00Z',
entitlement_status: 'ACTIVE',
status_code: 200
},
status_code: 200,
headers_observed: true,
reset_supported: false,
fetched_at: 1
})
await wrapper.vm.$nextTick()
expect(setupState.usageInfo?.grok_entitlement_status).toBe('ACTIVE')
expect(wrapper.text()).toContain('ACTIVE')
expect(wrapper.text()).not.toContain('admin.accounts.forbidden')
})
it('Grok billing-only success does not clear an active-probe forbidden state', async () => {
getUsage.mockResolvedValue({
is_forbidden: true,
forbidden_type: 'forbidden',
needs_verify: true,
is_banned: true,
grok_entitlement_status: 'forbidden'
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({ id: 4505, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
await flushPromises()
const setupState = wrapper.vm.$.setupState as {
handleGrokProbed: (result: Record<string, unknown>) => void
usageInfo: Record<string, unknown> | null
}
setupState.handleGrokProbed({
source: 'billing_probe',
billing: {
period_type: 'weekly',
usage_percent: 10,
plan: 'SuperGrok'
},
status_code: 200,
headers_observed: false,
reset_supported: false,
fetched_at: 1
})
await wrapper.vm.$nextTick()
expect(setupState.usageInfo).toMatchObject({
is_forbidden: true,
forbidden_type: 'forbidden',
needs_verify: true,
is_banned: true,
grok_entitlement_status: 'forbidden'
})
expect(wrapper.text()).toContain('forbidden')
})
it('Grok Free manual probes merge rolling 24h usage', async () => {
getUsage.mockResolvedValue({
grok_free_token_limit: 1_000_000,
subscription_tier: 'FREE',
grok_quota_snapshot_state: 'no_headers'
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({ id: 4502, platform: 'grok', type: 'oauth', extra: {} })
},
global: {
stubs: {
UsageProgressBar: {
props: ['label', 'utilization'],
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: {
emits: ['probed'],
template: `<button class="probe" @click="$emit('probed', {
source: 'hybrid_probe',
billing: { period_type: 'weekly', usage_percent: null, plan: '' },
local_usage_24h: { requests: 12, tokens: 750000, cost: 0, standard_cost: 0 },
headers_observed: false,
reset_supported: false,
fetched_at: 1
})">probe</button>`
}
}
}
})
await flushPromises()
await wrapper.get('.probe').trigger('click')
expect(wrapper.text()).toContain('24h|75')
expect(wrapper.text()).toContain('750.0K')
expect(wrapper.text()).not.toContain('7d|')
})
it('Key 账号在 today stats loading 时显示骨架屏', async () => {
const wrapper = mount(AccountUsageCell, {
props: {
@@ -1424,7 +1066,6 @@ describe('AccountUsageCell', () => {
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
@@ -1468,7 +1109,6 @@ describe('AccountUsageCell', () => {
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})