@@ -532,7 +532,7 @@ import { extractApiErrorMessage } from '@/utils/apiError'
import { sanitizeUrl } from '@/utils/url'
import { getFloatingPanelPosition } from '@/utils/floatingPanel'
import { formatMultiplier } from '@/utils/formatters'
-import type { Account, AccountPlatform, AccountSchedulerGroupScore, AccountType, AccountUsageInfo, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel, UpstreamBillingProbeSnapshot } from '@/types'
+import type { Account, AccountListItem, AccountPlatform, AccountSchedulerGroupScore, AccountType, AccountUsageInfo, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel, UpstreamBillingProbeSnapshot } from '@/types'
const { t } = useI18n()
const appStore = useAppStore()
@@ -540,6 +540,12 @@ const authStore = useAuthStore()
const proxies = ref
([])
const groups = ref([])
+const groupsByID = computed(() => new Map(groups.value.map(group => [group.id, group])))
+const accountGroupsForRow = (account: Pick): AdminGroup[] => {
+ const groupIDs = account.group_ids ?? []
+ if (groupIDs.length === 0) return []
+ return groupIDs.map(id => groupsByID.value.get(id)).filter((group): group is AdminGroup => Boolean(group))
+}
const accountTableRef = ref(null)
const dataTableRef = ref | null>(null)
type AccountBulkEditTarget =
@@ -1070,7 +1076,7 @@ const {
debouncedReload: baseDebouncedReload,
handlePageChange: baseHandlePageChange,
handlePageSizeChange: baseHandlePageSizeChange
-} = useTableLoader({
+} = useTableLoader({
fetchFn: adminAPI.accounts.list,
initialParams: {
platform: '',
@@ -1079,6 +1085,7 @@ const {
privacy_mode: '',
group: '',
search: '',
+ lite: '1',
include_scheduler_score: shouldIncludeSchedulerScore() ? '1' : '0',
sort_by: sortState.sort_by,
sort_order: sortState.sort_order
@@ -1099,7 +1106,7 @@ const {
toggleVisible,
selectVisible: selectCurrentPage,
batchUpdate
-} = useTableSelection({
+} = useTableSelection({
rows: accounts,
getId: (account) => account.id
})
@@ -1142,8 +1149,6 @@ const resetAutoRefreshCache = () => {
upstreamBillingRateETag.value = null
}
-const isFirstLoad = ref(true)
-
type AccountLoadOptions = {
refreshTodayStats?: boolean
}
@@ -1154,14 +1159,8 @@ const load = async (options: AccountLoadOptions = {}) => {
hasPendingListSync.value = false
resetAutoRefreshCache()
pendingTodayStatsRefresh.value = false
- if (isFirstLoad.value) {
- requestParams.lite = '1'
- }
+ requestParams.lite = '1'
await baseLoad()
- if (isFirstLoad.value) {
- isFirstLoad.value = false
- delete requestParams.lite
- }
if (options.refreshTodayStats !== false) await refreshTodayStatsBatch()
}
@@ -1822,7 +1821,27 @@ const cols = computed(() =>
)
)
-const handleEdit = (a: Account) => { edAcc.value = a; showEdit.value = true }
+const accountDetailLoading = new Set()
+const loadAccountDetails = async (account: Pick): Promise => {
+ if (accountDetailLoading.has(account.id)) return null
+ accountDetailLoading.add(account.id)
+ try {
+ return await adminAPI.accounts.getById(account.id)
+ } catch (error) {
+ console.error('Failed to load account details:', error)
+ appStore.showError(extractApiErrorMessage(error, t('common.error')))
+ return null
+ } finally {
+ accountDetailLoading.delete(account.id)
+ }
+}
+
+const handleEdit = async (a: AccountListItem) => {
+ const account = await loadAccountDetails(a)
+ if (!account) return
+ edAcc.value = account
+ showEdit.value = true
+}
const openMenu = (a: Account, e: MouseEvent) => {
menu.acc = a
@@ -2333,8 +2352,18 @@ const accountExportStepUp = useStepUp()
const closeTestModal = () => { showTest.value = false; testingAcc.value = null }
const closeStatsModal = () => { showStats.value = false; statsAcc.value = null }
const closeReAuthModal = () => { showReAuth.value = false; reAuthAcc.value = null }
-const handleTest = (a: Account) => { testingAcc.value = a; showTest.value = true }
-const handleViewStats = (a: Account) => { statsAcc.value = a; showStats.value = true }
+const handleTest = async (a: AccountListItem) => {
+ const account = await loadAccountDetails(a)
+ if (!account) return
+ testingAcc.value = account
+ showTest.value = true
+}
+const handleViewStats = async (a: AccountListItem) => {
+ const account = await loadAccountDetails(a)
+ if (!account) return
+ statsAcc.value = account
+ showStats.value = true
+}
const handleSchedule = async (a: Account) => {
scheduleAcc.value = a
scheduleModelOptions.value = []
diff --git a/frontend/src/views/admin/__tests__/AccountsView.lite.spec.ts b/frontend/src/views/admin/__tests__/AccountsView.lite.spec.ts
new file mode 100644
index 0000000000..8ad9b681f6
--- /dev/null
+++ b/frontend/src/views/admin/__tests__/AccountsView.lite.spec.ts
@@ -0,0 +1,250 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { flushPromises, mount } from '@vue/test-utils'
+import { defineComponent } from 'vue'
+
+import AccountsView from '../AccountsView.vue'
+import AccountActionMenu from '@/components/admin/account/AccountActionMenu.vue'
+
+const {
+ listAccounts,
+ listWithEtag,
+ getById,
+ getBatchTodayStats,
+ getUpstreamBillingProbeSettings,
+ getAllProxies,
+ getAllGroups,
+ showError
+} = vi.hoisted(() => ({
+ listAccounts: vi.fn(),
+ listWithEtag: vi.fn(),
+ getById: vi.fn(),
+ getBatchTodayStats: vi.fn(),
+ getUpstreamBillingProbeSettings: vi.fn(),
+ getAllProxies: vi.fn(),
+ getAllGroups: vi.fn(),
+ showError: vi.fn()
+}))
+
+vi.mock('@/api/admin', () => ({
+ adminAPI: {
+ accounts: {
+ list: listAccounts,
+ getById,
+ listWithEtag,
+ getBatchTodayStats,
+ getUpstreamBillingProbeSettings,
+ delete: vi.fn(),
+ batchClearError: vi.fn(),
+ batchRefresh: vi.fn(),
+ toggleSchedulable: vi.fn()
+ },
+ proxies: { getAll: getAllProxies },
+ groups: { getAll: getAllGroups }
+ }
+}))
+
+vi.mock('@/stores/app', () => ({
+ useAppStore: () => ({ showError, showSuccess: vi.fn(), showInfo: vi.fn() })
+}))
+
+vi.mock('@/stores/auth', () => ({
+ useAuthStore: () => ({ token: 'test-token', isSimpleMode: false })
+}))
+
+vi.mock('vue-i18n', async () => {
+ const actual = await vi.importActual('vue-i18n')
+ return { ...actual, useI18n: () => ({ t: (key: string) => key }) }
+})
+
+const DataTableStub = defineComponent({
+ props: { data: { type: Array, default: () => [] } },
+ template: `
+
+ `
+})
+
+const AccountGroupsCellStub = defineComponent({
+ props: { groups: { type: Array, default: () => [] } },
+ template: '{{ groups.map(group => group.name).join(",") }}'
+})
+
+const EditAccountModalStub = defineComponent({
+ props: { show: Boolean, account: { type: Object, default: null } },
+ template: '{{ show ? account?.name : "" }}
'
+})
+
+const AccountTestModalStub = defineComponent({
+ props: { show: Boolean, account: { type: Object, default: null } },
+ template: '{{ show ? account?.name : "" }}
'
+})
+
+const AccountStatsModalStub = defineComponent({
+ props: { show: Boolean, account: { type: Object, default: null } },
+ template: '{{ show ? account?.name : "" }}
'
+})
+
+function mountView() {
+ return mount(AccountsView, {
+ global: {
+ stubs: {
+ AppLayout: { template: '
' },
+ TablePageLayout: { template: '
' },
+ DataTable: DataTableStub,
+ AccountTableActions: { template: '
' },
+ AccountTableFilters: true,
+ AccountBulkActionsBar: true,
+ Pagination: true,
+ ConfirmDialog: true,
+ AccountActionMenu: true,
+ ImportDataModal: true,
+ ReAuthAccountModal: true,
+ AccountTestModal: AccountTestModalStub,
+ AccountStatsModal: AccountStatsModalStub,
+ ScheduledTestsPanel: true,
+ SyncFromCrsModal: true,
+ TempUnschedStatusModal: true,
+ ErrorPassthroughRulesModal: true,
+ TLSFingerprintProfilesModal: true,
+ CreateAccountModal: true,
+ EditAccountModal: EditAccountModalStub,
+ BulkEditAccountModal: true,
+ PlatformTypeBadge: true,
+ AccountCapacityCell: true,
+ AccountStatusIndicator: true,
+ AccountTodayStatsCell: true,
+ AccountGroupsCell: AccountGroupsCellStub,
+ AccountUsageCell: true,
+ UpstreamBillingRateCell: true,
+ HelpTooltip: true,
+ Icon: true,
+ Teleport: true
+ }
+ }
+ })
+}
+
+const listRow = {
+ id: 42,
+ name: 'compact row',
+ platform: 'openai',
+ type: 'oauth',
+ status: 'active',
+ schedulable: true,
+ concurrency: 2,
+ priority: 1,
+ group_ids: [7],
+ extra: {},
+ credentials: {}
+}
+
+const fullAccount = {
+ ...listRow,
+ groups: [{ id: 7, name: 'codex', platform: 'openai' }],
+ account_groups: [{ account_id: 42, group_id: 7 }],
+ credentials: { api_key: 'redacted' },
+ extra: { detail_only: true }
+}
+
+describe('admin AccountsView lite account list', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ listAccounts.mockReset().mockResolvedValue({ items: [listRow], total: 1, page: 1, page_size: 20, pages: 1 })
+ listWithEtag.mockReset().mockResolvedValue({ notModified: true, etag: 'compact-etag', data: null })
+ getById.mockReset().mockResolvedValue(fullAccount)
+ getBatchTodayStats.mockReset().mockResolvedValue({ stats: {} })
+ getUpstreamBillingProbeSettings.mockReset().mockResolvedValue({ enabled: true })
+ getAllProxies.mockReset().mockResolvedValue([])
+ getAllGroups.mockReset().mockResolvedValue([{ id: 7, name: 'codex', platform: 'openai' }])
+ showError.mockReset()
+ })
+
+ afterEach(() => {
+ vi.useRealTimers()
+ vi.restoreAllMocks()
+ })
+
+ it('keeps lite=1 on the initial list request', async () => {
+ const wrapper = mountView()
+ await flushPromises()
+
+ expect(listAccounts).toHaveBeenCalledWith(
+ 1,
+ 20,
+ expect.objectContaining({ lite: '1' }),
+ expect.objectContaining({ signal: expect.any(AbortSignal) })
+ )
+ wrapper.unmount()
+ })
+
+ it('maps group_ids through the group catalog for the table cell', async () => {
+ const wrapper = mountView()
+ await flushPromises()
+
+ expect(wrapper.get('[data-test="account-groups"]').text()).toBe('codex')
+ wrapper.unmount()
+ })
+
+ it('keeps lite=1 on automatic ETag refreshes', async () => {
+ vi.useFakeTimers()
+ vi.spyOn(document, 'hidden', 'get').mockReturnValue(false)
+ localStorage.setItem('account-auto-refresh', JSON.stringify({ enabled: true, interval_seconds: 5 }))
+ const wrapper = mountView()
+ await flushPromises()
+
+ await vi.advanceTimersByTimeAsync(6000)
+ await flushPromises()
+
+ expect(listWithEtag).toHaveBeenCalledWith(
+ 1,
+ 20,
+ expect.objectContaining({ lite: '1' }),
+ expect.objectContaining({ etag: null })
+ )
+ wrapper.unmount()
+ })
+
+ it('loads the full account by id before opening edit, test, and stats actions', async () => {
+ const wrapper = mountView()
+ await flushPromises()
+
+ const editButton = wrapper.findAll('button').find(button => button.text().includes('common.edit'))
+ expect(editButton).toBeTruthy()
+ await editButton!.trigger('click')
+ await flushPromises()
+ expect(getById).toHaveBeenCalledWith(42)
+ expect(wrapper.get('[data-test="edit-account"]').text()).toBe('compact row')
+
+ const menu = wrapper.findComponent(AccountActionMenu)
+ menu.vm.$emit('test', listRow)
+ await flushPromises()
+ expect(getById).toHaveBeenCalledTimes(2)
+ expect(wrapper.get('[data-test="test-account"]').text()).toBe('compact row')
+
+ menu.vm.$emit('stats', listRow)
+ await flushPromises()
+ expect(getById).toHaveBeenCalledTimes(3)
+ expect(wrapper.get('[data-test="stats-account"]').text()).toBe('compact row')
+ wrapper.unmount()
+ })
+
+ it('shows an error and keeps the modal closed when detail loading fails', async () => {
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
+ getById.mockRejectedValueOnce(new Error('detail failed'))
+ const wrapper = mountView()
+ await flushPromises()
+
+ const editButton = wrapper.findAll('button').find(button => button.text().includes('common.edit'))
+ await editButton!.trigger('click')
+ await flushPromises()
+
+ expect(showError).toHaveBeenCalledWith('detail failed')
+ expect(wrapper.get('[data-test="edit-account"]').text()).toBe('')
+ consoleError.mockRestore()
+ wrapper.unmount()
+ })
+})