diff --git a/dashboard-frontend/src/pages/Config.tsx b/dashboard-frontend/src/pages/Config.tsx index 21a08c5..06f981d 100644 --- a/dashboard-frontend/src/pages/Config.tsx +++ b/dashboard-frontend/src/pages/Config.tsx @@ -1,15 +1,1189 @@ -import { Settings } from 'lucide-react' +import { useState, useEffect, useCallback } from 'react' +import { + Settings, Bot, Wifi, MessageSquare, Database, Brain, Eye, + Terminal, Cpu, Cloud, Radio, BookOpen, Layers, Activity, + Thermometer, LayoutDashboard, Save, RotateCcw, RefreshCw, + Plus, Trash2, ChevronDown, ChevronRight, AlertTriangle, + Check, X, Eye as EyeIcon, EyeOff +} from 'lucide-react' + +// Types for config sections +interface BotConfig { + name: string + owner: string + respond_to_dms: boolean + filter_bbs_protocols: boolean +} + +interface ConnectionConfig { + type: string + serial_port: string + tcp_host: string + tcp_port: number +} + +interface ResponseConfig { + delay_min: number + delay_max: number + max_length: number + max_messages: number +} + +interface HistoryConfig { + database: string + max_messages_per_user: number + conversation_timeout: number + auto_cleanup: boolean + cleanup_interval_hours: number + max_age_days: number +} + +interface MemoryConfig { + enabled: boolean + window_size: number + summarize_threshold: number +} + +interface ContextConfig { + enabled: boolean + observe_channels: number[] + ignore_nodes: string[] + max_age: number + max_context_items: number +} + +interface CommandsConfig { + enabled: boolean + prefix: string + disabled_commands: string[] + custom_commands: Record +} + +interface LLMConfig { + backend: string + api_key: string + base_url: string + model: string + timeout: number + max_response_tokens: number + system_prompt: string + use_system_prompt: boolean + web_search: boolean + google_grounding: boolean +} + +interface WeatherConfig { + primary: string + fallback: string + default_location: string + openmeteo: { url: string } + wttr: { url: string } +} + +interface MeshMonitorConfig { + enabled: boolean + url: string + inject_into_prompt: boolean + refresh_interval: number + polite_mode: boolean +} + +interface KnowledgeConfig { + enabled: boolean + backend: string + qdrant_host: string + qdrant_port: number + qdrant_collection: string + tei_host: string + tei_port: number + sparse_host: string + sparse_port: number + use_sparse: boolean + db_path: string + top_k: number +} + +interface MeshSourceConfig { + name: string + type: string + url: string + api_token: string + refresh_interval: number + polite_mode: boolean + enabled: boolean +} + +interface RegionAnchor { + name: string + lat: number + lon: number + local_name: string + description: string + aliases: string[] + cities: string[] +} + +interface AlertRulesConfig { + infra_offline: boolean + infra_recovery: boolean + new_router: boolean + battery_trend_declining: boolean + battery_warning: boolean + battery_critical: boolean + battery_emergency: boolean + battery_warning_threshold: number + battery_critical_threshold: number + battery_emergency_threshold: number + power_source_change: boolean + solar_not_charging: boolean + sustained_high_util: boolean + high_util_threshold: number + high_util_hours: number + packet_flood: boolean + packet_flood_threshold: number + infra_single_gateway: boolean + feeder_offline: boolean + region_total_blackout: boolean + mesh_score_alert: boolean + mesh_score_threshold: number + region_score_alert: boolean + region_score_threshold: number +} + +interface MeshIntelligenceConfig { + enabled: boolean + regions: RegionAnchor[] + locality_radius_miles: number + offline_threshold_hours: number + packet_threshold: number + battery_warning_percent: number + critical_nodes: string[] + alert_channel: number + alert_cooldown_minutes: number + alert_rules: AlertRulesConfig +} + +interface NWSConfig { + enabled: boolean + tick_seconds: number + areas: string[] + severity_min: string + user_agent: string +} + +interface EnvironmentalConfig { + enabled: boolean + nws_zones: string[] + nws: NWSConfig + swpc: { enabled: boolean } + ducting: { enabled: boolean; tick_seconds: number; latitude: number; longitude: number } + fires: { enabled: boolean; tick_seconds: number; state: string } + avalanche: { enabled: boolean; tick_seconds: number; center_ids: string[]; season_months: number[] } +} + +interface DashboardConfig { + enabled: boolean + port: number + host: string +} + +interface FullConfig { + bot: BotConfig + connection: ConnectionConfig + response: ResponseConfig + history: HistoryConfig + memory: MemoryConfig + context: ContextConfig + commands: CommandsConfig + llm: LLMConfig + weather: WeatherConfig + meshmonitor: MeshMonitorConfig + knowledge: KnowledgeConfig + mesh_sources: MeshSourceConfig[] + mesh_intelligence: MeshIntelligenceConfig + environmental: EnvironmentalConfig + dashboard: DashboardConfig +} + +type SectionKey = keyof FullConfig + +const SECTIONS: { key: SectionKey; label: string; icon: typeof Settings }[] = [ + { key: 'bot', label: 'Bot', icon: Bot }, + { key: 'connection', label: 'Connection', icon: Wifi }, + { key: 'response', label: 'Response', icon: MessageSquare }, + { key: 'history', label: 'History', icon: Database }, + { key: 'memory', label: 'Memory', icon: Brain }, + { key: 'context', label: 'Context', icon: Eye }, + { key: 'commands', label: 'Commands', icon: Terminal }, + { key: 'llm', label: 'LLM', icon: Cpu }, + { key: 'weather', label: 'Weather', icon: Cloud }, + { key: 'meshmonitor', label: 'MeshMonitor', icon: Radio }, + { key: 'knowledge', label: 'Knowledge', icon: BookOpen }, + { key: 'mesh_sources', label: 'Mesh Sources', icon: Layers }, + { key: 'mesh_intelligence', label: 'Intelligence', icon: Activity }, + { key: 'environmental', label: 'Environmental', icon: Thermometer }, + { key: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, +] + +// Form components +function TextInput({ label, value, onChange, type = 'text', placeholder = '', helper = '' }: { + label: string + value: string + onChange: (v: string) => void + type?: string + placeholder?: string + helper?: string +}) { + const [showPassword, setShowPassword] = useState(false) + const isPassword = type === 'password' -export default function Config() { return ( -
-
- +
+ +
+ onChange(e.target.value)} + placeholder={placeholder} + className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 font-mono focus:outline-none focus:border-accent placeholder-slate-600" + /> + {isPassword && ( + + )} +
+ {helper &&

{helper}

} +
+ ) +} + +function NumberInput({ label, value, onChange, min, max, step = 1, helper = '' }: { + label: string + value: number + onChange: (v: number) => void + min?: number + max?: number + step?: number + helper?: string +}) { + return ( +
+ + onChange(Number(e.target.value))} + min={min} + max={max} + step={step} + className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 font-mono focus:outline-none focus:border-accent" + /> + {helper &&

{helper}

} +
+ ) +} + +function Toggle({ label, checked, onChange, helper = '' }: { + label: string + checked: boolean + onChange: (v: boolean) => void + helper?: string +}) { + return ( +
+
+ {label} + {helper &&

{helper}

} +
+ +
+ ) +} + +function SelectInput({ label, value, onChange, options, helper = '' }: { + label: string + value: string + onChange: (v: string) => void + options: { value: string; label: string }[] + helper?: string +}) { + return ( +
+ + + {helper &&

{helper}

} +
+ ) +} + +function TextArea({ label, value, onChange, rows = 4, helper = '' }: { + label: string + value: string + onChange: (v: string) => void + rows?: number + helper?: string +}) { + return ( +
+ +