'use client'; import { API_BASE } from '@/lib/api'; import { clearAdminSession, hasAdminSession, primeAdminSession } from '@/lib/adminSession'; import { controlPlaneFetch, controlPlaneJson } from '@/lib/controlPlane'; import { isNativeProtectedSettingsReady } from '@/lib/nativeProtectedSettings'; import { fetchPrivacyProfileSnapshot, fetchRnsStatusSnapshot, invalidatePrivacyProfileCache, invalidateRnsStatusCache, } from '@/mesh/controlPlaneStatusClient'; import { clearBrowserIdentityState, purgeBrowserContactGraph, purgeBrowserSigningMaterial, setSecureModeCached, } from '@/mesh/meshIdentity'; import { purgeBrowserDmState } from '@/mesh/meshDmWorkerClient'; import { connectWormhole, disconnectWormhole, fetchWormholeSettings, fetchWormholeState, invalidateWormholeRuntimeCache, joinWormhole, restartWormhole, type WormholeState, } from '@/mesh/wormholeClient'; import { fetchWormholeDmRootHealth, fetchWormholeIdentity, type WormholeDmRootHealth, } from '@/mesh/wormholeIdentityClient'; import { formatLegacyCompatibilitySeenAt, hasLegacyCompatibilityActivity, summarizeLegacyCompatibility, } from '@/mesh/wormholeCompatibility'; import { formatGateCompatSeenAt, getGateCompatTelemetryEventName, getGateCompatTelemetrySnapshot, summarizeGateCompatTelemetry, type GateCompatTelemetrySnapshot, } from '@/mesh/gateCompatTelemetry'; import { describeBrowserGateLocalRuntimeStatus, getBrowserGateLocalRuntimeEventName, getBrowserGateLocalRuntimeStatus, type BrowserGateLocalRuntimeStatus, } from '@/mesh/meshGateWorkerClient'; import { isNativeDesktop, companionStatus as fetchCompanionStatus, companionEnable, companionDisable, companionOpenBrowser, type CompanionStatus, } from '@/lib/desktopCompanion'; import React, { useState, useEffect, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Settings, ExternalLink, Key, Shield, X, Save, ChevronDown, ChevronUp, Rss, Plus, Trash2, RotateCcw, Satellite, Copy, Check, Radar, } from 'lucide-react'; import { // Issue #298: Sentinel credentials now live server-side. The legacy // browser-storage helpers (getSentinelCredentials / setSentinelCredentials // / clearSentinelCredentials / getSentinelCredentialStorageMode) have // been removed from sentinelHub.ts. We use the new status check + the // one-time migration helper instead. checkBackendSentinelStatus, migrateLegacySentinelBrowserKeys, } from '@/lib/sentinelHub'; import { getPrivacyProfilePreference, getPrivacyStrictPreference, getSessionModePreference, migrateSensitiveBrowserItems, setPrivacyProfilePreference, setPrivacyStrictPreference, setSessionModePreference, } from '@/lib/privacyBrowserStorage'; import { useTranslation, LOCALES, type Locale } from '@/i18n'; interface ApiEntry { id: string; name: string; description: string; category: string; url: string | null; required: boolean; has_key: boolean; env_key: string | null; is_set: boolean; } interface FeedEntry { name: string; url: string; weight: number; } interface EnvMeta { env_path: string; env_path_exists: boolean; env_path_writable: boolean; env_example_path: string; env_example_path_exists: boolean; operator_keys_env_path?: string; operator_keys_env_path_exists?: boolean; operator_keys_env_path_writable?: boolean; } const WEIGHT_LABELS: Record = { 1: 'LOW', 2: 'MED', 3: 'STD', 4: 'HIGH', 5: 'CRIT', }; const WEIGHT_COLORS: Record = { 1: 'text-gray-400 border-gray-600', 2: 'text-blue-400 border-blue-600', 3: 'text-cyan-400 border-cyan-600', 4: 'text-orange-400 border-orange-600', 5: 'text-red-400 border-red-600', }; const SETTINGS_FOCUS_KEY = 'sb_settings_focus'; const WORMHOLE_RETURN_KEY = 'sb_wormhole_return_target'; const WORMHOLE_READY_EVENT = 'sb:wormhole-ready'; // Issue #298 (tg12): Sentinel credentials moved from browser storage to // the backend ``.env`` (managed through the API Keys panel). The legacy // keys (``sb_sentinel_client_id`` / ``sb_sentinel_client_secret`` / // ``sb_sentinel_instance_id``) are no longer treated as sensitive // browser state because they are no longer written. ``SentinelTab`` // runs ``migrateLegacySentinelBrowserKeys()`` once on mount to clear // any leftover values from pre-#298 installs. const PRIVACY_SENSITIVE_BROWSER_KEYS = [ 'sb_infonet_head', 'sb_infonet_head_history', 'sb_infonet_peers', ] as const; async function applySecureModeBoundary(enabled: boolean): Promise { setSecureModeCached(enabled); if (!enabled) return; purgeBrowserSigningMaterial(); purgeBrowserContactGraph(); await purgeBrowserDmState(); } function migratePrivacySensitiveBrowserState(): void { migrateSensitiveBrowserItems([...PRIVACY_SENSITIVE_BROWSER_KEYS]); } const MAX_FEEDS = 50; // Category colors for the tactical UI const CATEGORY_COLORS: Record = { Aviation: 'text-cyan-400 border-cyan-500/30 bg-cyan-950/20', Maritime: 'text-blue-400 border-blue-500/30 bg-blue-950/20', Geophysical: 'text-orange-400 border-orange-500/30 bg-orange-950/20', Space: 'text-purple-400 border-purple-500/30 bg-purple-950/20', Intelligence: 'text-red-400 border-red-500/30 bg-red-950/20', Geolocation: 'text-green-400 border-green-500/30 bg-green-950/20', Weather: 'text-yellow-400 border-yellow-500/30 bg-yellow-950/20', Markets: 'text-emerald-400 border-emerald-500/30 bg-emerald-950/20', SIGINT: 'text-rose-400 border-rose-500/30 bg-rose-950/20', Reconnaissance: 'text-green-400 border-green-500/30 bg-green-950/20', }; function dmRootMonitorTone(state: string | undefined): string { switch (String(state || '').toLowerCase()) { case 'ok': return 'border-green-500/35 bg-green-950/16 text-green-300'; case 'warning': return 'border-yellow-500/35 bg-yellow-950/16 text-yellow-200'; case 'critical': return 'border-red-500/35 bg-red-950/16 text-red-200'; default: return 'border-cyan-500/25 bg-cyan-950/10 text-cyan-200'; } } function dmRootMonitorLabel(state: string | undefined): string { switch (String(state || '').toLowerCase()) { case 'ok': return 'HEALTHY'; case 'warning': return 'ATTENTION'; case 'critical': return 'BLOCKED'; default: return 'UNKNOWN'; } } function dmRootUrgencyTone(urgency: string | undefined): string { switch (String(urgency || '').toLowerCase()) { case 'page': return 'border-red-500/35 bg-red-950/18 text-red-200'; case 'ticket': return 'border-yellow-500/35 bg-yellow-950/18 text-yellow-200'; case 'watch': return 'border-cyan-500/35 bg-cyan-950/18 text-cyan-200'; default: return 'border-slate-600/35 bg-slate-900/18 text-slate-300'; } } function formatAgeWindow(ageS?: number, maxS?: number): string { const age = Math.max(0, Number(ageS || 0)); const max = Math.max(0, Number(maxS || 0)); const fmt = (value: number) => { if (value <= 0) return '0s'; if (value < 60) return `${value}s`; if (value < 3600) return `${Math.round(value / 60)}m`; return `${Math.round(value / 3600)}h`; }; return max > 0 ? `${fmt(age)} / ${fmt(max)} max` : fmt(age); } type Tab = 'api-keys' | 'news-feeds' | 'sentinel' | 'sar' | 'protocol'; const SettingsPanel = React.memo(function SettingsPanel({ isOpen, onClose, }: { isOpen: boolean; onClose: () => void; }) { const [activeTab, setActiveTab] = useState('api-keys'); // Native desktop bypass: when the native IPC bridge is present, protected // settings are authenticated through Rust-side admin-key ownership. The // browser admin-session flow is unnecessary and unavailable in packaged mode. const nativeProtected = isNativeProtectedSettingsReady(); const { t, locale, setLocale } = useTranslation(); // --- Admin Key (for protected endpoints) --- const [adminKey, setAdminKey] = useState(''); const [adminSessionReady, setAdminSessionReady] = useState(false); const [adminSessionBusy, setAdminSessionBusy] = useState(false); const [adminSessionMsg, setAdminSessionMsg] = useState(null); const [, setStrictPrivacy] = useState(() => getPrivacyStrictPreference()); const [privacyProfile, setPrivacyProfile] = useState(() => getPrivacyProfilePreference()); const [sessionMode, setSessionMode] = useState(() => getSessionModePreference()); const [browserWipeBusy, setBrowserWipeBusy] = useState(false); const [browserWipeMsg, setBrowserWipeMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>( null, ); const [wormholeEnabled, setWormholeEnabled] = useState(false); const [wormholeSaving, setWormholeSaving] = useState(false); const [wormholeMsg, setWormholeMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>( null, ); const [wormholeTransport, setWormholeTransport] = useState('direct'); const [wormholeSocksProxy, setWormholeSocksProxy] = useState(''); const [wormholeSocksDns, setWormholeSocksDns] = useState(true); const [wormholeAnonymousMode, setWormholeAnonymousMode] = useState(false); const [wormholeDirty, setWormholeDirty] = useState(false); const [wormholeStatus, setWormholeStatus] = useState(null); const [wormholeGuideNotice, setWormholeGuideNotice] = useState(null); const [showAdvancedWormhole, setShowAdvancedWormhole] = useState(false); const [wormholeQuickState, setWormholeQuickState] = useState<'idle' | 'ready' | 'connecting' | 'active'>('idle'); const [showOperatorTools, setShowOperatorTools] = useState(false); const [wormholeNodeId, setWormholeNodeId] = useState(null); const [wormholeKeyCopied, setWormholeKeyCopied] = useState(false); const [gateCompatTelemetry, setGateCompatTelemetry] = useState( () => getGateCompatTelemetrySnapshot(), ); const [gateLocalRuntimeStatus, setGateLocalRuntimeStatus] = useState( () => getBrowserGateLocalRuntimeStatus(), ); const [dmRootHealth, setDmRootHealth] = useState(null); const [dmRootHealthBusy, setDmRootHealthBusy] = useState(false); const [dmRootHealthMsg, setDmRootHealthMsg] = useState(null); // --- Time Machine --- const [tmEnabled, setTmEnabled] = useState(false); const [tmSaving, setTmSaving] = useState(false); // Fetch Time Machine status when protocol tab opens useEffect(() => { if (!isOpen || activeTab !== 'protocol') return; fetch(`${API_BASE}/api/settings/timemachine`) .then((r) => r.json()) .then((d) => setTmEnabled(!!d.enabled)) .catch(() => {}); }, [isOpen, activeTab]); const toggleTimeMachine = useCallback(async () => { setTmSaving(true); try { const res = await controlPlaneFetch('/api/settings/timemachine', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: !tmEnabled }), requireAdminSession: false, }); if (res.ok) { const data = await res.json(); setTmEnabled(!!data.enabled); } } catch {} setTmSaving(false); }, [tmEnabled]); // --- Browser Companion (desktop-only) --- const [companionAvailable] = useState(() => isNativeDesktop()); const [companion, setCompanion] = useState(null); const [companionBusy, setCompanionBusy] = useState(false); const [companionError, setCompanionError] = useState(null); const [companionLoadFailed, setCompanionLoadFailed] = useState(false); useEffect(() => { if (!isOpen || activeTab !== 'protocol' || !companionAvailable) return; setCompanionLoadFailed(false); fetchCompanionStatus() .then((s) => setCompanion(s)) .catch(() => { setCompanion(null); setCompanionLoadFailed(true); }); }, [isOpen, activeTab, companionAvailable]); useEffect(() => { const refreshTelemetry = () => setGateCompatTelemetry(getGateCompatTelemetrySnapshot()); refreshTelemetry(); if (typeof window === 'undefined') return; const eventName = getGateCompatTelemetryEventName(); window.addEventListener(eventName, refreshTelemetry as EventListener); return () => { window.removeEventListener(eventName, refreshTelemetry as EventListener); }; }, []); useEffect(() => { const refreshRuntimeStatus = () => setGateLocalRuntimeStatus(getBrowserGateLocalRuntimeStatus()); refreshRuntimeStatus(); if (typeof window === 'undefined') return; const eventName = getBrowserGateLocalRuntimeEventName(); window.addEventListener(eventName, refreshRuntimeStatus as EventListener); return () => { window.removeEventListener(eventName, refreshRuntimeStatus as EventListener); }; }, []); const toggleCompanion = useCallback(async () => { setCompanionBusy(true); setCompanionError(null); try { const result = companion?.enabled ? await companionDisable() : await companionEnable(); if (result) setCompanion(result); } catch (e) { setCompanionError(e instanceof Error ? e.message : String(e)); } setCompanionBusy(false); }, [companion?.enabled]); const openCompanionBrowser = useCallback(async () => { setCompanionBusy(true); setCompanionError(null); try { const result = await companionOpenBrowser(); if (result) setCompanion(result); } catch (e) { setCompanionError(e instanceof Error ? e.message : String(e)); } setCompanionBusy(false); }, []); const clearSessionIdentity = () => { if (typeof window === 'undefined') return; const keys = [ 'sb_mesh_pubkey', 'sb_mesh_privkey', 'sb_mesh_node_id', 'sb_mesh_sovereignty_accepted', 'sb_mesh_dh_pubkey', 'sb_mesh_dh_privkey', 'sb_mesh_dh_algo', 'sb_mesh_dh_last_ts', 'sb_mesh_contacts', 'sb_mesh_dm_notify', 'sb_mesh_sequence', 'sb_mesh_algo', ]; for (const key of keys) { try { sessionStorage.removeItem(key); } catch { /* ignore */ } } }; const [rnsStatus, setRnsStatus] = useState<{ enabled: boolean; ready: boolean; configured_peers: number; active_peers: number; } | null>(null); const wipeLocalMeshTraces = useCallback(async () => { setBrowserWipeBusy(true); setBrowserWipeMsg(null); try { await clearBrowserIdentityState(); await purgeBrowserDmState(); for (const key of PRIVACY_SENSITIVE_BROWSER_KEYS) { try { localStorage.removeItem(key); sessionStorage.removeItem(key); } catch { /* ignore */ } } setSessionModePreference(true); setSessionMode(true); setBrowserWipeMsg({ type: 'ok', text: wormholeEnabled ? 'Browser-held mesh traces cleared. The local Wormhole agent stays running, but this tab will need to reconnect to it.' : 'Browser-held mesh traces cleared from this browser.', }); } catch (error) { const message = error instanceof Error ? error.message : 'unknown error'; setBrowserWipeMsg({ type: 'err', text: `Could not clear browser-held mesh traces: ${message}`, }); } finally { setBrowserWipeBusy(false); } }, [wormholeEnabled]); const refreshAdminSession = useCallback(async () => { // In native desktop mode, protected settings are handled through Rust IPC // with native admin-key ownership — no browser admin-session needed. if (isNativeProtectedSettingsReady()) { setAdminSessionReady(true); setAdminSessionMsg(null); return true; } const ready = await hasAdminSession(); setAdminSessionReady(ready); if (!ready) { setAdminSessionMsg((prev) => (prev === 'LOCAL SESSION PRIMED' ? null : prev)); } return ready; }, []); useEffect(() => { if (activeTab !== 'protocol') { setShowOperatorTools(true); } }, [activeTab]); const ensureAdminSession = useCallback(async () => { // Native desktop: already authenticated via Rust IPC admin-key ownership. if (isNativeProtectedSettingsReady()) { setAdminSessionReady(true); setAdminSessionMsg(null); return; } try { await primeAdminSession(adminKey.trim() || undefined); setAdminSessionReady(true); if (adminKey.trim()) { setAdminKey(''); setAdminSessionMsg('LOCAL SESSION PRIMED'); } else { setAdminSessionMsg(null); } } catch (e) { const ready = await refreshAdminSession(); setAdminSessionReady(ready); const message = e instanceof Error && e.message === 'admin_session_required' ? 'ADMIN SESSION REQUIRED' : e instanceof Error ? e.message : 'ADMIN SESSION FAILED'; setAdminSessionMsg(message); throw e; } }, [adminKey, refreshAdminSession]); // --- API Keys state --- // API keys are write-only in-app. Values are sent once to the local backend, // stored server-side, and never returned to the browser. const [apis, setApis] = useState([]); const [apiKeyInputs, setApiKeyInputs] = useState>({}); const [apiKeyEditing, setApiKeyEditing] = useState>({}); const [apiKeySaving, setApiKeySaving] = useState(null); const [apiKeyMsg, setApiKeyMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [expandedCategories, setExpandedCategories] = useState>( new Set(['Aviation', 'Maritime']), ); const [envMeta, setEnvMeta] = useState(null); // --- News Feeds state --- const [feeds, setFeeds] = useState([]); const [feedsDirty, setFeedsDirty] = useState(false); const [feedSaving, setFeedSaving] = useState(false); const [feedMsg, setFeedMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const handleProtectedSettingsError = useCallback( async (error: unknown) => { const message = error instanceof Error ? error.message : 'Protected settings request failed'; if ( message === 'Forbidden — admin key not configured' || message === 'Forbidden — invalid or missing admin key' ) { await clearAdminSession(); setAdminSessionReady(false); setAdminSessionMsg( message === 'Forbidden — admin key not configured' ? 'BACKEND ADMIN KEY NOT CONFIGURED' : 'ADMIN KEY INVALID OR EXPIRED', ); setApis([]); setFeeds([]); setFeedsDirty(false); setDmRootHealth(null); setDmRootHealthMsg(message); } return message; }, [], ); const fetchKeys = useCallback(async () => { try { setApis(await controlPlaneJson('/api/settings/api-keys', { requireAdminSession: false, })); return true; } catch (e) { await handleProtectedSettingsError(e); return false; } }, [handleProtectedSettingsError]); const saveApiKey = useCallback( async (envKey: string | null) => { if (!envKey) return; const value = String(apiKeyInputs[envKey] || '').trim(); if (!value) { setApiKeyMsg({ type: 'err', text: `Enter a value for ${envKey}.` }); return; } setApiKeySaving(envKey); setApiKeyMsg(null); try { const result = await controlPlaneJson<{ keys?: ApiEntry[]; env?: EnvMeta; }>('/api/settings/api-keys', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [envKey]: value }), requireAdminSession: false, }); if (result.keys) setApis(result.keys); if (result.env) setEnvMeta(result.env); setApiKeyInputs((prev) => ({ ...prev, [envKey]: '' })); setApiKeyEditing((prev) => ({ ...prev, [envKey]: false })); setApiKeyMsg({ type: 'ok', text: `${envKey} saved locally. Restart or refresh feeds to use it.` }); } catch (e) { const message = e instanceof Error ? e.message : 'Could not save API key'; setApiKeyMsg({ type: 'err', text: message }); } finally { setApiKeySaving(null); } }, [apiKeyInputs], ); const fetchEnvMeta = useCallback(async () => { try { const res = await fetch('/api/settings/api-keys/meta'); if (!res.ok) return; const data: EnvMeta = await res.json(); setEnvMeta(data); } catch { // Non-fatal: the panel still works without the path hint. } }, []); const fetchFeeds = useCallback(async () => { try { setFeeds(await controlPlaneJson('/api/settings/news-feeds')); setFeedsDirty(false); return true; } catch (e) { await handleProtectedSettingsError(e); return false; } }, [handleProtectedSettingsError]); const fetchWormhole = useCallback(async () => { try { const data = await fetchWormholeSettings(true); setWormholeEnabled(Boolean(data?.enabled)); await applySecureModeBoundary(Boolean(data?.enabled)); setWormholeTransport(String(data?.transport || 'direct')); setWormholeSocksProxy(String(data?.socks_proxy || '')); setWormholeSocksDns(Boolean(data?.socks_dns ?? true)); setWormholeAnonymousMode(Boolean(data?.anonymous_mode)); setWormholeDirty(false); } catch (e) { console.error('Failed to fetch wormhole settings', e); } }, []); const fetchPrivacyProfile = useCallback(async () => { try { const data = await fetchPrivacyProfileSnapshot(true); const profile = String(data?.profile || 'default'); setPrivacyProfile(profile); if (typeof data?.wormhole_enabled === 'boolean') { setWormholeEnabled(Boolean(data.wormhole_enabled)); await applySecureModeBoundary(Boolean(data.wormhole_enabled)); } const high = profile === 'high'; setStrictPrivacy(high); const nextSessionMode = high || getSessionModePreference(); setSessionMode(nextSessionMode); setSessionModePreference(nextSessionMode); setPrivacyStrictPreference(high, { sessionMode: nextSessionMode }); setPrivacyProfilePreference(profile, { sessionMode: nextSessionMode }); migratePrivacySensitiveBrowserState(); } catch (e) { console.error('Failed to fetch privacy profile', e); } }, []); const fetchRnsStatus = useCallback(async () => { try { setRnsStatus(await fetchRnsStatusSnapshot(true)); } catch (e) { console.error('Failed to fetch RNS status', e); } }, []); const fetchWormholeStatus = useCallback(async () => { try { const state = await fetchWormholeState(true); setWormholeStatus(state); if (state.ready && !wormholeNodeId) { try { const id = await fetchWormholeIdentity(); if (id?.node_id) setWormholeNodeId(id.node_id); } catch { /* identity fetch is best-effort */ } } } catch (e) { console.error('Failed to fetch wormhole status', e); } }, [wormholeNodeId]); const fetchDmRootHealth = useCallback(async () => { if (!nativeProtected && !adminSessionReady) { setDmRootHealth(null); setDmRootHealthMsg(null); return false; } setDmRootHealthBusy(true); setDmRootHealthMsg(null); try { const data = await fetchWormholeDmRootHealth(); setDmRootHealth(data); return true; } catch (e) { const message = await handleProtectedSettingsError(e); setDmRootHealth(null); setDmRootHealthMsg(message); return false; } finally { setDmRootHealthBusy(false); } }, [adminSessionReady, handleProtectedSettingsError, nativeProtected]); useEffect(() => { if (isOpen) { if (typeof window !== 'undefined') { const focusTarget = sessionStorage.getItem(SETTINGS_FOCUS_KEY); if (focusTarget === 'wormhole-gates') { setActiveTab('protocol'); setWormholeGuideNotice( 'Gates use the Wormhole-backed experimental obfuscation lane. Press GET WORMHOLE KEY and we will walk the rest from here.', ); sessionStorage.removeItem(SETTINGS_FOCUS_KEY); } else { setWormholeGuideNotice(null); } } void (async () => { const ready = await refreshAdminSession(); await fetchKeys(); if (ready) { await fetchFeeds(); } else { setFeeds([]); setFeedsDirty(false); } void fetchWormhole(); void fetchRnsStatus(); void fetchPrivacyProfile(); void fetchWormholeStatus(); })(); } }, [ isOpen, fetchKeys, fetchFeeds, fetchWormhole, fetchRnsStatus, fetchPrivacyProfile, fetchWormholeStatus, refreshAdminSession, ]); useEffect(() => { if (!wormholeEnabled) { setWormholeQuickState('idle'); return; } if (wormholeStatus?.ready) { setWormholeQuickState('active'); if (typeof window !== 'undefined') { const returnTarget = sessionStorage.getItem(WORMHOLE_RETURN_KEY); if (returnTarget) { sessionStorage.removeItem(WORMHOLE_RETURN_KEY); sessionStorage.removeItem(SETTINGS_FOCUS_KEY); window.dispatchEvent(new CustomEvent(WORMHOLE_READY_EVENT, { detail: { target: returnTarget } })); onClose(); } } return; } if (wormholeSaving || wormholeStatus?.running) { setWormholeQuickState('connecting'); return; } setWormholeQuickState('ready'); }, [onClose, wormholeEnabled, wormholeSaving, wormholeStatus]); useEffect(() => { if (!isOpen) return; if (activeTab === 'api-keys') { void fetchKeys(); void fetchEnvMeta(); return; } if (!adminSessionReady) return; if (activeTab === 'news-feeds') { void fetchFeeds(); } }, [isOpen, adminSessionReady, activeTab, fetchKeys, fetchEnvMeta, fetchFeeds]); useEffect(() => { if (!isOpen || activeTab !== 'protocol' || !showOperatorTools) return; if (!nativeProtected && !adminSessionReady) { setDmRootHealth(null); setDmRootHealthMsg(null); return; } void fetchDmRootHealth(); }, [ isOpen, activeTab, showOperatorTools, nativeProtected, adminSessionReady, fetchDmRootHealth, ]); const toggleCategory = (cat: string) => { setExpandedCategories((prev) => { const next = new Set(prev); if (next.has(cat)) next.delete(cat); else next.add(cat); return next; }); }; const grouped = apis.reduce>((acc, api) => { if (!acc[api.category]) acc[api.category] = []; acc[api.category].push(api); return acc; }, {}); // News Feeds handlers const updateFeed = (idx: number, field: keyof FeedEntry, value: string | number) => { setFeeds((prev) => prev.map((f, i) => (i === idx ? { ...f, [field]: value } : f))); setFeedsDirty(true); setFeedMsg(null); }; const removeFeed = (idx: number) => { setFeeds((prev) => prev.filter((_, i) => i !== idx)); setFeedsDirty(true); setFeedMsg(null); }; const addFeed = () => { if (feeds.length >= MAX_FEEDS) return; setFeeds((prev) => [...prev, { name: '', url: '', weight: 3 }]); setFeedsDirty(true); setFeedMsg(null); }; const saveFeeds = async () => { setFeedSaving(true); setFeedMsg(null); try { const res = await controlPlaneFetch('/api/settings/news-feeds', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(feeds), }); if (res.ok) { setFeedsDirty(false); setFeedMsg({ type: 'ok', text: 'Feeds saved. Changes take effect on next news refresh (~30min) or manual /api/refresh.', }); } else { const d = await res.json().catch(() => ({})); setFeedMsg({ type: 'err', text: d.message || 'Save failed' }); } } catch { setFeedMsg({ type: 'err', text: 'Network error' }); } finally { setFeedSaving(false); } }; const resetFeeds = async () => { try { const res = await controlPlaneFetch('/api/settings/news-feeds/reset', { method: 'POST', }); if (res.ok) { const d = await res.json(); setFeeds(d.feeds || []); setFeedsDirty(false); setFeedMsg({ type: 'ok', text: 'Reset to defaults' }); } } catch { setFeedMsg({ type: 'err', text: 'Reset failed' }); } }; const saveWormholeSettings = async (enabledOverride?: boolean) => { setWormholeSaving(true); setWormholeMsg(null); try { invalidateWormholeRuntimeCache(); const next = typeof enabledOverride === 'boolean' ? enabledOverride : wormholeEnabled; const res = await controlPlaneFetch('/api/settings/wormhole', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: next, transport: wormholeTransport, socks_proxy: wormholeSocksProxy, socks_dns: wormholeSocksDns, anonymous_mode: wormholeAnonymousMode, }), }); if (res.ok) { const data = await res.json(); invalidateWormholeRuntimeCache(); setWormholeEnabled(Boolean(data?.enabled)); await applySecureModeBoundary(Boolean(data?.enabled)); setWormholeTransport(String(data?.transport || wormholeTransport)); setWormholeSocksProxy(String(data?.socks_proxy || wormholeSocksProxy)); setWormholeSocksDns(Boolean(data?.socks_dns ?? wormholeSocksDns)); setWormholeAnonymousMode(Boolean(data?.anonymous_mode ?? wormholeAnonymousMode)); setWormholeDirty(false); if (data?.runtime) setWormholeStatus(data.runtime as WormholeState); setWormholeMsg({ type: 'ok', text: next ? data?.runtime?.ready ? 'Local agent connected with the updated settings.' : 'Settings saved. Local agent is starting.' : 'Local agent disabled and disconnected.', }); } else { setWormholeMsg({ type: 'err', text: 'Failed to update local agent settings' }); } } catch { setWormholeMsg({ type: 'err', text: 'Network error updating local agent settings' }); } finally { setWormholeSaving(false); } }; const toggleWormhole = async () => { await saveWormholeSettings(!wormholeEnabled); }; const quickStartWormhole = async () => { setWormholeSaving(true); setWormholeQuickState('ready'); setWormholeMsg(null); try { const data = await joinWormhole(); invalidateWormholeRuntimeCache(); if (data?.identity?.node_id) { setWormholeNodeId(data.identity.node_id); } setWormholeEnabled(Boolean(data?.settings?.enabled ?? data?.runtime?.configured ?? true)); setWormholeTransport(String(data?.settings?.transport || 'direct')); setWormholeSocksProxy(String(data?.settings?.socks_proxy || '')); setWormholeSocksDns(Boolean(data?.settings?.socks_dns ?? true)); setWormholeAnonymousMode(Boolean(data?.settings?.anonymous_mode ?? false)); setWormholeDirty(false); await applySecureModeBoundary(true); setWormholeQuickState('connecting'); const runtime = (data?.runtime as WormholeState | undefined) ?? (await fetchWormholeState(true)); invalidateWormholeRuntimeCache(); setWormholeStatus(runtime); setWormholeEnabled(Boolean(runtime.configured)); setWormholeQuickState(runtime.ready ? 'active' : 'connecting'); setWormholeMsg({ type: 'ok', text: runtime.ready ? 'Wormhole key ready. Gates and the obfuscated inbox can open now.' : 'Wormhole key is provisioning. Wait for LOCAL AGENT ACTIVE.', }); } catch (e) { const message = e instanceof Error ? e.message : 'Wormhole quick start failed'; setWormholeMsg({ type: 'err', text: message }); setWormholeQuickState('idle'); } finally { setWormholeSaving(false); } }; const controlWormhole = async (action: 'connect' | 'disconnect' | 'restart') => { setWormholeSaving(true); setWormholeMsg(null); try { await ensureAdminSession(); const runtime = action === 'connect' ? await connectWormhole() : action === 'disconnect' ? await disconnectWormhole() : await restartWormhole(); invalidateWormholeRuntimeCache(); setWormholeStatus(runtime); setWormholeEnabled(Boolean(runtime.configured)); await applySecureModeBoundary(Boolean(runtime.configured)); setWormholeMsg({ type: 'ok', text: action === 'disconnect' ? 'Local agent disconnected.' : runtime.ready ? `Local agent ${action === 'restart' ? 'restarted' : 'connected'}.` : 'Local agent is starting. Mesh actions will unlock when ready.', }); } catch (e) { const message = e instanceof Error ? e.message : 'Local agent request failed'; setWormholeMsg({ type: 'err', text: message }); } finally { setWormholeSaving(false); } }; const setHighPrivacy = async (enabled: boolean) => { const profile = enabled ? 'high' : 'default'; const nextSessionMode = enabled || getSessionModePreference(); setSessionModePreference(nextSessionMode); setPrivacyStrictPreference(enabled, { sessionMode: nextSessionMode }); setPrivacyProfilePreference(profile, { sessionMode: nextSessionMode }); setPrivacyProfile(profile); setStrictPrivacy(enabled); setSessionMode(nextSessionMode); migratePrivacySensitiveBrowserState(); if (nextSessionMode) clearSessionIdentity(); try { const res = await controlPlaneFetch('/api/settings/privacy-profile', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile }), }); if (!res.ok) { setWormholeMsg({ type: 'err', text: 'Failed to save privacy profile' }); } else { invalidatePrivacyProfileCache(); invalidateRnsStatusCache(); const data = await res.json().catch(() => ({})); const forcedWormhole = Boolean(data?.wormhole_enabled); if (forcedWormhole) { setWormholeEnabled(true); await applySecureModeBoundary(true); } setWormholeMsg({ type: 'ok', text: forcedWormhole ? 'High Privacy requires the local agent. It was enabled for this device.' : 'Privacy profile saved.', }); } } catch { setWormholeMsg({ type: 'err', text: 'Failed to save privacy profile' }); } }; const unlockAdminSession = async () => { setAdminSessionBusy(true); setAdminSessionMsg(null); try { await ensureAdminSession(); await Promise.all([fetchKeys(), fetchFeeds()]); } catch (e) { const message = e instanceof Error ? e.message : 'ADMIN SESSION FAILED'; if (message === 'Forbidden — admin key not configured') { await clearAdminSession(); setAdminSessionReady(false); setAdminSessionMsg('BACKEND ADMIN KEY NOT CONFIGURED'); return; } setAdminSessionMsg(message.toUpperCase()); } finally { setAdminSessionBusy(false); } }; const lockAdminSession = async () => { setAdminSessionBusy(true); setAdminSessionMsg(null); try { await clearAdminSession(); setAdminKey(''); setAdminSessionReady(false); setDmRootHealth(null); setDmRootHealthMsg(null); setAdminSessionMsg('LOCAL SESSION CLEARED'); } finally { setAdminSessionBusy(false); } }; const configuredTransport = (wormholeStatus?.transport || wormholeTransport || '').toLowerCase(); const activeTransport = (wormholeStatus?.transport_active || '').toLowerCase(); const effectiveTransport = activeTransport || configuredTransport || 'direct'; const anonModeReady = Boolean(wormholeEnabled) && Boolean(wormholeStatus?.ready) && ['tor', 'tor_arti', 'i2p', 'mixnet'].includes(effectiveTransport) && wormholeAnonymousMode; const rnsReady = Boolean(wormholeStatus?.rns_ready ?? rnsStatus?.ready); const recentPrivateFallback = Boolean(wormholeStatus?.recent_private_clearnet_fallback); const recentPrivateFallbackReason = wormholeStatus?.recent_private_clearnet_fallback_reason || 'An obfuscated-tier payload recently fell back to clearnet relay.'; const legacyCompatibilityItems = summarizeLegacyCompatibility(wormholeStatus?.legacy_compatibility); const legacyCompatibilityActivity = hasLegacyCompatibilityActivity( wormholeStatus?.legacy_compatibility, ); const legacyCompatibilityAllBlocked = legacyCompatibilityItems.length > 0 && legacyCompatibilityItems.every((item) => item.blocked); const gateCompatTopReasons = summarizeGateCompatTelemetry(gateCompatTelemetry, 3); const trustModeLabel = !wormholeEnabled ? 'PUBLIC / DEGRADED' : wormholeStatus?.ready && rnsReady ? 'EXPERIMENTAL / OBFUSCATED+' : 'EXPERIMENTAL / OBFUSCATED'; const transportMismatch = Boolean(activeTransport) && Boolean(configuredTransport) && activeTransport !== configuredTransport; const wormholeQuickButtonLabel = wormholeQuickState === 'active' ? 'ACTIVE' : wormholeQuickState === 'connecting' ? 'CONNECTING' : wormholeQuickState === 'ready' ? 'READY' : 'GET WORMHOLE KEY'; const dmRootCardTone = dmRootMonitorTone( showOperatorTools ? dmRootHealth?.monitoring?.state || (dmRootHealthMsg ? 'critical' : 'warning') : 'warning', ); return ( {isOpen && ( <> {/* Backdrop */} {/* Settings Panel */} {/* Header */}

{t('settings.title').toUpperCase()}

SETTINGS & DATA SOURCES
{/* UI language toggle. Locale change is purely client-side (persists to localStorage('sb_locale')) — no network call, no telemetry. See frontend/src/i18n/index.ts for the list of available locales and CONTRIBUTING.md for the translation-neutrality policy. */}
{/* Operator Tools */} {activeTab === 'protocol' && !showOperatorTools ? (
WORMHOLE FIRST-RUN
Wormhole join below does not need operator tools. API/news tabs do.
) : ( <>
OPERATOR TOOLS setAdminKey(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && adminKey.trim() && !adminSessionBusy) { void unlockAdminSession(); } }} disabled={nativeProtected} placeholder={ nativeProtected ? 'Protected via native desktop bridge' : adminSessionReady ? 'Operator tools unlocked. Enter key only to reseed or recover...' : 'Enter operator key for protected settings tabs...' } className="flex-1 bg-[var(--bg-primary)]/60 border border-[var(--border-primary)] px-2 py-1 text-sm font-mono text-[var(--text-secondary)] outline-none focus:border-cyan-700 placeholder:text-[var(--text-muted)]/50" /> {nativeProtected ? null : adminSessionReady ? ( ) : ( )} {activeTab === 'protocol' && ( )} {nativeProtected ? 'NATIVE' : adminSessionReady ? 'ACTIVE' : 'LOCKED'}
{adminSessionMsg && (
{adminSessionMsg}
)} )} {adminSessionMsg === 'BACKEND ADMIN KEY NOT CONFIGURED' && activeTab !== 'protocol' && (
This is not an old market/API key problem. The backend admin secret itself is not configured, so protected Settings tabs cannot load.
Add ADMIN_KEY to{' '} backend/.env, restart the backend, then paste that same key above and unlock.
)}
{/* ==================== API KEYS TAB ==================== */} {/* ==================== MESH PROTOCOL TAB ==================== */} {activeTab === 'protocol' && (
WORMHOLE KEY SETUP
One click enters Wormhole on the recommended path for gates and the obfuscated inbox. Manual transport tuning stays hidden unless you ask for it.
STATUS
{wormholeStatus?.ready ? 'ACTIVE' : wormholeEnabled ? 'TURN ON CONNECT' : 'OFF'}
1. Press GET WORMHOLE KEY.
2. We handle the recommended setup path in the background.
3. Wait for ACTIVE.
4. We send you straight back into gates.
{wormholeGuideNotice && (
{wormholeGuideNotice}
)} {adminSessionMsg === 'BACKEND ADMIN KEY NOT CONFIGURED' && (
Operator key is only needed for protected Settings tabs. Wormhole join below now works without it.
)}
{wormholeMsg && (
{wormholeMsg.text}
)} {wormholeNodeId && (
YOUR WORMHOLE IDENTITY
{wormholeNodeId}
)}
DM ROOT HEALTH
External witness freshness, transparency readback, and the next operator action for strong DM trust.
{!showOperatorTools ? 'HIDDEN' : dmRootHealth ? dmRootMonitorLabel(dmRootHealth.monitoring?.state) : dmRootHealthBusy ? 'LOADING' : dmRootHealthMsg ? 'BLOCKED' : 'UNKNOWN'} {showOperatorTools && (nativeProtected || adminSessionReady) && ( )}
{!showOperatorTools ? (
Wormhole join stays visible without operator tools. Open operator tools to see external witness freshness, transparency readback, and remediation guidance.
) : !nativeProtected && !adminSessionReady ? (
Unlock operator tools above to load live DM root health, external witness freshness, and transparency monitoring status.
) : dmRootHealthBusy && !dmRootHealth ? (
Loading current DM root health...
) : dmRootHealth ? (
SUMMARY
{String(dmRootHealth.state || '').replaceAll('_', ' ').toUpperCase()}
{dmRootHealth.detail}
STRONG TRUST
{dmRootHealth.strong_trust_blocked ? 'BLOCKED' : 'CURRENT'}
{dmRootHealth.monitoring?.status_line || 'Operator monitoring active.'}
WITNESS
{String(dmRootHealth.witness.state || '').replaceAll('_', ' ').toUpperCase()}
Age {formatAgeWindow(dmRootHealth.witness.age_s, dmRootHealth.witness.freshness_window_s)}
{dmRootHealth.witness.source_label || dmRootHealth.witness.source_ref || dmRootHealth.witness.detail || 'Configured witness source unavailable.'}
TRANSPARENCY
{String(dmRootHealth.transparency.state || '').replaceAll('_', ' ').toUpperCase()}
Age{' '} {formatAgeWindow( dmRootHealth.transparency.age_s, dmRootHealth.transparency.freshness_window_s, )}
{dmRootHealth.transparency.source_ref || dmRootHealth.transparency.export_path || dmRootHealth.transparency.detail || 'Configured ledger readback unavailable.'}
{dmRootHealth.alerts.length > 0 && (
ACTIVE ALERTS
{dmRootHealth.alerts.slice(0, 2).map((alert) => (
{alert.code.replaceAll('_', ' ').toUpperCase()}
{alert.detail}
))}
)}
NEXT ACTION
{String(dmRootHealth.runbook?.urgency || 'none').toUpperCase()}
{( dmRootHealth.runbook?.next_action_detail && 'title' in dmRootHealth.runbook.next_action_detail && dmRootHealth.runbook.next_action_detail.title ) || dmRootHealth.runbook?.next_action || 'No action required.'}
{( dmRootHealth.runbook?.next_action_detail && 'summary' in dmRootHealth.runbook.next_action_detail && dmRootHealth.runbook.next_action_detail.summary ) || dmRootHealth.monitoring?.status_line || 'Current external assurance is within policy.'}
) : (
{dmRootHealthMsg || 'Could not load DM root health.'}
)}
{showAdvancedWormhole && ( <> {/* Privacy Mode */}
HIGH PRIVACY MODE (OPT-IN)

Enables High Privacy profile: session-only identity, stronger jitter, sharded transport (when available), and stricter sync behavior. High Privacy requires the local agent for mesh traffic and refuses clearnet fallback for obfuscated sends. This does not make you anonymous or fully hidden.

{privacyProfile === 'high' && (
Recommendation: use a reputable VPN or hidden transport. A VPN can help hide your IP from the backend and peers, but it does not eliminate metadata, endpoint compromise, or traffic analysis risks.
)}
{/* Session Identity Mode */}
EPHEMERAL SESSION ID (RECOMMENDED)

When enabled, agent keys are stored in session storage and reset on browser close. Your identity will not persist across restarts.

WIPE LOCAL MESH TRACES

Clears browser-held mesh identities, DM ratchet state, cached contacts, and privacy-sensitive browser storage. The local agent is not shut down.

{browserWipeMsg && (
{browserWipeMsg.text}
)}
{/* Wormhole Mode */}
LOCAL MESH AGENT (OPT-IN)

Runs a local mesh agent that handles traffic directly, removing the backend as a central observer. Experimental — does not guarantee privacy or anonymity.

TRANSPORT
{(wormholeTransport === 'tor' || wormholeTransport === 'i2p' || wormholeTransport === 'mixnet') && ( <> { setWormholeSocksProxy(e.target.value); setWormholeDirty(true); }} placeholder="SOCKS5 proxy (e.g. 127.0.0.1:9050)" className="w-full bg-black/30 border border-[var(--border-primary)]/40 px-2 py-1 text-sm font-mono text-[var(--text-muted)] outline-none focus:border-cyan-500/50" />
PROXY DNS
Hidden transport requires a local SOCKS5 proxy (Tor/I2P/Mixnet) already running. Save applies the new transport immediately.
)}
HIDDEN TRANSPORT MODE
Public mesh writes fail closed unless the local agent is active on Tor/I2P/Mixnet. Direct transport is blocked while this is on.
{wormholeAnonymousMode && (
{trustModeLabel} {anonModeReady ? 'Hidden transport is active. Public gate posting routes through the local agent.' : 'Connect the local agent over Tor, I2P, or Mixnet before posting publicly.'}
Mesh Terminal stays read-only for sensitive posting and DM actions while the hidden transport policy is active. Use MeshChat for the hardened path.
Relay fallback reduces metadata protection compared with direct obfuscated transport. Meshtastic/APRS remain degraded, integrity-only channels in this phase.
)} {!wormholeAnonymousMode && (
{trustModeLabel} Hidden transport is off. Public posting may use public or degraded transports until you require Tor, I2P, or Mixnet.
Meshtastic/APRS/JS8 remain public or degraded in this phase unless a separate obfuscated transport is explicitly enabled.
)}
{rnsStatus && (
RNS {rnsStatus.ready ? 'READY' : rnsStatus.enabled ? 'STARTING' : 'OFF'} peers {rnsStatus.active_peers}/{rnsStatus.configured_peers}
)} {wormholeStatus && (
{wormholeStatus.ready ? 'LOCAL AGENT ACTIVE' : wormholeStatus.running ? 'LOCAL AGENT STARTING' : wormholeStatus.configured ? 'LOCAL AGENT IDLE' : 'LOCAL AGENT OFF'} {wormholeStatus.pid > 0 && pid {wormholeStatus.pid}}
ACTIVE {effectiveTransport.toUpperCase()} {transportMismatch && ( FALLBACK )} {recentPrivateFallback && ( PRIVACY DOWNGRADE )} {wormholeStatus.proxy_active && ( proxy {wormholeStatus.proxy_active} )}
Public transport identity, gate personas, and the obfuscated DM alias are compartmentalized inside the local agent.
{recentPrivateFallback && (
{recentPrivateFallbackReason}
)} {wormholeStatus.last_error && (
{wormholeStatus.last_error}
)} {legacyCompatibilityItems.length > 0 && (
LEGACY SUNSET
{legacyCompatibilityAllBlocked ? 'DESKTOP DEFAULT: BLOCKING' : 'COMPATIBILITY STILL OPEN'}
{legacyCompatibilityItems.map((item) => (
{item.blocked ? 'BLOCKED' : 'ALLOWING'} {item.label} seen {item.count} {item.blockedCount > 0 ? ` • blocked ${item.blockedCount}` : ''}
{item.blocked ? 'remove after' : 'target'} {item.targetVersion} / {item.targetDate} {item.lastSeenAt > 0 ? ` • last seen ${formatLegacyCompatibilitySeenAt(item.lastSeenAt)}` : ' • never observed'}
{item.recentTargets.length > 0 && (
recent {item.recentTargets.join(' • ')}
)}
))}
{!legacyCompatibilityActivity && (
No live legacy traffic observed in this runtime. When this stays at zero, the final hard cutoff is low risk.
)}
)}
GATE COMPAT
required {gateCompatTelemetry.totalRequired} • used {gateCompatTelemetry.totalUsed}
{describeBrowserGateLocalRuntimeStatus(gateLocalRuntimeStatus)}
{gateCompatTopReasons.length > 0 ? (
{gateCompatTopReasons.map((item) => (
{item.label} need {item.requiredCount} {item.usedCount > 0 ? ` • used ${item.usedCount}` : ''}
{item.lastAt > 0 ? `last seen ${formatGateCompatSeenAt(item.lastAt)}` : 'never observed'} {item.recentGates.length > 0 ? ` • rooms ${item.recentGates.join(' • ')}` : ''}
))}
) : (
No browser gate compat issues recorded for this profile yet.
)}
)}
)} {/* ── Time Machine ────────────────────────── */}
TIME MACHINE
Records hourly snapshots of all entity positions (flights, ships, satellites) for historical playback via the timeline scrubber.
STORAGE: ~5-8 MB/day · ~200 MB/month (gzip compressed). Snapshots are stored locally and never leave your machine. {tmEnabled && ( Auto-snapshots are running. )}
{/* ── Browser Companion (desktop-only) ───── */} {companionAvailable && (companion || companionLoadFailed) && (
BROWSER COMPANION
{companionLoadFailed ? 'Could not load companion status from the native bridge.' : <> Open this app in a regular browser on localhost. {companion?.enabled && companion.url && ( Active at {companion.url} )} }
{companion && (
{companion.enabled && ( )}
)}
{companion?.warning && (
REDUCED TRUST:{' '} {companion.warning}
)} {companionLoadFailed && (
Companion service unavailable. The native bridge did not respond. Try reopening Settings or restarting the app.
)} {companionError && (
{companionError}
)}
)}
)} {activeTab === 'api-keys' && ( <> {/* Info Banner */}

API keys are saved locally by this backend. Values are write-only: the app stores the key and shows CONFIGURED, but it never reads the secret back into the browser. Keys marked with{' '} unlock the richest live aircraft and vessel feeds.

Configured keys stay hidden for shared dashboards. Unlock operator tools, then use ROTATE only when you intentionally want to replace a working credential.
{envMeta && (
local key store:{' '} {envMeta.operator_keys_env_path || envMeta.env_path} {' '} {envMeta.operator_keys_env_path_exists || envMeta.env_path_exists ? ( [exists] ) : ( [will be created on first save] )} {envMeta.env_path_exists && !envMeta.env_path_writable && ( [NOT WRITABLE — edit by hand] )}
{envMeta.env_example_path_exists && (
template:{' '} {envMeta.env_example_path} {' '} (copy to .env and fill in your keys; comments above each entry list the registration URL)
)}
)} {apiKeyMsg && (
{apiKeyMsg.text}
)}
{/* API List */}
{Object.entries(grouped).map(([category, categoryApis]) => { const colorClass = CATEGORY_COLORS[category] || 'text-gray-400 border-gray-700 bg-gray-900/20'; const isExpanded = expandedCategories.has(category); return (
{isExpanded && ( {categoryApis.map((api) => (
{api.required && ( )} {api.name}
{api.has_key ? ( api.is_set ? ( KEY SET ) : ( MISSING ) ) : ( PUBLIC )} {api.url && ( e.stopPropagation()} > )}

{api.description}

{api.has_key && (
{api.is_set ? (
CONFIGURED Secret hidden. Stored write-only on this backend as{' '} {api.env_key} .
{api.env_key && ( )}
{!(nativeProtected || adminSessionReady) && (
Operator tools are locked. Viewers can see source status but cannot replace saved credentials.
)}
) : (
NOT CONFIGURED Save {api.env_key} here to enable this source.
)} {(!api.is_set || (api.env_key && apiKeyEditing[api.env_key])) && (
{ if (!api.env_key) return; setApiKeyInputs((prev) => ({ ...prev, [api.env_key as string]: event.target.value, })); }} placeholder={ api.is_set ? 'Enter replacement key...' : `Enter ${api.env_key}...` } className="min-w-0 flex-1 bg-[var(--bg-primary)] border border-[var(--border-primary)] px-2 py-1.5 text-sm text-[var(--text-primary)] outline-none focus:border-cyan-500/70 placeholder:text-[var(--text-muted)]/50" autoComplete="off" />
)}
)}
))}
)}
); })}
{/* Footer */}
{apis.length} REGISTERED APIs {apis.filter((a) => a.has_key && a.is_set).length} KEYS CONFIGURED
)} {/* ==================== NEWS FEEDS TAB ==================== */} {activeTab === 'news-feeds' && ( <> {/* Info Banner */}

Configure RSS/Atom feeds for the Threat Intel news panel. Each feed is scored by keyword heuristics and weighted by the priority you set. Up to{' '} {MAX_FEEDS} sources.

{/* Feed List */}
{feeds.map((feed, idx) => (
{/* Row 1: Name + Weight + Delete */}
updateFeed(idx, 'name', e.target.value)} className="flex-1 bg-transparent border-b border-[var(--border-primary)] text-xs font-mono text-[var(--text-primary)] outline-none focus:border-cyan-500/70 transition-colors px-1 py-0.5" placeholder="Source name..." /> {/* Weight selector */}
{[1, 2, 3, 4, 5].map((w) => ( ))} {WEIGHT_LABELS[feed.weight] || 'STD'}
{/* Row 2: URL */} updateFeed(idx, 'url', e.target.value)} className="w-full bg-black/30 border border-[var(--border-primary)]/40 px-2 py-1 text-sm font-mono text-[var(--text-muted)] outline-none focus:border-cyan-500/50 focus:text-cyan-300 transition-colors" placeholder="https://example.com/rss.xml" />
))} {/* Add Feed Button */}
{/* Status message */} {feedMsg && (
{feedMsg.text}
)} {/* Footer */}
{feeds.length}/{MAX_FEEDS} SOURCES WEIGHT: 1=LOW 5=CRITICAL
)} {/* ==================== SENTINEL HUB TAB ==================== */} {activeTab === 'sentinel' && ( setActiveTab('api-keys')} /> )} {activeTab === 'sar' && }
)}
); }); // ─── Sentinel Hub Settings Tab ───────────────────────────────────────────── // Issue #298 (tg12): Sentinel credentials now live in the backend ``.env`` // and are managed through the existing API Keys panel — same flow as every // other third-party API key (OpenSky, AIS Stream, Finnhub, …). This tab no // longer collects credentials. It does three things: // 1. Runs migrateLegacySentinelBrowserKeys() once to wipe pre-#298 // values out of localStorage / sessionStorage. // 2. Shows the operator whether the backend has the credentials. // 3. Offers a one-click jump to the API Keys panel where they enter them. function SentinelTab({ onGoToApiKeys }: { onGoToApiKeys: () => void }) { const [backendConfigured, setBackendConfigured] = useState(null); const [migrationResult, setMigrationResult] = useState<{ cleared: string[] } | null>(null); const [refreshing, setRefreshing] = useState(false); useEffect(() => { // One-time legacy browser-key wipe. Idempotent — does nothing on a // fresh install. We do NOT silently POST any browser-stored values // to the backend; operators who relied on them re-enter once in the // API Keys panel. Doing the wipe regardless ensures pre-#298 secrets // don't linger in localStorage indefinitely. setMigrationResult(migrateLegacySentinelBrowserKeys()); // Check whether the backend has SENTINEL_CLIENT_ID/SECRET set. void checkBackendSentinelStatus().then(setBackendConfigured); }, []); const refresh = async () => { setRefreshing(true); try { // refreshSentinelStatus() invalidates the module-level cache so the // next check actually hits the backend instead of returning the // memoized value. Lazy-imported so SSR/tests don't choke. const { refreshSentinelStatus } = await import('@/lib/sentinelHub'); refreshSentinelStatus(); const ok = await checkBackendSentinelStatus(); setBackendConfigured(ok); } finally { setRefreshing(false); } }; const statusColor = backendConfigured === null ? 'text-[var(--text-muted)]' : backendConfigured ? 'text-green-400' : 'text-yellow-400'; const statusLabel = backendConfigured === null ? 'CHECKING…' : backendConfigured ? 'CONFIGURED ON BACKEND' : 'NOT CONFIGURED'; return (
{/* Setup Guide */}

COPERNICUS SENTINEL HUB SETUP

Sentinel Hub gives you access to ESA satellite imagery (Sentinel-2 true color, NDVI vegetation, false color IR, moisture index). Free tier: 10,000 processing units/month. Follow each step below:

STEP 1:{' '} Go to{' '} dataspace.copernicus.eu {' '}→ click Register (top right) → create a free account. Pick Public for User Category.

STEP 2:{' '} Once logged in, go to{' '} Sentinel Hub Dashboard {' '}→ click your user icon (top right) {' '}→ User Settings {' '}→ OAuth clients tab →{' '} click "+ Create new". Give it any name (e.g. "ShadowBroker"). Copy the{' '} Client ID and{' '} Client Secret it shows you.

STEP 3:{' '} Paste both values into the API Keys panel under SENTINEL_CLIENT_ID and{' '} SENTINEL_CLIENT_SECRET, then hit Save. The backend uses them to mint short-lived tokens — your browser never sees the secret again.

{/* Backend status */}
BACKEND STATUS {statusLabel}

{backendConfigured === false ? 'Sentinel credentials are not yet set in the backend .env. Open the API Keys panel to enter them — the tile overlay and Sentinel-2 Intel Card will work as soon as both fields are saved.' : backendConfigured === true ? 'Sentinel credentials are configured on the backend. The dashboard fetches tokens automatically; your browser does not handle the secret.' : 'Checking backend configuration…'}

{/* Migration notice (only if we actually cleared anything) */} {migrationResult && migrationResult.cleared.length > 0 && (

LEGACY BROWSER CREDENTIALS CLEARED

Found and removed pre-#298 Sentinel credentials from browser storage ({migrationResult.cleared.join(', ')}). Re-enter them in the API Keys panel above; they'll be stored server-side from now on and never sent back to the browser.

)} {/* Footer + Usage Meter */}

Credentials are stored in the backend .env{' '} and never sent to the browser. The tile proxy mints short-lived OAuth tokens on demand using those values.

); } function UsageMeter() { const [usage, setUsage] = useState({ month: '', tiles: 0, pu: 0 }); useEffect(() => { // Import dynamically to avoid SSR issues import('@/lib/sentinelHub').then(({ getSentinelUsage }) => { setUsage(getSentinelUsage()); }); // Refresh every 10s when tab is active const id = setInterval(() => { import('@/lib/sentinelHub').then(({ getSentinelUsage }) => { setUsage(getSentinelUsage()); }); }, 10_000); return () => clearInterval(id); }, []); const maxRequests = 10_000; const maxPU = 10_000; const pct = Math.min(100, (usage.tiles / maxRequests) * 100); const barColor = pct < 50 ? 'bg-purple-500' : pct < 80 ? 'bg-yellow-500' : 'bg-red-500'; const textColor = pct < 50 ? 'text-purple-400' : pct < 80 ? 'text-yellow-400' : 'text-red-400'; return (
MONTHLY USAGE {usage.month || '—'}
{/* Progress bar */}
{usage.tiles.toLocaleString()}
/ {maxRequests.toLocaleString()} tiles
{usage.pu.toLocaleString()}
/ {maxPU.toLocaleString()} PU
{Math.round(100 - pct)}%
remaining
); } // ─── SAR Ground-Change Settings Tab ─────────────────────────────────────────── function SarSettingsTab() { const [status, setStatus] = useState | null>(null); const [loading, setLoading] = useState(true); const [actionMsg, setActionMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [disabling, setDisabling] = useState(false); const fetchStatus = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/sar/status`, { credentials: 'include' }); if (res.ok) { const body = await res.json(); setStatus(body); } } catch { /* silent */ } setLoading(false); }, []); useEffect(() => { fetchStatus(); }, [fetchStatus]); const products = (status?.products ?? {}) as Record; const modeBEnabled = !!products.enabled; const catalogEnabled = !!(status?.catalog as Record)?.enabled; const openclawEnabled = !!status?.openclaw_enabled; const handleDisable = async () => { setDisabling(true); setActionMsg(null); try { const res = await fetch(`${API_BASE}/api/sar/mode-b/disable`, { method: 'POST', credentials: 'include', }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(typeof body?.detail === 'string' ? body.detail : `HTTP ${res.status}`); } setActionMsg({ type: 'ok', text: 'Mode B disabled. Credentials wiped.' }); await fetchStatus(); } catch (e) { setActionMsg({ type: 'err', text: e instanceof Error ? e.message : 'Failed to disable Mode B', }); } finally { setDisabling(false); } }; if (loading) { return (
Loading SAR status...
); } return (
{/* Status Overview */}

SAR GROUND-CHANGE STATUS

Mode A (Catalog):{' '} {catalogEnabled ? 'Active' : 'Disabled'}
Mode B (Anomalies):{' '} {modeBEnabled ? 'Active — credentials stored' : 'Not configured'}
OpenClaw SAR integration:{' '} {openclawEnabled ? 'Enabled' : 'Disabled'}
{/* Mode B Controls */} {modeBEnabled && (

MODE B CREDENTIALS

Earthdata credentials are stored server-side in{' '} backend/data/sar_runtime.json. Disabling Mode B wipes them from disk.

)} {/* Setup Guide (when Mode B not active) */} {!modeBEnabled && (

ENABLE MODE B

Mode B requires a free NASA Earthdata account. To set up:

  1. Register at{' '} urs.earthdata.nasa.gov
  2. Generate a user token from your Earthdata profile page
  3. Toggle the SAR Ground-Change layer ON in the left panel — the first-run wizard will prompt for your token
)} {/* Action feedback */} {actionMsg && (
{actionMsg.text}
)} {/* Info blurb */}

SAR (Synthetic Aperture Radar) detects ground changes through cloud cover, at night, anywhere on Earth. Mode A is a free Sentinel-1 scene catalog from Alaska Satellite Facility. Mode B adds real-time anomaly detection via NASA OPERA DISP, DSWx, DIST-ALERT, and Copernicus EGMS — all free with an Earthdata account.

); } export default SettingsPanel;