mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Implement generic pool member scoring and probing
This commit is contained in:
@@ -192,6 +192,66 @@ export interface PoolKeysPageResponse {
|
||||
keys: PoolKeyDetail[]
|
||||
}
|
||||
|
||||
export type PoolScoreHardState =
|
||||
| 'available'
|
||||
| 'unknown'
|
||||
| 'cooldown'
|
||||
| 'quota_exhausted'
|
||||
| 'auth_invalid'
|
||||
| 'banned'
|
||||
| 'inactive'
|
||||
|
||||
export type PoolScoreProbeStatus = 'never' | 'ok' | 'failed' | 'stale' | 'in_progress'
|
||||
|
||||
export interface PoolScoreKeySummary {
|
||||
id: string
|
||||
name: string
|
||||
auth_type: string
|
||||
is_active: boolean
|
||||
internal_priority: number
|
||||
last_used_at: number | null
|
||||
}
|
||||
|
||||
export interface PoolMemberScoreItem {
|
||||
id: string
|
||||
pool_kind: string
|
||||
pool_id: string
|
||||
member_kind: string
|
||||
member_id: string
|
||||
capability: string
|
||||
scope_kind: string
|
||||
scope_id: string | null
|
||||
score: number
|
||||
hard_state: PoolScoreHardState
|
||||
score_version: number
|
||||
score_reason: Record<string, unknown> | null
|
||||
last_ranked_at: number | null
|
||||
last_scheduled_at: number | null
|
||||
last_success_at: number | null
|
||||
last_failure_at: number | null
|
||||
failure_count: number
|
||||
last_probe_attempt_at: number | null
|
||||
last_probe_success_at: number | null
|
||||
last_probe_failure_at: number | null
|
||||
probe_failure_count: number
|
||||
probe_status: PoolScoreProbeStatus
|
||||
updated_at: number
|
||||
key?: PoolScoreKeySummary | null
|
||||
}
|
||||
|
||||
export interface PoolScoresResponse {
|
||||
provider_id: string
|
||||
page: number
|
||||
page_size: number
|
||||
filters: {
|
||||
api_format?: string | null
|
||||
model_id?: string | null
|
||||
hard_state?: string | null
|
||||
probe_status?: string | null
|
||||
}
|
||||
items: PoolMemberScoreItem[]
|
||||
}
|
||||
|
||||
export interface PoolKeysQuery {
|
||||
page?: number
|
||||
page_size?: number
|
||||
@@ -203,6 +263,15 @@ export interface PoolKeysQuery {
|
||||
sort_order?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
export interface PoolScoresQuery {
|
||||
page?: number
|
||||
page_size?: number
|
||||
api_format?: string
|
||||
model_id?: string
|
||||
hard_state?: string
|
||||
probe_status?: string
|
||||
}
|
||||
|
||||
export interface PoolKeySelectionRequest {
|
||||
search?: string
|
||||
quick_selectors?: string[]
|
||||
@@ -293,6 +362,29 @@ export async function listPoolKeys(
|
||||
)
|
||||
}
|
||||
|
||||
export async function listPoolScores(
|
||||
providerId: string,
|
||||
params: PoolScoresQuery = {},
|
||||
options: PoolReadOptions = {},
|
||||
): Promise<PoolScoresResponse> {
|
||||
const normalizedParams = { ...params }
|
||||
const cacheKey = buildCacheKey(
|
||||
`pool:scores:${providerId}`,
|
||||
normalizedParams as Record<string, unknown>,
|
||||
)
|
||||
return cachedRequest(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const response = await client.get<PoolScoresResponse>(
|
||||
`/api/admin/pool/${providerId}/scores`,
|
||||
{ params: normalizedParams },
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
options.cacheTtlMs ?? 0,
|
||||
)
|
||||
}
|
||||
|
||||
export async function resolvePoolKeySelection(
|
||||
providerId: string,
|
||||
body: PoolKeySelectionRequest,
|
||||
|
||||
@@ -548,6 +548,9 @@ export interface PoolAdvancedConfig {
|
||||
health_policy_enabled?: boolean
|
||||
unschedulable_rules?: Array<Record<string, unknown>> | null
|
||||
batch_concurrency?: number | null
|
||||
probe_concurrency?: number | null
|
||||
score_top_n?: number | null
|
||||
score_fallback_scan_limit?: number | null
|
||||
probing_enabled?: boolean
|
||||
probing_interval_minutes?: number | null
|
||||
auto_remove_banned_keys?: boolean
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-muted/30 p-4">
|
||||
<div class="grid gap-3 rounded-xl bg-muted/30 p-4 sm:grid-cols-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label>
|
||||
并发数
|
||||
@@ -247,6 +247,45 @@
|
||||
为空时沿用默认值;数值越大,批量操作越快,但会增加瞬时请求压力。
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label>
|
||||
探测并发
|
||||
</Label>
|
||||
<Input
|
||||
:model-value="form.probe_concurrency ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="64"
|
||||
placeholder="4"
|
||||
@update:model-value="(v) => form.probe_concurrency = parseNum(v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label>
|
||||
评分 Top-N
|
||||
</Label>
|
||||
<Input
|
||||
:model-value="form.score_top_n ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="4096"
|
||||
placeholder="128"
|
||||
@update:model-value="(v) => form.score_top_n = parseNum(v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label>
|
||||
回退扫描
|
||||
</Label>
|
||||
<Input
|
||||
:model-value="form.score_fallback_scan_limit ?? ''"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100000"
|
||||
placeholder="1024"
|
||||
@update:model-value="(v) => form.score_fallback_scan_limit = parseNum(v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -464,6 +503,9 @@ const form = ref({
|
||||
cost_limit_per_key_tokens: null as number | null | undefined,
|
||||
cost_soft_threshold_percent: null as number | null | undefined,
|
||||
batch_concurrency: null as number | null | undefined,
|
||||
probe_concurrency: null as number | null | undefined,
|
||||
score_top_n: null as number | null | undefined,
|
||||
score_fallback_scan_limit: null as number | null | undefined,
|
||||
probing_enabled: false,
|
||||
probing_interval_minutes: null as number | null | undefined,
|
||||
auto_remove_banned_keys: false,
|
||||
@@ -539,6 +581,9 @@ watch(() => props.modelValue, (open) => {
|
||||
cost_limit_per_key_tokens: cfg?.cost_limit_per_key_tokens ?? null,
|
||||
cost_soft_threshold_percent: cfg?.cost_soft_threshold_percent ?? null,
|
||||
batch_concurrency: cfg?.batch_concurrency ?? null,
|
||||
probe_concurrency: cfg?.probe_concurrency ?? null,
|
||||
score_top_n: cfg?.score_top_n ?? null,
|
||||
score_fallback_scan_limit: cfg?.score_fallback_scan_limit ?? null,
|
||||
probing_enabled: cfg?.probing_enabled ?? false,
|
||||
probing_interval_minutes: cfg?.probing_interval_minutes ?? null,
|
||||
auto_remove_banned_keys: cfg?.auto_remove_banned_keys ?? false,
|
||||
@@ -572,6 +617,9 @@ async function handleSave() {
|
||||
overload_cooldown_seconds: form.value.overload_cooldown_seconds ?? undefined,
|
||||
health_policy_enabled: form.value.health_policy_enabled,
|
||||
batch_concurrency: form.value.batch_concurrency ?? undefined,
|
||||
probe_concurrency: form.value.probe_concurrency ?? undefined,
|
||||
score_top_n: form.value.score_top_n ?? undefined,
|
||||
score_fallback_scan_limit: form.value.score_fallback_scan_limit ?? undefined,
|
||||
probing_enabled: form.value.probing_enabled,
|
||||
probing_interval_minutes: form.value.probing_enabled
|
||||
? (form.value.probing_interval_minutes ?? undefined)
|
||||
|
||||
@@ -1189,6 +1189,170 @@
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-if="selectedProviderId"
|
||||
variant="default"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<div class="flex flex-col gap-3 border-b border-border/60 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-6 sm:py-3.5">
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-base font-semibold">
|
||||
评分排序
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Top {{ poolScorePage.items.length }} / page {{ poolScorePage.page || 1 }}
|
||||
</p>
|
||||
</div>
|
||||
<RefreshButton
|
||||
:loading="scoresLoading"
|
||||
title="刷新评分"
|
||||
@click="() => loadScores()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 border-b border-border/60 bg-muted/20 px-4 py-3 sm:grid-cols-4 sm:px-6">
|
||||
<Input
|
||||
v-model="scoreApiFormatFilter"
|
||||
class="h-8 text-xs"
|
||||
placeholder="api_format"
|
||||
/>
|
||||
<Input
|
||||
v-model="scoreModelIdFilter"
|
||||
class="h-8 text-xs"
|
||||
placeholder="model_id"
|
||||
/>
|
||||
<Select v-model="scoreHardStateFilter">
|
||||
<SelectTrigger class="h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="硬状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="item in poolScoreHardStateOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select v-model="scoreProbeStatusFilter">
|
||||
<SelectTrigger class="h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="探测" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="item in poolScoreProbeStatusOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="scoresLoading && poolScorePage.items.length === 0"
|
||||
class="flex items-center justify-center py-10"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-7 w-7 border-b-2 border-primary" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="poolScorePage.items.length === 0"
|
||||
class="flex flex-col items-center justify-center py-10 text-center"
|
||||
>
|
||||
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
||||
<SlidersHorizontal class="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-muted-foreground">
|
||||
{{ hasPoolScoreFilters ? '未找到匹配评分' : '暂无评分数据' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="overflow-x-auto"
|
||||
>
|
||||
<Table class="w-full min-w-[760px] table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="w-[24%] font-semibold">
|
||||
账号
|
||||
</TableHead>
|
||||
<TableHead class="w-[24%] font-semibold">
|
||||
范围
|
||||
</TableHead>
|
||||
<TableHead class="w-[12%] text-center font-semibold">
|
||||
分数
|
||||
</TableHead>
|
||||
<TableHead class="w-[12%] text-center font-semibold">
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[12%] text-center font-semibold">
|
||||
探测
|
||||
</TableHead>
|
||||
<TableHead class="w-[16%] text-center font-semibold">
|
||||
更新时间
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="score in poolScorePage.items"
|
||||
:key="score.id"
|
||||
class="border-b border-border/40 last:border-b-0 hover:bg-muted/30"
|
||||
>
|
||||
<TableCell class="py-3">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm">
|
||||
{{ score.key?.name || score.member_id }}
|
||||
</div>
|
||||
<div class="truncate font-mono text-[10px] text-muted-foreground">
|
||||
{{ score.member_id }}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3">
|
||||
<div class="min-w-0 text-xs">
|
||||
<div class="truncate text-foreground/90">
|
||||
{{ score.capability || '-' }}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted-foreground">
|
||||
{{ score.scope_kind }}: {{ score.scope_id || '*' }}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 text-center">
|
||||
<span class="font-mono text-xs tabular-nums">
|
||||
{{ formatPoolScore(score.score) }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 text-center">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-[10px]"
|
||||
>
|
||||
{{ getPoolScoreHardStateLabel(score.hard_state) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 text-center">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-[10px]"
|
||||
>
|
||||
{{ getPoolScoreProbeStatusLabel(score.probe_status) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 text-center">
|
||||
<span class="text-[10px] text-muted-foreground">
|
||||
{{ formatUnixSeconds(score.updated_at) }}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<OAuthAccountDialog
|
||||
v-if="selectedProviderId"
|
||||
@@ -1309,6 +1473,7 @@ import {
|
||||
getPoolOverview,
|
||||
getPoolSchedulingPresets,
|
||||
listPoolKeys,
|
||||
listPoolScores,
|
||||
clearPoolCooldown,
|
||||
} from '@/api/endpoints/pool'
|
||||
import {
|
||||
@@ -1324,7 +1489,9 @@ import type {
|
||||
PoolOverviewItem,
|
||||
PoolKeyDetail,
|
||||
PoolKeysPageResponse,
|
||||
PoolMemberScoreItem,
|
||||
PoolPresetMeta,
|
||||
PoolScoresResponse,
|
||||
} from '@/api/endpoints/pool'
|
||||
import type {
|
||||
ClaudeCodeAdvancedConfig,
|
||||
@@ -1420,11 +1587,14 @@ let overviewRequestId = 0
|
||||
let selectProviderRequestId = 0
|
||||
let providerDataRequestId = 0
|
||||
let keysRequestId = 0
|
||||
let scoresRequestId = 0
|
||||
let keysSearchDebounceTimer: number | null = null
|
||||
let scoresFilterDebounceTimer: number | null = null
|
||||
let suppressFiltersWatch = false
|
||||
let hasHydratedInitialProviderSelection = false
|
||||
const POOL_OVERVIEW_CACHE_TTL_MS = 10 * 1000
|
||||
const POOL_KEYS_CACHE_TTL_MS = 10 * 1000
|
||||
const POOL_SCORES_CACHE_TTL_MS = 10 * 1000
|
||||
const POOL_SCHEDULING_PRESETS_CACHE_TTL_MS = 5 * 60 * 1000
|
||||
const poolKeyStatusFilterOptions: Array<{ value: PoolManagementViewState['status'], label: string }> = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
@@ -1432,6 +1602,24 @@ const poolKeyStatusFilterOptions: Array<{ value: PoolManagementViewState['status
|
||||
{ value: 'cooldown', label: '冷却中' },
|
||||
{ value: 'inactive', label: '禁用' },
|
||||
]
|
||||
const poolScoreHardStateOptions = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'available', label: '可用' },
|
||||
{ value: 'unknown', label: '未知' },
|
||||
{ value: 'cooldown', label: '冷却' },
|
||||
{ value: 'quota_exhausted', label: '额度耗尽' },
|
||||
{ value: 'auth_invalid', label: '授权无效' },
|
||||
{ value: 'banned', label: '封禁' },
|
||||
{ value: 'inactive', label: '禁用' },
|
||||
]
|
||||
const poolScoreProbeStatusOptions = [
|
||||
{ value: 'all', label: '全部探测' },
|
||||
{ value: 'never', label: '未探测' },
|
||||
{ value: 'ok', label: '正常' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
{ value: 'stale', label: '过期' },
|
||||
{ value: 'in_progress', label: '探测中' },
|
||||
]
|
||||
|
||||
async function loadOverview(options: { cacheTtlMs?: number } = {}) {
|
||||
const requestId = ++overviewRequestId
|
||||
@@ -1488,6 +1676,7 @@ async function loadOverview(options: { cacheTtlMs?: number } = {}) {
|
||||
showAccountBatchDialog.value = false
|
||||
closeProviderProxyPopovers()
|
||||
resetKeyPage()
|
||||
resetScorePage()
|
||||
}
|
||||
} catch (err) {
|
||||
if (requestId !== overviewRequestId) return
|
||||
@@ -1720,9 +1909,11 @@ async function selectProvider(
|
||||
}
|
||||
keysLoadedOnce.value = false
|
||||
resetKeyPage(currentPage.value, pageSize.value)
|
||||
resetScorePage()
|
||||
const keysTask = loadKeys({ cacheTtlMs: options.cacheTtlMs ?? 0 })
|
||||
// Provider summary is non-blocking for key list rendering.
|
||||
void loadProviderData(id)
|
||||
void loadScores({ cacheTtlMs: options.cacheTtlMs ? POOL_SCORES_CACHE_TTL_MS : 0 })
|
||||
await keysTask
|
||||
if (requestId !== selectProviderRequestId) return
|
||||
}
|
||||
@@ -1748,18 +1939,40 @@ function createEmptyKeyPage(page = 1, pageSizeValue = 50): PoolKeysPageResponse
|
||||
return { total: 0, page, page_size: pageSizeValue, keys: [] }
|
||||
}
|
||||
|
||||
function createEmptyScorePage(): PoolScoresResponse {
|
||||
return {
|
||||
provider_id: selectedProviderId.value || '',
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
filters: {},
|
||||
items: [],
|
||||
}
|
||||
}
|
||||
|
||||
const keyPage = ref<PoolKeysPageResponse>(createEmptyKeyPage())
|
||||
const poolScorePage = ref<PoolScoresResponse>(createEmptyScorePage())
|
||||
const keysLoading = ref(false)
|
||||
const scoresLoading = ref(false)
|
||||
const keysLoadedOnce = ref(false)
|
||||
const refreshingCurrentPageQuota = ref(false)
|
||||
const searchQuery = ref(restoredViewState.search)
|
||||
const statusFilter = ref(restoredViewState.status)
|
||||
const scoreApiFormatFilter = ref('')
|
||||
const scoreModelIdFilter = ref('')
|
||||
const scoreHardStateFilter = ref('all')
|
||||
const scoreProbeStatusFilter = ref('all')
|
||||
const currentPage = ref(restoredViewState.page)
|
||||
const pageSize = ref(restoredViewState.pageSize)
|
||||
const sortBy = ref<PoolManagementSortBy | null>(restoredViewState.sortBy)
|
||||
const sortOrder = ref<PoolManagementSortOrder>(restoredViewState.sortOrder)
|
||||
const poolStatsMode = ref<PoolManagementStatsMode>(restoredViewState.statsMode)
|
||||
const hasPoolKeyFilters = computed(() => searchQuery.value.trim().length > 0 || statusFilter.value !== 'all')
|
||||
const hasPoolScoreFilters = computed(() => {
|
||||
return scoreApiFormatFilter.value.trim().length > 0
|
||||
|| scoreModelIdFilter.value.trim().length > 0
|
||||
|| scoreHardStateFilter.value !== 'all'
|
||||
|| scoreProbeStatusFilter.value !== 'all'
|
||||
})
|
||||
const MANUAL_QUOTA_REFRESH_COOLDOWN_SECONDS = 5 * 60
|
||||
const refreshingOAuthKeyId = ref<string | null>(null)
|
||||
const resettingCycleKeyId = ref<string | null>(null)
|
||||
@@ -2060,6 +2273,10 @@ function resetKeyPage(page = currentPage.value, pageSizeValue = pageSize.value):
|
||||
keyPage.value = createEmptyKeyPage(page, pageSizeValue)
|
||||
}
|
||||
|
||||
function resetScorePage(): void {
|
||||
poolScorePage.value = createEmptyScorePage()
|
||||
}
|
||||
|
||||
function refreshOverviewInBackground(): void {
|
||||
void loadOverview()
|
||||
}
|
||||
@@ -2199,6 +2416,36 @@ async function refreshCurrentPage() {
|
||||
if (!quotaDidReload) {
|
||||
await refresh()
|
||||
}
|
||||
void loadScores()
|
||||
}
|
||||
|
||||
async function loadScores(options: { cacheTtlMs?: number } = {}) {
|
||||
if (!selectedProviderId.value) return
|
||||
const requestId = ++scoresRequestId
|
||||
const providerId = selectedProviderId.value
|
||||
scoresLoading.value = true
|
||||
try {
|
||||
const nextPage = await listPoolScores(providerId, {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
api_format: scoreApiFormatFilter.value.trim() || undefined,
|
||||
model_id: scoreModelIdFilter.value.trim() || undefined,
|
||||
hard_state: scoreHardStateFilter.value === 'all' ? undefined : scoreHardStateFilter.value,
|
||||
probe_status: scoreProbeStatusFilter.value === 'all' ? undefined : scoreProbeStatusFilter.value,
|
||||
}, {
|
||||
cacheTtlMs: options.cacheTtlMs ?? 0,
|
||||
})
|
||||
if (requestId !== scoresRequestId || selectedProviderId.value !== providerId) return
|
||||
poolScorePage.value = nextPage
|
||||
} catch (err) {
|
||||
if (requestId !== scoresRequestId || selectedProviderId.value !== providerId) return
|
||||
resetScorePage()
|
||||
showError(parseApiError(err, '读取评分失败'))
|
||||
} finally {
|
||||
if (requestId === scoresRequestId) {
|
||||
scoresLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKeys(options: { cacheTtlMs?: number } = {}) {
|
||||
@@ -2276,6 +2523,20 @@ watch(searchQuery, () => {
|
||||
}, 300)
|
||||
})
|
||||
|
||||
watch([scoreApiFormatFilter, scoreModelIdFilter], () => {
|
||||
if (scoresFilterDebounceTimer !== null) {
|
||||
clearTimeout(scoresFilterDebounceTimer)
|
||||
}
|
||||
scoresFilterDebounceTimer = window.setTimeout(() => {
|
||||
scoresFilterDebounceTimer = null
|
||||
void loadScores({ cacheTtlMs: POOL_SCORES_CACHE_TTL_MS })
|
||||
}, 300)
|
||||
})
|
||||
|
||||
watch([scoreHardStateFilter, scoreProbeStatusFilter], () => {
|
||||
void loadScores({ cacheTtlMs: POOL_SCORES_CACHE_TTL_MS })
|
||||
})
|
||||
|
||||
function normalizeAuthTypeForEdit(key: PoolKeyDetail): EndpointAPIKey['auth_type'] {
|
||||
if (isOAuthManagedCredential(key)) return 'oauth'
|
||||
if (isServiceAccountCredential(key)) return 'service_account'
|
||||
@@ -3584,6 +3845,26 @@ function formatStatUsd(value: number | string | null | undefined): string {
|
||||
return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
}
|
||||
|
||||
function formatPoolScore(value: number): string {
|
||||
const n = Number(value)
|
||||
if (!Number.isFinite(n)) return '0.000'
|
||||
return n.toFixed(3)
|
||||
}
|
||||
|
||||
function getPoolScoreHardStateLabel(value: PoolMemberScoreItem['hard_state']): string {
|
||||
return poolScoreHardStateOptions.find(item => item.value === value)?.label || value
|
||||
}
|
||||
|
||||
function getPoolScoreProbeStatusLabel(value: PoolMemberScoreItem['probe_status']): string {
|
||||
return poolScoreProbeStatusOptions.find(item => item.value === value)?.label || value
|
||||
}
|
||||
|
||||
function formatUnixSeconds(seconds: number | null | undefined): string {
|
||||
const raw = Number(seconds ?? 0)
|
||||
if (!Number.isFinite(raw) || raw <= 0) return '-'
|
||||
return formatRelativeTime(new Date(raw * 1000).toISOString())
|
||||
}
|
||||
|
||||
function formatRelativeTime(isoStr: string): string {
|
||||
const date = new Date(isoStr)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
@@ -3611,9 +3892,14 @@ onBeforeUnmount(() => {
|
||||
clearTimeout(keysSearchDebounceTimer)
|
||||
keysSearchDebounceTimer = null
|
||||
}
|
||||
if (scoresFilterDebounceTimer !== null) {
|
||||
clearTimeout(scoresFilterDebounceTimer)
|
||||
scoresFilterDebounceTimer = null
|
||||
}
|
||||
overviewRequestId += 1
|
||||
selectProviderRequestId += 1
|
||||
providerDataRequestId += 1
|
||||
keysRequestId += 1
|
||||
scoresRequestId += 1
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user