"use client"; import { API_BASE } from "@/lib/api"; 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 } from "lucide-react"; interface ApiEntry { id: string; name: string; description: string; category: string; url: string | null; required: boolean; has_key: boolean; env_key: string | null; value_obfuscated: string | null; is_set: boolean; } interface FeedEntry { name: string; url: string; weight: number; } 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 MAX_FEEDS = 20; // 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", }; type Tab = "api-keys" | "news-feeds"; const SettingsPanel = React.memo(function SettingsPanel({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { const [activeTab, setActiveTab] = useState("api-keys"); // --- API Keys state --- const [apis, setApis] = useState([]); const [editingId, setEditingId] = useState(null); const [editValue, setEditValue] = useState(""); const [saving, setSaving] = useState(false); const [expandedCategories, setExpandedCategories] = useState>(new Set(["Aviation", "Maritime"])); // --- 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 fetchKeys = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/settings/api-keys`); if (res.ok) setApis(await res.json()); } catch (e) { console.error("Failed to fetch API keys", e); } }, []); const fetchFeeds = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/settings/news-feeds`); if (res.ok) { setFeeds(await res.json()); setFeedsDirty(false); } } catch (e) { console.error("Failed to fetch news feeds", e); } }, []); useEffect(() => { if (isOpen) { fetchKeys(); fetchFeeds(); } }, [isOpen, fetchKeys, fetchFeeds]); // API Keys handlers const startEditing = (api: ApiEntry) => { setEditingId(api.id); setEditValue(""); }; const saveKey = async (api: ApiEntry) => { if (!api.env_key) return; setSaving(true); try { const res = await fetch(`${API_BASE}/api/settings/api-keys`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ env_key: api.env_key, value: editValue }), }); if (res.ok) { setEditingId(null); fetchKeys(); } } catch (e) { console.error("Failed to save API key", e); } finally { setSaving(false); } }; 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 fetch(`${API_BASE}/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 (e) { setFeedMsg({ type: "err", text: "Network error" }); } finally { setFeedSaving(false); } }; const resetFeeds = async () => { try { const res = await fetch(`${API_BASE}/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 (e) { setFeedMsg({ type: "err", text: "Reset failed" }); } }; return ( {isOpen && ( <> {/* Backdrop */} {/* Settings Panel */} {/* Header */}

SYSTEM CONFIG

SETTINGS & DATA SOURCES
{/* Tab Bar */}
{/* ==================== API KEYS TAB ==================== */} {activeTab === "api-keys" && ( <> {/* Info Banner */}

API keys are stored locally in the backend .env file. Keys marked with are required for full functionality. Public APIs need no key.

{/* 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 && (
{editingId === api.id ? (
setEditValue(e.target.value)} className="flex-1 bg-black/60 border border-cyan-900/50 rounded px-2 py-1.5 text-[11px] font-mono text-cyan-300 outline-none focus:border-cyan-500/70 transition-colors" placeholder="Enter API key..." autoFocus />
) : (
startEditing(api)}> {api.is_set ? api.value_obfuscated : "Click to set key..."}
)}
)}
))}
)}
); })}
{/* Footer */}
{apis.length} REGISTERED APIs {apis.filter(a => a.has_key).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 rounded px-2 py-1 text-[10px] 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
)}
)}
); }); export default SettingsPanel;