Last refresh
{{ refreshDetailLabel(p.discoveryEnabled, p.lastRefresh) }}
+
+
Discovery health
+ {{ healthBadgeLabel(p) }}
+
Last error
{{ p.lastErr }}
diff --git a/webui/src/lib/format.ts b/webui/src/lib/format.ts
index b21f431..c0b332b 100644
--- a/webui/src/lib/format.ts
+++ b/webui/src/lib/format.ts
@@ -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'
diff --git a/webui/src/types/api.ts b/webui/src/types/api.ts
index 0b7821b..11924e4 100644
--- a/webui/src/types/api.ts
+++ b/webui/src/types/api.ts
@@ -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: