From 3f70f2604ab9376facc8e78a5dde7473dee09b55 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Mon, 6 Jul 2026 01:32:57 +0000 Subject: [PATCH] fix(gui): make 'API key required' banner reflect live /api/secrets status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The banner used a static META.hasKey flag (false for firms+roads511), so those always showed 'API key not configured' even when keys were set — contradicting the ManagedSecret SET badge. Now fetch /api/secrets and show the banner only when a keyed adapter's secret is genuinely unset; copy softened to 'API key required — set it in the field below'. wzdx kept keyless (no banner). Keyless adapters never banner. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/pages/Environment.tsx | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/work/dashboard-frontend/src/pages/Environment.tsx b/work/dashboard-frontend/src/pages/Environment.tsx index d56dada..8525580 100644 --- a/work/dashboard-frontend/src/pages/Environment.tsx +++ b/work/dashboard-frontend/src/pages/Environment.tsx @@ -243,7 +243,7 @@ function AdapterPanel({ title, subtitle, enabled, onEnabled, feedSource, onFeedS {!hasKey && (
- API key not configured — contact admin + API key required — set it in the field below
)} {nativeOnly && ( @@ -287,6 +287,17 @@ const META: Record = { satpass: { label: 'Satellite Passes', subtitle: 'Observer pass alerts via Central', health: 'satpass', hasCentral: true, nativeOnly: false, hasKey: true }, } +// Keyed adapters → their secret env var (matches secrets_store.SECRET_LABELS). +// Adapters NOT listed here are keyless and never show the "API key required" +// banner. The banner for a keyed adapter is driven by the LIVE secret status +// from GET /api/secrets, not the static META.hasKey hint below. +const ADAPTER_SECRET_ENV: Partial> = { + firms: 'FIRMS_MAP_KEY', + roads511: 'ROADS511_API_KEY', + traffic: 'TOMTOM_API_KEY', + // wzdx is keyless (FHWA registry) — no banner +} + const FAMILIES: { key: string; label: string; icon: typeof Cloud; adapters: AdapterKey[] }[] = [ { key: 'central', label: 'Central', icon: Server, adapters: [] }, { key: 'weather', label: 'Weather', icon: Cloud, adapters: ['nws'] }, @@ -319,6 +330,11 @@ export default function Environment() { // include_in_llm_context per backend adapter name — fetched from /api/adapter-meta const [llmMeta, setLlmMeta] = useState>({}) + // Live secret set/unset status per env var — fetched from /api/secrets (same + // source the ManagedSecret widget uses). Drives the truthful "API key required" + // banner so it only shows when a keyed adapter's secret is genuinely unset. + const [secretStatus, setSecretStatus] = useState>({}) + // WFIGS/fires adapter config state const [wfigsConfig, setWfigsConfig] = useState({ allowed_incident_types: ['WF'], @@ -592,6 +608,21 @@ export default function Environment() { })() }, []) + // Fetch live secret status once on mount (best-effort) for the key banner. + useEffect(() => { + ;(async () => { + try { + const res = await fetch('/api/secrets') + if (res.ok) { + const data = await res.json() as { env_var: string; is_set: boolean }[] + const map: Record = {} + for (const s of data) map[s.env_var] = s.is_set + setSecretStatus(map) + } + } catch { /* best-effort */ } + })() + }, []) + useEffect(() => { const load = async () => { try { @@ -917,6 +948,17 @@ const save = async () => { const eventsFor = (key: AdapterKey): EnvEvent[] => events.filter((e) => e.source === META[key].health) + // Truthful key-banner logic: keyless adapters never show the banner; keyed + // adapters show it ONLY when their real secret (GET /api/secrets) is unset. + // While secret status is still loading (undefined), default to true so we + // never flash a false "API key required" banner for an already-set key. + const effectiveHasKey = (key: AdapterKey): boolean => { + const envVar = ADAPTER_SECRET_ENV[key] + if (!envVar) return true // keyless adapter — no key needed + const isSet = secretStatus[envVar] + return isSet === undefined ? true : isSet + } + const fam = FAMILIES.find((f) => f.key === family)! const activeAdapter: AdapterKey | null = fam.adapters.length === 0 ? null : (adapter && fam.adapters.includes(adapter) ? adapter : fam.adapters[0]) @@ -1735,7 +1777,7 @@ const save = async () => { onFeedSource={(v) => setAdapterField(activeAdapter, { feed_source: v })} hasCentral={META[activeAdapter].hasCentral} nativeOnly={META[activeAdapter].nativeOnly} - hasKey={META[activeAdapter].hasKey} + hasKey={effectiveHasKey(activeAdapter)} health={healthFor(activeAdapter)} events={eventsFor(activeAdapter)} llmContext={PANEL_META_KEY[activeAdapter] !== undefined ? (llmMeta[PANEL_META_KEY[activeAdapter]!] ?? true) : undefined}