mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-19 05:47:48 +00:00
fix(webui): match the health detail row color to its badge, add tests
ProvidersView.vue's accordion detail row hardcoded text-destructive red for any non-closed health state, while the trigger row's Badge deliberately used grey (secondary) for half-open and red (destructive) only for open. Add healthDetailClass so the detail row's color always matches the badge's own severity, instead of overstating a half-open probe as an error. Add a describe block for formatUntil to format.spec.ts, covering the unset sentinel, undefined, a future timestamp, and a past one. It had no test coverage since being added in the previous round.
This commit is contained in:
+10
-10
File diff suppressed because one or more lines are too long
@@ -194,6 +194,11 @@ function healthBadgeLabel(p: AdminProviderView): string {
|
|||||||
return until ? `${p.healthState} (${until})` : p.healthState
|
return until ? `${p.healthState} (${until})` : p.healthState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** healthDetailClass matches the accordion-content detail row's text color to healthBadgeVariant's own trigger-row Badge color, rather than hardcoding text-destructive for every non-closed state: destructive (red) for 'open', muted (grey, the same neutral tone secondary conveys on the Badge) for 'half-open'. */
|
||||||
|
function healthDetailClass(state: AdminProviderView['healthState']): string {
|
||||||
|
return state === 'open' ? 'text-destructive' : 'text-muted-foreground'
|
||||||
|
}
|
||||||
|
|
||||||
// --- model aliases (sortable DataTable, operator directive) ---
|
// --- model aliases (sortable DataTable, operator directive) ---
|
||||||
//
|
//
|
||||||
// The alias id gets the ModelChip copy treatment (an alias name IS the
|
// The alias id gets the ModelChip copy treatment (an alias name IS the
|
||||||
@@ -356,7 +361,7 @@ const aliasEmptyMessage = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="p.healthState !== 'closed'">
|
<div v-if="p.healthState !== 'closed'">
|
||||||
<dt class="text-xs text-muted-foreground">Discovery health</dt>
|
<dt class="text-xs text-muted-foreground">Discovery health</dt>
|
||||||
<dd class="text-destructive">{{ healthBadgeLabel(p) }}</dd>
|
<dd :class="healthDetailClass(p.healthState)">{{ healthBadgeLabel(p) }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="p.lastErr">
|
<div v-if="p.lastErr">
|
||||||
<dt class="text-xs text-muted-foreground">Last error</dt>
|
<dt class="text-xs text-muted-foreground">Last error</dt>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { formatCompactCount, formatContextWindow, formatModelCostHover, refreshDetailLabel, refreshLabel } from './format'
|
import { formatCompactCount, formatContextWindow, formatModelCostHover, formatUntil, refreshDetailLabel, refreshLabel } from './format'
|
||||||
|
|
||||||
const ZERO_TIME = '0001-01-01T00:00:00Z'
|
const ZERO_TIME = '0001-01-01T00:00:00Z'
|
||||||
|
|
||||||
@@ -98,6 +98,30 @@ describe('formatModelCostHover', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Discovery circuit breaker (feat/provider-health, round 3): openUntil's
|
||||||
|
// future-time counterpart to formatAgo's past-time "(Ns ago)". Only
|
||||||
|
// caller today is ProvidersView.vue's healthBadgeLabel, rendering the
|
||||||
|
// breaker's live "open (in Ns)" countdown.
|
||||||
|
describe('formatUntil', () => {
|
||||||
|
it('renders "" for the unset zero-time sentinel', () => {
|
||||||
|
expect(formatUntil(ZERO_TIME)).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders "" for undefined', () => {
|
||||||
|
expect(formatUntil(undefined)).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders "in Ns" for a future timestamp', () => {
|
||||||
|
const inFortyTwoSeconds = new Date(Date.now() + 42_000).toISOString()
|
||||||
|
expect(formatUntil(inFortyTwoSeconds)).toMatch(/^in \d+s$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders "" for a timestamp already in the past — a stale poll must not show a negative countdown', () => {
|
||||||
|
const oneMinuteAgo = new Date(Date.now() - 60_000).toISOString()
|
||||||
|
expect(formatUntil(oneMinuteAgo)).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Usage-count compact display (v0.23 addendum): SI-style, 3 significant
|
// Usage-count compact display (v0.23 addendum): SI-style, 3 significant
|
||||||
// figures, floor-not-round (the same honesty rule formatRatePercent
|
// figures, floor-not-round (the same honesty rule formatRatePercent
|
||||||
// already applies to the success-rate badge — a count must never appear
|
// already applies to the success-rate badge — a count must never appear
|
||||||
|
|||||||
Reference in New Issue
Block a user