diff --git a/webui/src/App.vue b/webui/src/App.vue index 3153723..8ac058d 100644 --- a/webui/src/App.vue +++ b/webui/src/App.vue @@ -1,7 +1,7 @@ + + diff --git a/webui/src/components/ProvidersView.vue b/webui/src/components/ProvidersView.vue index 93a45c1..49143bb 100644 --- a/webui/src/components/ProvidersView.vue +++ b/webui/src/components/ProvidersView.vue @@ -9,6 +9,7 @@ import { computed, h, reactive, watch } from 'vue' import DataTable from '@/components/DataTable.vue' import ModelChip from '@/components/ModelChip.vue' +import ProviderRateBadge from '@/components/ProviderRateBadge.vue' import SearchInput from '@/components/SearchInput.vue' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' import { Badge } from '@/components/ui/badge' @@ -20,10 +21,11 @@ import { CardTitle, } from '@/components/ui/card' import { useSearchQuery } from '@/composables/useSearchQuery' -import { formatAgo, formatTimestamp, routableModelId } from '@/lib/format' +import { formatAgo, formatTimestamp, refreshLabel, routableModelId } from '@/lib/format' +import { isModelDegraded } from '@/lib/provider-rate' import { type ExpandState, clearExpandOverrides, computeExpandedItems, toggleItemExpand } from '@/lib/search-expand' import { useDashboardStore } from '@/stores/dashboard' -import type { AdminAliasView, AdminProviderView } from '@/types/api' +import type { AdminAliasView, AdminModelRateView, AdminProviderView } from '@/types/api' // This component backs the "Providers" tab (App.vue) — a UI-label rename // only. The data it renders still comes from GET /admin/api/overview @@ -136,6 +138,20 @@ function visibleModels(p: AdminProviderView): string[] { return hasQuery.value ? p.models.filter((m) => modelMatches(p.name, m)) : p.models } +/** ZERO_MODEL_RATE is the fallback ProviderRateBadge reads for a model p.modelRates has no entry for — should not happen (admin.go's buildAdminOverview populates one entry per Models id, unconditionally), but a defensive fallback keeps a stale/mismatched client build from throwing rather than just under-reporting. */ +const ZERO_MODEL_RATE: AdminModelRateView = { attemptsDay: 0, failuresDay: 0, attemptsMinute: 0, failuresMinute: 0 } + +/** modelRateFor looks up one model's counters within p.modelRates (Feature A, v0.22), falling back to ZERO_MODEL_RATE. */ +function modelRateFor(p: AdminProviderView, model: string): AdminModelRateView { + return p.modelRates[model] ?? ZERO_MODEL_RATE +} + +/** modelIsDegraded gates the per-model rate badge (spec: "ONLY when that model is degraded") so a healthy or no-traffic model's ModelChip renders with no badge beside it at all. */ +function modelIsDegraded(p: AdminProviderView, model: string): boolean { + const r = modelRateFor(p, model) + return isModelDegraded(r.attemptsDay, r.failuresDay) +} + // --- model aliases (sortable DataTable, operator directive) --- // // The alias id gets the ModelChip copy treatment (an alias name IS the @@ -244,13 +260,19 @@ const aliasEmptyMessage = computed(() => {{ p.name }} {{ p.type }} {{ p.modelCount }} models +