From 23151f63ba40830728e64bf993b5ffa45d8fb4ad Mon Sep 17 00:00:00 2001 From: K7ZVX Date: Wed, 13 May 2026 16:04:36 +0000 Subject: [PATCH] fix(dashboard): info popover toggle and click-outside dismiss - Replace fixed overlay with useRef-based click-outside detection - Add X close button in top-right corner of popover - Click ? to toggle (open if closed, close if open) - Click anywhere outside popover to dismiss - Remove fixed inset-0 overlay that was blocking page interaction Co-Authored-By: Claude Opus 4.5 --- dashboard-frontend/src/pages/Config.tsx | 4820 +++++++++-------- .../static/assets/index-CYHGOAxN.css | 1 + .../{index-DrKrP8CJ.js => index-DARDkZhk.js} | 122 +- .../static/assets/index-E1oMzltW.css | 1 - meshai/dashboard/static/index.html | 4 +- 5 files changed, 2485 insertions(+), 2463 deletions(-) create mode 100644 meshai/dashboard/static/assets/index-CYHGOAxN.css rename meshai/dashboard/static/assets/{index-DrKrP8CJ.js => index-DARDkZhk.js} (89%) delete mode 100644 meshai/dashboard/static/assets/index-E1oMzltW.css diff --git a/dashboard-frontend/src/pages/Config.tsx b/dashboard-frontend/src/pages/Config.tsx index 863868b..6ac9222 100644 --- a/dashboard-frontend/src/pages/Config.tsx +++ b/dashboard-frontend/src/pages/Config.tsx @@ -1,2399 +1,2421 @@ -import { useState, useEffect, useCallback } from 'react' -import NodePicker from '@/components/NodePicker' -import ChannelPicker from '@/components/ChannelPicker' -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, ExternalLink -} 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 - host?: string - port?: number - username?: string - password?: string - topic_root?: string - use_tls?: 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[] } - usgs: { enabled: boolean; tick_seconds: number; sites: string[] } - traffic: { enabled: boolean; tick_seconds: number; api_key: string; corridors: { name: string; lat: number; lon: number }[] } - roads511: { enabled: boolean; tick_seconds: number; api_key: string; base_url: string; endpoints: string[]; bbox: number[] } - firms: { enabled: boolean; tick_seconds: number; map_key: string; source: string; bbox: number[]; day_range: number; confidence_min: string; proximity_km: 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 }, -] - -// Section descriptions -const SECTION_DESCRIPTIONS: Record = { - bot: 'Identity and behavior settings for the bot on the mesh network.', - connection: 'How MeshAI connects to your Meshtastic radio.', - response: 'Controls how quickly and how much the bot responds on the mesh.', - history: 'Conversation history storage and cleanup.', - memory: 'Short-term conversation memory management. Controls how the bot maintains context within a conversation.', - context: 'Passive channel monitoring. The bot listens to mesh channels and uses recent messages as context when responding.', - commands: 'Mesh commands available via the configured prefix. Toggle individual commands on or off.', - llm: 'AI model configuration. MeshAI uses an LLM to understand questions and generate responses.', - weather: 'Weather data for the !weather command. This is separate from NWS environmental alerts.', - meshmonitor: 'AIDA MeshMonitor integration. An additional data source for mesh network monitoring.', - knowledge: 'Knowledge base for answering questions from stored documents. Connects to Qdrant vector database or local SQLite.', - mesh_sources: 'Data sources for mesh network information. MeshAI can pull data from multiple sources simultaneously and merge them into a unified view.', - mesh_intelligence: 'Advanced mesh analysis: health scoring, region management, and automated alerting. The intelligence engine monitors your mesh and detects problems automatically.', - environmental: 'Live environmental data feeds for situational awareness. Each feed polls a public or authenticated API for real-time conditions affecting your area.', - dashboard: "Web dashboard settings. You're looking at it right now.", -} - -// Available commands with descriptions -const AVAILABLE_COMMANDS = [ - { name: 'help', description: 'Show available commands and usage' }, - { name: 'health', description: 'Mesh network health overview with status dots' }, - { name: 'status', description: 'Quick mesh status summary' }, - { name: 'region', description: 'List regions or get detailed region breakdown' }, - { name: 'neighbors', description: 'Show top infrastructure neighbors with signal quality' }, - { name: 'ping', description: 'Test bot responsiveness' }, - { name: 'clear', description: 'Clear your conversation history' }, - { name: 'reset', description: 'Reset conversation context' }, - { name: 'sub', description: 'Subscribe to scheduled reports or alerts' }, - { name: 'unsub', description: 'Remove a subscription' }, - { name: 'mysubs', description: 'List your active subscriptions' }, - { name: 'alerts', description: 'Active NWS weather alerts for mesh area' }, - { name: 'solar', description: 'Space weather and HF propagation conditions' }, - { name: 'hf', description: 'HF radio propagation (alias for !solar)' }, - { name: 'fire', description: 'Active wildfires near the mesh' }, - { name: 'avy', description: 'Avalanche advisories for configured zones' }, - { name: 'hotspots', description: 'NASA FIRMS satellite fire detections' }, - { name: 'streams', description: 'USGS stream gauge readings' }, - { name: 'roads', description: 'Road conditions and closures' }, - { name: 'traffic', description: 'Traffic flow on monitored corridors' }, -] - -// US States for dropdown -const US_STATES = [ - { value: 'US-AL', label: 'Alabama' }, { value: 'US-AK', label: 'Alaska' }, - { value: 'US-AZ', label: 'Arizona' }, { value: 'US-AR', label: 'Arkansas' }, - { value: 'US-CA', label: 'California' }, { value: 'US-CO', label: 'Colorado' }, - { value: 'US-CT', label: 'Connecticut' }, { value: 'US-DE', label: 'Delaware' }, - { value: 'US-FL', label: 'Florida' }, { value: 'US-GA', label: 'Georgia' }, - { value: 'US-HI', label: 'Hawaii' }, { value: 'US-ID', label: 'Idaho' }, - { value: 'US-IL', label: 'Illinois' }, { value: 'US-IN', label: 'Indiana' }, - { value: 'US-IA', label: 'Iowa' }, { value: 'US-KS', label: 'Kansas' }, - { value: 'US-KY', label: 'Kentucky' }, { value: 'US-LA', label: 'Louisiana' }, - { value: 'US-ME', label: 'Maine' }, { value: 'US-MD', label: 'Maryland' }, - { value: 'US-MA', label: 'Massachusetts' }, { value: 'US-MI', label: 'Michigan' }, - { value: 'US-MN', label: 'Minnesota' }, { value: 'US-MS', label: 'Mississippi' }, - { value: 'US-MO', label: 'Missouri' }, { value: 'US-MT', label: 'Montana' }, - { value: 'US-NE', label: 'Nebraska' }, { value: 'US-NV', label: 'Nevada' }, - { value: 'US-NH', label: 'New Hampshire' }, { value: 'US-NJ', label: 'New Jersey' }, - { value: 'US-NM', label: 'New Mexico' }, { value: 'US-NY', label: 'New York' }, - { value: 'US-NC', label: 'North Carolina' }, { value: 'US-ND', label: 'North Dakota' }, - { value: 'US-OH', label: 'Ohio' }, { value: 'US-OK', label: 'Oklahoma' }, - { value: 'US-OR', label: 'Oregon' }, { value: 'US-PA', label: 'Pennsylvania' }, - { value: 'US-RI', label: 'Rhode Island' }, { value: 'US-SC', label: 'South Carolina' }, - { value: 'US-SD', label: 'South Dakota' }, { value: 'US-TN', label: 'Tennessee' }, - { value: 'US-TX', label: 'Texas' }, { value: 'US-UT', label: 'Utah' }, - { value: 'US-VT', label: 'Vermont' }, { value: 'US-VA', label: 'Virginia' }, - { value: 'US-WA', label: 'Washington' }, { value: 'US-WV', label: 'West Virginia' }, - { value: 'US-WI', label: 'Wisconsin' }, { value: 'US-WY', label: 'Wyoming' }, -] - -// InfoButton component -function InfoButton({ info, link, linkText = 'Learn more' }: { info: string; link?: string; linkText?: string }) { - const [open, setOpen] = useState(false) - - return ( -
- - {open && ( - <> -
setOpen(false)} /> -
- {info} - {link && ( - e.stopPropagation()} - > - {linkText} - - )} -
- - )} -
- ) -} - -// Section description component -function SectionDescription({ text }: { text: string }) { - return ( -

{text}

- ) -} - -// Form components -function TextInput({ label, value, onChange, type = 'text', placeholder = '', helper = '', info = '', infoLink = '' }: { - label: string - value: string - onChange: (v: string) => void - type?: string - placeholder?: string - helper?: string - info?: string - infoLink?: string -}) { - const [showPassword, setShowPassword] = useState(false) - const isPassword = type === 'password' - - 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 = '', info = '', infoLink = '' }: { - label: string - value: number - onChange: (v: number) => void - min?: number - max?: number - step?: number - helper?: string - info?: string - infoLink?: 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 = '', info = '', infoLink = '' }: { - label: string - checked: boolean - onChange: (v: boolean) => void - helper?: string - info?: string - infoLink?: string -}) { - return ( -
-
- - {label} - {info && } - - {helper &&

{helper}

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

{helper}

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