fix(gui): make 'API key required' banner reflect live /api/secrets status (#50)

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: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-07-05 19:33:01 -06:00 committed by GitHub
commit 339d7e3c80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -243,7 +243,7 @@ function AdapterPanel({ title, subtitle, enabled, onEnabled, feedSource, onFeedS
</div>
{!hasKey && (
<div className="text-xs text-accent bg-accent/10 p-2">
API key not configured contact admin
API key required set it in the field below
</div>
)}
{nativeOnly && (
@ -287,6 +287,17 @@ const META: Record<AdapterKey, AdapterMeta> = {
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<Record<AdapterKey, string>> = {
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<Record<string, boolean>>({})
// 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<Record<string, boolean>>({})
// WFIGS/fires adapter config state
const [wfigsConfig, setWfigsConfig] = useState<WfigsConfig>({
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<string, boolean> = {}
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}