mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
fix(gui): make 'API key required' banner reflect live /api/secrets status
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) <noreply@anthropic.com>
This commit is contained in:
parent
adfe9ff4f2
commit
3f70f2604a
1 changed files with 44 additions and 2 deletions
|
|
@ -243,7 +243,7 @@ function AdapterPanel({ title, subtitle, enabled, onEnabled, feedSource, onFeedS
|
||||||
</div>
|
</div>
|
||||||
{!hasKey && (
|
{!hasKey && (
|
||||||
<div className="text-xs text-accent bg-accent/10 p-2">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{nativeOnly && (
|
{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 },
|
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[] }[] = [
|
const FAMILIES: { key: string; label: string; icon: typeof Cloud; adapters: AdapterKey[] }[] = [
|
||||||
{ key: 'central', label: 'Central', icon: Server, adapters: [] },
|
{ key: 'central', label: 'Central', icon: Server, adapters: [] },
|
||||||
{ key: 'weather', label: 'Weather', icon: Cloud, adapters: ['nws'] },
|
{ 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
|
// include_in_llm_context per backend adapter name — fetched from /api/adapter-meta
|
||||||
const [llmMeta, setLlmMeta] = useState<Record<string, boolean>>({})
|
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
|
// WFIGS/fires adapter config state
|
||||||
const [wfigsConfig, setWfigsConfig] = useState<WfigsConfig>({
|
const [wfigsConfig, setWfigsConfig] = useState<WfigsConfig>({
|
||||||
allowed_incident_types: ['WF'],
|
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(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -917,6 +948,17 @@ const save = async () => {
|
||||||
const eventsFor = (key: AdapterKey): EnvEvent[] =>
|
const eventsFor = (key: AdapterKey): EnvEvent[] =>
|
||||||
events.filter((e) => e.source === META[key].health)
|
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 fam = FAMILIES.find((f) => f.key === family)!
|
||||||
const activeAdapter: AdapterKey | null =
|
const activeAdapter: AdapterKey | null =
|
||||||
fam.adapters.length === 0 ? null : (adapter && fam.adapters.includes(adapter) ? adapter : fam.adapters[0])
|
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 })}
|
onFeedSource={(v) => setAdapterField(activeAdapter, { feed_source: v })}
|
||||||
hasCentral={META[activeAdapter].hasCentral}
|
hasCentral={META[activeAdapter].hasCentral}
|
||||||
nativeOnly={META[activeAdapter].nativeOnly}
|
nativeOnly={META[activeAdapter].nativeOnly}
|
||||||
hasKey={META[activeAdapter].hasKey}
|
hasKey={effectiveHasKey(activeAdapter)}
|
||||||
health={healthFor(activeAdapter)}
|
health={healthFor(activeAdapter)}
|
||||||
events={eventsFor(activeAdapter)}
|
events={eventsFor(activeAdapter)}
|
||||||
llmContext={PANEL_META_KEY[activeAdapter] !== undefined ? (llmMeta[PANEL_META_KEY[activeAdapter]!] ?? true) : undefined}
|
llmContext={PANEL_META_KEY[activeAdapter] !== undefined ? (llmMeta[PANEL_META_KEY[activeAdapter]!] ?? true) : undefined}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue