mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-13 04:58:50 +00:00
feat(webui): surface discovery health state in the Providers tab
Add healthState and openUntil to AdminProviderView, and a formatUntil helper alongside the existing formatAgo/formatTimestamp pair, rendering a future ISO timestamp as "in Ns". ProvidersView.vue shows a Badge next to a provider's other status indicators, destructive for open and secondary for half-open, using the existing shadcn-vue Badge component rather than a hand-rolled indicator. A closed provider, the healthy common case, renders no badge at all, matching the existing convention for the per-model rate badge. The expanded accordion content adds a matching detail row. This was the brief's stated motivation for the admin surfacing work: a broken provider looked identical to a working one in the dashboard. That gap was previously closed at the API layer only; this closes it in the UI clients actually see. Regenerates admin_assets_gen.go from the rebuilt webui via make admin-ui, per this repository's own convention of committing the generated asset alongside its source change.
This commit is contained in:
+6
-6
File diff suppressed because one or more lines are too long
@@ -21,7 +21,7 @@ import {
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { useSearchQuery } from '@/composables/useSearchQuery'
|
||||
import { formatAgo, formatContextWindow, refreshDetailLabel, refreshLabel, routableModelId } from '@/lib/format'
|
||||
import { formatAgo, formatContextWindow, formatUntil, refreshDetailLabel, 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'
|
||||
@@ -169,6 +169,31 @@ function modelMetaFor(p: AdminProviderView, model: string): AdminModelMetaView {
|
||||
return p.modelMeta?.[model] ?? ZERO_MODEL_META
|
||||
}
|
||||
|
||||
// --- discovery circuit breaker (feat/provider-health) ---
|
||||
//
|
||||
// Badge shown ONLY for 'open'/'half-open' — mirroring modelIsDegraded's
|
||||
// own "no badge when healthy" convention above: a 'closed' provider (the
|
||||
// common case) renders no breaker badge at all, exactly like a
|
||||
// non-degraded model gets no ProviderRateBadge.
|
||||
|
||||
/** healthBadgeVariant maps healthState to the shadcn-vue Badge variant: destructive (backing off) for 'open', secondary (in progress) for 'half-open', undefined (no badge) for 'closed'. */
|
||||
function healthBadgeVariant(state: AdminProviderView['healthState']): 'destructive' | 'secondary' | undefined {
|
||||
switch (state) {
|
||||
case 'open':
|
||||
return 'destructive'
|
||||
case 'half-open':
|
||||
return 'secondary'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** healthBadgeLabel renders the breaker badge's text: "open (in Ns)" while backing off (formatUntil omits the parenthetical once openUntil has passed or is unset), or "half-open" while probing. */
|
||||
function healthBadgeLabel(p: AdminProviderView): string {
|
||||
const until = formatUntil(p.openUntil)
|
||||
return until ? `${p.healthState} (${until})` : p.healthState
|
||||
}
|
||||
|
||||
// --- model aliases (sortable DataTable, operator directive) ---
|
||||
//
|
||||
// The alias id gets the ModelChip copy treatment (an alias name IS the
|
||||
@@ -295,6 +320,21 @@ const aliasEmptyMessage = computed(() =>
|
||||
:attempts-minute="p.attemptsMinute"
|
||||
:failures-minute="p.failuresMinute"
|
||||
/>
|
||||
<!--
|
||||
Discovery circuit breaker (feat/provider-health): no
|
||||
badge at all while 'closed' (the healthy, common case),
|
||||
mirroring modelIsDegraded's own convention for the
|
||||
per-model rate badge above.
|
||||
-->
|
||||
<Badge
|
||||
v-if="p.healthState !== 'closed'"
|
||||
as="span"
|
||||
:variant="healthBadgeVariant(p.healthState)"
|
||||
class="font-normal"
|
||||
:title="p.healthState === 'open' ? 'discovery is backing off after repeated failures' : 'a discovery probe is deciding whether to recover'"
|
||||
>
|
||||
{{ healthBadgeLabel(p) }}
|
||||
</Badge>
|
||||
<FontAwesomeIcon
|
||||
v-if="p.lastErr"
|
||||
:icon="faTriangleExclamation"
|
||||
@@ -314,6 +354,10 @@ const aliasEmptyMessage = computed(() =>
|
||||
<dt class="text-xs text-muted-foreground">Last refresh</dt>
|
||||
<dd>{{ refreshDetailLabel(p.discoveryEnabled, p.lastRefresh) }}</dd>
|
||||
</div>
|
||||
<div v-if="p.healthState !== 'closed'">
|
||||
<dt class="text-xs text-muted-foreground">Discovery health</dt>
|
||||
<dd class="text-destructive">{{ healthBadgeLabel(p) }}</dd>
|
||||
</div>
|
||||
<div v-if="p.lastErr">
|
||||
<dt class="text-xs text-muted-foreground">Last error</dt>
|
||||
<dd class="text-destructive">{{ p.lastErr }}</dd>
|
||||
|
||||
@@ -156,6 +156,24 @@ export function formatAgo(iso: string | undefined): string {
|
||||
return ` (${secs}s ago)`
|
||||
}
|
||||
|
||||
/**
|
||||
* formatUntil renders a FUTURE ISO timestamp as "in Ns" — the discovery
|
||||
* circuit breaker's openUntil (feat/provider-health, admin.go's
|
||||
* adminProviderView.OpenUntil) is the only caller — or "" for the unset
|
||||
* zero-time sentinel, an unparseable value, or a time already in the
|
||||
* past (a stale poll reading an openUntil the breaker has since moved
|
||||
* past; the next 5s refresh corrects it, so this just omits the detail
|
||||
* rather than showing a negative/nonsensical countdown).
|
||||
*/
|
||||
export function formatUntil(iso: string | undefined): string {
|
||||
if (!iso || iso === ZERO_TIME) return ''
|
||||
const then = new Date(iso).getTime()
|
||||
if (Number.isNaN(then)) return ''
|
||||
const secs = Math.round((then - Date.now()) / 1000)
|
||||
if (secs <= 0) return ''
|
||||
return `in ${secs}s`
|
||||
}
|
||||
|
||||
/** formatTimestamp renders an ISO timestamp for display, or "never" for the unset zero-time sentinel. */
|
||||
export function formatTimestamp(iso: string | undefined): string {
|
||||
if (!iso || iso === ZERO_TIME) return 'never'
|
||||
|
||||
@@ -64,6 +64,25 @@ export interface AdminProviderView {
|
||||
* zero lastRefresh.
|
||||
*/
|
||||
discoveryEnabled: boolean
|
||||
/**
|
||||
* healthState is the provider's DISCOVERY circuit breaker state
|
||||
* (admin.go: adminProviderView.HealthState — feat/provider-health):
|
||||
* 'closed' (normal), 'open' (backing off after repeated discovery
|
||||
* failures — openUntil below is when it next probes), or 'half-open'
|
||||
* (a probe is in flight, deciding whether to close again). Reflects
|
||||
* the discovery endpoint ONLY, not request-path health — a provider
|
||||
* whose model listing 401s while its actual completion endpoint works
|
||||
* fine still reads 'open' here. A provider with discovery disabled, or
|
||||
* one that has never failed a refresh, always reads 'closed'.
|
||||
*/
|
||||
healthState: 'closed' | 'open' | 'half-open'
|
||||
/**
|
||||
* openUntil is when this provider's breaker will next attempt a
|
||||
* half-open probe (admin.go: adminProviderView.OpenUntil). The unset
|
||||
* zero-time sentinel (formatAgo/formatTimestamp's ZERO_TIME) when
|
||||
* healthState is not 'open'.
|
||||
*/
|
||||
openUntil: string
|
||||
/**
|
||||
* attemptsDay/failuresDay/attemptsMinute/failuresMinute are this
|
||||
* provider's current-window upstream-attempt counters (admin.go:
|
||||
|
||||
Reference in New Issue
Block a user