From 549ae4bdfb3e8ea1e4d8a34a09935479e170c8a2 Mon Sep 17 00:00:00 2001 From: K7ZVX Date: Tue, 12 May 2026 17:21:43 +0000 Subject: [PATCH] feat(env): NWS weather alerts, NOAA space weather, tropospheric ducting - Environmental feed system with tick-based adapters - NWS Active Alerts: polls api.weather.gov, zone-based filtering - NOAA SWPC: Kp, SFI, R/S/G scales, band assessment, alert detection - Tropospheric ducting: Open-Meteo GFS refractivity profile, duct classification - !alerts command for active weather warnings - !solar / !hf commands for RF propagation (HF + UHF ducting) - Alert engine integration: severe weather, R3+ blackout, ducting events - LLM context injection for weather/propagation queries - Dashboard RF Propagation card with HF + UHF ducting display - EnvironmentalConfig with per-feed toggles in config.yaml --- config.example.yaml | 304 +-- dashboard-frontend/src/lib/api.ts | 384 ++-- dashboard-frontend/src/pages/Dashboard.tsx | 869 +++++---- meshai/alert_engine.py | 81 + meshai/commands/alerts_cmd.py | 49 + meshai/commands/dispatcher.py | 23 + meshai/commands/solar_cmd.py | 63 + meshai/config.py | 907 +++++---- meshai/dashboard/api/env_routes.py | 55 +- .../{index-DnO02g6m.js => index-CELmCk_K.js} | 65 +- .../static/assets/index-DKYlTqQ1.css | 1 + .../static/assets/index-DdqEB3wX.css | 1 - meshai/dashboard/static/index.html | 4 +- meshai/env/__init__.py | 1 + meshai/env/ducting.py | 273 +++ meshai/env/nws.py | 193 ++ meshai/env/store.py | 168 ++ meshai/env/swpc.py | 256 +++ meshai/main.py | 1402 +++++++------- meshai/router.py | 1695 +++++++++-------- 20 files changed, 4142 insertions(+), 2652 deletions(-) create mode 100644 meshai/commands/alerts_cmd.py create mode 100644 meshai/commands/solar_cmd.py rename meshai/dashboard/static/assets/{index-DnO02g6m.js => index-CELmCk_K.js} (50%) create mode 100644 meshai/dashboard/static/assets/index-DKYlTqQ1.css delete mode 100644 meshai/dashboard/static/assets/index-DdqEB3wX.css create mode 100644 meshai/env/__init__.py create mode 100644 meshai/env/ducting.py create mode 100644 meshai/env/nws.py create mode 100644 meshai/env/store.py create mode 100644 meshai/env/swpc.py diff --git a/config.example.yaml b/config.example.yaml index 0e2eb52..cb45e3f 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,131 +1,173 @@ -# MeshAI Configuration -# LLM-powered Meshtastic assistant -# -# Copy this to config.yaml and customize as needed -# For Docker: mount as /data/config.yaml - -# === BOT IDENTITY === -bot: - name: ai # Bot's display name - owner: "" # Owner's callsign (optional) - respond_to_dms: true # Respond to direct messages - filter_bbs_protocols: true # Ignore advBBS sync/notification messages - -# === MESHTASTIC CONNECTION === -connection: - type: tcp # serial | tcp - serial_port: /dev/ttyUSB0 # For serial connection - tcp_host: localhost # For TCP connection (meshtasticd) - tcp_port: 4403 - -# === RESPONSE BEHAVIOR === -response: - delay_min: 2.2 # Min delay before responding (seconds) - delay_max: 3.0 # Max delay before responding - max_length: 200 # Max chars per message chunk - max_messages: 3 # Max message chunks per response - -# === CONVERSATION HISTORY === -history: - database: /data/conversations.db - max_messages_per_user: 50 # Messages to keep per user - conversation_timeout: 86400 # Conversation expiry (seconds, 86400=24h) - auto_cleanup: true # Auto-delete old conversations - cleanup_interval_hours: 24 # How often to run cleanup - max_age_days: 30 # Delete conversations older than this - -# === MEMORY OPTIMIZATION === -memory: - enabled: true # Enable rolling summary memory - window_size: 4 # Recent message pairs to keep in full - summarize_threshold: 8 # Messages before re-summarizing - -# === MESH CONTEXT === -context: - enabled: true # Observe channel traffic for LLM context - observe_channels: [] # Channel indices to observe (empty = all) - ignore_nodes: [] # Node IDs to exclude from observation - max_age: 2592000 # Max age in seconds (default 30 days) - max_context_items: 20 # Max observations injected into LLM context - -# === LLM BACKEND === -llm: - backend: openai # openai | anthropic | google - api_key: "" # API key (or use LLM_API_KEY env var) - base_url: https://api.openai.com/v1 # API base URL - model: gpt-4o-mini # Model name - timeout: 30 # Request timeout (seconds) - system_prompt: >- - You are a helpful assistant on a Meshtastic mesh network. - Keep responses very brief - 1-2 short sentences, under 300 characters. - Only give longer answers if the user explicitly asks for detail or explanation. - Be concise but friendly. No markdown formatting. - google_grounding: false # Enable Google Search grounding (Gemini only, $35/1k queries) - -# === WEATHER === -weather: - primary: openmeteo # openmeteo | wttr | llm - fallback: llm # openmeteo | wttr | llm | none - default_location: "" # Default location for !weather (optional) - -# === MESHMONITOR INTEGRATION === -meshmonitor: - enabled: false # Enable MeshMonitor trigger sync - url: "" # MeshMonitor web UI URL (e.g. http://192.168.1.100:3333) - inject_into_prompt: true # Include trigger list in LLM prompt - refresh_interval: 300 # Seconds between trigger refreshes - -# === KNOWLEDGE BASE (RAG) === -knowledge: - enabled: false # Enable knowledge base search - db_path: "" # Path to knowledge SQLite database - top_k: 5 # Number of chunks to retrieve per query - -# === MESH DATA SOURCES === -# Connect to Meshview and/or MeshMonitor instances for live mesh -# network analysis. Supports multiple sources. Configure via TUI -# with meshai --config (Mesh Sources menu). -# -# mesh_sources: -# - name: "my-meshview" -# type: meshview -# url: "https://meshview.example.com" -# refresh_interval: 300 -# enabled: true -# -# - name: "my-meshmonitor" -# type: meshmonitor -# url: "http://192.168.1.100:3333" -# api_token: "${MM_API_TOKEN}" -# refresh_interval: 300 -# enabled: true -mesh_sources: [] - -# === MESH INTELLIGENCE === -# Geographic clustering and health scoring for mesh analysis. -# Requires mesh_sources to be configured with at least one data source. -# -# mesh_intelligence: -# enabled: true -# region_radius_miles: 40.0 # Radius for region clustering -# locality_radius_miles: 8.0 # Radius for locality clustering -# offline_threshold_hours: 24 # Hours before node considered offline -# packet_threshold: 500 # Non-text packets per 24h to flag -# battery_warning_percent: 20 # Battery level for warnings -# infra_overrides: [] # Node IDs to exclude from infrastructure -# region_labels: {} # Override auto-names: {"Twin Falls": "Magic Valley"} -mesh_intelligence: - enabled: false - region_radius_miles: 40.0 - locality_radius_miles: 8.0 - offline_threshold_hours: 24 - packet_threshold: 500 - battery_warning_percent: 20 - infra_overrides: [] - region_labels: {} - -# === WEB DASHBOARD === -dashboard: - enabled: true - port: 8080 - host: "0.0.0.0" +# MeshAI Configuration +# LLM-powered Meshtastic assistant +# +# Copy this to config.yaml and customize as needed +# For Docker: mount as /data/config.yaml + +# === BOT IDENTITY === +bot: + name: ai # Bot's display name + owner: "" # Owner's callsign (optional) + respond_to_dms: true # Respond to direct messages + filter_bbs_protocols: true # Ignore advBBS sync/notification messages + +# === MESHTASTIC CONNECTION === +connection: + type: tcp # serial | tcp + serial_port: /dev/ttyUSB0 # For serial connection + tcp_host: localhost # For TCP connection (meshtasticd) + tcp_port: 4403 + +# === RESPONSE BEHAVIOR === +response: + delay_min: 2.2 # Min delay before responding (seconds) + delay_max: 3.0 # Max delay before responding + max_length: 200 # Max chars per message chunk + max_messages: 3 # Max message chunks per response + +# === CONVERSATION HISTORY === +history: + database: /data/conversations.db + max_messages_per_user: 50 # Messages to keep per user + conversation_timeout: 86400 # Conversation expiry (seconds, 86400=24h) + auto_cleanup: true # Auto-delete old conversations + cleanup_interval_hours: 24 # How often to run cleanup + max_age_days: 30 # Delete conversations older than this + +# === MEMORY OPTIMIZATION === +memory: + enabled: true # Enable rolling summary memory + window_size: 4 # Recent message pairs to keep in full + summarize_threshold: 8 # Messages before re-summarizing + +# === MESH CONTEXT === +context: + enabled: true # Observe channel traffic for LLM context + observe_channels: [] # Channel indices to observe (empty = all) + ignore_nodes: [] # Node IDs to exclude from observation + max_age: 2592000 # Max age in seconds (default 30 days) + max_context_items: 20 # Max observations injected into LLM context + +# === LLM BACKEND === +llm: + backend: openai # openai | anthropic | google + api_key: "" # API key (or use LLM_API_KEY env var) + base_url: https://api.openai.com/v1 # API base URL + model: gpt-4o-mini # Model name + timeout: 30 # Request timeout (seconds) + system_prompt: >- + You are a helpful assistant on a Meshtastic mesh network. + Keep responses very brief - 1-2 short sentences, under 300 characters. + Only give longer answers if the user explicitly asks for detail or explanation. + Be concise but friendly. No markdown formatting. + google_grounding: false # Enable Google Search grounding (Gemini only, $35/1k queries) + +# === WEATHER === +weather: + primary: openmeteo # openmeteo | wttr | llm + fallback: llm # openmeteo | wttr | llm | none + default_location: "" # Default location for !weather (optional) + +# === MESHMONITOR INTEGRATION === +meshmonitor: + enabled: false # Enable MeshMonitor trigger sync + url: "" # MeshMonitor web UI URL (e.g. http://192.168.1.100:3333) + inject_into_prompt: true # Include trigger list in LLM prompt + refresh_interval: 300 # Seconds between trigger refreshes + +# === KNOWLEDGE BASE (RAG) === +knowledge: + enabled: false # Enable knowledge base search + db_path: "" # Path to knowledge SQLite database + top_k: 5 # Number of chunks to retrieve per query + +# === MESH DATA SOURCES === +# Connect to Meshview and/or MeshMonitor instances for live mesh +# network analysis. Supports multiple sources. Configure via TUI +# with meshai --config (Mesh Sources menu). +# +# mesh_sources: +# - name: "my-meshview" +# type: meshview +# url: "https://meshview.example.com" +# refresh_interval: 300 +# enabled: true +# +# - name: "my-meshmonitor" +# type: meshmonitor +# url: "http://192.168.1.100:3333" +# api_token: "${MM_API_TOKEN}" +# refresh_interval: 300 +# enabled: true +mesh_sources: [] + +# === MESH INTELLIGENCE === +# Geographic clustering and health scoring for mesh analysis. +# Requires mesh_sources to be configured with at least one data source. +# +# mesh_intelligence: +# enabled: true +# region_radius_miles: 40.0 # Radius for region clustering +# locality_radius_miles: 8.0 # Radius for locality clustering +# offline_threshold_hours: 24 # Hours before node considered offline +# packet_threshold: 500 # Non-text packets per 24h to flag +# battery_warning_percent: 20 # Battery level for warnings +# infra_overrides: [] # Node IDs to exclude from infrastructure +# region_labels: {} # Override auto-names: {"Twin Falls": "Magic Valley"} +mesh_intelligence: + enabled: false + region_radius_miles: 40.0 + locality_radius_miles: 8.0 + offline_threshold_hours: 24 + packet_threshold: 500 + battery_warning_percent: 20 + infra_overrides: [] + region_labels: {} + +# === ENVIRONMENTAL FEEDS === +# Live situational awareness from NWS, NOAA Space Weather, and Open-Meteo. +# Provides weather alerts, HF propagation assessment, and tropospheric ducting. +# +environmental: + enabled: false + nws_zones: + - "IDZ016" # Western Magic Valley + - "IDZ030" # Southern Twin Falls County + + # NWS Weather Alerts (api.weather.gov) + nws: + enabled: true + tick_seconds: 60 + areas: ["ID"] + severity_min: "moderate" + user_agent: "(meshai.example.com, ops@example.com)" # REQUIRED by NWS + + # NOAA Space Weather (services.swpc.noaa.gov) + swpc: + enabled: true + + # Tropospheric ducting assessment (Open-Meteo GFS, no auth) + ducting: + enabled: true + tick_seconds: 10800 # 3 hours + latitude: 42.56 # center of mesh coverage area + longitude: -114.47 + + # NIFC Fire Perimeters (Phase 2) + fires: + enabled: false + tick_seconds: 600 + state: "US-ID" + + # Avalanche Advisories (Phase 2) + avalanche: + enabled: false + tick_seconds: 1800 + center_ids: ["SNFAC"] + season_months: [12, 1, 2, 3, 4] + +# === WEB DASHBOARD === +dashboard: + enabled: true + port: 8080 + host: "0.0.0.0" diff --git a/dashboard-frontend/src/lib/api.ts b/dashboard-frontend/src/lib/api.ts index 20eaa2c..d5f63b2 100644 --- a/dashboard-frontend/src/lib/api.ts +++ b/dashboard-frontend/src/lib/api.ts @@ -1,157 +1,227 @@ -// API types matching actual backend responses - -export interface SystemStatus { - version: string - uptime_seconds: number - bot_name: string - connection_type: string - connection_target: string - connected: boolean - node_count: number - source_count: number - env_feeds_enabled: boolean - dashboard_port: number -} - -export interface MeshHealth { - score: number - tier: string - pillars: { - infrastructure: number - utilization: number - behavior: number - power: number - } - infra_online: number - infra_total: number - util_percent: number - flagged_nodes: number - battery_warnings: number - total_nodes: number - total_regions: number - unlocated_count: number - last_computed: string - recommendations: string[] -} - -export interface NodeInfo { - node_num: number - node_id_hex: string - short_name: string - long_name: string - role: string - latitude: number | null - longitude: number | null - last_heard: string | null - battery_level: number | null - voltage: number | null - snr: number | null - firmware: string - hardware: string - uptime: number | null - sources: string[] -} - -export interface EdgeInfo { - from_node: number - to_node: number - snr: number - quality: string -} - -export interface SourceHealth { - name: string - type: string - url: string - is_loaded: boolean - last_error: string | null - consecutive_errors: number - response_time_ms: number | null - tick_count: number - node_count: number -} - -export interface Alert { - type: string - severity: string - message: string - timestamp: string - scope_type?: string - scope_value?: string -} - -export interface EnvStatus { - enabled: boolean - feeds: unknown[] -} - -export interface EnvEvent { - type: string - [key: string]: unknown -} - -// API fetch helpers - -async function fetchJson(url: string): Promise { - const response = await fetch(url) - if (!response.ok) { - throw new Error(`API error: ${response.status} ${response.statusText}`) - } - return response.json() -} - -export async function fetchStatus(): Promise { - return fetchJson('/api/status') -} - -export async function fetchHealth(): Promise { - return fetchJson('/api/health') -} - -export async function fetchNodes(): Promise { - return fetchJson('/api/nodes') -} - -export async function fetchEdges(): Promise { - return fetchJson('/api/edges') -} - -export async function fetchSources(): Promise { - return fetchJson('/api/sources') -} - -export async function fetchConfig(section?: string): Promise { - const url = section ? `/api/config/${section}` : '/api/config' - return fetchJson(url) -} - -export async function updateConfig( - section: string, - data: unknown -): Promise<{ saved: boolean; restart_required: boolean }> { - const response = await fetch(`/api/config/${section}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - if (!response.ok) { - throw new Error(`API error: ${response.status} ${response.statusText}`) - } - return response.json() -} - -export async function fetchAlerts(): Promise { - return fetchJson('/api/alerts/active') -} - -export async function fetchEnvStatus(): Promise { - return fetchJson('/api/env/status') -} - -export async function fetchEnvActive(): Promise { - return fetchJson('/api/env/active') -} - -export async function fetchRegions(): Promise { - return fetchJson('/api/regions') -} +// API types matching actual backend responses + +export interface SystemStatus { + version: string + uptime_seconds: number + bot_name: string + connection_type: string + connection_target: string + connected: boolean + node_count: number + source_count: number + env_feeds_enabled: boolean + dashboard_port: number +} + +export interface MeshHealth { + score: number + tier: string + pillars: { + infrastructure: number + utilization: number + behavior: number + power: number + } + infra_online: number + infra_total: number + util_percent: number + flagged_nodes: number + battery_warnings: number + total_nodes: number + total_regions: number + unlocated_count: number + last_computed: string + recommendations: string[] +} + +export interface NodeInfo { + node_num: number + node_id_hex: string + short_name: string + long_name: string + role: string + latitude: number | null + longitude: number | null + last_heard: string | null + battery_level: number | null + voltage: number | null + snr: number | null + firmware: string + hardware: string + uptime: number | null + sources: string[] +} + +export interface EdgeInfo { + from_node: number + to_node: number + snr: number + quality: string +} + +export interface SourceHealth { + name: string + type: string + url: string + is_loaded: boolean + last_error: string | null + consecutive_errors: number + response_time_ms: number | null + tick_count: number + node_count: number +} + +export interface Alert { + type: string + severity: string + message: string + timestamp: string + scope_type?: string + scope_value?: string +} + +export interface EnvStatus { + enabled: boolean + feeds: EnvFeedHealth[] +} + +export interface EnvFeedHealth { + source: string + is_loaded: boolean + last_error: string | null + consecutive_errors: number + event_count: number + last_fetch: number +} + +export interface EnvEvent { + source: string + event_id: string + event_type: string + severity: string + headline: string + description?: string + expires?: number + fetched_at: number + [key: string]: unknown +} + +export interface SWPCStatus { + enabled: boolean + kp_current?: number + kp_timestamp?: string + sfi?: number + r_scale?: number + s_scale?: number + g_scale?: number + band_assessment?: string + band_detail?: string + active_warnings?: string[] +} + +export interface DuctingStatus { + enabled: boolean + condition?: string + min_gradient?: number + duct_thickness_m?: number | null + duct_base_m?: number | null + assessment?: string + last_update?: string +} + +export interface RFPropagation { + hf: { + kp_current?: number + sfi?: number + r_scale?: number + s_scale?: number + g_scale?: number + band_assessment?: string + band_detail?: string + active_warnings?: string[] + } + uhf_ducting: { + condition?: string + min_gradient?: number + duct_thickness_m?: number | null + assessment?: string + } +} + +// API fetch helpers + +async function fetchJson(url: string): Promise { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + +export async function fetchStatus(): Promise { + return fetchJson('/api/status') +} + +export async function fetchHealth(): Promise { + return fetchJson('/api/health') +} + +export async function fetchNodes(): Promise { + return fetchJson('/api/nodes') +} + +export async function fetchEdges(): Promise { + return fetchJson('/api/edges') +} + +export async function fetchSources(): Promise { + return fetchJson('/api/sources') +} + +export async function fetchConfig(section?: string): Promise { + const url = section ? `/api/config/${section}` : '/api/config' + return fetchJson(url) +} + +export async function updateConfig( + section: string, + data: unknown +): Promise<{ saved: boolean; restart_required: boolean }> { + const response = await fetch(`/api/config/${section}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + +export async function fetchAlerts(): Promise { + return fetchJson('/api/alerts/active') +} + +export async function fetchEnvStatus(): Promise { + return fetchJson('/api/env/status') +} + +export async function fetchEnvActive(): Promise { + return fetchJson('/api/env/active') +} + +export async function fetchRFPropagation(): Promise { + return fetchJson('/api/env/propagation') +} + +export async function fetchSWPC(): Promise { + return fetchJson('/api/env/swpc') +} + +export async function fetchDucting(): Promise { + return fetchJson('/api/env/ducting') +} + +export async function fetchRegions(): Promise { + return fetchJson('/api/regions') +} diff --git a/dashboard-frontend/src/pages/Dashboard.tsx b/dashboard-frontend/src/pages/Dashboard.tsx index d046efb..add6352 100644 --- a/dashboard-frontend/src/pages/Dashboard.tsx +++ b/dashboard-frontend/src/pages/Dashboard.tsx @@ -1,381 +1,488 @@ -import { useEffect, useState } from 'react' -import { - fetchHealth, - fetchSources, - fetchAlerts, - fetchEnvStatus, - type MeshHealth, - type SourceHealth, - type Alert, - type EnvStatus, -} from '@/lib/api' -import { useWebSocket } from '@/hooks/useWebSocket' -import { - AlertTriangle, - AlertCircle, - Info, - CheckCircle, - Radio, - Cpu, - Activity, - MapPin, -} from 'lucide-react' - -function HealthGauge({ health }: { health: MeshHealth }) { - const score = health.score - const tier = health.tier - - // Color based on score - const getColor = (s: number) => { - if (s >= 80) return '#22c55e' - if (s >= 60) return '#f59e0b' - return '#ef4444' - } - - const color = getColor(score) - const circumference = 2 * Math.PI * 45 - const progress = (score / 100) * circumference - - return ( -
- - {/* Background circle */} - - {/* Progress arc */} - - {/* Score text */} - - {score.toFixed(1)} - - - {tier} - - -
- ) -} - -function PillarBar({ - label, - value, -}: { - label: string - value: number -}) { - const getColor = (v: number) => { - if (v >= 80) return 'bg-green-500' - if (v >= 60) return 'bg-amber-500' - return 'bg-red-500' - } - - return ( -
-
{label}
-
-
-
-
- {value.toFixed(1)} -
-
- ) -} - -function AlertItem({ alert }: { alert: Alert }) { - const getSeverityStyles = (severity: string) => { - switch (severity.toLowerCase()) { - case 'critical': - case 'emergency': - return { - bg: 'bg-red-500/10', - border: 'border-red-500', - icon: AlertCircle, - iconColor: 'text-red-500', - } - case 'warning': - return { - bg: 'bg-amber-500/10', - border: 'border-amber-500', - icon: AlertTriangle, - iconColor: 'text-amber-500', - } - default: - return { - bg: 'bg-green-500/10', - border: 'border-green-500', - icon: Info, - iconColor: 'text-green-500', - } - } - } - - const styles = getSeverityStyles(alert.severity) - const Icon = styles.icon - - return ( -
- -
-
{alert.message}
-
- {alert.timestamp || 'Just now'} -
-
-
- ) -} - -function SourceCard({ source }: { source: SourceHealth }) { - const getStatusColor = () => { - if (!source.is_loaded) return 'bg-red-500' - if (source.last_error) return 'bg-amber-500' - return 'bg-green-500' - } - - return ( -
-
-
-
{source.name}
-
- {source.node_count} nodes • {source.type} -
-
-
- ) -} - -function StatCard({ - icon: Icon, - label, - value, - subvalue, -}: { - icon: typeof Radio - label: string - value: string | number - subvalue?: string -}) { - return ( -
-
- - {label} -
-
{value}
- {subvalue && ( -
{subvalue}
- )} -
- ) -} - -export default function Dashboard() { - const [health, setHealth] = useState(null) - const [sources, setSources] = useState([]) - const [alerts, setAlerts] = useState([]) - const [envStatus, setEnvStatus] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - const { lastHealth } = useWebSocket() - - useEffect(() => { - Promise.all([ - fetchHealth(), - fetchSources(), - fetchAlerts(), - fetchEnvStatus(), - ]) - .then(([h, src, a, e]) => { - setHealth(h) - setSources(src) - setAlerts(a) - setEnvStatus(e) - setLoading(false) - }) - .catch((err) => { - setError(err.message) - setLoading(false) - }) - }, []) - - // Update health from WebSocket - useEffect(() => { - if (lastHealth) { - setHealth(lastHealth) - } - }, [lastHealth]) - - if (loading) { - return ( -
-
Loading...
-
- ) - } - - if (error) { - return ( -
-
Error: {error}
-
- ) - } - - return ( -
- {/* Mesh Health */} -
-

Mesh Health

- {health && ( - <> - -
- - - - -
- - )} -
- - {/* Alerts + Stats */} -
- {/* Active Alerts */} -
-

- Active Alerts -

- {alerts.length > 0 ? ( -
- {alerts.map((alert, i) => ( - - ))} -
- ) : ( -
- - No active alerts -
- )} -
- - {/* Quick Stats */} -
- - - - -
-
- - {/* Mesh Sources */} -
-

- Mesh Sources ({sources.length}) -

- {sources.length > 0 ? ( -
- {sources.map((source, i) => ( - - ))} -
- ) : ( -
No sources configured
- )} -
- - {/* Environmental Feeds */} -
-

- Environmental Feeds -

- {envStatus?.enabled ? ( -
- {envStatus.feeds.length} feeds active -
- ) : ( -
-

Environmental feeds not enabled.

-

- Enable in Config → Mesh Intelligence -

-
- )} -
- - {/* HF Propagation placeholder */} -
-

- HF Propagation -

-
-

Space weather data not enabled.

-

Coming in Phase 1

-
-
-
- ) -} +import { useEffect, useState } from 'react' +import { + fetchHealth, + fetchSources, + fetchAlerts, + fetchEnvStatus, + fetchRFPropagation, + type MeshHealth, + type SourceHealth, + type Alert, + type EnvStatus, + type RFPropagation, +} from '@/lib/api' +import { useWebSocket } from '@/hooks/useWebSocket' +import { + AlertTriangle, + AlertCircle, + Info, + CheckCircle, + Radio, + Cpu, + Activity, + MapPin, + Zap, +} from 'lucide-react' + +function HealthGauge({ health }: { health: MeshHealth }) { + const score = health.score + const tier = health.tier + + // Color based on score + const getColor = (s: number) => { + if (s >= 80) return '#22c55e' + if (s >= 60) return '#f59e0b' + return '#ef4444' + } + + const color = getColor(score) + const circumference = 2 * Math.PI * 45 + const progress = (score / 100) * circumference + + return ( +
+ + {/* Background circle */} + + {/* Progress arc */} + + {/* Score text */} + + {score.toFixed(1)} + + + {tier} + + +
+ ) +} + +function PillarBar({ + label, + value, +}: { + label: string + value: number +}) { + const getColor = (v: number) => { + if (v >= 80) return 'bg-green-500' + if (v >= 60) return 'bg-amber-500' + return 'bg-red-500' + } + + return ( +
+
{label}
+
+
+
+
+ {value.toFixed(1)} +
+
+ ) +} + +function AlertItem({ alert }: { alert: Alert }) { + const getSeverityStyles = (severity: string) => { + switch (severity.toLowerCase()) { + case 'critical': + case 'emergency': + return { + bg: 'bg-red-500/10', + border: 'border-red-500', + icon: AlertCircle, + iconColor: 'text-red-500', + } + case 'warning': + return { + bg: 'bg-amber-500/10', + border: 'border-amber-500', + icon: AlertTriangle, + iconColor: 'text-amber-500', + } + default: + return { + bg: 'bg-green-500/10', + border: 'border-green-500', + icon: Info, + iconColor: 'text-green-500', + } + } + } + + const styles = getSeverityStyles(alert.severity) + const Icon = styles.icon + + return ( +
+ +
+
{alert.message}
+
+ {alert.timestamp || 'Just now'} +
+
+
+ ) +} + +function SourceCard({ source }: { source: SourceHealth }) { + const getStatusColor = () => { + if (!source.is_loaded) return 'bg-red-500' + if (source.last_error) return 'bg-amber-500' + return 'bg-green-500' + } + + return ( +
+
+
+
{source.name}
+
+ {source.node_count} nodes * {source.type} +
+
+
+ ) +} + +function StatCard({ + icon: Icon, + label, + value, + subvalue, +}: { + icon: typeof Radio + label: string + value: string | number + subvalue?: string +}) { + return ( +
+
+ + {label} +
+
{value}
+ {subvalue && ( +
{subvalue}
+ )} +
+ ) +} + +function RFPropagationCard({ propagation }: { propagation: RFPropagation | null }) { + if (!propagation) { + return ( +
+

+ RF Propagation +

+
+

Loading propagation data...

+
+
+ ) + } + + const hf = propagation.hf + const ducting = propagation.uhf_ducting + + const getAssessmentColor = (assessment?: string) => { + if (!assessment) return 'text-slate-400' + switch (assessment.toLowerCase()) { + case 'excellent': + return 'text-green-400' + case 'good': + return 'text-green-500' + case 'fair': + return 'text-amber-500' + case 'poor': + return 'text-red-500' + default: + return 'text-slate-400' + } + } + + const getDuctingColor = (condition?: string) => { + if (!condition) return 'text-slate-400' + switch (condition) { + case 'normal': + return 'text-green-500' + case 'super_refraction': + return 'text-amber-500' + case 'surface_duct': + case 'elevated_duct': + return 'text-blue-400' + default: + return 'text-slate-400' + } + } + + const hasHF = hf && (hf.band_assessment || hf.sfi || hf.kp_current !== undefined) + const hasDucting = ducting && ducting.condition + + return ( +
+

+ + RF Propagation +

+ + {/* HF Section */} +
+
HF Bands
+ {hasHF ? ( +
+
+ {hf.band_assessment || 'Unknown'} +
+
+ SFI {hf.sfi?.toFixed(0) || '?'} / Kp {hf.kp_current?.toFixed(1) || '?'} +
+ {hf.r_scale !== undefined && hf.r_scale > 0 && ( +
+ R{hf.r_scale} Radio Blackout +
+ )} +
+ ) : ( +
No HF data
+ )} +
+ + {/* UHF Ducting Section */} +
+
UHF 906 MHz
+ {hasDucting ? ( +
+
+ {ducting.condition === 'normal' + ? 'Normal' + : ducting.condition?.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())} +
+ {ducting.condition !== 'normal' && ducting.min_gradient !== undefined && ( +
+ dM/dz: {ducting.min_gradient} M-units/km +
+ )} + {ducting.condition !== 'normal' && ( +
+ Extended range likely +
+ )} +
+ ) : ( +
No ducting data
+ )} +
+
+ ) +} + +export default function Dashboard() { + const [health, setHealth] = useState(null) + const [sources, setSources] = useState([]) + const [alerts, setAlerts] = useState([]) + const [envStatus, setEnvStatus] = useState(null) + const [rfProp, setRFProp] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const { lastHealth } = useWebSocket() + + useEffect(() => { + Promise.all([ + fetchHealth(), + fetchSources(), + fetchAlerts(), + fetchEnvStatus(), + fetchRFPropagation().catch(() => null), + ]) + .then(([h, src, a, e, rf]) => { + setHealth(h) + setSources(src) + setAlerts(a) + setEnvStatus(e) + setRFProp(rf) + setLoading(false) + }) + .catch((err) => { + setError(err.message) + setLoading(false) + }) + }, []) + + // Update health from WebSocket + useEffect(() => { + if (lastHealth) { + setHealth(lastHealth) + } + }, [lastHealth]) + + if (loading) { + return ( +
+
Loading...
+
+ ) + } + + if (error) { + return ( +
+
Error: {error}
+
+ ) + } + + return ( +
+ {/* Mesh Health */} +
+

Mesh Health

+ {health && ( + <> + +
+ + + + +
+ + )} +
+ + {/* Alerts + Stats */} +
+ {/* Active Alerts */} +
+

+ Active Alerts +

+ {alerts.length > 0 ? ( +
+ {alerts.map((alert, i) => ( + + ))} +
+ ) : ( +
+ + No active alerts +
+ )} +
+ + {/* Quick Stats */} +
+ + + + +
+
+ + {/* Mesh Sources */} +
+

+ Mesh Sources ({sources.length}) +

+ {sources.length > 0 ? ( +
+ {sources.map((source, i) => ( + + ))} +
+ ) : ( +
No sources configured
+ )} +
+ + {/* Environmental Feeds */} +
+

+ Environmental Feeds +

+ {envStatus?.enabled ? ( +
+ {envStatus.feeds.length} feeds active +
+ ) : ( +
+

Environmental feeds not enabled.

+

+ Enable in config.yaml +

+
+ )} +
+ + {/* RF Propagation */} + +
+ ) +} diff --git a/meshai/alert_engine.py b/meshai/alert_engine.py index 1e73914..fe305ef 100644 --- a/meshai/alert_engine.py +++ b/meshai/alert_engine.py @@ -581,3 +581,84 @@ class AlertEngine: scope_type=alert.get("scope_type"), scope_value=alert.get("scope_value"), ) + + def check_environmental(self, env_store) -> list[dict]: + """Check environmental feeds for alertable conditions. + + Args: + env_store: EnvironmentalStore instance + + Returns: + List of alert dicts + """ + alerts = [] + now = time.time() + + # NWS severe weather affecting mesh zones + mesh_zones = set(getattr(env_store, "_mesh_zones", [])) + for evt in env_store.get_active(source="nws"): + if evt.get("severity") not in ("severe", "extreme", "warning"): + continue + event_zones = set(evt.get("areas", [])) + if mesh_zones and not (event_zones & mesh_zones): + continue + key = f"env_nws_{evt['event_id']}" + state = self._get_state(key) + if not state.should_fire(now): + continue + state.fire(now) + alerts.append({ + "type": "weather_warning", + "message": f"Warning: {evt['event_type']}: {evt.get('headline', '')[:150]}", + "severity": evt["severity"], + "node_num": None, + "node_name": evt["event_type"], + "node_short": "NWS", + "region": "", + "scope_type": "mesh", + "scope_value": None, + "is_critical": evt["severity"] in ("extreme", "emergency"), + }) + + # SWPC R-scale >= 3 (HF blackout affecting mesh backhaul) + swpc = env_store.get_swpc_status() + if swpc and swpc.get("r_scale", 0) >= 3: + r_scale = swpc["r_scale"] + key = f"env_swpc_r{r_scale}" + state = self._get_state(key) + if state.should_fire(now): + state.fire(now) + alerts.append({ + "type": "hf_blackout", + "message": f"Warning: R{r_scale} HF Radio Blackout -- mesh backhaul links may degrade", + "severity": "warning", + "node_num": None, + "node_name": f"R{r_scale} Blackout", + "node_short": "SWPC", + "region": "", + "scope_type": "mesh", + "scope_value": None, + "is_critical": r_scale >= 4, + }) + + # UHF ducting (informational -- not critical but operators want to know) + ducting = env_store.get_ducting_status() + if ducting and ducting.get("condition") in ("surface_duct", "elevated_duct"): + key = "env_ducting_active" + state = self._get_state(key) + if state.should_fire(now): + state.fire(now) + alerts.append({ + "type": "uhf_ducting", + "message": "UHF ducting detected -- 906 MHz range may be extended, expect distant nodes", + "severity": "info", + "node_num": None, + "node_name": "Ducting", + "node_short": "UHF", + "region": "", + "scope_type": "mesh", + "scope_value": None, + "is_critical": False, + }) + + return alerts diff --git a/meshai/commands/alerts_cmd.py b/meshai/commands/alerts_cmd.py new file mode 100644 index 0000000..388c431 --- /dev/null +++ b/meshai/commands/alerts_cmd.py @@ -0,0 +1,49 @@ +"""Alerts command handler.""" + +import time +from datetime import datetime + +from .base import CommandContext, CommandHandler + + +class AlertsCommand(CommandHandler): + """Active weather alerts for mesh area.""" + + name = "alerts" + description = "Active weather alerts for mesh area" + usage = "!alerts" + + def __init__(self, env_store): + self._env_store = env_store + + async def execute(self, args: str, context: CommandContext) -> str: + """Execute the alerts command.""" + if not self._env_store: + return "Environmental feeds not enabled." + + zones = self._env_store._mesh_zones + alerts = self._env_store.get_for_zones(zones) + + if not alerts: + alerts = self._env_store.get_active(source="nws") + + if not alerts: + return "No active weather alerts for the mesh area." + + lines = [f"Active Alerts ({len(alerts)}):"] + for a in alerts[:5]: + # Format expiry time + expires = a.get("expires", 0) + if expires: + try: + dt = datetime.fromtimestamp(expires) + expires_str = dt.strftime("%b %d %H:%MZ") + except Exception: + expires_str = "Unknown" + else: + expires_str = "Unknown" + + lines.append(f"* {a['event_type']} -- {a.get('area_desc', '')[:60]}") + lines.append(f" Until {expires_str}") + + return "\n".join(lines) diff --git a/meshai/commands/dispatcher.py b/meshai/commands/dispatcher.py index 3a4849c..d96ba80 100644 --- a/meshai/commands/dispatcher.py +++ b/meshai/commands/dispatcher.py @@ -161,6 +161,7 @@ def create_dispatcher( data_store=None, health_engine=None, subscription_manager=None, + env_store=None, ) -> CommandDispatcher: """Create and populate command dispatcher with default commands. @@ -172,6 +173,7 @@ def create_dispatcher( data_store: MeshDataStore for neighbor data health_engine: MeshHealthEngine for infrastructure detection subscription_manager: SubscriptionManager for subscription commands + env_store: EnvironmentalStore for weather/propagation commands Returns: Configured CommandDispatcher @@ -243,6 +245,27 @@ def create_dispatcher( alias_handler.name = alias dispatcher.register(alias_handler) + # Register environmental commands + if env_store: + from .alerts_cmd import AlertsCommand + from .solar_cmd import SolarCommand + + alerts_cmd = AlertsCommand(env_store) + dispatcher.register(alerts_cmd) + + solar_cmd = SolarCommand(env_store) + dispatcher.register(solar_cmd) + + # Register !hf as an alias for !solar + hf_cmd = SolarCommand(env_store) + hf_cmd.name = "hf" + dispatcher.register(hf_cmd) + + # Register !wx-alerts as an alias for !alerts + wx_cmd = AlertsCommand(env_store) + wx_cmd.name = "wx-alerts" + dispatcher.register(wx_cmd) + # Register custom commands if custom_commands: for name, response in custom_commands.items(): diff --git a/meshai/commands/solar_cmd.py b/meshai/commands/solar_cmd.py new file mode 100644 index 0000000..a74ab8a --- /dev/null +++ b/meshai/commands/solar_cmd.py @@ -0,0 +1,63 @@ +"""Solar/RF propagation command handler.""" + +from .base import CommandContext, CommandHandler + + +class SolarCommand(CommandHandler): + """Space weather & RF propagation.""" + + name = "solar" + description = "Space weather & RF propagation" + usage = "!solar" + + def __init__(self, env_store): + self._env_store = env_store + + async def execute(self, args: str, context: CommandContext) -> str: + """Execute the solar command.""" + if not self._env_store: + return "Environmental feeds not enabled." + + lines = [] + + # HF section + s = self._env_store.get_swpc_status() + if s: + assessment = s.get("band_assessment", "Unknown") + kp = s.get("kp_current", "?") + sfi = s.get("sfi", "?") + r = s.get("r_scale", 0) + s_sc = s.get("s_scale", 0) + g = s.get("g_scale", 0) + + lines.append(f"HF: {assessment} -- SFI {sfi}, Kp {kp}") + lines.append(f" R{r}/S{s_sc}/G{g} scales") + + if assessment in ("Excellent", "Good"): + lines.append(" 10m-20m open, solid DX") + elif assessment == "Fair": + lines.append(" 20m-40m usable, upper bands marginal") + else: + lines.append(" Degraded -- lower bands only") + + warnings = s.get("active_warnings", []) + for w in warnings[:2]: + lines.append(f" Warning: {w[:100]}") + else: + lines.append("HF: Data not available") + + # UHF ducting section + d = self._env_store.get_ducting_status() + if d: + cond = d.get("condition", "unknown") + if cond == "normal": + lines.append("UHF: Normal propagation (906 MHz)") + else: + gradient = d.get("min_gradient", "?") + lines.append(f"UHF: {cond.replace('_', ' ').title()} (906 MHz)") + lines.append(f" dM/dz: {gradient} M-units/km") + lines.append(" Extended range -- expect distant nodes") + else: + lines.append("UHF: Ducting data not available") + + return "\n".join(lines) diff --git a/meshai/config.py b/meshai/config.py index a498718..4e2d714 100644 --- a/meshai/config.py +++ b/meshai/config.py @@ -1,418 +1,489 @@ -"""Configuration management for MeshAI.""" - -import logging -import os -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional - -import yaml - -_config_logger = logging.getLogger(__name__) - - -@dataclass -class BotConfig: - """Bot identity and trigger settings.""" - - name: str = "ai" - owner: str = "" - respond_to_dms: bool = True - filter_bbs_protocols: bool = True - - -@dataclass -class ConnectionConfig: - """Meshtastic connection settings.""" - - type: str = "serial" # serial or tcp - serial_port: str = "/dev/ttyUSB0" - tcp_host: str = "192.168.1.100" - tcp_port: int = 4403 - - -@dataclass -class ResponseConfig: - """Response behavior settings.""" - - delay_min: float = 1.5 - delay_max: float = 2.5 - max_length: int = 200 - max_messages: int = 3 - - -@dataclass -class HistoryConfig: - """Conversation history settings.""" - - database: str = "conversations.db" - max_messages_per_user: int = 50 - conversation_timeout: int = 86400 # 24 hours - - # Cleanup settings - auto_cleanup: bool = True - cleanup_interval_hours: int = 24 - max_age_days: int = 30 # Delete conversations older than this - - -@dataclass -class MemoryConfig: - """Rolling summary memory settings.""" - - enabled: bool = True # Enable memory optimization - window_size: int = 4 # Recent message pairs to keep in full - summarize_threshold: int = 8 # Messages before re-summarizing - - -@dataclass -class ContextConfig: - """Passive mesh context settings.""" - - enabled: bool = True - observe_channels: list[int] = field(default_factory=list) # Empty = all channels - ignore_nodes: list[str] = field(default_factory=list) # Node IDs to ignore - max_age: int = 2_592_000 # 30 days in seconds - max_context_items: int = 20 # Max observations injected into LLM context - - -@dataclass -class CommandsConfig: - """Command settings.""" - - enabled: bool = True - prefix: str = "!" - disabled_commands: list[str] = field(default_factory=list) - custom_commands: dict = field(default_factory=dict) - - -@dataclass -class LLMConfig: - """LLM backend settings.""" - - backend: str = "openai" # openai, anthropic, google - api_key: str = "" - base_url: str = "https://api.openai.com/v1" - model: str = "gpt-4o-mini" - timeout: int = 30 - max_response_tokens: int = 8192 # Let LLM generate full responses; chunker handles size - - system_prompt: str = ( - "RESPONSE RULES:\n" - "- For casual conversation, keep responses brief (1-2 sentences).\n" - "- For mesh health questions, give detailed data-driven responses.\n" - "- Be concise but friendly. No markdown formatting.\n" - "- If asked about mesh activity and no recent traffic is shown, say you haven't " - "observed any yet.\n" - "- When asked about yourself or commands, answer conversationally based on " - "the command list provided below. Don't dump lists unless asked.\n" - "- You are part of the freq51 mesh.\n" - "- When asked about yourself or commands, answer conversationally. Don't dump lists.\n" - "- You are part of the freq51 mesh in the Twin Falls, Idaho area.\n" - "- NEVER use markdown formatting (no bold, no asterisks, no bullet points, no numbered lists). Plain text only.\n" - "- NEVER say 'Want me to keep going?' -- the system handles continuation prompts automatically." - ) - use_system_prompt: bool = True # Toggle to disable sending system prompt - web_search: bool = False # Enable web search (Open WebUI feature) - google_grounding: bool = False # Enable Google Search grounding (Gemini only) - - -@dataclass -class OpenMeteoConfig: - """Open-Meteo weather provider settings.""" - - url: str = "https://api.open-meteo.com/v1" - - -@dataclass -class WttrConfig: - """wttr.in weather provider settings.""" - - url: str = "https://wttr.in" - - -@dataclass -class WeatherConfig: - """Weather command settings.""" - - primary: str = "openmeteo" # openmeteo, wttr, llm - fallback: str = "llm" # openmeteo, wttr, llm, none - default_location: str = "" - openmeteo: OpenMeteoConfig = field(default_factory=OpenMeteoConfig) - wttr: WttrConfig = field(default_factory=WttrConfig) - - -@dataclass -class MeshMonitorConfig: - """MeshMonitor trigger sync settings.""" - - enabled: bool = False - url: str = "" # e.g., http://100.64.0.11:3333 - inject_into_prompt: bool = True # Tell LLM about MeshMonitor commands - refresh_interval: int = 30 # Tick interval in seconds (default 30) - polite_mode: bool = False # Reduces polling frequency for shared instances # Seconds between refreshes - - -@dataclass -class KnowledgeConfig: - """Knowledge base settings.""" - - enabled: bool = False - backend: str = "auto" # "qdrant", "sqlite", or "auto" (try qdrant, fall back to sqlite) - - # Qdrant / RECON settings - qdrant_host: str = "" # e.g., "192.168.1.150" - qdrant_port: int = 6333 - qdrant_collection: str = "recon_knowledge_hybrid" - tei_host: str = "" # TEI embedding service host - tei_port: int = 8090 - sparse_host: str = "" # Sparse embedding service host - sparse_port: int = 8091 - use_sparse: bool = True # Enable hybrid dense+sparse search - - # SQLite fallback settings - db_path: str = "" - top_k: int = 5 - - -@dataclass -class MeshSourceConfig: - """Configuration for a mesh data source.""" - - name: str = "" - type: str = "" # "meshview" or "meshmonitor" - url: str = "" - api_token: str = "" # MeshMonitor only, supports ${ENV_VAR} - refresh_interval: int = 30 # Tick interval in seconds (default 30) - polite_mode: bool = False # Reduces polling frequency for shared instances - enabled: bool = True - - -@dataclass -class RegionAnchor: - """A fixed region anchor point with geographic context.""" - - name: str = "" - lat: float = 0.0 - lon: float = 0.0 - local_name: str = "" # e.g., "Magic Valley" - description: str = "" # e.g., "Twin Falls, Burley, Jerome along I-84/US-93" - aliases: list[str] = field(default_factory=list) # e.g., ["southern Idaho", "magic valley"] - cities: list[str] = field(default_factory=list) # e.g., ["Twin Falls", "Burley", "Jerome"] - - -@dataclass -class AlertRulesConfig: - """Per-condition alert toggles and thresholds.""" - - # Infrastructure - infra_offline: bool = True - infra_recovery: bool = True - new_router: bool = True - - # Power - battery_trend_declining: bool = True - battery_warning: bool = True - battery_critical: bool = True - battery_emergency: bool = True - battery_warning_threshold: int = 50 - battery_critical_threshold: int = 25 - battery_emergency_threshold: int = 10 - power_source_change: bool = True - solar_not_charging: bool = True - - # Utilization - sustained_high_util: bool = True - high_util_threshold: float = 20.0 - high_util_hours: int = 6 - packet_flood: bool = True - packet_flood_threshold: int = 500 - - # Coverage - infra_single_gateway: bool = True - feeder_offline: bool = True - region_total_blackout: bool = True - - # Health Scores - mesh_score_alert: bool = True - mesh_score_threshold: int = 70 - region_score_alert: bool = True - region_score_threshold: int = 60 - - -@dataclass -class MeshIntelligenceConfig: - """Mesh intelligence and health scoring settings.""" - - enabled: bool = False - regions: list[RegionAnchor] = field(default_factory=list) # Fixed region anchors - locality_radius_miles: float = 8.0 # Radius for locality clustering within regions - offline_threshold_hours: int = 24 # Hours before node considered offline - packet_threshold: int = 500 # Non-text packets per 24h to flag - battery_warning_percent: int = 20 # Battery level for warnings - - # Alert settings - critical_nodes: list[str] = field(default_factory=list) # Short names of critical nodes (e.g., ["MHR", "HPR"]) - alert_channel: int = -1 # Channel to broadcast alerts on. -1 = disabled, 0+ = channel index - alert_cooldown_minutes: int = 30 # Min minutes between repeated alerts for same condition - alert_rules: AlertRulesConfig = field(default_factory=AlertRulesConfig) - - - - -@dataclass -class DashboardConfig: - """Web dashboard settings.""" - - enabled: bool = True - port: int = 8080 - host: str = "0.0.0.0" - -@dataclass -class Config: - """Main configuration container.""" - - bot: BotConfig = field(default_factory=BotConfig) - connection: ConnectionConfig = field(default_factory=ConnectionConfig) - response: ResponseConfig = field(default_factory=ResponseConfig) - history: HistoryConfig = field(default_factory=HistoryConfig) - memory: MemoryConfig = field(default_factory=MemoryConfig) - context: ContextConfig = field(default_factory=ContextConfig) - commands: CommandsConfig = field(default_factory=CommandsConfig) - llm: LLMConfig = field(default_factory=LLMConfig) - weather: WeatherConfig = field(default_factory=WeatherConfig) - meshmonitor: MeshMonitorConfig = field(default_factory=MeshMonitorConfig) - knowledge: KnowledgeConfig = field(default_factory=KnowledgeConfig) - mesh_sources: list[MeshSourceConfig] = field(default_factory=list) - mesh_intelligence: MeshIntelligenceConfig = field(default_factory=MeshIntelligenceConfig) - dashboard: DashboardConfig = field(default_factory=DashboardConfig) - - _config_path: Optional[Path] = field(default=None, repr=False) - - def resolve_api_key(self) -> str: - """Resolve API key from config or environment.""" - if self.llm.api_key: - # Check if it's an env var reference like ${LLM_API_KEY} - if self.llm.api_key.startswith("${") and self.llm.api_key.endswith("}"): - env_var = self.llm.api_key[2:-1] - return os.environ.get(env_var, "") - return self.llm.api_key - # Fall back to common env vars - for env_var in ["LLM_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"]: - if value := os.environ.get(env_var): - return value - return "" - - -def _dict_to_dataclass(cls, data: dict): - """Recursively convert dict to dataclass, handling nested structures.""" - if data is None: - return cls() - - field_types = {f.name: f.type for f in cls.__dataclass_fields__.values()} - kwargs = {} - - for key, value in data.items(): - if key.startswith("_"): - continue - if key not in field_types: - continue - - field_type = field_types[key] - - # Handle nested dataclasses - if hasattr(field_type, "__dataclass_fields__") and isinstance(value, dict): - kwargs[key] = _dict_to_dataclass(field_type, value) - # Handle list of MeshSourceConfig - elif key == "mesh_sources" and isinstance(value, list): - kwargs[key] = [ - _dict_to_dataclass(MeshSourceConfig, item) - if isinstance(item, dict) else item - for item in value - ] - # Handle list of RegionAnchor - elif key == "regions" and isinstance(value, list): - kwargs[key] = [ - _dict_to_dataclass(RegionAnchor, item) - if isinstance(item, dict) else item - for item in value - ] - # Handle AlertRulesConfig - elif key == "alert_rules" and isinstance(value, dict): - kwargs[key] = _dict_to_dataclass(AlertRulesConfig, value) - else: - kwargs[key] = value - - return cls(**kwargs) - - -def _dataclass_to_dict(obj) -> dict: - """Recursively convert dataclass to dict for YAML serialization.""" - if not hasattr(obj, "__dataclass_fields__"): - return obj - - result = {} - for field_name in obj.__dataclass_fields__: - if field_name.startswith("_"): - continue - value = getattr(obj, field_name) - if hasattr(value, "__dataclass_fields__"): - result[field_name] = _dataclass_to_dict(value) - elif isinstance(value, list): - # Handle list of dataclasses (like mesh_sources) - result[field_name] = [ - _dataclass_to_dict(item) if hasattr(item, "__dataclass_fields__") else item - for item in value - ] - else: - result[field_name] = value - return result - - -def load_config(config_path: Optional[Path] = None) -> Config: - """Load configuration from YAML file. - - Args: - config_path: Path to config file. Defaults to ./config.yaml - - Returns: - Config object with loaded settings - """ - if config_path is None: - config_path = Path("config.yaml") - - config_path = Path(config_path) - - if not config_path.exists(): - # Return default config if file doesn't exist - config = Config() - config._config_path = config_path - return config - - with open(config_path, "r") as f: - data = yaml.safe_load(f) or {} - - config = _dict_to_dataclass(Config, data) - config._config_path = config_path - return config - - -def save_config(config: Config, config_path: Optional[Path] = None) -> None: - """Save configuration to YAML file. - - Args: - config: Config object to save - config_path: Path to save to. Uses config._config_path if not specified - """ - if config_path is None: - config_path = config._config_path or Path("config.yaml") - - config_path = Path(config_path) - - data = _dataclass_to_dict(config) - - # Add header comment - header = "# MeshAI Configuration\n# Generated by meshai --config\n\n" - - with open(config_path, "w") as f: - f.write(header) - yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True) +"""Configuration management for MeshAI.""" + +import logging +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +import yaml + +_config_logger = logging.getLogger(__name__) + + +@dataclass +class BotConfig: + """Bot identity and trigger settings.""" + + name: str = "ai" + owner: str = "" + respond_to_dms: bool = True + filter_bbs_protocols: bool = True + + +@dataclass +class ConnectionConfig: + """Meshtastic connection settings.""" + + type: str = "serial" # serial or tcp + serial_port: str = "/dev/ttyUSB0" + tcp_host: str = "192.168.1.100" + tcp_port: int = 4403 + + +@dataclass +class ResponseConfig: + """Response behavior settings.""" + + delay_min: float = 1.5 + delay_max: float = 2.5 + max_length: int = 200 + max_messages: int = 3 + + +@dataclass +class HistoryConfig: + """Conversation history settings.""" + + database: str = "conversations.db" + max_messages_per_user: int = 50 + conversation_timeout: int = 86400 # 24 hours + + # Cleanup settings + auto_cleanup: bool = True + cleanup_interval_hours: int = 24 + max_age_days: int = 30 # Delete conversations older than this + + +@dataclass +class MemoryConfig: + """Rolling summary memory settings.""" + + enabled: bool = True # Enable memory optimization + window_size: int = 4 # Recent message pairs to keep in full + summarize_threshold: int = 8 # Messages before re-summarizing + + +@dataclass +class ContextConfig: + """Passive mesh context settings.""" + + enabled: bool = True + observe_channels: list[int] = field(default_factory=list) # Empty = all channels + ignore_nodes: list[str] = field(default_factory=list) # Node IDs to ignore + max_age: int = 2_592_000 # 30 days in seconds + max_context_items: int = 20 # Max observations injected into LLM context + + +@dataclass +class CommandsConfig: + """Command settings.""" + + enabled: bool = True + prefix: str = "!" + disabled_commands: list[str] = field(default_factory=list) + custom_commands: dict = field(default_factory=dict) + + +@dataclass +class LLMConfig: + """LLM backend settings.""" + + backend: str = "openai" # openai, anthropic, google + api_key: str = "" + base_url: str = "https://api.openai.com/v1" + model: str = "gpt-4o-mini" + timeout: int = 30 + max_response_tokens: int = 8192 # Let LLM generate full responses; chunker handles size + + system_prompt: str = ( + "RESPONSE RULES:\n" + "- For casual conversation, keep responses brief (1-2 sentences).\n" + "- For mesh health questions, give detailed data-driven responses.\n" + "- Be concise but friendly. No markdown formatting.\n" + "- If asked about mesh activity and no recent traffic is shown, say you haven't " + "observed any yet.\n" + "- When asked about yourself or commands, answer conversationally based on " + "the command list provided below. Don't dump lists unless asked.\n" + "- You are part of the freq51 mesh.\n" + "- When asked about yourself or commands, answer conversationally. Don't dump lists.\n" + "- You are part of the freq51 mesh in the Twin Falls, Idaho area.\n" + "- NEVER use markdown formatting (no bold, no asterisks, no bullet points, no numbered lists). Plain text only.\n" + "- NEVER say 'Want me to keep going?' -- the system handles continuation prompts automatically." + ) + use_system_prompt: bool = True # Toggle to disable sending system prompt + web_search: bool = False # Enable web search (Open WebUI feature) + google_grounding: bool = False # Enable Google Search grounding (Gemini only) + + +@dataclass +class OpenMeteoConfig: + """Open-Meteo weather provider settings.""" + + url: str = "https://api.open-meteo.com/v1" + + +@dataclass +class WttrConfig: + """wttr.in weather provider settings.""" + + url: str = "https://wttr.in" + + +@dataclass +class WeatherConfig: + """Weather command settings.""" + + primary: str = "openmeteo" # openmeteo, wttr, llm + fallback: str = "llm" # openmeteo, wttr, llm, none + default_location: str = "" + openmeteo: OpenMeteoConfig = field(default_factory=OpenMeteoConfig) + wttr: WttrConfig = field(default_factory=WttrConfig) + + +@dataclass +class MeshMonitorConfig: + """MeshMonitor trigger sync settings.""" + + enabled: bool = False + url: str = "" # e.g., http://100.64.0.11:3333 + inject_into_prompt: bool = True # Tell LLM about MeshMonitor commands + refresh_interval: int = 30 # Tick interval in seconds (default 30) + polite_mode: bool = False # Reduces polling frequency for shared instances # Seconds between refreshes + + +@dataclass +class KnowledgeConfig: + """Knowledge base settings.""" + + enabled: bool = False + backend: str = "auto" # "qdrant", "sqlite", or "auto" (try qdrant, fall back to sqlite) + + # Qdrant / RECON settings + qdrant_host: str = "" # e.g., "192.168.1.150" + qdrant_port: int = 6333 + qdrant_collection: str = "recon_knowledge_hybrid" + tei_host: str = "" # TEI embedding service host + tei_port: int = 8090 + sparse_host: str = "" # Sparse embedding service host + sparse_port: int = 8091 + use_sparse: bool = True # Enable hybrid dense+sparse search + + # SQLite fallback settings + db_path: str = "" + top_k: int = 5 + + +@dataclass +class MeshSourceConfig: + """Configuration for a mesh data source.""" + + name: str = "" + type: str = "" # "meshview" or "meshmonitor" + url: str = "" + api_token: str = "" # MeshMonitor only, supports ${ENV_VAR} + refresh_interval: int = 30 # Tick interval in seconds (default 30) + polite_mode: bool = False # Reduces polling frequency for shared instances + enabled: bool = True + + +@dataclass +class RegionAnchor: + """A fixed region anchor point with geographic context.""" + + name: str = "" + lat: float = 0.0 + lon: float = 0.0 + local_name: str = "" # e.g., "Magic Valley" + description: str = "" # e.g., "Twin Falls, Burley, Jerome along I-84/US-93" + aliases: list[str] = field(default_factory=list) # e.g., ["southern Idaho", "magic valley"] + cities: list[str] = field(default_factory=list) # e.g., ["Twin Falls", "Burley", "Jerome"] + + +@dataclass +class AlertRulesConfig: + """Per-condition alert toggles and thresholds.""" + + # Infrastructure + infra_offline: bool = True + infra_recovery: bool = True + new_router: bool = True + + # Power + battery_trend_declining: bool = True + battery_warning: bool = True + battery_critical: bool = True + battery_emergency: bool = True + battery_warning_threshold: int = 50 + battery_critical_threshold: int = 25 + battery_emergency_threshold: int = 10 + power_source_change: bool = True + solar_not_charging: bool = True + + # Utilization + sustained_high_util: bool = True + high_util_threshold: float = 20.0 + high_util_hours: int = 6 + packet_flood: bool = True + packet_flood_threshold: int = 500 + + # Coverage + infra_single_gateway: bool = True + feeder_offline: bool = True + region_total_blackout: bool = True + + # Health Scores + mesh_score_alert: bool = True + mesh_score_threshold: int = 70 + region_score_alert: bool = True + region_score_threshold: int = 60 + + +@dataclass +class MeshIntelligenceConfig: + """Mesh intelligence and health scoring settings.""" + + enabled: bool = False + regions: list[RegionAnchor] = field(default_factory=list) # Fixed region anchors + locality_radius_miles: float = 8.0 # Radius for locality clustering within regions + offline_threshold_hours: int = 24 # Hours before node considered offline + packet_threshold: int = 500 # Non-text packets per 24h to flag + battery_warning_percent: int = 20 # Battery level for warnings + + # Alert settings + critical_nodes: list[str] = field(default_factory=list) # Short names of critical nodes (e.g., ["MHR", "HPR"]) + alert_channel: int = -1 # Channel to broadcast alerts on. -1 = disabled, 0+ = channel index + alert_cooldown_minutes: int = 30 # Min minutes between repeated alerts for same condition + alert_rules: AlertRulesConfig = field(default_factory=AlertRulesConfig) + + +# Environmental feed configs +@dataclass +class NWSConfig: + """NWS weather alerts settings.""" + + enabled: bool = True + tick_seconds: int = 60 + areas: list = field(default_factory=lambda: ["ID"]) + severity_min: str = "moderate" + user_agent: str = "" + + +@dataclass +class SWPCConfig: + """NOAA Space Weather settings.""" + + enabled: bool = True + + +@dataclass +class DuctingConfig: + """Tropospheric ducting settings.""" + + enabled: bool = True + tick_seconds: int = 10800 # 3 hours + latitude: float = 42.56 # Twin Falls area default + longitude: float = -114.47 + + +@dataclass +class NICFFiresConfig: + """NIFC fire perimeters settings (Phase 2).""" + + enabled: bool = False + tick_seconds: int = 600 + state: str = "US-ID" + + +@dataclass +class AvalancheConfig: + """Avalanche advisory settings (Phase 2).""" + + enabled: bool = False + tick_seconds: int = 1800 + center_ids: list = field(default_factory=lambda: ["SNFAC"]) + season_months: list = field(default_factory=lambda: [12, 1, 2, 3, 4]) + + +@dataclass +class EnvironmentalConfig: + """Environmental feeds settings.""" + + enabled: bool = False + nws_zones: list = field(default_factory=lambda: ["IDZ016", "IDZ030"]) + nws: NWSConfig = field(default_factory=NWSConfig) + swpc: SWPCConfig = field(default_factory=SWPCConfig) + ducting: DuctingConfig = field(default_factory=DuctingConfig) + fires: NICFFiresConfig = field(default_factory=NICFFiresConfig) + avalanche: AvalancheConfig = field(default_factory=AvalancheConfig) + + +@dataclass +class DashboardConfig: + """Web dashboard settings.""" + + enabled: bool = True + port: int = 8080 + host: str = "0.0.0.0" + +@dataclass +class Config: + """Main configuration container.""" + + bot: BotConfig = field(default_factory=BotConfig) + connection: ConnectionConfig = field(default_factory=ConnectionConfig) + response: ResponseConfig = field(default_factory=ResponseConfig) + history: HistoryConfig = field(default_factory=HistoryConfig) + memory: MemoryConfig = field(default_factory=MemoryConfig) + context: ContextConfig = field(default_factory=ContextConfig) + commands: CommandsConfig = field(default_factory=CommandsConfig) + llm: LLMConfig = field(default_factory=LLMConfig) + weather: WeatherConfig = field(default_factory=WeatherConfig) + meshmonitor: MeshMonitorConfig = field(default_factory=MeshMonitorConfig) + knowledge: KnowledgeConfig = field(default_factory=KnowledgeConfig) + mesh_sources: list[MeshSourceConfig] = field(default_factory=list) + mesh_intelligence: MeshIntelligenceConfig = field(default_factory=MeshIntelligenceConfig) + environmental: EnvironmentalConfig = field(default_factory=EnvironmentalConfig) + dashboard: DashboardConfig = field(default_factory=DashboardConfig) + + _config_path: Optional[Path] = field(default=None, repr=False) + + def resolve_api_key(self) -> str: + """Resolve API key from config or environment.""" + if self.llm.api_key: + # Check if it's an env var reference like ${LLM_API_KEY} + if self.llm.api_key.startswith("${") and self.llm.api_key.endswith("}"): + env_var = self.llm.api_key[2:-1] + return os.environ.get(env_var, "") + return self.llm.api_key + # Fall back to common env vars + for env_var in ["LLM_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"]: + if value := os.environ.get(env_var): + return value + return "" + + +def _dict_to_dataclass(cls, data: dict): + """Recursively convert dict to dataclass, handling nested structures.""" + if data is None: + return cls() + + field_types = {f.name: f.type for f in cls.__dataclass_fields__.values()} + kwargs = {} + + for key, value in data.items(): + if key.startswith("_"): + continue + if key not in field_types: + continue + + field_type = field_types[key] + + # Handle nested dataclasses + if hasattr(field_type, "__dataclass_fields__") and isinstance(value, dict): + kwargs[key] = _dict_to_dataclass(field_type, value) + # Handle list of MeshSourceConfig + elif key == "mesh_sources" and isinstance(value, list): + kwargs[key] = [ + _dict_to_dataclass(MeshSourceConfig, item) + if isinstance(item, dict) else item + for item in value + ] + # Handle list of RegionAnchor + elif key == "regions" and isinstance(value, list): + kwargs[key] = [ + _dict_to_dataclass(RegionAnchor, item) + if isinstance(item, dict) else item + for item in value + ] + # Handle AlertRulesConfig + elif key == "alert_rules" and isinstance(value, dict): + kwargs[key] = _dict_to_dataclass(AlertRulesConfig, value) + # Handle nested environmental configs + elif key == "nws" and isinstance(value, dict): + kwargs[key] = _dict_to_dataclass(NWSConfig, value) + elif key == "swpc" and isinstance(value, dict): + kwargs[key] = _dict_to_dataclass(SWPCConfig, value) + elif key == "ducting" and isinstance(value, dict): + kwargs[key] = _dict_to_dataclass(DuctingConfig, value) + elif key == "fires" and isinstance(value, dict): + kwargs[key] = _dict_to_dataclass(NICFFiresConfig, value) + elif key == "avalanche" and isinstance(value, dict): + kwargs[key] = _dict_to_dataclass(AvalancheConfig, value) + else: + kwargs[key] = value + + return cls(**kwargs) + + +def _dataclass_to_dict(obj) -> dict: + """Recursively convert dataclass to dict for YAML serialization.""" + if not hasattr(obj, "__dataclass_fields__"): + return obj + + result = {} + for field_name in obj.__dataclass_fields__: + if field_name.startswith("_"): + continue + value = getattr(obj, field_name) + if hasattr(value, "__dataclass_fields__"): + result[field_name] = _dataclass_to_dict(value) + elif isinstance(value, list): + # Handle list of dataclasses (like mesh_sources) + result[field_name] = [ + _dataclass_to_dict(item) if hasattr(item, "__dataclass_fields__") else item + for item in value + ] + else: + result[field_name] = value + return result + + +def load_config(config_path: Optional[Path] = None) -> Config: + """Load configuration from YAML file. + + Args: + config_path: Path to config file. Defaults to ./config.yaml + + Returns: + Config object with loaded settings + """ + if config_path is None: + config_path = Path("config.yaml") + + config_path = Path(config_path) + + if not config_path.exists(): + # Return default config if file doesn't exist + config = Config() + config._config_path = config_path + return config + + with open(config_path, "r") as f: + data = yaml.safe_load(f) or {} + + config = _dict_to_dataclass(Config, data) + config._config_path = config_path + return config + + +def save_config(config: Config, config_path: Optional[Path] = None) -> None: + """Save configuration to YAML file. + + Args: + config: Config object to save + config_path: Path to save to. Uses config._config_path if not specified + """ + if config_path is None: + config_path = config._config_path or Path("config.yaml") + + config_path = Path(config_path) + + data = _dataclass_to_dict(config) + + # Add header comment + header = "# MeshAI Configuration\n# Generated by meshai --config\n\n" + + with open(config_path, "w") as f: + f.write(header) + yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True) diff --git a/meshai/dashboard/api/env_routes.py b/meshai/dashboard/api/env_routes.py index acf9c6a..c354d21 100644 --- a/meshai/dashboard/api/env_routes.py +++ b/meshai/dashboard/api/env_routes.py @@ -1,4 +1,4 @@ -"""Environmental data API routes (Phase 1 placeholder).""" +"""Environmental data API routes.""" from fastapi import APIRouter, Request @@ -8,37 +8,70 @@ router = APIRouter(tags=["environment"]) @router.get("/env/status") async def get_env_status(request: Request): """Get environmental feeds status.""" - env_store = request.app.state.env_store + env_store = getattr(request.app.state, "env_store", None) if not env_store: return {"enabled": False, "feeds": []} - # Will be populated in Phase 1 when env_store exists return { "enabled": True, - "feeds": [], + "feeds": env_store.get_source_health(), } @router.get("/env/active") async def get_active_env(request: Request): - """Get active environmental conditions.""" - env_store = request.app.state.env_store + """Get active environmental events.""" + env_store = getattr(request.app.state, "env_store", None) if not env_store: return [] - # Will be populated in Phase 1 - return [] + return env_store.get_active() @router.get("/env/swpc") async def get_swpc_data(request: Request): """Get SWPC space weather data.""" - env_store = request.app.state.env_store + env_store = getattr(request.app.state, "env_store", None) if not env_store: return {"enabled": False} - # Will be populated in Phase 1 - return {"enabled": False} + status = env_store.get_swpc_status() + if not status: + return {"enabled": False} + + return { + "enabled": True, + **status, + } + + +@router.get("/env/propagation") +async def get_rf_propagation(request: Request): + """Get combined HF + UHF propagation data for dashboard.""" + env_store = getattr(request.app.state, "env_store", None) + + if not env_store: + return {"hf": {}, "uhf_ducting": {}} + + return env_store.get_rf_propagation() + + +@router.get("/env/ducting") +async def get_ducting_data(request: Request): + """Get tropospheric ducting assessment.""" + env_store = getattr(request.app.state, "env_store", None) + + if not env_store: + return {"enabled": False} + + status = env_store.get_ducting_status() + if not status: + return {"enabled": False} + + return { + "enabled": True, + **status, + } diff --git a/meshai/dashboard/static/assets/index-DnO02g6m.js b/meshai/dashboard/static/assets/index-CELmCk_K.js similarity index 50% rename from meshai/dashboard/static/assets/index-DnO02g6m.js rename to meshai/dashboard/static/assets/index-CELmCk_K.js index 88092d2..24431f0 100644 --- a/meshai/dashboard/static/assets/index-DnO02g6m.js +++ b/meshai/dashboard/static/assets/index-CELmCk_K.js @@ -1,4 +1,4 @@ -function tf(e,t){for(var n=0;nr[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function nf(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Cs={exports:{}},vl={},Ns={exports:{}},T={};/** +function tf(e,t){for(var n=0;nr[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();function nf(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Cs={exports:{}},vl={},Ns={exports:{}},T={};/** * @license React * react.production.min.js * @@ -6,7 +6,7 @@ function tf(e,t){for(var n=0;n>>1,J=_[Q];if(0>>1;Ql(Il,R))Stl(pr,Il)?(_[Q]=pr,_[St]=R,Q=St):(_[Q]=Il,_[wt]=R,Q=wt);else if(Stl(pr,R))_[Q]=pr,_[St]=R,Q=St;else break e}}return L}function l(_,L){var R=_.sortIndex-L.sortIndex;return R!==0?R:_.id-L.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],h=1,p=null,m=3,g=!1,x=!1,y=!1,E=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function d(_){for(var L=n(a);L!==null;){if(L.callback===null)r(a);else if(L.startTime<=_)r(a),L.sortIndex=L.expirationTime,t(s,L);else break;L=n(a)}}function w(_){if(y=!1,d(_),!x)if(n(s)!==null)x=!0,Ml(C);else{var L=n(a);L!==null&&Ol(w,L.startTime-_)}}function C(_,L){x=!1,y&&(y=!1,f(z),z=-1),g=!0;var R=m;try{for(d(L),p=n(s);p!==null&&(!(p.expirationTime>L)||_&&!ze());){var Q=p.callback;if(typeof Q=="function"){p.callback=null,m=p.priorityLevel;var J=Q(p.expirationTime<=L);L=e.unstable_now(),typeof J=="function"?p.callback=J:p===n(s)&&r(s),d(L)}else r(s);p=n(s)}if(p!==null)var dr=!0;else{var wt=n(a);wt!==null&&Ol(w,wt.startTime-L),dr=!1}return dr}finally{p=null,m=R,g=!1}}var P=!1,j=null,z=-1,H=5,M=-1;function ze(){return!(e.unstable_now()-M_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):H=0<_?Math.floor(1e3/_):5},e.unstable_getCurrentPriorityLevel=function(){return m},e.unstable_getFirstCallbackNode=function(){return n(s)},e.unstable_next=function(_){switch(m){case 1:case 2:case 3:var L=3;break;default:L=m}var R=m;m=L;try{return _()}finally{m=R}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(_,L){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var R=m;m=_;try{return L()}finally{m=R}},e.unstable_scheduleCallback=function(_,L,R){var Q=e.unstable_now();switch(typeof R=="object"&&R!==null?(R=R.delay,R=typeof R=="number"&&0Q?(_.sortIndex=R,t(a,_),n(s)===null&&_===n(a)&&(y?(f(z),z=-1):y=!0,Ol(w,R-Q))):(_.sortIndex=J,t(s,_),x||g||(x=!0,Ml(C))),_},e.unstable_shouldYield=ze,e.unstable_wrapCallback=function(_){var L=m;return function(){var R=m;m=L;try{return _.apply(this,arguments)}finally{m=R}}}})(Us);Fs.exports=Us;var _f=Fs.exports;/** + */(function(e){function t(_,L){var R=_.length;_.push(L);e:for(;0>>1,J=_[Q];if(0>>1;Ql(Il,R))Stl(pr,Il)?(_[Q]=pr,_[St]=R,Q=St):(_[Q]=Il,_[wt]=R,Q=wt);else if(Stl(pr,R))_[Q]=pr,_[St]=R,Q=St;else break e}}return L}function l(_,L){var R=_.sortIndex-L.sortIndex;return R!==0?R:_.id-L.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var o=Date,u=o.now();e.unstable_now=function(){return o.now()-u}}var s=[],a=[],h=1,p=null,m=3,g=!1,w=!1,x=!1,E=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function d(_){for(var L=n(a);L!==null;){if(L.callback===null)r(a);else if(L.startTime<=_)r(a),L.sortIndex=L.expirationTime,t(s,L);else break;L=n(a)}}function y(_){if(x=!1,d(_),!w)if(n(s)!==null)w=!0,Ml(C);else{var L=n(a);L!==null&&Ol(y,L.startTime-_)}}function C(_,L){w=!1,x&&(x=!1,f(z),z=-1),g=!0;var R=m;try{for(d(L),p=n(s);p!==null&&(!(p.expirationTime>L)||_&&!ze());){var Q=p.callback;if(typeof Q=="function"){p.callback=null,m=p.priorityLevel;var J=Q(p.expirationTime<=L);L=e.unstable_now(),typeof J=="function"?p.callback=J:p===n(s)&&r(s),d(L)}else r(s);p=n(s)}if(p!==null)var dr=!0;else{var wt=n(a);wt!==null&&Ol(y,wt.startTime-L),dr=!1}return dr}finally{p=null,m=R,g=!1}}var P=!1,j=null,z=-1,H=5,M=-1;function ze(){return!(e.unstable_now()-M_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):H=0<_?Math.floor(1e3/_):5},e.unstable_getCurrentPriorityLevel=function(){return m},e.unstable_getFirstCallbackNode=function(){return n(s)},e.unstable_next=function(_){switch(m){case 1:case 2:case 3:var L=3;break;default:L=m}var R=m;m=L;try{return _()}finally{m=R}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(_,L){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var R=m;m=_;try{return L()}finally{m=R}},e.unstable_scheduleCallback=function(_,L,R){var Q=e.unstable_now();switch(typeof R=="object"&&R!==null?(R=R.delay,R=typeof R=="number"&&0Q?(_.sortIndex=R,t(a,_),n(s)===null&&_===n(a)&&(x?(f(z),z=-1):x=!0,Ol(y,R-Q))):(_.sortIndex=J,t(s,_),w||g||(w=!0,Ml(C))),_},e.unstable_shouldYield=ze,e.unstable_wrapCallback=function(_){var L=m;return function(){var R=m;m=L;try{return _.apply(this,arguments)}finally{m=R}}}})(Us);Ds.exports=Us;var _f=Ds.exports;/** * @license React * react-dom.production.min.js * @@ -30,14 +30,14 @@ function tf(e,t){for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),co=Object.prototype.hasOwnProperty,jf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,su={},au={};function zf(e){return co.call(au,e)?!0:co.call(su,e)?!1:jf.test(e)?au[e]=!0:(su[e]=!0,!1)}function Lf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Rf(e,t,n,r){if(t===null||typeof t>"u"||Lf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ce(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var ne={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ne[e]=new ce(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ne[t]=new ce(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ne[e]=new ce(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ne[e]=new ce(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ne[e]=new ce(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ne[e]=new ce(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ne[e]=new ce(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ne[e]=new ce(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ne[e]=new ce(e,5,!1,e.toLowerCase(),null,!1,!1)});var ci=/[\-:]([a-z])/g;function fi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ci,fi);ne[t]=new ce(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ci,fi);ne[t]=new ce(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ci,fi);ne[t]=new ce(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ne[e]=new ce(e,1,!1,e.toLowerCase(),null,!1,!1)});ne.xlinkHref=new ce("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ne[e]=new ce(e,1,!1,e.toLowerCase(),null,!0,!0)});function di(e,t,n,r){var l=ne.hasOwnProperty(t)?ne[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var s=` -`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{Ul=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?_n(e):""}function Tf(e){switch(e.tag){case 5:return _n(e.type);case 16:return _n("Lazy");case 13:return _n("Suspense");case 19:return _n("SuspenseList");case 0:case 2:case 15:return e=$l(e.type,!1),e;case 11:return e=$l(e.type.render,!1),e;case 1:return e=$l(e.type,!0),e;default:return""}}function mo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Bt:return"Fragment";case At:return"Portal";case fo:return"Profiler";case pi:return"StrictMode";case po:return"Suspense";case ho:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Bs:return(e.displayName||"Context")+".Consumer";case As:return(e._context.displayName||"Context")+".Provider";case hi:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case mi:return t=e.displayName||null,t!==null?t:mo(e.type)||"Memo";case be:t=e._payload,e=e._init;try{return mo(e(t))}catch{}}return null}function Mf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return mo(t);case 8:return t===pi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function mt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Vs(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Of(e){var t=Vs(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function vr(e){e._valueTracker||(e._valueTracker=Of(e))}function Hs(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Vs(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Qr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function vo(e,t){var n=t.checked;return W({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function fu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=mt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Qs(e,t){t=t.checked,t!=null&&di(e,"checked",t,!1)}function go(e,t){Qs(e,t);var n=mt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?yo(e,t.type,n):t.hasOwnProperty("defaultValue")&&yo(e,t.type,mt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function du(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function yo(e,t,n){(t!=="number"||Qr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Pn=Array.isArray;function qt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=gr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function An(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Ln={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},If=["Webkit","ms","Moz","O"];Object.keys(Ln).forEach(function(e){If.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ln[t]=Ln[e]})});function Gs(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Ln.hasOwnProperty(e)&&Ln[e]?(""+t).trim():t+"px"}function Zs(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Gs(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Df=W({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function So(e,t){if(t){if(Df[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(S(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(S(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(S(61))}if(t.style!=null&&typeof t.style!="object")throw Error(S(62))}}function ko(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Eo=null;function vi(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Co=null,bt=null,en=null;function mu(e){if(e=ur(e)){if(typeof Co!="function")throw Error(S(280));var t=e.stateNode;t&&(t=Sl(t),Co(e.stateNode,e.type,t))}}function Js(e){bt?en?en.push(e):en=[e]:bt=e}function qs(){if(bt){var e=bt,t=en;if(en=bt=null,mu(e),t)for(e=0;e>>=0,e===0?32:31-(Yf(e)/Xf|0)|0}var yr=64,xr=4194304;function jn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Gr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=jn(u):(o&=i,o!==0&&(r=jn(o)))}else i=n&~l,i!==0?r=jn(i):o!==0&&(r=jn(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function or(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Oe(t),e[t]=n}function qf(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Tn),Cu=" ",Nu=!1;function ya(e,t){switch(e){case"keyup":return _d.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function xa(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Wt=!1;function jd(e,t){switch(e){case"compositionend":return xa(t);case"keypress":return t.which!==32?null:(Nu=!0,Cu);case"textInput":return e=t.data,e===Cu&&Nu?null:e;default:return null}}function zd(e,t){if(Wt)return e==="compositionend"||!Ci&&ya(e,t)?(e=va(),Dr=Si=rt=null,Wt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=zu(n)}}function Ea(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ea(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ca(){for(var e=window,t=Qr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Qr(e.document)}return t}function Ni(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Ud(e){var t=Ca(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Ea(n.ownerDocument.documentElement,n)){if(r!==null&&Ni(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Lu(n,o);var i=Lu(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Vt=null,Lo=null,On=null,Ro=!1;function Ru(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ro||Vt==null||Vt!==Qr(r)||(r=Vt,"selectionStart"in r&&Ni(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),On&&Kn(On,r)||(On=r,r=qr(Lo,"onSelect"),0Kt||(e.current=Fo[Kt],Fo[Kt]=null,Kt--)}function D(e,t){Kt++,Fo[Kt]=e.current,e.current=t}var vt={},ie=yt(vt),pe=yt(!1),zt=vt;function on(e,t){var n=e.type.contextTypes;if(!n)return vt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function he(e){return e=e.childContextTypes,e!=null}function el(){U(pe),U(ie)}function Uu(e,t,n){if(ie.current!==vt)throw Error(S(168));D(ie,t),D(pe,n)}function Ma(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(S(108,Mf(e)||"Unknown",l));return W({},n,r)}function tl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||vt,zt=ie.current,D(ie,e),D(pe,pe.current),!0}function $u(e,t,n){var r=e.stateNode;if(!r)throw Error(S(169));n?(e=Ma(e,t,zt),r.__reactInternalMemoizedMergedChildContext=e,U(pe),U(ie),D(ie,e)):U(pe),D(pe,n)}var Ve=null,kl=!1,bl=!1;function Oa(e){Ve===null?Ve=[e]:Ve.push(e)}function Zd(e){kl=!0,Oa(e)}function xt(){if(!bl&&Ve!==null){bl=!0;var e=0,t=I;try{var n=Ve;for(I=1;e>=i,l-=i,He=1<<32-Oe(t)+l|n<z?(H=j,j=null):H=j.sibling;var M=m(f,j,d[z],w);if(M===null){j===null&&(j=H);break}e&&j&&M.alternate===null&&t(f,j),c=o(M,c,z),P===null?C=M:P.sibling=M,P=M,j=H}if(z===d.length)return n(f,j),$&&kt(f,z),C;if(j===null){for(;zz?(H=j,j=null):H=j.sibling;var ze=m(f,j,M.value,w);if(ze===null){j===null&&(j=H);break}e&&j&&ze.alternate===null&&t(f,j),c=o(ze,c,z),P===null?C=ze:P.sibling=ze,P=ze,j=H}if(M.done)return n(f,j),$&&kt(f,z),C;if(j===null){for(;!M.done;z++,M=d.next())M=p(f,M.value,w),M!==null&&(c=o(M,c,z),P===null?C=M:P.sibling=M,P=M);return $&&kt(f,z),C}for(j=r(f,j);!M.done;z++,M=d.next())M=g(j,f,z,M.value,w),M!==null&&(e&&M.alternate!==null&&j.delete(M.key===null?z:M.key),c=o(M,c,z),P===null?C=M:P.sibling=M,P=M);return e&&j.forEach(function(vn){return t(f,vn)}),$&&kt(f,z),C}function E(f,c,d,w){if(typeof d=="object"&&d!==null&&d.type===Bt&&d.key===null&&(d=d.props.children),typeof d=="object"&&d!==null){switch(d.$$typeof){case mr:e:{for(var C=d.key,P=c;P!==null;){if(P.key===C){if(C=d.type,C===Bt){if(P.tag===7){n(f,P.sibling),c=l(P,d.props.children),c.return=f,f=c;break e}}else if(P.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===be&&Wu(C)===P.type){n(f,P.sibling),c=l(P,d.props),c.ref=En(f,P,d),c.return=f,f=c;break e}n(f,P);break}else t(f,P);P=P.sibling}d.type===Bt?(c=jt(d.props.children,f.mode,w,d.key),c.return=f,f=c):(w=Hr(d.type,d.key,d.props,null,f.mode,w),w.ref=En(f,c,d),w.return=f,f=w)}return i(f);case At:e:{for(P=d.key;c!==null;){if(c.key===P)if(c.tag===4&&c.stateNode.containerInfo===d.containerInfo&&c.stateNode.implementation===d.implementation){n(f,c.sibling),c=l(c,d.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=uo(d,f.mode,w),c.return=f,f=c}return i(f);case be:return P=d._init,E(f,c,P(d._payload),w)}if(Pn(d))return x(f,c,d,w);if(yn(d))return y(f,c,d,w);_r(f,d)}return typeof d=="string"&&d!==""||typeof d=="number"?(d=""+d,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,d),c.return=f,f=c):(n(f,c),c=io(d,f.mode,w),c.return=f,f=c),i(f)):n(f,c)}return E}var sn=Ua(!0),$a=Ua(!1),ll=yt(null),ol=null,Gt=null,zi=null;function Li(){zi=Gt=ol=null}function Ri(e){var t=ll.current;U(ll),e._currentValue=t}function Ao(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function nn(e,t){ol=e,zi=Gt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(de=!0),e.firstContext=null)}function _e(e){var t=e._currentValue;if(zi!==e)if(e={context:e,memoizedValue:t,next:null},Gt===null){if(ol===null)throw Error(S(308));Gt=e,ol.dependencies={lanes:0,firstContext:e}}else Gt=Gt.next=e;return t}var Nt=null;function Ti(e){Nt===null?Nt=[e]:Nt.push(e)}function Aa(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Ti(t)):(n.next=l.next,l.next=n),t.interleaved=n,Ge(e,r)}function Ge(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var et=!1;function Mi(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ba(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ke(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function ct(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,O&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Ge(e,n)}return l=r.interleaved,l===null?(t.next=t,Ti(r)):(t.next=l.next,l.next=t),r.interleaved=t,Ge(e,n)}function Ur(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,yi(e,n)}}function Vu(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=i:o=o.next=i,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function il(e,t,n,r){var l=e.updateQueue;et=!1;var o=l.firstBaseUpdate,i=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var s=u,a=s.next;s.next=null,i===null?o=a:i.next=a,i=s;var h=e.alternate;h!==null&&(h=h.updateQueue,u=h.lastBaseUpdate,u!==i&&(u===null?h.firstBaseUpdate=a:u.next=a,h.lastBaseUpdate=s))}if(o!==null){var p=l.baseState;i=0,h=a=s=null,u=o;do{var m=u.lane,g=u.eventTime;if((r&m)===m){h!==null&&(h=h.next={eventTime:g,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var x=e,y=u;switch(m=t,g=n,y.tag){case 1:if(x=y.payload,typeof x=="function"){p=x.call(g,p,m);break e}p=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=y.payload,m=typeof x=="function"?x.call(g,p,m):x,m==null)break e;p=W({},p,m);break e;case 2:et=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,m=l.effects,m===null?l.effects=[u]:m.push(u))}else g={eventTime:g,lane:m,tag:u.tag,payload:u.payload,callback:u.callback,next:null},h===null?(a=h=g,s=p):h=h.next=g,i|=m;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;m=u,u=m.next,m.next=null,l.lastBaseUpdate=m,l.shared.pending=null}}while(!0);if(h===null&&(s=p),l.baseState=s,l.firstBaseUpdate=a,l.lastBaseUpdate=h,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);Tt|=i,e.lanes=i,e.memoizedState=p}}function Hu(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=to.transition;to.transition={};try{e(!1),t()}finally{I=n,to.transition=r}}function lc(){return Pe().memoizedState}function ep(e,t,n){var r=dt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},oc(e))ic(t,n);else if(n=Aa(e,t,n,r),n!==null){var l=se();Ie(n,e,r,l),uc(n,t,r)}}function tp(e,t,n){var r=dt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(oc(e))ic(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,De(u,i)){var s=t.interleaved;s===null?(l.next=l,Ti(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Aa(e,t,l,r),n!==null&&(l=se(),Ie(n,e,r,l),uc(n,t,r))}}function oc(e){var t=e.alternate;return e===B||t!==null&&t===B}function ic(e,t){In=sl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function uc(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,yi(e,n)}}var al={readContext:_e,useCallback:re,useContext:re,useEffect:re,useImperativeHandle:re,useInsertionEffect:re,useLayoutEffect:re,useMemo:re,useReducer:re,useRef:re,useState:re,useDebugValue:re,useDeferredValue:re,useTransition:re,useMutableSource:re,useSyncExternalStore:re,useId:re,unstable_isNewReconciler:!1},np={readContext:_e,useCallback:function(e,t){return Ue().memoizedState=[e,t===void 0?null:t],e},useContext:_e,useEffect:Ku,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Ar(4194308,4,ba.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ar(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ar(4,2,e,t)},useMemo:function(e,t){var n=Ue();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ue();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=ep.bind(null,B,e),[r.memoizedState,e]},useRef:function(e){var t=Ue();return e={current:e},t.memoizedState=e},useState:Qu,useDebugValue:Bi,useDeferredValue:function(e){return Ue().memoizedState=e},useTransition:function(){var e=Qu(!1),t=e[0];return e=bd.bind(null,e[1]),Ue().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=B,l=Ue();if($){if(n===void 0)throw Error(S(407));n=n()}else{if(n=t(),b===null)throw Error(S(349));Rt&30||Qa(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Ku(Ya.bind(null,r,o,e),[e]),r.flags|=2048,er(9,Ka.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ue(),t=b.identifierPrefix;if($){var n=Qe,r=He;n=(r&~(1<<32-Oe(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=qn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[$e]=t,e[Gn]=r,gc(e,t,!1,!1),t.stateNode=e;e:{switch(i=ko(n,r),n){case"dialog":F("cancel",e),F("close",e),l=r;break;case"iframe":case"object":case"embed":F("load",e),l=r;break;case"video":case"audio":for(l=0;lfn&&(t.flags|=128,r=!0,Cn(o,!1),t.lanes=4194304)}else{if(!r)if(e=ul(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Cn(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!$)return le(t),null}else 2*K()-o.renderingStartTime>fn&&n!==1073741824&&(t.flags|=128,r=!0,Cn(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=K(),t.sibling=null,n=A.current,D(A,r?n&1|2:n&1),t):(le(t),null);case 22:case 23:return Yi(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ve&1073741824&&(le(t),t.subtreeFlags&6&&(t.flags|=8192)):le(t),null;case 24:return null;case 25:return null}throw Error(S(156,t.tag))}function cp(e,t){switch(Pi(t),t.tag){case 1:return he(t.type)&&el(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return an(),U(pe),U(ie),Di(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Ii(t),null;case 13:if(U(A),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(S(340));un()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return U(A),null;case 4:return an(),null;case 10:return Ri(t.type._context),null;case 22:case 23:return Yi(),null;case 24:return null;default:return null}}var jr=!1,oe=!1,fp=typeof WeakSet=="function"?WeakSet:Set,N=null;function Zt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){V(e,t,r)}else n.current=null}function Go(e,t,n){try{n()}catch(r){V(e,t,r)}}var rs=!1;function dp(e,t){if(To=Zr,e=Ca(),Ni(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,h=0,p=e,m=null;t:for(;;){for(var g;p!==n||l!==0&&p.nodeType!==3||(u=i+l),p!==o||r!==0&&p.nodeType!==3||(s=i+r),p.nodeType===3&&(i+=p.nodeValue.length),(g=p.firstChild)!==null;)m=p,p=g;for(;;){if(p===e)break t;if(m===n&&++a===l&&(u=i),m===o&&++h===r&&(s=i),(g=p.nextSibling)!==null)break;p=m,m=p.parentNode}p=g}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(Mo={focusedElem:e,selectionRange:n},Zr=!1,N=t;N!==null;)if(t=N,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,N=e;else for(;N!==null;){t=N;try{var x=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var y=x.memoizedProps,E=x.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?y:Re(t.type,y),E);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var d=t.stateNode.containerInfo;d.nodeType===1?d.textContent="":d.nodeType===9&&d.documentElement&&d.removeChild(d.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(S(163))}}catch(w){V(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,N=e;break}N=t.return}return x=rs,rs=!1,x}function Dn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Go(t,n,o)}l=l.next}while(l!==r)}}function Nl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Zo(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function wc(e){var t=e.alternate;t!==null&&(e.alternate=null,wc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[$e],delete t[Gn],delete t[Do],delete t[Xd],delete t[Gd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Sc(e){return e.tag===5||e.tag===3||e.tag===4}function ls(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Sc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Jo(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=br));else if(r!==4&&(e=e.child,e!==null))for(Jo(e,t,n),e=e.sibling;e!==null;)Jo(e,t,n),e=e.sibling}function qo(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(qo(e,t,n),e=e.sibling;e!==null;)qo(e,t,n),e=e.sibling}var ee=null,Te=!1;function qe(e,t,n){for(n=n.child;n!==null;)kc(e,t,n),n=n.sibling}function kc(e,t,n){if(Ae&&typeof Ae.onCommitFiberUnmount=="function")try{Ae.onCommitFiberUnmount(gl,n)}catch{}switch(n.tag){case 5:oe||Zt(n,t);case 6:var r=ee,l=Te;ee=null,qe(e,t,n),ee=r,Te=l,ee!==null&&(Te?(e=ee,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ee.removeChild(n.stateNode));break;case 18:ee!==null&&(Te?(e=ee,n=n.stateNode,e.nodeType===8?ql(e.parentNode,n):e.nodeType===1&&ql(e,n),Hn(e)):ql(ee,n.stateNode));break;case 4:r=ee,l=Te,ee=n.stateNode.containerInfo,Te=!0,qe(e,t,n),ee=r,Te=l;break;case 0:case 11:case 14:case 15:if(!oe&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Go(n,t,i),l=l.next}while(l!==r)}qe(e,t,n);break;case 1:if(!oe&&(Zt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){V(n,t,u)}qe(e,t,n);break;case 21:qe(e,t,n);break;case 22:n.mode&1?(oe=(r=oe)||n.memoizedState!==null,qe(e,t,n),oe=r):qe(e,t,n);break;default:qe(e,t,n)}}function os(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new fp),t.forEach(function(r){var l=Sp.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Le(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=K()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*hp(r/1960))-r,10e?16:e,lt===null)var r=!1;else{if(e=lt,lt=null,dl=0,O&6)throw Error(S(331));var l=O;for(O|=4,N=e.current;N!==null;){var o=N,i=o.child;if(N.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sK()-Qi?Pt(e,0):Hi|=n),me(e,t)}function Lc(e,t){t===0&&(e.mode&1?(t=xr,xr<<=1,!(xr&130023424)&&(xr=4194304)):t=1);var n=se();e=Ge(e,t),e!==null&&(or(e,t,n),me(e,n))}function wp(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Lc(e,n)}function Sp(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(S(314))}r!==null&&r.delete(t),Lc(e,n)}var Rc;Rc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||pe.current)de=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return de=!1,sp(e,t,n);de=!!(e.flags&131072)}else de=!1,$&&t.flags&1048576&&Ia(t,rl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Br(e,t),e=t.pendingProps;var l=on(t,ie.current);nn(t,n),l=Ui(null,t,r,e,l,n);var o=$i();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,he(r)?(o=!0,tl(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Mi(t),l.updater=Cl,t.stateNode=l,l._reactInternals=t,Wo(t,r,e,n),t=Qo(null,t,r,!0,o,n)):(t.tag=0,$&&o&&_i(t),ue(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Br(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Ep(r),e=Re(r,e),l){case 0:t=Ho(null,t,r,e,n);break e;case 1:t=es(null,t,r,e,n);break e;case 11:t=qu(null,t,r,e,n);break e;case 14:t=bu(null,t,r,Re(r.type,e),n);break e}throw Error(S(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Ho(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),es(e,t,r,l,n);case 3:e:{if(hc(t),e===null)throw Error(S(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Ba(e,t),il(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=cn(Error(S(423)),t),t=ts(e,t,r,n,l);break e}else if(r!==l){l=cn(Error(S(424)),t),t=ts(e,t,r,n,l);break e}else for(ge=at(t.stateNode.containerInfo.firstChild),ye=t,$=!0,Me=null,n=$a(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(un(),r===l){t=Ze(e,t,n);break e}ue(e,t,r,n)}t=t.child}return t;case 5:return Wa(t),e===null&&$o(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,Oo(r,l)?i=null:o!==null&&Oo(r,o)&&(t.flags|=32),pc(e,t),ue(e,t,i,n),t.child;case 6:return e===null&&$o(t),null;case 13:return mc(e,t,n);case 4:return Oi(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=sn(t,null,r,n):ue(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),qu(e,t,r,l,n);case 7:return ue(e,t,t.pendingProps,n),t.child;case 8:return ue(e,t,t.pendingProps.children,n),t.child;case 12:return ue(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,D(ll,r._currentValue),r._currentValue=i,o!==null)if(De(o.value,i)){if(o.children===l.children&&!pe.current){t=Ze(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=Ke(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var h=a.pending;h===null?s.next=s:(s.next=h.next,h.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),Ao(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(S(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Ao(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}ue(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,nn(t,n),l=_e(l),r=r(l),t.flags|=1,ue(e,t,r,n),t.child;case 14:return r=t.type,l=Re(r,t.pendingProps),l=Re(r.type,l),bu(e,t,r,l,n);case 15:return fc(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Br(e,t),t.tag=1,he(r)?(e=!0,tl(t)):e=!1,nn(t,n),sc(t,r,l),Wo(t,r,l,n),Qo(null,t,r,!0,e,n);case 19:return vc(e,t,n);case 22:return dc(e,t,n)}throw Error(S(156,t.tag))};function Tc(e,t){return oa(e,t)}function kp(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ce(e,t,n,r){return new kp(e,t,n,r)}function Gi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Ep(e){if(typeof e=="function")return Gi(e)?1:0;if(e!=null){if(e=e.$$typeof,e===hi)return 11;if(e===mi)return 14}return 2}function pt(e,t){var n=e.alternate;return n===null?(n=Ce(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Hr(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Gi(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case Bt:return jt(n.children,l,o,t);case pi:i=8,l|=8;break;case fo:return e=Ce(12,n,t,l|2),e.elementType=fo,e.lanes=o,e;case po:return e=Ce(13,n,t,l),e.elementType=po,e.lanes=o,e;case ho:return e=Ce(19,n,t,l),e.elementType=ho,e.lanes=o,e;case Ws:return Pl(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case As:i=10;break e;case Bs:i=9;break e;case hi:i=11;break e;case mi:i=14;break e;case be:i=16,r=null;break e}throw Error(S(130,e==null?e:typeof e,""))}return t=Ce(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function jt(e,t,n,r){return e=Ce(7,e,r,t),e.lanes=n,e}function Pl(e,t,n,r){return e=Ce(22,e,r,t),e.elementType=Ws,e.lanes=n,e.stateNode={isHidden:!1},e}function io(e,t,n){return e=Ce(6,e,null,t),e.lanes=n,e}function uo(e,t,n){return t=Ce(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Cp(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Bl(0),this.expirationTimes=Bl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Bl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Zi(e,t,n,r,l,o,i,u,s){return e=new Cp(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Ce(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Mi(o),e}function Np(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Dc)}catch(e){console.error(e)}}Dc(),Ds.exports=we;var Lp=Ds.exports,ps=Lp;ao.createRoot=ps.createRoot,ao.hydrateRoot=ps.hydrateRoot;/** + */var Pf=k,xe=_f;function S(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ci=Object.prototype.hasOwnProperty,jf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,su={},au={};function zf(e){return ci.call(au,e)?!0:ci.call(su,e)?!1:jf.test(e)?au[e]=!0:(su[e]=!0,!1)}function Lf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Rf(e,t,n,r){if(t===null||typeof t>"u"||Lf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ce(e,t,n,r,l,i,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var ne={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ne[e]=new ce(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ne[t]=new ce(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ne[e]=new ce(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ne[e]=new ce(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ne[e]=new ce(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ne[e]=new ce(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ne[e]=new ce(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ne[e]=new ce(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ne[e]=new ce(e,5,!1,e.toLowerCase(),null,!1,!1)});var ao=/[\-:]([a-z])/g;function co(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ao,co);ne[t]=new ce(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ao,co);ne[t]=new ce(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ao,co);ne[t]=new ce(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ne[e]=new ce(e,1,!1,e.toLowerCase(),null,!1,!1)});ne.xlinkHref=new ce("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ne[e]=new ce(e,1,!1,e.toLowerCase(),null,!0,!0)});function fo(e,t,n,r){var l=ne.hasOwnProperty(t)?ne[t]:null;(l!==null?l.type!==0:r||!(2u||l[o]!==i[u]){var s=` +`+l[o].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=o&&0<=u);break}}}finally{Ul=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Pn(e):""}function Tf(e){switch(e.tag){case 5:return Pn(e.type);case 16:return Pn("Lazy");case 13:return Pn("Suspense");case 19:return Pn("SuspenseList");case 0:case 2:case 15:return e=$l(e.type,!1),e;case 11:return e=$l(e.type.render,!1),e;case 1:return e=$l(e.type,!0),e;default:return""}}function hi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Bt:return"Fragment";case At:return"Portal";case fi:return"Profiler";case po:return"StrictMode";case di:return"Suspense";case pi:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Bs:return(e.displayName||"Context")+".Consumer";case As:return(e._context.displayName||"Context")+".Provider";case ho:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case mo:return t=e.displayName||null,t!==null?t:hi(e.type)||"Memo";case be:t=e._payload,e=e._init;try{return hi(e(t))}catch{}}return null}function Mf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return hi(t);case 8:return t===po?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function mt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Vs(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Of(e){var t=Vs(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(o){r=""+o,i.call(this,o)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(o){r=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function vr(e){e._valueTracker||(e._valueTracker=Of(e))}function Hs(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Vs(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Qr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function mi(e,t){var n=t.checked;return W({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function fu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=mt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Qs(e,t){t=t.checked,t!=null&&fo(e,"checked",t,!1)}function vi(e,t){Qs(e,t);var n=mt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?gi(e,t.type,n):t.hasOwnProperty("defaultValue")&&gi(e,t.type,mt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function du(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function gi(e,t,n){(t!=="number"||Qr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var jn=Array.isArray;function qt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=gr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Bn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Rn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},If=["Webkit","ms","Moz","O"];Object.keys(Rn).forEach(function(e){If.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Rn[t]=Rn[e]})});function Gs(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Rn.hasOwnProperty(e)&&Rn[e]?(""+t).trim():t+"px"}function Zs(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Gs(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Ff=W({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function wi(e,t){if(t){if(Ff[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(S(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(S(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(S(61))}if(t.style!=null&&typeof t.style!="object")throw Error(S(62))}}function Si(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ki=null;function vo(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Ei=null,bt=null,en=null;function mu(e){if(e=sr(e)){if(typeof Ei!="function")throw Error(S(280));var t=e.stateNode;t&&(t=Sl(t),Ei(e.stateNode,e.type,t))}}function Js(e){bt?en?en.push(e):en=[e]:bt=e}function qs(){if(bt){var e=bt,t=en;if(en=bt=null,mu(e),t)for(e=0;e>>=0,e===0?32:31-(Yf(e)/Xf|0)|0}var yr=64,xr=4194304;function zn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Gr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,o=n&268435455;if(o!==0){var u=o&~l;u!==0?r=zn(u):(i&=o,i!==0&&(r=zn(i)))}else o=n&~l,o!==0?r=zn(o):i!==0&&(r=zn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function or(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Oe(t),e[t]=n}function qf(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Mn),Cu=" ",Nu=!1;function ya(e,t){switch(e){case"keyup":return _d.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function xa(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Wt=!1;function jd(e,t){switch(e){case"compositionend":return xa(t);case"keypress":return t.which!==32?null:(Nu=!0,Cu);case"textInput":return e=t.data,e===Cu&&Nu?null:e;default:return null}}function zd(e,t){if(Wt)return e==="compositionend"||!Co&&ya(e,t)?(e=va(),Fr=So=rt=null,Wt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=zu(n)}}function Ea(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ea(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ca(){for(var e=window,t=Qr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Qr(e.document)}return t}function No(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Ud(e){var t=Ca(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Ea(n.ownerDocument.documentElement,n)){if(r!==null&&No(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=Lu(n,i);var o=Lu(n,r);l&&o&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Vt=null,zi=null,In=null,Li=!1;function Ru(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Li||Vt==null||Vt!==Qr(r)||(r=Vt,"selectionStart"in r&&No(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),In&&Yn(In,r)||(In=r,r=qr(zi,"onSelect"),0Kt||(e.current=Fi[Kt],Fi[Kt]=null,Kt--)}function F(e,t){Kt++,Fi[Kt]=e.current,e.current=t}var vt={},oe=yt(vt),pe=yt(!1),zt=vt;function on(e,t){var n=e.type.contextTypes;if(!n)return vt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function he(e){return e=e.childContextTypes,e!=null}function el(){U(pe),U(oe)}function Uu(e,t,n){if(oe.current!==vt)throw Error(S(168));F(oe,t),F(pe,n)}function Ma(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(S(108,Mf(e)||"Unknown",l));return W({},n,r)}function tl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||vt,zt=oe.current,F(oe,e),F(pe,pe.current),!0}function $u(e,t,n){var r=e.stateNode;if(!r)throw Error(S(169));n?(e=Ma(e,t,zt),r.__reactInternalMemoizedMergedChildContext=e,U(pe),U(oe),F(oe,e)):U(pe),F(pe,n)}var Ve=null,kl=!1,bl=!1;function Oa(e){Ve===null?Ve=[e]:Ve.push(e)}function Zd(e){kl=!0,Oa(e)}function xt(){if(!bl&&Ve!==null){bl=!0;var e=0,t=I;try{var n=Ve;for(I=1;e>=o,l-=o,He=1<<32-Oe(t)+l|n<z?(H=j,j=null):H=j.sibling;var M=m(f,j,d[z],y);if(M===null){j===null&&(j=H);break}e&&j&&M.alternate===null&&t(f,j),c=i(M,c,z),P===null?C=M:P.sibling=M,P=M,j=H}if(z===d.length)return n(f,j),$&&kt(f,z),C;if(j===null){for(;zz?(H=j,j=null):H=j.sibling;var ze=m(f,j,M.value,y);if(ze===null){j===null&&(j=H);break}e&&j&&ze.alternate===null&&t(f,j),c=i(ze,c,z),P===null?C=ze:P.sibling=ze,P=ze,j=H}if(M.done)return n(f,j),$&&kt(f,z),C;if(j===null){for(;!M.done;z++,M=d.next())M=p(f,M.value,y),M!==null&&(c=i(M,c,z),P===null?C=M:P.sibling=M,P=M);return $&&kt(f,z),C}for(j=r(f,j);!M.done;z++,M=d.next())M=g(j,f,z,M.value,y),M!==null&&(e&&M.alternate!==null&&j.delete(M.key===null?z:M.key),c=i(M,c,z),P===null?C=M:P.sibling=M,P=M);return e&&j.forEach(function(gn){return t(f,gn)}),$&&kt(f,z),C}function E(f,c,d,y){if(typeof d=="object"&&d!==null&&d.type===Bt&&d.key===null&&(d=d.props.children),typeof d=="object"&&d!==null){switch(d.$$typeof){case mr:e:{for(var C=d.key,P=c;P!==null;){if(P.key===C){if(C=d.type,C===Bt){if(P.tag===7){n(f,P.sibling),c=l(P,d.props.children),c.return=f,f=c;break e}}else if(P.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===be&&Wu(C)===P.type){n(f,P.sibling),c=l(P,d.props),c.ref=Cn(f,P,d),c.return=f,f=c;break e}n(f,P);break}else t(f,P);P=P.sibling}d.type===Bt?(c=jt(d.props.children,f.mode,y,d.key),c.return=f,f=c):(y=Hr(d.type,d.key,d.props,null,f.mode,y),y.ref=Cn(f,c,d),y.return=f,f=y)}return o(f);case At:e:{for(P=d.key;c!==null;){if(c.key===P)if(c.tag===4&&c.stateNode.containerInfo===d.containerInfo&&c.stateNode.implementation===d.implementation){n(f,c.sibling),c=l(c,d.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=ui(d,f.mode,y),c.return=f,f=c}return o(f);case be:return P=d._init,E(f,c,P(d._payload),y)}if(jn(d))return w(f,c,d,y);if(xn(d))return x(f,c,d,y);_r(f,d)}return typeof d=="string"&&d!==""||typeof d=="number"?(d=""+d,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,d),c.return=f,f=c):(n(f,c),c=oi(d,f.mode,y),c.return=f,f=c),o(f)):n(f,c)}return E}var sn=Ua(!0),$a=Ua(!1),ll=yt(null),il=null,Gt=null,zo=null;function Lo(){zo=Gt=il=null}function Ro(e){var t=ll.current;U(ll),e._currentValue=t}function $i(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function nn(e,t){il=e,zo=Gt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(de=!0),e.firstContext=null)}function Pe(e){var t=e._currentValue;if(zo!==e)if(e={context:e,memoizedValue:t,next:null},Gt===null){if(il===null)throw Error(S(308));Gt=e,il.dependencies={lanes:0,firstContext:e}}else Gt=Gt.next=e;return t}var Nt=null;function To(e){Nt===null?Nt=[e]:Nt.push(e)}function Aa(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,To(t)):(n.next=l.next,l.next=n),t.interleaved=n,Ge(e,r)}function Ge(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var et=!1;function Mo(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ba(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ke(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function ct(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,O&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Ge(e,n)}return l=r.interleaved,l===null?(t.next=t,To(r)):(t.next=l.next,l.next=t),r.interleaved=t,Ge(e,n)}function Ur(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,yo(e,n)}}function Vu(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=o:i=i.next=o,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function ol(e,t,n,r){var l=e.updateQueue;et=!1;var i=l.firstBaseUpdate,o=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var s=u,a=s.next;s.next=null,o===null?i=a:o.next=a,o=s;var h=e.alternate;h!==null&&(h=h.updateQueue,u=h.lastBaseUpdate,u!==o&&(u===null?h.firstBaseUpdate=a:u.next=a,h.lastBaseUpdate=s))}if(i!==null){var p=l.baseState;o=0,h=a=s=null,u=i;do{var m=u.lane,g=u.eventTime;if((r&m)===m){h!==null&&(h=h.next={eventTime:g,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var w=e,x=u;switch(m=t,g=n,x.tag){case 1:if(w=x.payload,typeof w=="function"){p=w.call(g,p,m);break e}p=w;break e;case 3:w.flags=w.flags&-65537|128;case 0:if(w=x.payload,m=typeof w=="function"?w.call(g,p,m):w,m==null)break e;p=W({},p,m);break e;case 2:et=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,m=l.effects,m===null?l.effects=[u]:m.push(u))}else g={eventTime:g,lane:m,tag:u.tag,payload:u.payload,callback:u.callback,next:null},h===null?(a=h=g,s=p):h=h.next=g,o|=m;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;m=u,u=m.next,m.next=null,l.lastBaseUpdate=m,l.shared.pending=null}}while(!0);if(h===null&&(s=p),l.baseState=s,l.firstBaseUpdate=a,l.lastBaseUpdate=h,t=l.shared.interleaved,t!==null){l=t;do o|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Tt|=o,e.lanes=o,e.memoizedState=p}}function Hu(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=ti.transition;ti.transition={};try{e(!1),t()}finally{I=n,ti.transition=r}}function lc(){return je().memoizedState}function ep(e,t,n){var r=dt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ic(e))oc(t,n);else if(n=Aa(e,t,n,r),n!==null){var l=se();Ie(n,e,r,l),uc(n,t,r)}}function tp(e,t,n){var r=dt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ic(e))oc(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var o=t.lastRenderedState,u=i(o,n);if(l.hasEagerState=!0,l.eagerState=u,Fe(u,o)){var s=t.interleaved;s===null?(l.next=l,To(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Aa(e,t,l,r),n!==null&&(l=se(),Ie(n,e,r,l),uc(n,t,r))}}function ic(e){var t=e.alternate;return e===B||t!==null&&t===B}function oc(e,t){Fn=sl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function uc(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,yo(e,n)}}var al={readContext:Pe,useCallback:re,useContext:re,useEffect:re,useImperativeHandle:re,useInsertionEffect:re,useLayoutEffect:re,useMemo:re,useReducer:re,useRef:re,useState:re,useDebugValue:re,useDeferredValue:re,useTransition:re,useMutableSource:re,useSyncExternalStore:re,useId:re,unstable_isNewReconciler:!1},np={readContext:Pe,useCallback:function(e,t){return Ue().memoizedState=[e,t===void 0?null:t],e},useContext:Pe,useEffect:Ku,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Ar(4194308,4,ba.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ar(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ar(4,2,e,t)},useMemo:function(e,t){var n=Ue();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ue();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=ep.bind(null,B,e),[r.memoizedState,e]},useRef:function(e){var t=Ue();return e={current:e},t.memoizedState=e},useState:Qu,useDebugValue:Bo,useDeferredValue:function(e){return Ue().memoizedState=e},useTransition:function(){var e=Qu(!1),t=e[0];return e=bd.bind(null,e[1]),Ue().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=B,l=Ue();if($){if(n===void 0)throw Error(S(407));n=n()}else{if(n=t(),b===null)throw Error(S(349));Rt&30||Qa(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Ku(Ya.bind(null,r,i,e),[e]),r.flags|=2048,tr(9,Ka.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ue(),t=b.identifierPrefix;if($){var n=Qe,r=He;n=(r&~(1<<32-Oe(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=bn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=o.createElement(n,{is:r.is}):(e=o.createElement(n),n==="select"&&(o=e,r.multiple?o.multiple=!0:r.size&&(o.size=r.size))):e=o.createElementNS(e,n),e[$e]=t,e[Zn]=r,gc(e,t,!1,!1),t.stateNode=e;e:{switch(o=Si(n,r),n){case"dialog":D("cancel",e),D("close",e),l=r;break;case"iframe":case"object":case"embed":D("load",e),l=r;break;case"video":case"audio":for(l=0;lfn&&(t.flags|=128,r=!0,Nn(i,!1),t.lanes=4194304)}else{if(!r)if(e=ul(o),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Nn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!o.alternate&&!$)return le(t),null}else 2*K()-i.renderingStartTime>fn&&n!==1073741824&&(t.flags|=128,r=!0,Nn(i,!1),t.lanes=4194304);i.isBackwards?(o.sibling=t.child,t.child=o):(n=i.last,n!==null?n.sibling=o:t.child=o,i.last=o)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=K(),t.sibling=null,n=A.current,F(A,r?n&1|2:n&1),t):(le(t),null);case 22:case 23:return Yo(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ve&1073741824&&(le(t),t.subtreeFlags&6&&(t.flags|=8192)):le(t),null;case 24:return null;case 25:return null}throw Error(S(156,t.tag))}function cp(e,t){switch(Po(t),t.tag){case 1:return he(t.type)&&el(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return an(),U(pe),U(oe),Fo(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Io(t),null;case 13:if(U(A),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(S(340));un()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return U(A),null;case 4:return an(),null;case 10:return Ro(t.type._context),null;case 22:case 23:return Yo(),null;case 24:return null;default:return null}}var jr=!1,ie=!1,fp=typeof WeakSet=="function"?WeakSet:Set,N=null;function Zt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){V(e,t,r)}else n.current=null}function Xi(e,t,n){try{n()}catch(r){V(e,t,r)}}var rs=!1;function dp(e,t){if(Ri=Zr,e=Ca(),No(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var o=0,u=-1,s=-1,a=0,h=0,p=e,m=null;t:for(;;){for(var g;p!==n||l!==0&&p.nodeType!==3||(u=o+l),p!==i||r!==0&&p.nodeType!==3||(s=o+r),p.nodeType===3&&(o+=p.nodeValue.length),(g=p.firstChild)!==null;)m=p,p=g;for(;;){if(p===e)break t;if(m===n&&++a===l&&(u=o),m===i&&++h===r&&(s=o),(g=p.nextSibling)!==null)break;p=m,m=p.parentNode}p=g}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ti={focusedElem:e,selectionRange:n},Zr=!1,N=t;N!==null;)if(t=N,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,N=e;else for(;N!==null;){t=N;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var x=w.memoizedProps,E=w.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?x:Re(t.type,x),E);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var d=t.stateNode.containerInfo;d.nodeType===1?d.textContent="":d.nodeType===9&&d.documentElement&&d.removeChild(d.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(S(163))}}catch(y){V(t,t.return,y)}if(e=t.sibling,e!==null){e.return=t.return,N=e;break}N=t.return}return w=rs,rs=!1,w}function Dn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Xi(t,n,i)}l=l.next}while(l!==r)}}function Nl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Gi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function wc(e){var t=e.alternate;t!==null&&(e.alternate=null,wc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[$e],delete t[Zn],delete t[Ii],delete t[Xd],delete t[Gd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Sc(e){return e.tag===5||e.tag===3||e.tag===4}function ls(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Sc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=br));else if(r!==4&&(e=e.child,e!==null))for(Zi(e,t,n),e=e.sibling;e!==null;)Zi(e,t,n),e=e.sibling}function Ji(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Ji(e,t,n),e=e.sibling;e!==null;)Ji(e,t,n),e=e.sibling}var ee=null,Te=!1;function qe(e,t,n){for(n=n.child;n!==null;)kc(e,t,n),n=n.sibling}function kc(e,t,n){if(Ae&&typeof Ae.onCommitFiberUnmount=="function")try{Ae.onCommitFiberUnmount(gl,n)}catch{}switch(n.tag){case 5:ie||Zt(n,t);case 6:var r=ee,l=Te;ee=null,qe(e,t,n),ee=r,Te=l,ee!==null&&(Te?(e=ee,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ee.removeChild(n.stateNode));break;case 18:ee!==null&&(Te?(e=ee,n=n.stateNode,e.nodeType===8?ql(e.parentNode,n):e.nodeType===1&&ql(e,n),Qn(e)):ql(ee,n.stateNode));break;case 4:r=ee,l=Te,ee=n.stateNode.containerInfo,Te=!0,qe(e,t,n),ee=r,Te=l;break;case 0:case 11:case 14:case 15:if(!ie&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,o=i.destroy;i=i.tag,o!==void 0&&(i&2||i&4)&&Xi(n,t,o),l=l.next}while(l!==r)}qe(e,t,n);break;case 1:if(!ie&&(Zt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){V(n,t,u)}qe(e,t,n);break;case 21:qe(e,t,n);break;case 22:n.mode&1?(ie=(r=ie)||n.memoizedState!==null,qe(e,t,n),ie=r):qe(e,t,n);break;default:qe(e,t,n)}}function is(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new fp),t.forEach(function(r){var l=Sp.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Le(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=o),r&=~i}if(r=l,r=K()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*hp(r/1960))-r,10e?16:e,lt===null)var r=!1;else{if(e=lt,lt=null,dl=0,O&6)throw Error(S(331));var l=O;for(O|=4,N=e.current;N!==null;){var i=N,o=i.child;if(N.flags&16){var u=i.deletions;if(u!==null){for(var s=0;sK()-Qo?Pt(e,0):Ho|=n),me(e,t)}function Lc(e,t){t===0&&(e.mode&1?(t=xr,xr<<=1,!(xr&130023424)&&(xr=4194304)):t=1);var n=se();e=Ge(e,t),e!==null&&(or(e,t,n),me(e,n))}function wp(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Lc(e,n)}function Sp(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(S(314))}r!==null&&r.delete(t),Lc(e,n)}var Rc;Rc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||pe.current)de=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return de=!1,sp(e,t,n);de=!!(e.flags&131072)}else de=!1,$&&t.flags&1048576&&Ia(t,rl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Br(e,t),e=t.pendingProps;var l=on(t,oe.current);nn(t,n),l=Uo(null,t,r,e,l,n);var i=$o();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,he(r)?(i=!0,tl(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Mo(t),l.updater=Cl,t.stateNode=l,l._reactInternals=t,Bi(t,r,e,n),t=Hi(null,t,r,!0,i,n)):(t.tag=0,$&&i&&_o(t),ue(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Br(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Ep(r),e=Re(r,e),l){case 0:t=Vi(null,t,r,e,n);break e;case 1:t=es(null,t,r,e,n);break e;case 11:t=qu(null,t,r,e,n);break e;case 14:t=bu(null,t,r,Re(r.type,e),n);break e}throw Error(S(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Vi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),es(e,t,r,l,n);case 3:e:{if(hc(t),e===null)throw Error(S(387));r=t.pendingProps,i=t.memoizedState,l=i.element,Ba(e,t),ol(t,r,null,n);var o=t.memoizedState;if(r=o.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:o.cache,pendingSuspenseBoundaries:o.pendingSuspenseBoundaries,transitions:o.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=cn(Error(S(423)),t),t=ts(e,t,r,n,l);break e}else if(r!==l){l=cn(Error(S(424)),t),t=ts(e,t,r,n,l);break e}else for(ge=at(t.stateNode.containerInfo.firstChild),ye=t,$=!0,Me=null,n=$a(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(un(),r===l){t=Ze(e,t,n);break e}ue(e,t,r,n)}t=t.child}return t;case 5:return Wa(t),e===null&&Ui(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,o=l.children,Mi(r,l)?o=null:i!==null&&Mi(r,i)&&(t.flags|=32),pc(e,t),ue(e,t,o,n),t.child;case 6:return e===null&&Ui(t),null;case 13:return mc(e,t,n);case 4:return Oo(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=sn(t,null,r,n):ue(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),qu(e,t,r,l,n);case 7:return ue(e,t,t.pendingProps,n),t.child;case 8:return ue(e,t,t.pendingProps.children,n),t.child;case 12:return ue(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,o=l.value,F(ll,r._currentValue),r._currentValue=o,i!==null)if(Fe(i.value,o)){if(i.children===l.children&&!pe.current){t=Ze(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){o=i.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(i.tag===1){s=Ke(-1,n&-n),s.tag=2;var a=i.updateQueue;if(a!==null){a=a.shared;var h=a.pending;h===null?s.next=s:(s.next=h.next,h.next=s),a.pending=s}}i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),$i(i.return,n,t),u.lanes|=n;break}s=s.next}}else if(i.tag===10)o=i.type===t.type?null:i.child;else if(i.tag===18){if(o=i.return,o===null)throw Error(S(341));o.lanes|=n,u=o.alternate,u!==null&&(u.lanes|=n),$i(o,n,t),o=i.sibling}else o=i.child;if(o!==null)o.return=i;else for(o=i;o!==null;){if(o===t){o=null;break}if(i=o.sibling,i!==null){i.return=o.return,o=i;break}o=o.return}i=o}ue(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,nn(t,n),l=Pe(l),r=r(l),t.flags|=1,ue(e,t,r,n),t.child;case 14:return r=t.type,l=Re(r,t.pendingProps),l=Re(r.type,l),bu(e,t,r,l,n);case 15:return fc(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Br(e,t),t.tag=1,he(r)?(e=!0,tl(t)):e=!1,nn(t,n),sc(t,r,l),Bi(t,r,l,n),Hi(null,t,r,!0,e,n);case 19:return vc(e,t,n);case 22:return dc(e,t,n)}throw Error(S(156,t.tag))};function Tc(e,t){return ia(e,t)}function kp(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ne(e,t,n,r){return new kp(e,t,n,r)}function Go(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Ep(e){if(typeof e=="function")return Go(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ho)return 11;if(e===mo)return 14}return 2}function pt(e,t){var n=e.alternate;return n===null?(n=Ne(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Hr(e,t,n,r,l,i){var o=2;if(r=e,typeof e=="function")Go(e)&&(o=1);else if(typeof e=="string")o=5;else e:switch(e){case Bt:return jt(n.children,l,i,t);case po:o=8,l|=8;break;case fi:return e=Ne(12,n,t,l|2),e.elementType=fi,e.lanes=i,e;case di:return e=Ne(13,n,t,l),e.elementType=di,e.lanes=i,e;case pi:return e=Ne(19,n,t,l),e.elementType=pi,e.lanes=i,e;case Ws:return Pl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case As:o=10;break e;case Bs:o=9;break e;case ho:o=11;break e;case mo:o=14;break e;case be:o=16,r=null;break e}throw Error(S(130,e==null?e:typeof e,""))}return t=Ne(o,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function jt(e,t,n,r){return e=Ne(7,e,r,t),e.lanes=n,e}function Pl(e,t,n,r){return e=Ne(22,e,r,t),e.elementType=Ws,e.lanes=n,e.stateNode={isHidden:!1},e}function oi(e,t,n){return e=Ne(6,e,null,t),e.lanes=n,e}function ui(e,t,n){return t=Ne(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Cp(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Bl(0),this.expirationTimes=Bl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Bl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Zo(e,t,n,r,l,i,o,u,s){return e=new Cp(e,t,n,u,s),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Ne(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Mo(i),e}function Np(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Fc)}catch(e){console.error(e)}}Fc(),Fs.exports=we;var Lp=Fs.exports,ps=Lp;ai.createRoot=ps.createRoot,ai.hydrateRoot=ps.hydrateRoot;/** * @remix-run/router v1.23.2 * * Copyright (c) Remix Software Inc. @@ -46,7 +46,7 @@ Error generating stack: `+o.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */function nr(){return nr=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u")throw new Error(t)}function eu(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Tp(){return Math.random().toString(36).substr(2,8)}function ms(e,t){return{usr:e.state,key:e.key,idx:t}}function ri(e,t,n,r){return n===void 0&&(n=null),nr({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?mn(t):t,{state:n,key:t&&t.key||r||Tp()})}function ml(e){let{pathname:t="/",search:n="",hash:r=""}=e;return n&&n!=="?"&&(t+=n.charAt(0)==="?"?n:"?"+n),r&&r!=="#"&&(t+=r.charAt(0)==="#"?r:"#"+r),t}function mn(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substr(n),e=e.substr(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substr(r),e=e.substr(0,r)),e&&(t.pathname=e)}return t}function Mp(e,t,n,r){r===void 0&&(r={});let{window:l=document.defaultView,v5Compat:o=!1}=r,i=l.history,u=ot.Pop,s=null,a=h();a==null&&(a=0,i.replaceState(nr({},i.state,{idx:a}),""));function h(){return(i.state||{idx:null}).idx}function p(){u=ot.Pop;let E=h(),f=E==null?null:E-a;a=E,s&&s({action:u,location:y.location,delta:f})}function m(E,f){u=ot.Push;let c=ri(y.location,E,f);a=h()+1;let d=ms(c,a),w=y.createHref(c);try{i.pushState(d,"",w)}catch(C){if(C instanceof DOMException&&C.name==="DataCloneError")throw C;l.location.assign(w)}o&&s&&s({action:u,location:y.location,delta:1})}function g(E,f){u=ot.Replace;let c=ri(y.location,E,f);a=h();let d=ms(c,a),w=y.createHref(c);i.replaceState(d,"",w),o&&s&&s({action:u,location:y.location,delta:0})}function x(E){let f=l.location.origin!=="null"?l.location.origin:l.location.href,c=typeof E=="string"?E:ml(E);return c=c.replace(/ $/,"%20"),X(f,"No window.location.(origin|href) available to create URL for href: "+c),new URL(c,f)}let y={get action(){return u},get location(){return e(l,i)},listen(E){if(s)throw new Error("A history only accepts one active listener");return l.addEventListener(hs,p),s=E,()=>{l.removeEventListener(hs,p),s=null}},createHref(E){return t(l,E)},createURL:x,encodeLocation(E){let f=x(E);return{pathname:f.pathname,search:f.search,hash:f.hash}},push:m,replace:g,go(E){return i.go(E)}};return y}var vs;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(vs||(vs={}));function Op(e,t,n){return n===void 0&&(n="/"),Ip(e,t,n)}function Ip(e,t,n,r){let l=typeof t=="string"?mn(t):t,o=tu(l.pathname||"/",n);if(o==null)return null;let i=Fc(e);Dp(i);let u=null;for(let s=0;u==null&&s{let s={relativePath:u===void 0?o.path||"":u,caseSensitive:o.caseSensitive===!0,childrenIndex:i,route:o};s.relativePath.startsWith("/")&&(X(s.relativePath.startsWith(r),'Absolute route path "'+s.relativePath+'" nested under path '+('"'+r+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),s.relativePath=s.relativePath.slice(r.length));let a=ht([r,s.relativePath]),h=n.concat(s);o.children&&o.children.length>0&&(X(o.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+a+'".')),Fc(o.children,t,h,a)),!(o.path==null&&!o.index)&&t.push({path:a,score:Vp(a,o.index),routesMeta:h})};return e.forEach((o,i)=>{var u;if(o.path===""||!((u=o.path)!=null&&u.includes("?")))l(o,i);else for(let s of Uc(o.path))l(o,i,s)}),t}function Uc(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,l=n.endsWith("?"),o=n.replace(/\?$/,"");if(r.length===0)return l?[o,""]:[o];let i=Uc(r.join("/")),u=[];return u.push(...i.map(s=>s===""?o:[o,s].join("/"))),l&&u.push(...i),u.map(s=>e.startsWith("/")&&s===""?"/":s)}function Dp(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:Hp(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}const Fp=/^:[\w-]+$/,Up=3,$p=2,Ap=1,Bp=10,Wp=-2,gs=e=>e==="*";function Vp(e,t){let n=e.split("/"),r=n.length;return n.some(gs)&&(r+=Wp),t&&(r+=$p),n.filter(l=>!gs(l)).reduce((l,o)=>l+(Fp.test(o)?Up:o===""?Ap:Bp),r)}function Hp(e,t){return e.length===t.length&&e.slice(0,-1).every((r,l)=>r===t[l])?e[e.length-1]-t[t.length-1]:0}function Qp(e,t,n){let{routesMeta:r}=e,l={},o="/",i=[];for(let u=0;u{let{paramName:m,isOptional:g}=h;if(m==="*"){let y=u[p]||"";i=o.slice(0,o.length-y.length).replace(/(.)\/+$/,"$1")}const x=u[p];return g&&!x?a[m]=void 0:a[m]=(x||"").replace(/%2F/g,"/"),a},{}),pathname:o,pathnameBase:i,pattern:e}}function Yp(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!0),eu(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let r=[],l="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(i,u,s)=>(r.push({paramName:u,isOptional:s!=null}),s?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(r.push({paramName:"*"}),l+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?l+="\\/*$":e!==""&&e!=="/"&&(l+="(?:(?=\\/|$))"),[new RegExp(l,t?void 0:"i"),r]}function Xp(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return eu(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+t+").")),e}}function tu(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}const Gp=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Zp=e=>Gp.test(e);function Jp(e,t){t===void 0&&(t="/");let{pathname:n,search:r="",hash:l=""}=typeof e=="string"?mn(e):e,o;if(n)if(Zp(n))o=n;else{if(n.includes("//")){let i=n;n=n.replace(/\/\/+/g,"/"),eu(!1,"Pathnames cannot have embedded double slashes - normalizing "+(i+" -> "+n))}n.startsWith("/")?o=ys(n.substring(1),"/"):o=ys(n,t)}else o=t;return{pathname:o,search:eh(r),hash:th(l)}}function ys(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(l=>{l===".."?n.length>1&&n.pop():l!=="."&&n.push(l)}),n.length>1?n.join("/"):"/"}function so(e,t,n,r){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+t+"` field ["+JSON.stringify(r)+"]. Please separate it out to the ")+("`to."+n+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function qp(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function $c(e,t){let n=qp(e);return t?n.map((r,l)=>l===n.length-1?r.pathname:r.pathnameBase):n.map(r=>r.pathnameBase)}function Ac(e,t,n,r){r===void 0&&(r=!1);let l;typeof e=="string"?l=mn(e):(l=nr({},e),X(!l.pathname||!l.pathname.includes("?"),so("?","pathname","search",l)),X(!l.pathname||!l.pathname.includes("#"),so("#","pathname","hash",l)),X(!l.search||!l.search.includes("#"),so("#","search","hash",l)));let o=e===""||l.pathname==="",i=o?"/":l.pathname,u;if(i==null)u=n;else{let p=t.length-1;if(!r&&i.startsWith("..")){let m=i.split("/");for(;m[0]==="..";)m.shift(),p-=1;l.pathname=m.join("/")}u=p>=0?t[p]:"/"}let s=Jp(l,u),a=i&&i!=="/"&&i.endsWith("/"),h=(o||i===".")&&n.endsWith("/");return!s.pathname.endsWith("/")&&(a||h)&&(s.pathname+="/"),s}const ht=e=>e.join("/").replace(/\/\/+/g,"/"),bp=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),eh=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,th=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function nh(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const Bc=["post","put","patch","delete"];new Set(Bc);const rh=["get",...Bc];new Set(rh);/** + */function rr(){return rr=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u")throw new Error(t)}function eu(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Tp(){return Math.random().toString(36).substr(2,8)}function ms(e,t){return{usr:e.state,key:e.key,idx:t}}function no(e,t,n,r){return n===void 0&&(n=null),rr({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?mn(t):t,{state:n,key:t&&t.key||r||Tp()})}function ml(e){let{pathname:t="/",search:n="",hash:r=""}=e;return n&&n!=="?"&&(t+=n.charAt(0)==="?"?n:"?"+n),r&&r!=="#"&&(t+=r.charAt(0)==="#"?r:"#"+r),t}function mn(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substr(n),e=e.substr(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substr(r),e=e.substr(0,r)),e&&(t.pathname=e)}return t}function Mp(e,t,n,r){r===void 0&&(r={});let{window:l=document.defaultView,v5Compat:i=!1}=r,o=l.history,u=it.Pop,s=null,a=h();a==null&&(a=0,o.replaceState(rr({},o.state,{idx:a}),""));function h(){return(o.state||{idx:null}).idx}function p(){u=it.Pop;let E=h(),f=E==null?null:E-a;a=E,s&&s({action:u,location:x.location,delta:f})}function m(E,f){u=it.Push;let c=no(x.location,E,f);a=h()+1;let d=ms(c,a),y=x.createHref(c);try{o.pushState(d,"",y)}catch(C){if(C instanceof DOMException&&C.name==="DataCloneError")throw C;l.location.assign(y)}i&&s&&s({action:u,location:x.location,delta:1})}function g(E,f){u=it.Replace;let c=no(x.location,E,f);a=h();let d=ms(c,a),y=x.createHref(c);o.replaceState(d,"",y),i&&s&&s({action:u,location:x.location,delta:0})}function w(E){let f=l.location.origin!=="null"?l.location.origin:l.location.href,c=typeof E=="string"?E:ml(E);return c=c.replace(/ $/,"%20"),X(f,"No window.location.(origin|href) available to create URL for href: "+c),new URL(c,f)}let x={get action(){return u},get location(){return e(l,o)},listen(E){if(s)throw new Error("A history only accepts one active listener");return l.addEventListener(hs,p),s=E,()=>{l.removeEventListener(hs,p),s=null}},createHref(E){return t(l,E)},createURL:w,encodeLocation(E){let f=w(E);return{pathname:f.pathname,search:f.search,hash:f.hash}},push:m,replace:g,go(E){return o.go(E)}};return x}var vs;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(vs||(vs={}));function Op(e,t,n){return n===void 0&&(n="/"),Ip(e,t,n)}function Ip(e,t,n,r){let l=typeof t=="string"?mn(t):t,i=tu(l.pathname||"/",n);if(i==null)return null;let o=Dc(e);Fp(o);let u=null;for(let s=0;u==null&&s{let s={relativePath:u===void 0?i.path||"":u,caseSensitive:i.caseSensitive===!0,childrenIndex:o,route:i};s.relativePath.startsWith("/")&&(X(s.relativePath.startsWith(r),'Absolute route path "'+s.relativePath+'" nested under path '+('"'+r+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),s.relativePath=s.relativePath.slice(r.length));let a=ht([r,s.relativePath]),h=n.concat(s);i.children&&i.children.length>0&&(X(i.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+a+'".')),Dc(i.children,t,h,a)),!(i.path==null&&!i.index)&&t.push({path:a,score:Vp(a,i.index),routesMeta:h})};return e.forEach((i,o)=>{var u;if(i.path===""||!((u=i.path)!=null&&u.includes("?")))l(i,o);else for(let s of Uc(i.path))l(i,o,s)}),t}function Uc(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,l=n.endsWith("?"),i=n.replace(/\?$/,"");if(r.length===0)return l?[i,""]:[i];let o=Uc(r.join("/")),u=[];return u.push(...o.map(s=>s===""?i:[i,s].join("/"))),l&&u.push(...o),u.map(s=>e.startsWith("/")&&s===""?"/":s)}function Fp(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:Hp(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}const Dp=/^:[\w-]+$/,Up=3,$p=2,Ap=1,Bp=10,Wp=-2,gs=e=>e==="*";function Vp(e,t){let n=e.split("/"),r=n.length;return n.some(gs)&&(r+=Wp),t&&(r+=$p),n.filter(l=>!gs(l)).reduce((l,i)=>l+(Dp.test(i)?Up:i===""?Ap:Bp),r)}function Hp(e,t){return e.length===t.length&&e.slice(0,-1).every((r,l)=>r===t[l])?e[e.length-1]-t[t.length-1]:0}function Qp(e,t,n){let{routesMeta:r}=e,l={},i="/",o=[];for(let u=0;u{let{paramName:m,isOptional:g}=h;if(m==="*"){let x=u[p]||"";o=i.slice(0,i.length-x.length).replace(/(.)\/+$/,"$1")}const w=u[p];return g&&!w?a[m]=void 0:a[m]=(w||"").replace(/%2F/g,"/"),a},{}),pathname:i,pathnameBase:o,pattern:e}}function Yp(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!0),eu(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let r=[],l="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(o,u,s)=>(r.push({paramName:u,isOptional:s!=null}),s?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(r.push({paramName:"*"}),l+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?l+="\\/*$":e!==""&&e!=="/"&&(l+="(?:(?=\\/|$))"),[new RegExp(l,t?void 0:"i"),r]}function Xp(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return eu(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+t+").")),e}}function tu(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}const Gp=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Zp=e=>Gp.test(e);function Jp(e,t){t===void 0&&(t="/");let{pathname:n,search:r="",hash:l=""}=typeof e=="string"?mn(e):e,i;if(n)if(Zp(n))i=n;else{if(n.includes("//")){let o=n;n=n.replace(/\/\/+/g,"/"),eu(!1,"Pathnames cannot have embedded double slashes - normalizing "+(o+" -> "+n))}n.startsWith("/")?i=ys(n.substring(1),"/"):i=ys(n,t)}else i=t;return{pathname:i,search:eh(r),hash:th(l)}}function ys(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(l=>{l===".."?n.length>1&&n.pop():l!=="."&&n.push(l)}),n.length>1?n.join("/"):"/"}function si(e,t,n,r){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+t+"` field ["+JSON.stringify(r)+"]. Please separate it out to the ")+("`to."+n+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function qp(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function $c(e,t){let n=qp(e);return t?n.map((r,l)=>l===n.length-1?r.pathname:r.pathnameBase):n.map(r=>r.pathnameBase)}function Ac(e,t,n,r){r===void 0&&(r=!1);let l;typeof e=="string"?l=mn(e):(l=rr({},e),X(!l.pathname||!l.pathname.includes("?"),si("?","pathname","search",l)),X(!l.pathname||!l.pathname.includes("#"),si("#","pathname","hash",l)),X(!l.search||!l.search.includes("#"),si("#","search","hash",l)));let i=e===""||l.pathname==="",o=i?"/":l.pathname,u;if(o==null)u=n;else{let p=t.length-1;if(!r&&o.startsWith("..")){let m=o.split("/");for(;m[0]==="..";)m.shift(),p-=1;l.pathname=m.join("/")}u=p>=0?t[p]:"/"}let s=Jp(l,u),a=o&&o!=="/"&&o.endsWith("/"),h=(i||o===".")&&n.endsWith("/");return!s.pathname.endsWith("/")&&(a||h)&&(s.pathname+="/"),s}const ht=e=>e.join("/").replace(/\/\/+/g,"/"),bp=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),eh=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,th=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function nh(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const Bc=["post","put","patch","delete"];new Set(Bc);const rh=["get",...Bc];new Set(rh);/** * React Router v6.30.3 * * Copyright (c) Remix Software Inc. @@ -55,7 +55,7 @@ Error generating stack: `+o.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */function rr(){return rr=Object.assign?Object.assign.bind():function(e){for(var t=1;t{u.current=!0}),k.useCallback(function(a,h){if(h===void 0&&(h={}),!u.current)return;if(typeof a=="number"){r.go(a);return}let p=Ac(a,JSON.parse(i),o,h.relative==="path");e==null&&t!=="/"&&(p.pathname=p.pathname==="/"?t:ht([t,p.pathname])),(h.replace?r.replace:r.push)(p,h.state,h)},[t,r,i,o,e])}function Hc(e,t){let{relative:n}=t===void 0?{}:t,{future:r}=k.useContext(Dt),{matches:l}=k.useContext(Ft),{pathname:o}=cr(),i=JSON.stringify($c(l,r.v7_relativeSplatPath));return k.useMemo(()=>Ac(e,JSON.parse(i),o,n==="path"),[e,i,o,n])}function sh(e,t){return ah(e,t)}function ah(e,t,n,r){ar()||X(!1);let{navigator:l}=k.useContext(Dt),{matches:o}=k.useContext(Ft),i=o[o.length-1],u=i?i.params:{};i&&i.pathname;let s=i?i.pathnameBase:"/";i&&i.route;let a=cr(),h;if(t){var p;let E=typeof t=="string"?mn(t):t;s==="/"||(p=E.pathname)!=null&&p.startsWith(s)||X(!1),h=E}else h=a;let m=h.pathname||"/",g=m;if(s!=="/"){let E=s.replace(/^\//,"").split("/");g="/"+m.replace(/^\//,"").split("/").slice(E.length).join("/")}let x=Op(e,{pathname:g}),y=hh(x&&x.map(E=>Object.assign({},E,{params:Object.assign({},u,E.params),pathname:ht([s,l.encodeLocation?l.encodeLocation(E.pathname).pathname:E.pathname]),pathnameBase:E.pathnameBase==="/"?s:ht([s,l.encodeLocation?l.encodeLocation(E.pathnameBase).pathname:E.pathnameBase])})),o,n,r);return t&&y?k.createElement(Tl.Provider,{value:{location:rr({pathname:"/",search:"",hash:"",state:null,key:"default"},h),navigationType:ot.Pop}},y):y}function ch(){let e=yh(),t=nh(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,l={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return k.createElement(k.Fragment,null,k.createElement("h2",null,"Unexpected Application Error!"),k.createElement("h3",{style:{fontStyle:"italic"}},t),n?k.createElement("pre",{style:l},n):null,null)}const fh=k.createElement(ch,null);class dh extends k.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,n){return n.location!==t.location||n.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:n.error,location:n.location,revalidation:t.revalidation||n.revalidation}}componentDidCatch(t,n){console.error("React Router caught the following error during render",t,n)}render(){return this.state.error!==void 0?k.createElement(Ft.Provider,{value:this.props.routeContext},k.createElement(Wc.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function ph(e){let{routeContext:t,match:n,children:r}=e,l=k.useContext(nu);return l&&l.static&&l.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(l.staticContext._deepestRenderedBoundaryId=n.route.id),k.createElement(Ft.Provider,{value:t},r)}function hh(e,t,n,r){var l;if(t===void 0&&(t=[]),n===void 0&&(n=null),r===void 0&&(r=null),e==null){var o;if(!n)return null;if(n.errors)e=n.matches;else if((o=r)!=null&&o.v7_partialHydration&&t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let i=e,u=(l=n)==null?void 0:l.errors;if(u!=null){let h=i.findIndex(p=>p.route.id&&(u==null?void 0:u[p.route.id])!==void 0);h>=0||X(!1),i=i.slice(0,Math.min(i.length,h+1))}let s=!1,a=-1;if(n&&r&&r.v7_partialHydration)for(let h=0;h=0?i=i.slice(0,a+1):i=[i[0]];break}}}return i.reduceRight((h,p,m)=>{let g,x=!1,y=null,E=null;n&&(g=u&&p.route.id?u[p.route.id]:void 0,y=p.route.errorElement||fh,s&&(a<0&&m===0?(wh("route-fallback"),x=!0,E=null):a===m&&(x=!0,E=p.route.hydrateFallbackElement||null)));let f=t.concat(i.slice(0,m+1)),c=()=>{let d;return g?d=y:x?d=E:p.route.Component?d=k.createElement(p.route.Component,null):p.route.element?d=p.route.element:d=h,k.createElement(ph,{match:p,routeContext:{outlet:h,matches:f,isDataRoute:n!=null},children:d})};return n&&(p.route.ErrorBoundary||p.route.errorElement||m===0)?k.createElement(dh,{location:n.location,revalidation:n.revalidation,component:y,error:g,children:c(),routeContext:{outlet:null,matches:f,isDataRoute:!0}}):c()},null)}var Qc=function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e}(Qc||{}),Kc=function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e}(Kc||{});function mh(e){let t=k.useContext(nu);return t||X(!1),t}function vh(e){let t=k.useContext(lh);return t||X(!1),t}function gh(e){let t=k.useContext(Ft);return t||X(!1),t}function Yc(e){let t=gh(),n=t.matches[t.matches.length-1];return n.route.id||X(!1),n.route.id}function yh(){var e;let t=k.useContext(Wc),n=vh(),r=Yc();return t!==void 0?t:(e=n.errors)==null?void 0:e[r]}function xh(){let{router:e}=mh(Qc.UseNavigateStable),t=Yc(Kc.UseNavigateStable),n=k.useRef(!1);return Vc(()=>{n.current=!0}),k.useCallback(function(l,o){o===void 0&&(o={}),n.current&&(typeof l=="number"?e.navigate(l):e.navigate(l,rr({fromRouteId:t},o)))},[e,t])}const xs={};function wh(e,t,n){xs[e]||(xs[e]=!0)}function Sh(e,t){e==null||e.v7_startTransition,e==null||e.v7_relativeSplatPath}function $t(e){X(!1)}function kh(e){let{basename:t="/",children:n=null,location:r,navigationType:l=ot.Pop,navigator:o,static:i=!1,future:u}=e;ar()&&X(!1);let s=t.replace(/^\/*/,"/"),a=k.useMemo(()=>({basename:s,navigator:o,static:i,future:rr({v7_relativeSplatPath:!1},u)}),[s,u,o,i]);typeof r=="string"&&(r=mn(r));let{pathname:h="/",search:p="",hash:m="",state:g=null,key:x="default"}=r,y=k.useMemo(()=>{let E=tu(h,s);return E==null?null:{location:{pathname:E,search:p,hash:m,state:g,key:x},navigationType:l}},[s,h,p,m,g,x,l]);return y==null?null:k.createElement(Dt.Provider,{value:a},k.createElement(Tl.Provider,{children:n,value:y}))}function Eh(e){let{children:t,location:n}=e;return sh(li(t),n)}new Promise(()=>{});function li(e,t){t===void 0&&(t=[]);let n=[];return k.Children.forEach(e,(r,l)=>{if(!k.isValidElement(r))return;let o=[...t,l];if(r.type===k.Fragment){n.push.apply(n,li(r.props.children,o));return}r.type!==$t&&X(!1),!r.props.index||!r.props.children||X(!1);let i={id:r.props.id||o.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,loader:r.props.loader,action:r.props.action,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(i.children=li(r.props.children,o)),n.push(i)}),n}/** + */function lr(){return lr=Object.assign?Object.assign.bind():function(e){for(var t=1;t{u.current=!0}),k.useCallback(function(a,h){if(h===void 0&&(h={}),!u.current)return;if(typeof a=="number"){r.go(a);return}let p=Ac(a,JSON.parse(o),i,h.relative==="path");e==null&&t!=="/"&&(p.pathname=p.pathname==="/"?t:ht([t,p.pathname])),(h.replace?r.replace:r.push)(p,h.state,h)},[t,r,o,i,e])}function Hc(e,t){let{relative:n}=t===void 0?{}:t,{future:r}=k.useContext(Ft),{matches:l}=k.useContext(Dt),{pathname:i}=fr(),o=JSON.stringify($c(l,r.v7_relativeSplatPath));return k.useMemo(()=>Ac(e,JSON.parse(o),i,n==="path"),[e,o,i,n])}function sh(e,t){return ah(e,t)}function ah(e,t,n,r){cr()||X(!1);let{navigator:l}=k.useContext(Ft),{matches:i}=k.useContext(Dt),o=i[i.length-1],u=o?o.params:{};o&&o.pathname;let s=o?o.pathnameBase:"/";o&&o.route;let a=fr(),h;if(t){var p;let E=typeof t=="string"?mn(t):t;s==="/"||(p=E.pathname)!=null&&p.startsWith(s)||X(!1),h=E}else h=a;let m=h.pathname||"/",g=m;if(s!=="/"){let E=s.replace(/^\//,"").split("/");g="/"+m.replace(/^\//,"").split("/").slice(E.length).join("/")}let w=Op(e,{pathname:g}),x=hh(w&&w.map(E=>Object.assign({},E,{params:Object.assign({},u,E.params),pathname:ht([s,l.encodeLocation?l.encodeLocation(E.pathname).pathname:E.pathname]),pathnameBase:E.pathnameBase==="/"?s:ht([s,l.encodeLocation?l.encodeLocation(E.pathnameBase).pathname:E.pathnameBase])})),i,n,r);return t&&x?k.createElement(Tl.Provider,{value:{location:lr({pathname:"/",search:"",hash:"",state:null,key:"default"},h),navigationType:it.Pop}},x):x}function ch(){let e=yh(),t=nh(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,l={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return k.createElement(k.Fragment,null,k.createElement("h2",null,"Unexpected Application Error!"),k.createElement("h3",{style:{fontStyle:"italic"}},t),n?k.createElement("pre",{style:l},n):null,null)}const fh=k.createElement(ch,null);class dh extends k.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,n){return n.location!==t.location||n.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:n.error,location:n.location,revalidation:t.revalidation||n.revalidation}}componentDidCatch(t,n){console.error("React Router caught the following error during render",t,n)}render(){return this.state.error!==void 0?k.createElement(Dt.Provider,{value:this.props.routeContext},k.createElement(Wc.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function ph(e){let{routeContext:t,match:n,children:r}=e,l=k.useContext(nu);return l&&l.static&&l.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(l.staticContext._deepestRenderedBoundaryId=n.route.id),k.createElement(Dt.Provider,{value:t},r)}function hh(e,t,n,r){var l;if(t===void 0&&(t=[]),n===void 0&&(n=null),r===void 0&&(r=null),e==null){var i;if(!n)return null;if(n.errors)e=n.matches;else if((i=r)!=null&&i.v7_partialHydration&&t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let o=e,u=(l=n)==null?void 0:l.errors;if(u!=null){let h=o.findIndex(p=>p.route.id&&(u==null?void 0:u[p.route.id])!==void 0);h>=0||X(!1),o=o.slice(0,Math.min(o.length,h+1))}let s=!1,a=-1;if(n&&r&&r.v7_partialHydration)for(let h=0;h=0?o=o.slice(0,a+1):o=[o[0]];break}}}return o.reduceRight((h,p,m)=>{let g,w=!1,x=null,E=null;n&&(g=u&&p.route.id?u[p.route.id]:void 0,x=p.route.errorElement||fh,s&&(a<0&&m===0?(wh("route-fallback"),w=!0,E=null):a===m&&(w=!0,E=p.route.hydrateFallbackElement||null)));let f=t.concat(o.slice(0,m+1)),c=()=>{let d;return g?d=x:w?d=E:p.route.Component?d=k.createElement(p.route.Component,null):p.route.element?d=p.route.element:d=h,k.createElement(ph,{match:p,routeContext:{outlet:h,matches:f,isDataRoute:n!=null},children:d})};return n&&(p.route.ErrorBoundary||p.route.errorElement||m===0)?k.createElement(dh,{location:n.location,revalidation:n.revalidation,component:x,error:g,children:c(),routeContext:{outlet:null,matches:f,isDataRoute:!0}}):c()},null)}var Qc=function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e}(Qc||{}),Kc=function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e}(Kc||{});function mh(e){let t=k.useContext(nu);return t||X(!1),t}function vh(e){let t=k.useContext(lh);return t||X(!1),t}function gh(e){let t=k.useContext(Dt);return t||X(!1),t}function Yc(e){let t=gh(),n=t.matches[t.matches.length-1];return n.route.id||X(!1),n.route.id}function yh(){var e;let t=k.useContext(Wc),n=vh(),r=Yc();return t!==void 0?t:(e=n.errors)==null?void 0:e[r]}function xh(){let{router:e}=mh(Qc.UseNavigateStable),t=Yc(Kc.UseNavigateStable),n=k.useRef(!1);return Vc(()=>{n.current=!0}),k.useCallback(function(l,i){i===void 0&&(i={}),n.current&&(typeof l=="number"?e.navigate(l):e.navigate(l,lr({fromRouteId:t},i)))},[e,t])}const xs={};function wh(e,t,n){xs[e]||(xs[e]=!0)}function Sh(e,t){e==null||e.v7_startTransition,e==null||e.v7_relativeSplatPath}function $t(e){X(!1)}function kh(e){let{basename:t="/",children:n=null,location:r,navigationType:l=it.Pop,navigator:i,static:o=!1,future:u}=e;cr()&&X(!1);let s=t.replace(/^\/*/,"/"),a=k.useMemo(()=>({basename:s,navigator:i,static:o,future:lr({v7_relativeSplatPath:!1},u)}),[s,u,i,o]);typeof r=="string"&&(r=mn(r));let{pathname:h="/",search:p="",hash:m="",state:g=null,key:w="default"}=r,x=k.useMemo(()=>{let E=tu(h,s);return E==null?null:{location:{pathname:E,search:p,hash:m,state:g,key:w},navigationType:l}},[s,h,p,m,g,w,l]);return x==null?null:k.createElement(Ft.Provider,{value:a},k.createElement(Tl.Provider,{children:n,value:x}))}function Eh(e){let{children:t,location:n}=e;return sh(ro(t),n)}new Promise(()=>{});function ro(e,t){t===void 0&&(t=[]);let n=[];return k.Children.forEach(e,(r,l)=>{if(!k.isValidElement(r))return;let i=[...t,l];if(r.type===k.Fragment){n.push.apply(n,ro(r.props.children,i));return}r.type!==$t&&X(!1),!r.props.index||!r.props.children||X(!1);let o={id:r.props.id||i.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,loader:r.props.loader,action:r.props.action,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(o.children=ro(r.props.children,i)),n.push(o)}),n}/** * React Router DOM v6.30.3 * * Copyright (c) Remix Software Inc. @@ -64,7 +64,7 @@ Error generating stack: `+o.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */function oi(){return oi=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(n[l]=e[l]);return n}function Nh(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function _h(e,t){return e.button===0&&(!t||t==="_self")&&!Nh(e)}const Ph=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],jh="6";try{window.__reactRouterVersion=jh}catch{}const zh="startTransition",ws=xf[zh];function Lh(e){let{basename:t,children:n,future:r,window:l}=e,o=k.useRef();o.current==null&&(o.current=Rp({window:l,v5Compat:!0}));let i=o.current,[u,s]=k.useState({action:i.action,location:i.location}),{v7_startTransition:a}=r||{},h=k.useCallback(p=>{a&&ws?ws(()=>s(p)):s(p)},[s,a]);return k.useLayoutEffect(()=>i.listen(h),[i,h]),k.useEffect(()=>Sh(r),[r]),k.createElement(kh,{basename:t,children:n,location:u.location,navigationType:u.action,navigator:i,future:r})}const Rh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",Th=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Mh=k.forwardRef(function(t,n){let{onClick:r,relative:l,reloadDocument:o,replace:i,state:u,target:s,to:a,preventScrollReset:h,viewTransition:p}=t,m=Ch(t,Ph),{basename:g}=k.useContext(Dt),x,y=!1;if(typeof a=="string"&&Th.test(a)&&(x=a,Rh))try{let d=new URL(window.location.href),w=a.startsWith("//")?new URL(d.protocol+a):new URL(a),C=tu(w.pathname,g);w.origin===d.origin&&C!=null?a=C+w.search+w.hash:y=!0}catch{}let E=oh(a,{relative:l}),f=Oh(a,{replace:i,state:u,target:s,preventScrollReset:h,relative:l,viewTransition:p});function c(d){r&&r(d),d.defaultPrevented||f(d)}return k.createElement("a",oi({},m,{href:x||E,onClick:y||o?r:c,ref:n,target:s}))});var Ss;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(Ss||(Ss={}));var ks;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(ks||(ks={}));function Oh(e,t){let{target:n,replace:r,state:l,preventScrollReset:o,relative:i,viewTransition:u}=t===void 0?{}:t,s=ih(),a=cr(),h=Hc(e,{relative:i});return k.useCallback(p=>{if(_h(p,n)){p.preventDefault();let m=r!==void 0?r:ml(a)===ml(h);s(e,{replace:m,state:l,preventScrollReset:o,relative:i,viewTransition:u})}},[a,s,h,r,l,n,e,o,i,u])}/** + */function lo(){return lo=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(n[l]=e[l]);return n}function Nh(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function _h(e,t){return e.button===0&&(!t||t==="_self")&&!Nh(e)}const Ph=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],jh="6";try{window.__reactRouterVersion=jh}catch{}const zh="startTransition",ws=xf[zh];function Lh(e){let{basename:t,children:n,future:r,window:l}=e,i=k.useRef();i.current==null&&(i.current=Rp({window:l,v5Compat:!0}));let o=i.current,[u,s]=k.useState({action:o.action,location:o.location}),{v7_startTransition:a}=r||{},h=k.useCallback(p=>{a&&ws?ws(()=>s(p)):s(p)},[s,a]);return k.useLayoutEffect(()=>o.listen(h),[o,h]),k.useEffect(()=>Sh(r),[r]),k.createElement(kh,{basename:t,children:n,location:u.location,navigationType:u.action,navigator:o,future:r})}const Rh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",Th=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Mh=k.forwardRef(function(t,n){let{onClick:r,relative:l,reloadDocument:i,replace:o,state:u,target:s,to:a,preventScrollReset:h,viewTransition:p}=t,m=Ch(t,Ph),{basename:g}=k.useContext(Ft),w,x=!1;if(typeof a=="string"&&Th.test(a)&&(w=a,Rh))try{let d=new URL(window.location.href),y=a.startsWith("//")?new URL(d.protocol+a):new URL(a),C=tu(y.pathname,g);y.origin===d.origin&&C!=null?a=C+y.search+y.hash:x=!0}catch{}let E=ih(a,{relative:l}),f=Oh(a,{replace:o,state:u,target:s,preventScrollReset:h,relative:l,viewTransition:p});function c(d){r&&r(d),d.defaultPrevented||f(d)}return k.createElement("a",lo({},m,{href:w||E,onClick:x||i?r:c,ref:n,target:s}))});var Ss;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(Ss||(Ss={}));var ks;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(ks||(ks={}));function Oh(e,t){let{target:n,replace:r,state:l,preventScrollReset:i,relative:o,viewTransition:u}=t===void 0?{}:t,s=oh(),a=fr(),h=Hc(e,{relative:o});return k.useCallback(p=>{if(_h(p,n)){p.preventDefault();let m=r!==void 0?r:ml(a)===ml(h);s(e,{replace:m,state:l,preventScrollReset:i,relative:o,viewTransition:u})}},[a,s,h,r,l,n,e,i,o,u])}/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. @@ -74,74 +74,79 @@ Error generating stack: `+o.message+` * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */var Dh={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + */var Fh={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Fh=k.forwardRef(({color:e="currentColor",size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:l="",children:o,iconNode:i,...u},s)=>k.createElement("svg",{ref:s,...Dh,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:Xc("lucide",l),...u},[...i.map(([a,h])=>k.createElement(a,h)),...Array.isArray(o)?o:[o]]));/** + */const Dh=k.forwardRef(({color:e="currentColor",size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:l="",children:i,iconNode:o,...u},s)=>k.createElement("svg",{ref:s,...Fh,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:Xc("lucide",l),...u},[...o.map(([a,h])=>k.createElement(a,h)),...Array.isArray(i)?i:[i]]));/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const je=(e,t)=>{const n=k.forwardRef(({className:r,...l},o)=>k.createElement(Fh,{ref:o,iconNode:t,className:Xc(`lucide-${Ih(e)}`,r),...l}));return n.displayName=`${e}`,n};/** + */const ke=(e,t)=>{const n=k.forwardRef(({className:r,...l},i)=>k.createElement(Dh,{ref:i,iconNode:t,className:Xc(`lucide-${Ih(e)}`,r),...l}));return n.displayName=`${e}`,n};/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Uh=je("Activity",[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]]);/** + */const Uh=ke("Activity",[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Gc=je("Bell",[["path",{d:"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9",key:"1qo2s2"}],["path",{d:"M10.3 21a1.94 1.94 0 0 0 3.4 0",key:"qgo35s"}]]);/** + */const Gc=ke("Bell",[["path",{d:"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9",key:"1qo2s2"}],["path",{d:"M10.3 21a1.94 1.94 0 0 0 3.4 0",key:"qgo35s"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const $h=je("CircleAlert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);/** + */const $h=ke("CircleAlert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Ah=je("CircleCheckBig",[["path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14",key:"g774vq"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]);/** + */const Ah=ke("CircleCheckBig",[["path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14",key:"g774vq"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Zc=je("Cloud",[["path",{d:"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z",key:"p7xjir"}]]);/** + */const Zc=ke("Cloud",[["path",{d:"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z",key:"p7xjir"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Bh=je("Cpu",[["rect",{width:"16",height:"16",x:"4",y:"4",rx:"2",key:"14l7u7"}],["rect",{width:"6",height:"6",x:"9",y:"9",rx:"1",key:"5aljv4"}],["path",{d:"M15 2v2",key:"13l42r"}],["path",{d:"M15 20v2",key:"15mkzm"}],["path",{d:"M2 15h2",key:"1gxd5l"}],["path",{d:"M2 9h2",key:"1bbxkp"}],["path",{d:"M20 15h2",key:"19e6y8"}],["path",{d:"M20 9h2",key:"19tzq7"}],["path",{d:"M9 2v2",key:"165o2o"}],["path",{d:"M9 20v2",key:"i2bqo8"}]]);/** + */const Bh=ke("Cpu",[["rect",{width:"16",height:"16",x:"4",y:"4",rx:"2",key:"14l7u7"}],["rect",{width:"6",height:"6",x:"9",y:"9",rx:"1",key:"5aljv4"}],["path",{d:"M15 2v2",key:"13l42r"}],["path",{d:"M15 20v2",key:"15mkzm"}],["path",{d:"M2 15h2",key:"1gxd5l"}],["path",{d:"M2 9h2",key:"1bbxkp"}],["path",{d:"M20 15h2",key:"19e6y8"}],["path",{d:"M20 9h2",key:"19tzq7"}],["path",{d:"M9 2v2",key:"165o2o"}],["path",{d:"M9 20v2",key:"i2bqo8"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Wh=je("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** + */const Wh=ke("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Vh=je("LayoutDashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/** + */const Vh=ke("LayoutDashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Hh=je("MapPin",[["path",{d:"M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z",key:"2oe9fu"}],["circle",{cx:"12",cy:"10",r:"3",key:"ilqhr7"}]]);/** + */const Hh=ke("MapPin",[["path",{d:"M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z",key:"2oe9fu"}],["circle",{cx:"12",cy:"10",r:"3",key:"ilqhr7"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const ru=je("Radio",[["path",{d:"M4.9 19.1C1 15.2 1 8.8 4.9 4.9",key:"1vaf9d"}],["path",{d:"M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5",key:"u1ii0m"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}],["path",{d:"M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5",key:"1j5fej"}],["path",{d:"M19.1 4.9C23 8.8 23 15.1 19.1 19",key:"10b0cb"}]]);/** + */const ru=ke("Radio",[["path",{d:"M4.9 19.1C1 15.2 1 8.8 4.9 4.9",key:"1vaf9d"}],["path",{d:"M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5",key:"u1ii0m"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}],["path",{d:"M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5",key:"1j5fej"}],["path",{d:"M19.1 4.9C23 8.8 23 15.1 19.1 19",key:"10b0cb"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Jc=je("Settings",[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",key:"1qme2f"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** + */const Jc=ke("Settings",[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",key:"1qme2f"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** * @license lucide-react v0.383.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Qh=je("TriangleAlert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);async function fr(e){const t=await fetch(e);if(!t.ok)throw new Error(`API error: ${t.status} ${t.statusText}`);return t.json()}async function Es(){return fr("/api/status")}async function Kh(){return fr("/api/health")}async function Yh(){return fr("/api/sources")}async function Xh(){return fr("/api/alerts/active")}async function Gh(){return fr("/api/env/status")}function qc(){const[e,t]=k.useState(!1),[n,r]=k.useState(null),[l,o]=k.useState(null),i=k.useRef(null),u=k.useRef(null),s=k.useRef(1e3),a=k.useCallback(()=>{var m;if(((m=i.current)==null?void 0:m.readyState)===WebSocket.OPEN)return;const p=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws/live`;try{const g=new WebSocket(p);i.current=g,g.onopen=()=>{t(!0),s.current=1e3},g.onmessage=y=>{try{const E=JSON.parse(y.data);switch(E.type){case"health_update":r(E.data);break;case"alert_fired":o(E.data);break}}catch(E){console.error("Failed to parse WebSocket message:",E)}},g.onclose=()=>{t(!1),i.current=null;const y=Math.min(s.current,3e4);u.current=window.setTimeout(()=>{s.current=Math.min(y*2,3e4),a()},y)},g.onerror=()=>{g.close()};const x=setInterval(()=>{g.readyState===WebSocket.OPEN&&g.send("ping")},3e4);g.addEventListener("close",()=>{clearInterval(x)})}catch(g){console.error("Failed to create WebSocket:",g)}},[]);return k.useEffect(()=>(a(),()=>{u.current&&clearTimeout(u.current),i.current&&i.current.close()}),[a]),{connected:e,lastHealth:n,lastAlert:l}}const bc=[{path:"/",label:"Dashboard",icon:Vh},{path:"/mesh",label:"Mesh",icon:ru},{path:"/environment",label:"Environment",icon:Zc},{path:"/config",label:"Config",icon:Jc},{path:"/alerts",label:"Alerts",icon:Gc}];function Zh(e){const t=Math.floor(e/86400),n=Math.floor(e%86400/3600),r=Math.floor(e%3600/60);return t>0?`${t}d ${n}h`:n>0?`${n}h ${r}m`:`${r}m`}function Jh(e){const t=bc.find(n=>n.path===e);return(t==null?void 0:t.label)||"Dashboard"}function qh({children:e}){var s;const t=cr(),{connected:n}=qc(),[r,l]=k.useState(null),[o,i]=k.useState(new Date);k.useEffect(()=>{Es().then(l).catch(console.error);const a=setInterval(()=>{Es().then(l).catch(console.error)},3e4);return()=>clearInterval(a)},[]),k.useEffect(()=>{const a=setInterval(()=>i(new Date),1e3);return()=>clearInterval(a)},[]);const u=o.toLocaleTimeString("en-US",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"});return v.jsxs("div",{className:"flex h-screen overflow-hidden bg-bg text-slate-200",children:[v.jsxs("aside",{className:"w-[220px] flex-shrink-0 bg-bg-card border-r border-border flex flex-col overflow-y-auto",children:[v.jsx("div",{className:"p-5 border-b border-border",children:v.jsxs("div",{className:"flex items-center gap-3",children:[v.jsx("div",{className:"w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center text-white font-bold text-xl",children:"M"}),v.jsxs("div",{children:[v.jsx("div",{className:"font-semibold text-lg",children:"MeshAI"}),v.jsxs("div",{className:"text-xs text-slate-500 font-mono",children:["v",(r==null?void 0:r.version)||"..."]})]})]})}),v.jsx("nav",{className:"flex-1 py-4",children:bc.map(a=>{const h=t.pathname===a.path,p=a.icon;return v.jsxs(Mh,{to:a.path,className:`flex items-center gap-3 px-5 py-3 text-sm transition-colors relative ${h?"text-blue-400 bg-blue-500/10":"text-slate-400 hover:text-slate-200 hover:bg-bg-hover"}`,children:[h&&v.jsx("div",{className:"absolute left-0 top-0 bottom-0 w-0.5 bg-blue-500"}),v.jsx(p,{size:18}),a.label]},a.path)})}),v.jsxs("div",{className:"p-5 border-t border-border",children:[v.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[v.jsx("div",{className:`w-2 h-2 rounded-full ${r!=null&&r.connected?"bg-green-500":"bg-red-500"}`}),v.jsx("span",{className:"text-xs text-slate-400",children:r!=null&&r.connected?"Connected":"Disconnected"})]}),v.jsxs("div",{className:"text-xs text-slate-500 font-mono truncate",children:[(s=r==null?void 0:r.connection_type)==null?void 0:s.toUpperCase(),": ",r==null?void 0:r.connection_target]}),v.jsxs("div",{className:"text-xs text-slate-500 mt-1",children:["Uptime: ",r?Zh(r.uptime_seconds):"..."]})]})]}),v.jsxs("div",{className:"flex-1 flex flex-col overflow-hidden",children:[v.jsxs("header",{className:"h-14 flex-shrink-0 border-b border-border bg-bg-card flex items-center justify-between px-6",children:[v.jsx("h1",{className:"text-lg font-semibold",children:Jh(t.pathname)}),v.jsxs("div",{className:"flex items-center gap-6",children:[v.jsxs("div",{className:"flex items-center gap-2",children:[v.jsx("div",{className:`w-2 h-2 rounded-full ${n?"bg-green-500 animate-pulse-slow":"bg-slate-500"}`}),v.jsx("span",{className:"text-xs text-slate-400",children:n?"Live":"Offline"})]}),v.jsxs("div",{className:"text-sm font-mono text-slate-400",children:[u," MT"]})]})]}),v.jsx("main",{className:"flex-1 overflow-y-auto p-6",children:e})]})]})}function bh({health:e}){const t=e.score,n=e.tier,l=(u=>u>=80?"#22c55e":u>=60?"#f59e0b":"#ef4444")(t),o=2*Math.PI*45,i=t/100*o;return v.jsx("div",{className:"flex flex-col items-center",children:v.jsxs("svg",{width:"140",height:"140",viewBox:"0 0 100 100",children:[v.jsx("circle",{cx:"50",cy:"50",r:"45",fill:"none",stroke:"#1e2a3a",strokeWidth:"8"}),v.jsx("circle",{cx:"50",cy:"50",r:"45",fill:"none",stroke:l,strokeWidth:"8",strokeLinecap:"round",strokeDasharray:o,strokeDashoffset:o-i,transform:"rotate(-90 50 50)",className:"transition-all duration-500"}),v.jsx("text",{x:"50",y:"46",textAnchor:"middle",className:"fill-slate-100 font-mono text-2xl font-bold",style:{fontSize:"24px"},children:t.toFixed(1)}),v.jsx("text",{x:"50",y:"62",textAnchor:"middle",className:"fill-slate-400 text-xs",style:{fontSize:"10px"},children:n})]})})}function Rr({label:e,value:t}){const n=r=>r>=80?"bg-green-500":r>=60?"bg-amber-500":"bg-red-500";return v.jsxs("div",{className:"flex items-center gap-3",children:[v.jsx("div",{className:"w-24 text-xs text-slate-400 truncate",children:e}),v.jsx("div",{className:"flex-1 h-2 bg-border rounded-full overflow-hidden",children:v.jsx("div",{className:`h-full ${n(t)} transition-all duration-300`,style:{width:`${t}%`}})}),v.jsx("div",{className:"w-12 text-right text-xs font-mono text-slate-300",children:t.toFixed(1)})]})}function em({alert:e}){const n=(l=>{switch(l.toLowerCase()){case"critical":case"emergency":return{bg:"bg-red-500/10",border:"border-red-500",icon:$h,iconColor:"text-red-500"};case"warning":return{bg:"bg-amber-500/10",border:"border-amber-500",icon:Qh,iconColor:"text-amber-500"};default:return{bg:"bg-green-500/10",border:"border-green-500",icon:Wh,iconColor:"text-green-500"}}})(e.severity),r=n.icon;return v.jsxs("div",{className:`p-3 rounded-lg ${n.bg} border-l-2 ${n.border} flex items-start gap-3`,children:[v.jsx(r,{size:16,className:n.iconColor}),v.jsxs("div",{className:"flex-1 min-w-0",children:[v.jsx("div",{className:"text-sm text-slate-200",children:e.message}),v.jsx("div",{className:"text-xs text-slate-500 mt-1",children:e.timestamp||"Just now"})]})]})}function tm({source:e}){const t=()=>e.is_loaded?e.last_error?"bg-amber-500":"bg-green-500":"bg-red-500";return v.jsxs("div",{className:"flex items-center gap-3 p-3 rounded-lg bg-bg-hover",children:[v.jsx("div",{className:`w-2 h-2 rounded-full ${t()}`}),v.jsxs("div",{className:"flex-1 min-w-0",children:[v.jsx("div",{className:"text-sm text-slate-200 truncate",children:e.name}),v.jsxs("div",{className:"text-xs text-slate-500",children:[e.node_count," nodes • ",e.type]})]})]})}function Tr({icon:e,label:t,value:n,subvalue:r}){return v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-4",children:[v.jsxs("div",{className:"flex items-center gap-2 text-slate-400 mb-2",children:[v.jsx(e,{size:14}),v.jsx("span",{className:"text-xs",children:t})]}),v.jsx("div",{className:"font-mono text-xl text-slate-100",children:n}),r&&v.jsx("div",{className:"text-xs text-slate-500 mt-1",children:r})]})}function nm(){var g;const[e,t]=k.useState(null),[n,r]=k.useState([]),[l,o]=k.useState([]),[i,u]=k.useState(null),[s,a]=k.useState(!0),[h,p]=k.useState(null),{lastHealth:m}=qc();return k.useEffect(()=>{Promise.all([Kh(),Yh(),Xh(),Gh()]).then(([x,y,E,f])=>{t(x),r(y),o(E),u(f),a(!1)}).catch(x=>{p(x.message),a(!1)})},[]),k.useEffect(()=>{m&&t(m)},[m]),s?v.jsx("div",{className:"flex items-center justify-center h-64",children:v.jsx("div",{className:"text-slate-400",children:"Loading..."})}):h?v.jsx("div",{className:"flex items-center justify-center h-64",children:v.jsxs("div",{className:"text-red-400",children:["Error: ",h]})}):v.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-3 gap-6",children:[v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsx("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:"Mesh Health"}),e&&v.jsxs(v.Fragment,{children:[v.jsx(bh,{health:e}),v.jsxs("div",{className:"mt-6 space-y-3",children:[v.jsx(Rr,{label:"Infrastructure",value:e.pillars.infrastructure}),v.jsx(Rr,{label:"Utilization",value:e.pillars.utilization}),v.jsx(Rr,{label:"Behavior",value:e.pillars.behavior}),v.jsx(Rr,{label:"Power",value:e.pillars.power})]})]})]}),v.jsxs("div",{className:"lg:col-span-2 space-y-6",children:[v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsx("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:"Active Alerts"}),l.length>0?v.jsx("div",{className:"space-y-3",children:l.map((x,y)=>v.jsx(em,{alert:x},y))}):v.jsxs("div",{className:"flex items-center gap-2 text-slate-500 py-4",children:[v.jsx(Ah,{size:16,className:"text-green-500"}),v.jsx("span",{children:"No active alerts"})]})]}),v.jsxs("div",{className:"grid grid-cols-2 lg:grid-cols-4 gap-4",children:[v.jsx(Tr,{icon:ru,label:"Nodes Online",value:(e==null?void 0:e.total_nodes)||0,subvalue:`${(e==null?void 0:e.unlocated_count)||0} unlocated`}),v.jsx(Tr,{icon:Bh,label:"Infrastructure",value:`${(e==null?void 0:e.infra_online)||0}/${(e==null?void 0:e.infra_total)||0}`,subvalue:(e==null?void 0:e.infra_online)===(e==null?void 0:e.infra_total)?"All online":"Some offline"}),v.jsx(Tr,{icon:Uh,label:"Utilization",value:`${((g=e==null?void 0:e.util_percent)==null?void 0:g.toFixed(1))||0}%`,subvalue:`${(e==null?void 0:e.flagged_nodes)||0} flagged`}),v.jsx(Tr,{icon:Hh,label:"Regions",value:(e==null?void 0:e.total_regions)||0,subvalue:`${(e==null?void 0:e.battery_warnings)||0} battery warnings`})]})]}),v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsxs("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:["Mesh Sources (",n.length,")"]}),n.length>0?v.jsx("div",{className:"space-y-2",children:n.map((x,y)=>v.jsx(tm,{source:x},y))}):v.jsx("div",{className:"text-slate-500 py-4",children:"No sources configured"})]}),v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsx("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:"Environmental Feeds"}),i!=null&&i.enabled?v.jsxs("div",{className:"text-slate-400",children:[i.feeds.length," feeds active"]}):v.jsxs("div",{className:"text-slate-500",children:[v.jsx("p",{children:"Environmental feeds not enabled."}),v.jsx("p",{className:"text-xs mt-2",children:"Enable in Config → Mesh Intelligence"})]})]}),v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsx("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:"HF Propagation"}),v.jsxs("div",{className:"text-slate-500",children:[v.jsx("p",{children:"Space weather data not enabled."}),v.jsx("p",{className:"text-xs mt-2",children:"Coming in Phase 1"})]})]})]})}function rm(){return v.jsxs("div",{className:"flex flex-col items-center justify-center h-[60vh] text-center",children:[v.jsx("div",{className:"w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6",children:v.jsx(ru,{size:32,className:"text-slate-500"})}),v.jsx("h2",{className:"text-xl font-semibold text-slate-300 mb-2",children:"Mesh"}),v.jsx("p",{className:"text-slate-500 max-w-md",children:"Topology graph and geographic map coming in Phase 6"})]})}function lm(){return v.jsxs("div",{className:"flex flex-col items-center justify-center h-[60vh] text-center",children:[v.jsx("div",{className:"w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6",children:v.jsx(Zc,{size:32,className:"text-slate-500"})}),v.jsx("h2",{className:"text-xl font-semibold text-slate-300 mb-2",children:"Environment"}),v.jsx("p",{className:"text-slate-500 max-w-md",children:"Environmental feeds and space weather detail coming soon"})]})}function om(){return v.jsxs("div",{className:"flex flex-col items-center justify-center h-[60vh] text-center",children:[v.jsx("div",{className:"w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6",children:v.jsx(Jc,{size:32,className:"text-slate-500"})}),v.jsx("h2",{className:"text-xl font-semibold text-slate-300 mb-2",children:"Configuration"}),v.jsx("p",{className:"text-slate-500 max-w-md",children:"Configuration management coming in Phase 5"})]})}function im(){return v.jsxs("div",{className:"flex flex-col items-center justify-center h-[60vh] text-center",children:[v.jsx("div",{className:"w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6",children:v.jsx(Gc,{size:32,className:"text-slate-500"})}),v.jsx("h2",{className:"text-xl font-semibold text-slate-300 mb-2",children:"Alerts"}),v.jsx("p",{className:"text-slate-500 max-w-md",children:"Alert history and subscriptions coming in Phase 11"})]})}function um(){return v.jsx(qh,{children:v.jsxs(Eh,{children:[v.jsx($t,{path:"/",element:v.jsx(nm,{})}),v.jsx($t,{path:"/mesh",element:v.jsx(rm,{})}),v.jsx($t,{path:"/environment",element:v.jsx(lm,{})}),v.jsx($t,{path:"/config",element:v.jsx(om,{})}),v.jsx($t,{path:"/alerts",element:v.jsx(im,{})})]})})}ao.createRoot(document.getElementById("root")).render(v.jsx(Os.StrictMode,{children:v.jsx(Lh,{children:v.jsx(um,{})})})); + */const Qh=ke("TriangleAlert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);/** + * @license lucide-react v0.383.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Kh=ke("Zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]]);async function vn(e){const t=await fetch(e);if(!t.ok)throw new Error(`API error: ${t.status} ${t.statusText}`);return t.json()}async function Es(){return vn("/api/status")}async function Yh(){return vn("/api/health")}async function Xh(){return vn("/api/sources")}async function Gh(){return vn("/api/alerts/active")}async function Zh(){return vn("/api/env/status")}async function Jh(){return vn("/api/env/propagation")}function qc(){const[e,t]=k.useState(!1),[n,r]=k.useState(null),[l,i]=k.useState(null),o=k.useRef(null),u=k.useRef(null),s=k.useRef(1e3),a=k.useCallback(()=>{var m;if(((m=o.current)==null?void 0:m.readyState)===WebSocket.OPEN)return;const p=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws/live`;try{const g=new WebSocket(p);o.current=g,g.onopen=()=>{t(!0),s.current=1e3},g.onmessage=x=>{try{const E=JSON.parse(x.data);switch(E.type){case"health_update":r(E.data);break;case"alert_fired":i(E.data);break}}catch(E){console.error("Failed to parse WebSocket message:",E)}},g.onclose=()=>{t(!1),o.current=null;const x=Math.min(s.current,3e4);u.current=window.setTimeout(()=>{s.current=Math.min(x*2,3e4),a()},x)},g.onerror=()=>{g.close()};const w=setInterval(()=>{g.readyState===WebSocket.OPEN&&g.send("ping")},3e4);g.addEventListener("close",()=>{clearInterval(w)})}catch(g){console.error("Failed to create WebSocket:",g)}},[]);return k.useEffect(()=>(a(),()=>{u.current&&clearTimeout(u.current),o.current&&o.current.close()}),[a]),{connected:e,lastHealth:n,lastAlert:l}}const bc=[{path:"/",label:"Dashboard",icon:Vh},{path:"/mesh",label:"Mesh",icon:ru},{path:"/environment",label:"Environment",icon:Zc},{path:"/config",label:"Config",icon:Jc},{path:"/alerts",label:"Alerts",icon:Gc}];function qh(e){const t=Math.floor(e/86400),n=Math.floor(e%86400/3600),r=Math.floor(e%3600/60);return t>0?`${t}d ${n}h`:n>0?`${n}h ${r}m`:`${r}m`}function bh(e){const t=bc.find(n=>n.path===e);return(t==null?void 0:t.label)||"Dashboard"}function em({children:e}){var s;const t=fr(),{connected:n}=qc(),[r,l]=k.useState(null),[i,o]=k.useState(new Date);k.useEffect(()=>{Es().then(l).catch(console.error);const a=setInterval(()=>{Es().then(l).catch(console.error)},3e4);return()=>clearInterval(a)},[]),k.useEffect(()=>{const a=setInterval(()=>o(new Date),1e3);return()=>clearInterval(a)},[]);const u=i.toLocaleTimeString("en-US",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"});return v.jsxs("div",{className:"flex h-screen overflow-hidden bg-bg text-slate-200",children:[v.jsxs("aside",{className:"w-[220px] flex-shrink-0 bg-bg-card border-r border-border flex flex-col overflow-y-auto",children:[v.jsx("div",{className:"p-5 border-b border-border",children:v.jsxs("div",{className:"flex items-center gap-3",children:[v.jsx("div",{className:"w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center text-white font-bold text-xl",children:"M"}),v.jsxs("div",{children:[v.jsx("div",{className:"font-semibold text-lg",children:"MeshAI"}),v.jsxs("div",{className:"text-xs text-slate-500 font-mono",children:["v",(r==null?void 0:r.version)||"..."]})]})]})}),v.jsx("nav",{className:"flex-1 py-4",children:bc.map(a=>{const h=t.pathname===a.path,p=a.icon;return v.jsxs(Mh,{to:a.path,className:`flex items-center gap-3 px-5 py-3 text-sm transition-colors relative ${h?"text-blue-400 bg-blue-500/10":"text-slate-400 hover:text-slate-200 hover:bg-bg-hover"}`,children:[h&&v.jsx("div",{className:"absolute left-0 top-0 bottom-0 w-0.5 bg-blue-500"}),v.jsx(p,{size:18}),a.label]},a.path)})}),v.jsxs("div",{className:"p-5 border-t border-border",children:[v.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[v.jsx("div",{className:`w-2 h-2 rounded-full ${r!=null&&r.connected?"bg-green-500":"bg-red-500"}`}),v.jsx("span",{className:"text-xs text-slate-400",children:r!=null&&r.connected?"Connected":"Disconnected"})]}),v.jsxs("div",{className:"text-xs text-slate-500 font-mono truncate",children:[(s=r==null?void 0:r.connection_type)==null?void 0:s.toUpperCase(),": ",r==null?void 0:r.connection_target]}),v.jsxs("div",{className:"text-xs text-slate-500 mt-1",children:["Uptime: ",r?qh(r.uptime_seconds):"..."]})]})]}),v.jsxs("div",{className:"flex-1 flex flex-col overflow-hidden",children:[v.jsxs("header",{className:"h-14 flex-shrink-0 border-b border-border bg-bg-card flex items-center justify-between px-6",children:[v.jsx("h1",{className:"text-lg font-semibold",children:bh(t.pathname)}),v.jsxs("div",{className:"flex items-center gap-6",children:[v.jsxs("div",{className:"flex items-center gap-2",children:[v.jsx("div",{className:`w-2 h-2 rounded-full ${n?"bg-green-500 animate-pulse-slow":"bg-slate-500"}`}),v.jsx("span",{className:"text-xs text-slate-400",children:n?"Live":"Offline"})]}),v.jsxs("div",{className:"text-sm font-mono text-slate-400",children:[u," MT"]})]})]}),v.jsx("main",{className:"flex-1 overflow-y-auto p-6",children:e})]})]})}function tm({health:e}){const t=e.score,n=e.tier,l=(u=>u>=80?"#22c55e":u>=60?"#f59e0b":"#ef4444")(t),i=2*Math.PI*45,o=t/100*i;return v.jsx("div",{className:"flex flex-col items-center",children:v.jsxs("svg",{width:"140",height:"140",viewBox:"0 0 100 100",children:[v.jsx("circle",{cx:"50",cy:"50",r:"45",fill:"none",stroke:"#1e2a3a",strokeWidth:"8"}),v.jsx("circle",{cx:"50",cy:"50",r:"45",fill:"none",stroke:l,strokeWidth:"8",strokeLinecap:"round",strokeDasharray:i,strokeDashoffset:i-o,transform:"rotate(-90 50 50)",className:"transition-all duration-500"}),v.jsx("text",{x:"50",y:"46",textAnchor:"middle",className:"fill-slate-100 font-mono text-2xl font-bold",style:{fontSize:"24px"},children:t.toFixed(1)}),v.jsx("text",{x:"50",y:"62",textAnchor:"middle",className:"fill-slate-400 text-xs",style:{fontSize:"10px"},children:n})]})})}function Rr({label:e,value:t}){const n=r=>r>=80?"bg-green-500":r>=60?"bg-amber-500":"bg-red-500";return v.jsxs("div",{className:"flex items-center gap-3",children:[v.jsx("div",{className:"w-24 text-xs text-slate-400 truncate",children:e}),v.jsx("div",{className:"flex-1 h-2 bg-border rounded-full overflow-hidden",children:v.jsx("div",{className:`h-full ${n(t)} transition-all duration-300`,style:{width:`${t}%`}})}),v.jsx("div",{className:"w-12 text-right text-xs font-mono text-slate-300",children:t.toFixed(1)})]})}function nm({alert:e}){const n=(l=>{switch(l.toLowerCase()){case"critical":case"emergency":return{bg:"bg-red-500/10",border:"border-red-500",icon:$h,iconColor:"text-red-500"};case"warning":return{bg:"bg-amber-500/10",border:"border-amber-500",icon:Qh,iconColor:"text-amber-500"};default:return{bg:"bg-green-500/10",border:"border-green-500",icon:Wh,iconColor:"text-green-500"}}})(e.severity),r=n.icon;return v.jsxs("div",{className:`p-3 rounded-lg ${n.bg} border-l-2 ${n.border} flex items-start gap-3`,children:[v.jsx(r,{size:16,className:n.iconColor}),v.jsxs("div",{className:"flex-1 min-w-0",children:[v.jsx("div",{className:"text-sm text-slate-200",children:e.message}),v.jsx("div",{className:"text-xs text-slate-500 mt-1",children:e.timestamp||"Just now"})]})]})}function rm({source:e}){const t=()=>e.is_loaded?e.last_error?"bg-amber-500":"bg-green-500":"bg-red-500";return v.jsxs("div",{className:"flex items-center gap-3 p-3 rounded-lg bg-bg-hover",children:[v.jsx("div",{className:`w-2 h-2 rounded-full ${t()}`}),v.jsxs("div",{className:"flex-1 min-w-0",children:[v.jsx("div",{className:"text-sm text-slate-200 truncate",children:e.name}),v.jsxs("div",{className:"text-xs text-slate-500",children:[e.node_count," nodes * ",e.type]})]})]})}function Tr({icon:e,label:t,value:n,subvalue:r}){return v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-4",children:[v.jsxs("div",{className:"flex items-center gap-2 text-slate-400 mb-2",children:[v.jsx(e,{size:14}),v.jsx("span",{className:"text-xs",children:t})]}),v.jsx("div",{className:"font-mono text-xl text-slate-100",children:n}),r&&v.jsx("div",{className:"text-xs text-slate-500 mt-1",children:r})]})}function lm({propagation:e}){var u,s,a;if(!e)return v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsx("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:"RF Propagation"}),v.jsx("div",{className:"text-slate-500",children:v.jsx("p",{children:"Loading propagation data..."})})]});const t=e.hf,n=e.uhf_ducting,r=h=>{if(!h)return"text-slate-400";switch(h.toLowerCase()){case"excellent":return"text-green-400";case"good":return"text-green-500";case"fair":return"text-amber-500";case"poor":return"text-red-500";default:return"text-slate-400"}},l=h=>{if(!h)return"text-slate-400";switch(h){case"normal":return"text-green-500";case"super_refraction":return"text-amber-500";case"surface_duct":case"elevated_duct":return"text-blue-400";default:return"text-slate-400"}},i=t&&(t.band_assessment||t.sfi||t.kp_current!==void 0),o=n&&n.condition;return v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsxs("h2",{className:"text-sm font-medium text-slate-400 mb-4 flex items-center gap-2",children:[v.jsx(Kh,{size:14}),"RF Propagation"]}),v.jsxs("div",{className:"mb-4",children:[v.jsx("div",{className:"text-xs text-slate-500 mb-1",children:"HF Bands"}),i?v.jsxs("div",{className:"space-y-1",children:[v.jsx("div",{className:`text-sm font-medium ${r(t.band_assessment)}`,children:t.band_assessment||"Unknown"}),v.jsxs("div",{className:"text-xs text-slate-400",children:["SFI ",((u=t.sfi)==null?void 0:u.toFixed(0))||"?"," / Kp ",((s=t.kp_current)==null?void 0:s.toFixed(1))||"?"]}),t.r_scale!==void 0&&t.r_scale>0&&v.jsxs("div",{className:"text-xs text-amber-500",children:["R",t.r_scale," Radio Blackout"]})]}):v.jsx("div",{className:"text-sm text-slate-500",children:"No HF data"})]}),v.jsxs("div",{children:[v.jsx("div",{className:"text-xs text-slate-500 mb-1",children:"UHF 906 MHz"}),o?v.jsxs("div",{className:"space-y-1",children:[v.jsx("div",{className:`text-sm font-medium ${l(n.condition)}`,children:n.condition==="normal"?"Normal":(a=n.condition)==null?void 0:a.replace("_"," ").replace(/\b\w/g,h=>h.toUpperCase())}),n.condition!=="normal"&&n.min_gradient!==void 0&&v.jsxs("div",{className:"text-xs text-slate-400",children:["dM/dz: ",n.min_gradient," M-units/km"]}),n.condition!=="normal"&&v.jsx("div",{className:"text-xs text-blue-400",children:"Extended range likely"})]}):v.jsx("div",{className:"text-sm text-slate-500",children:"No ducting data"})]})]})}function im(){var x;const[e,t]=k.useState(null),[n,r]=k.useState([]),[l,i]=k.useState([]),[o,u]=k.useState(null),[s,a]=k.useState(null),[h,p]=k.useState(!0),[m,g]=k.useState(null),{lastHealth:w}=qc();return k.useEffect(()=>{Promise.all([Yh(),Xh(),Gh(),Zh(),Jh().catch(()=>null)]).then(([E,f,c,d,y])=>{t(E),r(f),i(c),u(d),a(y),p(!1)}).catch(E=>{g(E.message),p(!1)})},[]),k.useEffect(()=>{w&&t(w)},[w]),h?v.jsx("div",{className:"flex items-center justify-center h-64",children:v.jsx("div",{className:"text-slate-400",children:"Loading..."})}):m?v.jsx("div",{className:"flex items-center justify-center h-64",children:v.jsxs("div",{className:"text-red-400",children:["Error: ",m]})}):v.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-3 gap-6",children:[v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsx("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:"Mesh Health"}),e&&v.jsxs(v.Fragment,{children:[v.jsx(tm,{health:e}),v.jsxs("div",{className:"mt-6 space-y-3",children:[v.jsx(Rr,{label:"Infrastructure",value:e.pillars.infrastructure}),v.jsx(Rr,{label:"Utilization",value:e.pillars.utilization}),v.jsx(Rr,{label:"Behavior",value:e.pillars.behavior}),v.jsx(Rr,{label:"Power",value:e.pillars.power})]})]})]}),v.jsxs("div",{className:"lg:col-span-2 space-y-6",children:[v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsx("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:"Active Alerts"}),l.length>0?v.jsx("div",{className:"space-y-3",children:l.map((E,f)=>v.jsx(nm,{alert:E},f))}):v.jsxs("div",{className:"flex items-center gap-2 text-slate-500 py-4",children:[v.jsx(Ah,{size:16,className:"text-green-500"}),v.jsx("span",{children:"No active alerts"})]})]}),v.jsxs("div",{className:"grid grid-cols-2 lg:grid-cols-4 gap-4",children:[v.jsx(Tr,{icon:ru,label:"Nodes Online",value:(e==null?void 0:e.total_nodes)||0,subvalue:`${(e==null?void 0:e.unlocated_count)||0} unlocated`}),v.jsx(Tr,{icon:Bh,label:"Infrastructure",value:`${(e==null?void 0:e.infra_online)||0}/${(e==null?void 0:e.infra_total)||0}`,subvalue:(e==null?void 0:e.infra_online)===(e==null?void 0:e.infra_total)?"All online":"Some offline"}),v.jsx(Tr,{icon:Uh,label:"Utilization",value:`${((x=e==null?void 0:e.util_percent)==null?void 0:x.toFixed(1))||0}%`,subvalue:`${(e==null?void 0:e.flagged_nodes)||0} flagged`}),v.jsx(Tr,{icon:Hh,label:"Regions",value:(e==null?void 0:e.total_regions)||0,subvalue:`${(e==null?void 0:e.battery_warnings)||0} battery warnings`})]})]}),v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsxs("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:["Mesh Sources (",n.length,")"]}),n.length>0?v.jsx("div",{className:"space-y-2",children:n.map((E,f)=>v.jsx(rm,{source:E},f))}):v.jsx("div",{className:"text-slate-500 py-4",children:"No sources configured"})]}),v.jsxs("div",{className:"bg-bg-card border border-border rounded-lg p-6",children:[v.jsx("h2",{className:"text-sm font-medium text-slate-400 mb-4",children:"Environmental Feeds"}),o!=null&&o.enabled?v.jsxs("div",{className:"text-slate-400",children:[o.feeds.length," feeds active"]}):v.jsxs("div",{className:"text-slate-500",children:[v.jsx("p",{children:"Environmental feeds not enabled."}),v.jsx("p",{className:"text-xs mt-2",children:"Enable in config.yaml"})]})]}),v.jsx(lm,{propagation:s})]})}function om(){return v.jsxs("div",{className:"flex flex-col items-center justify-center h-[60vh] text-center",children:[v.jsx("div",{className:"w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6",children:v.jsx(ru,{size:32,className:"text-slate-500"})}),v.jsx("h2",{className:"text-xl font-semibold text-slate-300 mb-2",children:"Mesh"}),v.jsx("p",{className:"text-slate-500 max-w-md",children:"Topology graph and geographic map coming in Phase 6"})]})}function um(){return v.jsxs("div",{className:"flex flex-col items-center justify-center h-[60vh] text-center",children:[v.jsx("div",{className:"w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6",children:v.jsx(Zc,{size:32,className:"text-slate-500"})}),v.jsx("h2",{className:"text-xl font-semibold text-slate-300 mb-2",children:"Environment"}),v.jsx("p",{className:"text-slate-500 max-w-md",children:"Environmental feeds and space weather detail coming soon"})]})}function sm(){return v.jsxs("div",{className:"flex flex-col items-center justify-center h-[60vh] text-center",children:[v.jsx("div",{className:"w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6",children:v.jsx(Jc,{size:32,className:"text-slate-500"})}),v.jsx("h2",{className:"text-xl font-semibold text-slate-300 mb-2",children:"Configuration"}),v.jsx("p",{className:"text-slate-500 max-w-md",children:"Configuration management coming in Phase 5"})]})}function am(){return v.jsxs("div",{className:"flex flex-col items-center justify-center h-[60vh] text-center",children:[v.jsx("div",{className:"w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6",children:v.jsx(Gc,{size:32,className:"text-slate-500"})}),v.jsx("h2",{className:"text-xl font-semibold text-slate-300 mb-2",children:"Alerts"}),v.jsx("p",{className:"text-slate-500 max-w-md",children:"Alert history and subscriptions coming in Phase 11"})]})}function cm(){return v.jsx(em,{children:v.jsxs(Eh,{children:[v.jsx($t,{path:"/",element:v.jsx(im,{})}),v.jsx($t,{path:"/mesh",element:v.jsx(om,{})}),v.jsx($t,{path:"/environment",element:v.jsx(um,{})}),v.jsx($t,{path:"/config",element:v.jsx(sm,{})}),v.jsx($t,{path:"/alerts",element:v.jsx(am,{})})]})})}ai.createRoot(document.getElementById("root")).render(v.jsx(Os.StrictMode,{children:v.jsx(Lh,{children:v.jsx(cm,{})})})); diff --git a/meshai/dashboard/static/assets/index-DKYlTqQ1.css b/meshai/dashboard/static/assets/index-DKYlTqQ1.css new file mode 100644 index 0000000..f7edcdd --- /dev/null +++ b/meshai/dashboard/static/assets/index-DKYlTqQ1.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.absolute{position:absolute}.relative{position:relative}.bottom-0{bottom:0}.left-0{left:0}.top-0{top:0}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.grid{display:grid}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-64{height:16rem}.h-\[60vh\]{height:60vh}.h-full{height:100%}.h-screen{height:100vh}.w-0\.5{width:.125rem}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-24{width:6rem}.w-\[220px\]{width:220px}.min-w-0{min-width:0px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-500{--tw-border-opacity: 1;border-color:rgb(245 158 11 / var(--tw-border-opacity, 1))}.border-border{--tw-border-opacity: 1;border-color:rgb(30 42 58 / var(--tw-border-opacity, 1))}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-bg{--tw-bg-opacity: 1;background-color:rgb(10 14 23 / var(--tw-bg-opacity, 1))}.bg-bg-card{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-bg-hover{--tw-bg-opacity: 1;background-color:rgb(26 35 50 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-border{--tw-bg-opacity: 1;background-color:rgb(30 42 58 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-slate-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity, 1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-blue-700{--tw-gradient-to: #1d4ed8 var(--tw-gradient-to-position)}.fill-slate-100{fill:#f1f5f9}.fill-slate-400{fill:#94a3b8}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity, 1))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}body{background:#0a0e17;margin:0;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#0a0e17}::-webkit-scrollbar-thumb{background:#2d3a4d;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#3b4a5d}.font-mono{font-family:JetBrains Mono,monospace}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse-slow{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.hover\:bg-bg-hover:hover{--tw-bg-opacity: 1;background-color:rgb(26 35 50 / var(--tw-bg-opacity, 1))}.hover\:text-slate-200:hover{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}@media (min-width: 1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} diff --git a/meshai/dashboard/static/assets/index-DdqEB3wX.css b/meshai/dashboard/static/assets/index-DdqEB3wX.css deleted file mode 100644 index 38bf7bc..0000000 --- a/meshai/dashboard/static/assets/index-DdqEB3wX.css +++ /dev/null @@ -1 +0,0 @@ -*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.absolute{position:absolute}.relative{position:relative}.bottom-0{bottom:0}.left-0{left:0}.top-0{top:0}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.grid{display:grid}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-64{height:16rem}.h-\[60vh\]{height:60vh}.h-full{height:100%}.h-screen{height:100vh}.w-0\.5{width:.125rem}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-24{width:6rem}.w-\[220px\]{width:220px}.min-w-0{min-width:0px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-500{--tw-border-opacity: 1;border-color:rgb(245 158 11 / var(--tw-border-opacity, 1))}.border-border{--tw-border-opacity: 1;border-color:rgb(30 42 58 / var(--tw-border-opacity, 1))}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-bg{--tw-bg-opacity: 1;background-color:rgb(10 14 23 / var(--tw-bg-opacity, 1))}.bg-bg-card{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-bg-hover{--tw-bg-opacity: 1;background-color:rgb(26 35 50 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-border{--tw-bg-opacity: 1;background-color:rgb(30 42 58 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-slate-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity, 1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-blue-700{--tw-gradient-to: #1d4ed8 var(--tw-gradient-to-position)}.fill-slate-100{fill:#f1f5f9}.fill-slate-400{fill:#94a3b8}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity, 1))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}body{background:#0a0e17;margin:0;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#0a0e17}::-webkit-scrollbar-thumb{background:#2d3a4d;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#3b4a5d}.font-mono{font-family:JetBrains Mono,monospace}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse-slow{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.hover\:bg-bg-hover:hover{--tw-bg-opacity: 1;background-color:rgb(26 35 50 / var(--tw-bg-opacity, 1))}.hover\:text-slate-200:hover{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}@media (min-width: 1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} diff --git a/meshai/dashboard/static/index.html b/meshai/dashboard/static/index.html index b94443a..e4791aa 100644 --- a/meshai/dashboard/static/index.html +++ b/meshai/dashboard/static/index.html @@ -8,8 +8,8 @@ - - + +
diff --git a/meshai/env/__init__.py b/meshai/env/__init__.py new file mode 100644 index 0000000..9d1d379 --- /dev/null +++ b/meshai/env/__init__.py @@ -0,0 +1 @@ +"""Environmental feeds package.""" diff --git a/meshai/env/ducting.py b/meshai/env/ducting.py new file mode 100644 index 0000000..ef5ea91 --- /dev/null +++ b/meshai/env/ducting.py @@ -0,0 +1,273 @@ +"""Tropospheric ducting assessment adapter using Open-Meteo GFS.""" + +import json +import logging +import math +import time +from datetime import datetime +from typing import TYPE_CHECKING +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +if TYPE_CHECKING: + from ..config import DuctingConfig + +logger = logging.getLogger(__name__) + + +# Pressure levels and approximate heights (meters) +PRESSURE_LEVELS = { + 1000: 110, # ~110m + 925: 760, # ~760m + 850: 1500, # ~1500m + 700: 3000, # ~3000m +} + + +class DuctingAdapter: + """Tropospheric ducting assessment from Open-Meteo GFS pressure levels.""" + + def __init__(self, config: "DuctingConfig"): + self._lat = config.latitude or 42.56 + self._lon = config.longitude or -114.47 + self._tick_interval = config.tick_seconds or 10800 # 3 hours + self._last_tick = 0.0 + self._status = {} + self._consecutive_errors = 0 + self._last_error = None + self._is_loaded = False + + def tick(self) -> bool: + """Execute one polling tick. + + Returns: + True if data changed + """ + now = time.time() + + # Check tick interval + if now - self._last_tick < self._tick_interval: + return False + + self._last_tick = now + return self._fetch() + + def _fetch(self) -> bool: + """Fetch GFS data from Open-Meteo API. + + Returns: + True on success + """ + # Build API URL + hourly_vars = [ + "temperature_1000hPa", "temperature_925hPa", + "temperature_850hPa", "temperature_700hPa", + "relative_humidity_1000hPa", "relative_humidity_925hPa", + "relative_humidity_850hPa", "relative_humidity_700hPa", + "surface_pressure", + ] + + url = ( + f"https://api.open-meteo.com/v1/gfs" + f"?latitude={self._lat}&longitude={self._lon}" + f"&hourly={','.join(hourly_vars)}" + f"&forecast_days=1&timezone=auto" + ) + + headers = { + "User-Agent": "MeshAI/1.0", + "Accept": "application/json", + } + + try: + req = Request(url, headers=headers) + with urlopen(req, timeout=30) as resp: + data = json.loads(resp.read().decode("utf-8")) + + except HTTPError as e: + logger.warning(f"Ducting API HTTP error: {e.code}") + self._last_error = f"HTTP {e.code}" + self._consecutive_errors += 1 + return False + + except URLError as e: + logger.warning(f"Ducting API connection error: {e.reason}") + self._last_error = str(e.reason) + self._consecutive_errors += 1 + return False + + except Exception as e: + logger.warning(f"Ducting API error: {e}") + self._last_error = str(e) + self._consecutive_errors += 1 + return False + + # Parse response + try: + self._parse_response(data) + self._consecutive_errors = 0 + self._last_error = None + self._is_loaded = True + logger.info(f"Ducting assessment updated: {self._status.get('condition', 'unknown')}") + return True + + except Exception as e: + logger.warning(f"Ducting parse error: {e}") + self._last_error = f"parse error: {e}" + return False + + def _parse_response(self, data): + """Parse Open-Meteo response and compute ducting assessment.""" + hourly = data.get("hourly", {}) + times = hourly.get("time", []) + + if not times: + raise ValueError("No time data in response") + + # Find index closest to current time + now = datetime.now() + idx = 0 + for i, t in enumerate(times): + try: + dt = datetime.fromisoformat(t) + if dt <= now: + idx = i + except Exception: + pass + + # Extract values for current hour + def get_val(key): + vals = hourly.get(key, []) + return vals[idx] if idx < len(vals) else None + + # Build profile for each pressure level + profile = [] + gradients = [] + + levels = sorted(PRESSURE_LEVELS.keys(), reverse=True) # 1000, 925, 850, 700 + + for i, pressure in enumerate(levels): + temp_key = f"temperature_{pressure}hPa" + rh_key = f"relative_humidity_{pressure}hPa" + + t_celsius = get_val(temp_key) + rh = get_val(rh_key) + + if t_celsius is None or rh is None: + continue + + height_m = PRESSURE_LEVELS[pressure] + + # Calculate radio refractivity N + t_kelvin = t_celsius + 273.15 + + # Saturation vapor pressure (Magnus formula) + e_sat = 6.112 * math.exp(17.67 * t_celsius / (t_celsius + 243.5)) + # Actual vapor pressure + e = (rh / 100.0) * e_sat + + # Radio refractivity + n = 77.6 * (pressure / t_kelvin) + 3.73e5 * (e / t_kelvin**2) + + # Modified refractivity (accounts for Earth curvature) + h_km = height_m / 1000.0 + m = n + 157.0 * h_km + + profile.append({ + "level_hPa": pressure, + "height_m": height_m, + "N": round(n, 1), + "M": round(m, 1), + "T_C": round(t_celsius, 1), + "RH": round(rh, 1), + }) + + # Compute gradients between adjacent levels + for i in range(len(profile) - 1): + lower = profile[i] + upper = profile[i + 1] + + dM = upper["M"] - lower["M"] + dz = (upper["height_m"] - lower["height_m"]) / 1000.0 # km + + if dz > 0: + gradient = dM / dz + gradients.append({ + "from_level": lower["level_hPa"], + "to_level": upper["level_hPa"], + "from_height_m": lower["height_m"], + "to_height_m": upper["height_m"], + "gradient": round(gradient, 1), + }) + + # Classify conditions based on minimum gradient + # Standard atmosphere: ~118 M-units/km + # Normal: > 79 + # Super-refraction: 0 to 79 + # Ducting: < 0 (negative = trapping layer) + + min_gradient = min((g["gradient"] for g in gradients), default=118) + min_gradient_layer = None + for g in gradients: + if g["gradient"] == min_gradient: + min_gradient_layer = g + break + + if min_gradient < 0: + # Ducting detected + if min_gradient_layer and min_gradient_layer["from_level"] == 1000: + condition = "surface_duct" + else: + condition = "elevated_duct" + + duct_base = min_gradient_layer["from_height_m"] if min_gradient_layer else 0 + duct_thickness = ( + min_gradient_layer["to_height_m"] - min_gradient_layer["from_height_m"] + if min_gradient_layer else 0 + ) + assessment = "Ducting -- extended UHF range likely" + + elif min_gradient < 79: + condition = "super_refraction" + duct_base = None + duct_thickness = None + assessment = "Enhanced range possible" + + else: + condition = "normal" + duct_base = None + duct_thickness = None + assessment = "Normal propagation" + + # Update status + self._status = { + "condition": condition, + "min_gradient": round(min_gradient, 1), + "duct_thickness_m": duct_thickness, + "duct_base_m": duct_base, + "profile": profile, + "gradients": gradients, + "assessment": assessment, + "last_update": times[idx] if idx < len(times) else None, + "fetched_at": time.time(), + "location": { + "lat": self._lat, + "lon": self._lon, + }, + } + + def get_status(self) -> dict: + """Get current ducting status.""" + return self._status + + @property + def health_status(self) -> dict: + """Get adapter health status.""" + return { + "source": "ducting", + "is_loaded": self._is_loaded, + "last_error": str(self._last_error) if self._last_error else None, + "consecutive_errors": self._consecutive_errors, + "event_count": 0, + "last_fetch": self._last_tick, + } diff --git a/meshai/env/nws.py b/meshai/env/nws.py new file mode 100644 index 0000000..c9a0c64 --- /dev/null +++ b/meshai/env/nws.py @@ -0,0 +1,193 @@ +"""NWS Active Alerts adapter.""" + +import json +import logging +import time +from datetime import datetime +from typing import TYPE_CHECKING +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +if TYPE_CHECKING: + from ..config import NWSConfig + +logger = logging.getLogger(__name__) + + +class NWSAlertsAdapter: + """NWS Active Alerts -- polls api.weather.gov""" + + def __init__(self, config: "NWSConfig"): + self._areas = config.areas or ["ID"] + self._user_agent = config.user_agent or "(meshai, ops@example.com)" + self._severity_min = config.severity_min or "moderate" + self._tick_interval = config.tick_seconds or 60 + self._last_tick = 0.0 + self._events = [] + self._consecutive_errors = 0 + self._last_error = None + self._backoff_until = 0.0 + self._is_loaded = False + + def tick(self) -> bool: + """Execute one polling tick. + + Returns: + True if data changed + """ + now = time.time() + + # Rate limit backoff + if now < self._backoff_until: + return False + + # Check tick interval + if now - self._last_tick < self._tick_interval: + return False + + self._last_tick = now + return self._fetch() + + def _fetch(self) -> bool: + """Fetch alerts from NWS API. + + Returns: + True if data changed + """ + areas = ",".join(self._areas) + url = f"https://api.weather.gov/alerts/active?area={areas}" + + headers = { + "User-Agent": self._user_agent, + "Accept": "application/geo+json", + } + + try: + req = Request(url, headers=headers) + with urlopen(req, timeout=15) as resp: + data = json.loads(resp.read().decode("utf-8")) + + except HTTPError as e: + if e.code == 429: + self._backoff_until = time.time() + 5 + logger.warning("NWS rate limited, backing off 5s") + else: + logger.warning(f"NWS HTTP error: {e.code}") + self._last_error = f"HTTP {e.code}" + self._consecutive_errors += 1 + return False + + except URLError as e: + logger.warning(f"NWS connection error: {e.reason}") + self._last_error = str(e.reason) + self._consecutive_errors += 1 + return False + + except Exception as e: + logger.warning(f"NWS fetch error: {e}") + self._last_error = str(e) + self._consecutive_errors += 1 + return False + + # Parse response + features = data.get("features", []) + new_events = [] + + # Severity levels for filtering + severity_levels = ["unknown", "minor", "moderate", "severe", "extreme"] + try: + min_idx = severity_levels.index(self._severity_min.lower()) + except ValueError: + min_idx = 2 # default to moderate + + for feature in features: + props = feature.get("properties", {}) + + # Severity filtering + severity = (props.get("severity") or "Unknown").lower() + try: + sev_idx = severity_levels.index(severity) + except ValueError: + sev_idx = 0 + + if sev_idx < min_idx: + continue + + # Parse timestamps + onset = self._parse_iso(props.get("onset")) + expires = self._parse_iso(props.get("expires")) + + event = { + "source": "nws", + "event_id": props.get("id", ""), + "event_type": props.get("event", "Unknown"), + "severity": severity, + "headline": props.get("headline", ""), + "description": (props.get("description") or "")[:500], + "onset": onset, + "expires": expires, + "areas": props.get("geocode", {}).get("UGC", []), + "area_desc": props.get("areaDesc", ""), + "fetched_at": time.time(), + } + + # Try to get centroid from geometry + geom = feature.get("geometry") + if geom and geom.get("coordinates"): + try: + coords = geom["coordinates"] + if geom.get("type") == "Polygon" and coords: + # Compute centroid of first ring + ring = coords[0] + lat_sum = sum(c[1] for c in ring) + lon_sum = sum(c[0] for c in ring) + event["lat"] = lat_sum / len(ring) + event["lon"] = lon_sum / len(ring) + except Exception: + pass + + new_events.append(event) + + # Check if data changed + old_ids = {e["event_id"] for e in self._events} + new_ids = {e["event_id"] for e in new_events} + changed = old_ids != new_ids + + self._events = new_events + self._consecutive_errors = 0 + self._last_error = None + self._is_loaded = True + + if changed: + logger.info(f"NWS alerts updated: {len(new_events)} active") + + return changed + + def _parse_iso(self, iso_str: str) -> float: + """Parse ISO timestamp to epoch float.""" + if not iso_str: + return 0.0 + try: + # Handle various ISO formats + if iso_str.endswith("Z"): + iso_str = iso_str[:-1] + "+00:00" + dt = datetime.fromisoformat(iso_str) + return dt.timestamp() + except Exception: + return 0.0 + + def get_events(self) -> list: + """Get current events.""" + return self._events + + @property + def health_status(self) -> dict: + """Get adapter health status.""" + return { + "source": "nws", + "is_loaded": self._is_loaded, + "last_error": str(self._last_error) if self._last_error else None, + "consecutive_errors": self._consecutive_errors, + "event_count": len(self._events), + "last_fetch": self._last_tick, + } diff --git a/meshai/env/store.py b/meshai/env/store.py new file mode 100644 index 0000000..688ea50 --- /dev/null +++ b/meshai/env/store.py @@ -0,0 +1,168 @@ +"""Environmental data store with tick-based adapter polling.""" + +import logging +import time +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..config import EnvironmentalConfig + +logger = logging.getLogger(__name__) + + +class EnvironmentalStore: + """Cache and tick-driver for all environmental feed adapters.""" + + def __init__(self, config: "EnvironmentalConfig"): + self._adapters = {} # name -> adapter instance + self._events = {} # (source, event_id) -> event dict + self._swpc_status = {} # Kp/SFI/scales snapshot + self._ducting_status = {} # tropo ducting assessment + self._mesh_zones = config.nws_zones or [] + + # Create adapter instances based on config + if config.nws.enabled: + from .nws import NWSAlertsAdapter + self._adapters["nws"] = NWSAlertsAdapter(config.nws) + + if config.swpc.enabled: + from .swpc import SWPCAdapter + self._adapters["swpc"] = SWPCAdapter(config.swpc) + + if config.ducting.enabled: + from .ducting import DuctingAdapter + self._adapters["ducting"] = DuctingAdapter(config.ducting) + + logger.info(f"EnvironmentalStore initialized with {len(self._adapters)} adapters") + + def refresh(self) -> bool: + """Called every second from main loop. Ticks each adapter. + + Returns: + True if any data changed + """ + changed = False + for name, adapter in self._adapters.items(): + try: + if adapter.tick(): + changed = True + self._ingest(name, adapter) + except Exception as e: + logger.warning("Env adapter %s error: %s", name, e) + + self._purge_expired() + return changed + + def _ingest(self, name: str, adapter): + """Ingest data from an adapter after it ticks.""" + if name == "swpc": + self._swpc_status = adapter.get_status() + # Also ingest any alert events (R-scale >= 3) + for evt in adapter.get_events(): + self._events[(evt["source"], evt["event_id"])] = evt + elif name == "ducting": + self._ducting_status = adapter.get_status() + else: + for evt in adapter.get_events(): + self._events[(evt["source"], evt["event_id"])] = evt + + def _purge_expired(self): + """Remove expired events.""" + now = time.time() + expired = [ + k for k, v in self._events.items() + if v.get("expires") and v["expires"] < now + ] + for k in expired: + del self._events[k] + + def get_active(self, source: str = None) -> list: + """Get active events, optionally filtered by source. + + Args: + source: Filter to specific source (nws, swpc, etc.) + + Returns: + List of event dicts sorted by fetched_at (newest first) + """ + events = list(self._events.values()) + if source: + events = [e for e in events if e["source"] == source] + return sorted(events, key=lambda e: e.get("fetched_at", 0), reverse=True) + + def get_for_zones(self, zones: list) -> list: + """Get events affecting specific NWS zones. + + Args: + zones: List of UGC zone codes (e.g., ["IDZ016", "IDZ030"]) + + Returns: + List of events with overlapping zone coverage + """ + zone_set = set(zones) + return [ + e for e in self._events.values() + if set(e.get("areas", [])) & zone_set + ] + + def get_swpc_status(self) -> dict: + """Get current SWPC space weather status.""" + return self._swpc_status + + def get_ducting_status(self) -> dict: + """Get current tropospheric ducting status.""" + return self._ducting_status + + def get_rf_propagation(self) -> dict: + """Combined HF + UHF propagation summary for dashboard/LLM.""" + return { + "hf": self._swpc_status, + "uhf_ducting": self._ducting_status, + } + + def get_summary(self) -> str: + """Compact text block for LLM context injection.""" + lines = [] + lines.append(f"### Current Conditions (as of {time.strftime('%H:%M:%S MT')}):") + + # NWS alerts + nws = self.get_active(source="nws") + if nws: + lines.append(f"NWS: {len(nws)} active alert(s):") + for a in nws[:3]: + lines.append(f" - {a['event_type']}: {a['headline'][:120]}") + else: + lines.append("NWS: No active alerts for mesh area.") + + # HF + s = self._swpc_status + if s: + kp = s.get("kp_current", "?") + sfi = s.get("sfi", "?") + assessment = s.get("band_assessment", "Unknown") + lines.append(f"HF: {assessment} -- SFI {sfi}, Kp {kp}") + warnings = s.get("active_warnings", []) + if warnings: + for w in warnings[:2]: + lines.append(f" Warning: {w}") + else: + lines.append("HF: Space weather data not available.") + + # UHF ducting + d = self._ducting_status + if d: + condition = d.get("condition", "unknown") + if condition == "normal": + lines.append("UHF Ducting: Normal propagation, no ducting detected.") + elif condition in ("super_refraction", "ducting", "surface_duct", "elevated_duct"): + gradient = d.get("min_gradient", "?") + thickness = d.get("duct_thickness_m", "?") + lines.append(f"UHF Ducting: {condition.replace('_', ' ').title()} detected") + lines.append(f" dM/dz: {gradient} M-units/km, duct ~{thickness}m thick") + lines.append(" Extended range likely on 906 MHz -- expect distant nodes") + + return "\n".join(lines) + + def get_source_health(self) -> list: + """Get health status for all adapters.""" + return [a.health_status for a in self._adapters.values()] diff --git a/meshai/env/swpc.py b/meshai/env/swpc.py new file mode 100644 index 0000000..c2b00b8 --- /dev/null +++ b/meshai/env/swpc.py @@ -0,0 +1,256 @@ +"""NOAA Space Weather Prediction Center adapter.""" + +import json +import logging +import time +from typing import TYPE_CHECKING +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +if TYPE_CHECKING: + from ..config import SWPCConfig + +logger = logging.getLogger(__name__) + + +class SWPCAdapter: + """NOAA Space Weather -- multi-endpoint with staggered ticks.""" + + # Endpoint definitions: (url, interval_seconds) + ENDPOINTS = { + "scales": ("https://services.swpc.noaa.gov/products/noaa-scales.json", 300), + "kp": ("https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json", 600), + "alerts": ("https://services.swpc.noaa.gov/products/alerts.json", 120), + "f107": ("https://services.swpc.noaa.gov/json/f107_cm_flux.json", 86400), + } + + def __init__(self, config: "SWPCConfig"): + self._last_tick = {} # endpoint -> last_tick timestamp + self._status = {} + self._events = [] + self._consecutive_errors = 0 + self._last_error = None + self._is_loaded = False + + # Initialize tick times to 0 + for endpoint in self.ENDPOINTS: + self._last_tick[endpoint] = 0.0 + + def tick(self) -> bool: + """Execute one polling tick. + + Returns: + True if data changed + """ + changed = False + now = time.time() + + for endpoint, (url, interval) in self.ENDPOINTS.items(): + if now - self._last_tick[endpoint] >= interval: + self._last_tick[endpoint] = now + if self._fetch_endpoint(endpoint, url): + changed = True + + if changed: + self._update_assessment() + + return changed + + def _fetch_endpoint(self, endpoint: str, url: str) -> bool: + """Fetch a single endpoint. + + Returns: + True on success + """ + headers = { + "User-Agent": "MeshAI/1.0", + "Accept": "application/json", + } + + try: + req = Request(url, headers=headers) + with urlopen(req, timeout=15) as resp: + data = json.loads(resp.read().decode("utf-8")) + + except HTTPError as e: + logger.warning(f"SWPC {endpoint} HTTP error: {e.code}") + self._last_error = f"{endpoint}: HTTP {e.code}" + self._consecutive_errors += 1 + return False + + except URLError as e: + logger.warning(f"SWPC {endpoint} connection error: {e.reason}") + self._last_error = f"{endpoint}: {e.reason}" + self._consecutive_errors += 1 + return False + + except Exception as e: + logger.warning(f"SWPC {endpoint} error: {e}") + self._last_error = f"{endpoint}: {e}" + self._consecutive_errors += 1 + return False + + # Parse based on endpoint + try: + if endpoint == "scales": + self._parse_scales(data) + elif endpoint == "kp": + self._parse_kp(data) + elif endpoint == "alerts": + self._parse_alerts(data) + elif endpoint == "f107": + self._parse_f107(data) + + self._consecutive_errors = 0 + self._last_error = None + self._is_loaded = True + return True + + except Exception as e: + logger.warning(f"SWPC {endpoint} parse error: {e}") + self._last_error = f"{endpoint}: parse error" + return False + + def _parse_scales(self, data): + """Parse noaa-scales.json. + + Data format: {""-1": {...}, "0": {...}, "1": {...}, ...} + "0" is current. + """ + current = data.get("0", {}) + + r_data = current.get("R", {}) + s_data = current.get("S", {}) + g_data = current.get("G", {}) + + # Handle empty string or None Scale values + def parse_scale(val): + if val is None or val == "": + return 0 + try: + return int(val) + except (ValueError, TypeError): + return 0 + + self._status["r_scale"] = parse_scale(r_data.get("Scale")) + self._status["s_scale"] = parse_scale(s_data.get("Scale")) + self._status["g_scale"] = parse_scale(g_data.get("Scale")) + + def _parse_kp(self, data): + """Parse noaa-planetary-k-index.json. + + Data format: array of arrays + First row is header: ["time_tag", "Kp", "a_running", "station_count"] + Last row is most recent. + """ + if not data or len(data) < 2: + return + + # Find Kp column index from header + header = data[0] + try: + kp_idx = header.index("Kp") + except ValueError: + kp_idx = 1 + + # Get last row + last_row = data[-1] + if len(last_row) > kp_idx: + try: + self._status["kp_current"] = float(last_row[kp_idx]) + except (ValueError, TypeError): + pass + + # Get timestamp + if len(last_row) > 0: + self._status["kp_timestamp"] = last_row[0] + + def _parse_alerts(self, data): + """Parse alerts.json. + + Data format: array of objects with product_id, issue_datetime, message + """ + warnings = [] + if isinstance(data, list): + for alert in data[:5]: # Keep most recent 5 + message = alert.get("message", "") + # Extract first line as headline + headline = message.split("\n")[0].strip() + if headline: + warnings.append(headline) + + self._status["active_warnings"] = warnings + + def _parse_f107(self, data): + """Parse f107_cm_flux.json. + + Data format: array of objects with time_tag, flux + """ + if not data: + return + + # Get most recent entry (last in list) + if isinstance(data, list) and data: + last = data[-1] + if isinstance(last, dict): + try: + self._status["sfi"] = float(last.get("flux", 0)) + except (ValueError, TypeError): + pass + + def _update_assessment(self): + """Compute band assessment from SFI and Kp.""" + sfi = self._status.get("sfi", 0) + kp = self._status.get("kp_current", 0) + + # Band assessment formula + if sfi > 150 and kp <= 1: + assessment = "Excellent" + detail = "Upper HF bands (10m-20m) open, solid DX conditions" + elif sfi >= 100 and kp <= 3: + assessment = "Good" + detail = "Upper HF bands (10m-20m) open, solid DX conditions" + elif sfi >= 80 and kp <= 4: + assessment = "Fair" + detail = "Mid HF bands (20m-40m) usable, upper bands marginal" + else: + assessment = "Poor" + detail = "HF conditions degraded, stick to lower bands (40m-80m)" + + self._status["band_assessment"] = assessment + self._status["band_detail"] = detail + + # Generate events for R-scale >= 3 + self._events = [] + r_scale = self._status.get("r_scale", 0) + if r_scale >= 3: + self._events.append({ + "source": "swpc", + "event_id": f"swpc_r{r_scale}_{int(time.time())}", + "event_type": f"R{r_scale} Radio Blackout", + "severity": "warning" if r_scale >= 3 else "advisory", + "headline": f"R{r_scale} HF Radio Blackout -- HF comms degraded", + "expires": time.time() + 3600, # 1hr TTL + "areas": [], + "fetched_at": time.time(), + }) + + def get_status(self) -> dict: + """Get current SWPC status.""" + return self._status + + def get_events(self) -> list: + """Get current alert events.""" + return self._events + + @property + def health_status(self) -> dict: + """Get adapter health status.""" + return { + "source": "swpc", + "is_loaded": self._is_loaded, + "last_error": str(self._last_error) if self._last_error else None, + "consecutive_errors": self._consecutive_errors, + "event_count": len(self._events), + "last_fetch": max(self._last_tick.values()) if self._last_tick else 0, + } diff --git a/meshai/main.py b/meshai/main.py index 138a53a..eeebdf0 100644 --- a/meshai/main.py +++ b/meshai/main.py @@ -1,684 +1,718 @@ -"""Main entry point for MeshAI.""" - -import argparse -import asyncio -import logging -import os -import signal -import sys -import time -from pathlib import Path -from typing import Optional - -from . import __version__ -from .backends import AnthropicBackend, GoogleBackend, LLMBackend, OpenAIBackend -from .cli import run_configurator -from .commands import CommandDispatcher -from .commands.dispatcher import create_dispatcher -from .commands.status import set_start_time -from .config import Config, load_config -from .connector import MeshConnector, MeshMessage -from .context import MeshContext -from .history import ConversationHistory -from .memory import ConversationSummary -from .responder import Responder -from .router import MessageRouter, RouteType - -logger = logging.getLogger(__name__) - - -class MeshAI: - """Main application class.""" - - def __init__(self, config: Config): - self.config = config - self.connector: Optional[MeshConnector] = None - self.history: Optional[ConversationHistory] = None - self.dispatcher: Optional[CommandDispatcher] = None - self.llm: Optional[LLMBackend] = None - self.context: Optional[MeshContext] = None - self.meshmonitor_sync = None - self.knowledge = None - self.data_store = None # Replaces source_manager - self.health_engine = None - self.mesh_reporter = None - self.subscription_manager = None - self.alert_engine = None - self._last_sub_check: float = 0.0 - self.router: Optional[MessageRouter] = None - self.responder: Optional[Responder] = None - self._running = False - self._loop: Optional[asyncio.AbstractEventLoop] = None - self._last_cleanup: float = 0.0 - self._last_health_compute: float = 0.0 - self.broadcaster = None # Dashboard WebSocket broadcaster - - async def start(self) -> None: - """Start the bot.""" - logger.info(f"Starting MeshAI v{__version__}") - set_start_time(time.time()) - - # Initialize components - await self._init_components() - - # Connect to Meshtastic - self.connector.connect() - self.connector.set_message_callback(self._on_message, asyncio.get_event_loop()) - - # Add own node ID to context ignore list - if self.context and self.connector.my_node_id: - self.context._ignore_nodes.add(self.connector.my_node_id) - - self._running = True - self._loop = asyncio.get_event_loop() - self._last_cleanup = time.time() - self._last_health_compute = 0.0 - - # Write PID file - self._write_pid() - - logger.info("MeshAI started successfully") - - # Keep running - while self._running: - await asyncio.sleep(1) - - # Periodic MeshMonitor refresh - if self.meshmonitor_sync: - self.meshmonitor_sync.maybe_refresh() - - # Periodic data store refresh and health computation - if self.data_store: - refreshed = self.data_store.refresh() - # Recompute health after refresh - if refreshed and self.health_engine: - self.health_engine.compute(self.data_store) - self._last_health_compute = time.time() - - # Broadcast health update to dashboard - if self.broadcaster and self.health_engine.mesh_health: - try: - mh = self.health_engine.mesh_health - health_dict = { - "score": round(mh.score.composite, 1), - "tier": mh.score.tier, - "total_nodes": mh.total_nodes, - "total_regions": mh.total_regions, - "infra_online": mh.score.infra_online, - "infra_total": mh.score.infra_total, - "last_computed": mh.last_computed, - } - await self.broadcaster.broadcast("health_update", health_dict) - except Exception as e: - logger.debug("Dashboard broadcast error: %s", e) - - # Check for alertable conditions - if self.alert_engine: - alerts = self.alert_engine.check() - if alerts: - await self._dispatch_alerts(alerts) - - # Broadcast alerts to dashboard - if self.broadcaster: - for alert in alerts: - try: - await self.broadcaster.broadcast("alert_fired", alert) - except Exception: - pass - - # Check scheduled subscriptions (every 60 seconds) - if self.subscription_manager and self.mesh_reporter: - if time.time() - self._last_sub_check >= 60: - await self._check_scheduled_subs() - self._last_sub_check = time.time() - - # Periodic cleanup - if time.time() - self._last_cleanup >= 3600: - await self.history.cleanup_expired() - if self.context: - self.context.prune() - self._last_cleanup = time.time() - - async def stop(self) -> None: - """Stop the bot.""" - logger.info("Stopping MeshAI...") - self._running = False - - if self.connector: - self.connector.disconnect() - - if self.history: - await self.history.close() - - if self.llm: - await self.llm.close() - if self.knowledge: - self.knowledge.close() - if self.data_store: - self.data_store.close() - if self.subscription_manager: - self.subscription_manager.close() - - self._remove_pid() - logger.info("MeshAI stopped") - - async def _init_components(self) -> None: - """Initialize all components.""" - # Conversation history - self.history = ConversationHistory(self.config.history) - await self.history.initialize() - - # LLM backend - api_key = self.config.resolve_api_key() - if not api_key: - logger.warning("No API key configured - LLM responses will fail") - - # Memory config - mem_cfg = self.config.memory - window_size = mem_cfg.window_size if mem_cfg.enabled else 0 - summarize_threshold = mem_cfg.summarize_threshold - - # Create backend - backend = self.config.llm.backend.lower() - if backend == "openai": - self.llm = OpenAIBackend( - self.config.llm, api_key, window_size, summarize_threshold - ) - elif backend == "anthropic": - self.llm = AnthropicBackend( - self.config.llm, api_key, window_size, summarize_threshold - ) - elif backend == "google": - self.llm = GoogleBackend( - self.config.llm, api_key, window_size, summarize_threshold - ) - else: - logger.warning(f"Unknown backend '{backend}', defaulting to OpenAI") - self.llm = OpenAIBackend( - self.config.llm, api_key, window_size, summarize_threshold - ) - - # Load persisted summaries into memory cache - await self._load_summaries() - - # Meshtastic connector - self.connector = MeshConnector(self.config.connection) - - # Passive mesh context buffer - ctx_cfg = self.config.context - if ctx_cfg.enabled: - self.context = MeshContext( - observe_channels=ctx_cfg.observe_channels or None, - ignore_nodes=ctx_cfg.ignore_nodes or None, - max_age=ctx_cfg.max_age, - ) - logger.info("Mesh context buffer enabled") - else: - self.context = None - - # MeshMonitor trigger sync - mm_cfg = self.config.meshmonitor - if mm_cfg.enabled and mm_cfg.url: - from .meshmonitor import MeshMonitorSync - self.meshmonitor_sync = MeshMonitorSync( - url=mm_cfg.url, - refresh_interval=mm_cfg.refresh_interval, - ) - count = self.meshmonitor_sync.load() - logger.info(f"MeshMonitor sync enabled, loaded {count} triggers") - else: - self.meshmonitor_sync = None - - # Mesh data store (replaces MeshSourceManager) - # mesh_sources may be dicts or MeshSourceConfig objects depending on config version - enabled_sources = [ - s for s in self.config.mesh_sources - if (s.enabled if hasattr(s, 'enabled') else s.get('enabled', True)) - ] - if enabled_sources: - from .mesh_data_store import MeshDataStore - self.data_store = MeshDataStore( - source_configs=enabled_sources, - db_path="/data/mesh_history.db", - ) - # Initial fetch and backfill - self.data_store.force_refresh() - # Log status - for status in self.data_store.get_status(): - if status["is_loaded"]: - logger.info( - f"Mesh source '{status['name']}' ({status['type']}): " - f"{status['node_count']} nodes" - ) - else: - logger.warning( - f"Mesh source '{status['name']}' ({status['type']}): " - f"failed - {status.get('last_error', 'unknown error')}" - ) - else: - self.data_store = None - - # Mesh health engine - mi_cfg = self.config.mesh_intelligence - if mi_cfg.enabled and self.data_store: - from .mesh_health import MeshHealthEngine - self.health_engine = MeshHealthEngine( - regions=mi_cfg.regions, - locality_radius=mi_cfg.locality_radius_miles, - offline_threshold_hours=mi_cfg.offline_threshold_hours, - packet_threshold=mi_cfg.packet_threshold, - battery_warning_percent=mi_cfg.battery_warning_percent, - ) - # Initial health computation - mesh_health = self.health_engine.compute(self.data_store) - self._last_health_compute = time.time() - logger.info( - f"Mesh intelligence enabled: {mesh_health.total_nodes} nodes, " - f"{mesh_health.total_regions} regions, " - f"score {mesh_health.score.composite:.0f}/100 ({mesh_health.score.tier})" - ) - else: - self.health_engine = None - - # Mesh reporter (for LLM prompt injection and commands) - if self.health_engine and self.data_store: - from .mesh_reporter import MeshReporter - mi_regions = self.config.mesh_intelligence.regions if self.config.mesh_intelligence else [] - self.mesh_reporter = MeshReporter(self.health_engine, self.data_store, region_configs=mi_regions) - logger.info("Mesh reporter enabled") - else: - self.mesh_reporter = None - - # Subscription manager (uses same db as data_store) - if self.data_store: - from .subscriptions import SubscriptionManager - self.subscription_manager = SubscriptionManager(db_path="/data/mesh_history.db") - logger.info("Subscription manager enabled") - else: - self.subscription_manager = None - - # Alert engine (needs health engine, reporter, and subscription manager) - if self.health_engine and self.mesh_reporter and self.subscription_manager: - from .alert_engine import AlertEngine - mi = self.config.mesh_intelligence - self.alert_engine = AlertEngine( - health_engine=self.health_engine, - reporter=self.mesh_reporter, - subscription_manager=self.subscription_manager, - config=mi, - db_path="/data/mesh_history.db", - ) - logger.info(f"Alert engine initialized (critical: {mi.critical_nodes}, channel: {mi.alert_channel})") - - # Knowledge base (optional - Qdrant with SQLite fallback) - kb_cfg = self.config.knowledge - self.knowledge = None - if kb_cfg.enabled: - # Try Qdrant first if configured - if kb_cfg.backend in ("qdrant", "auto") and kb_cfg.qdrant_host: - try: - from .knowledge import QdrantKnowledgeSearch - qdrant = QdrantKnowledgeSearch( - qdrant_host=kb_cfg.qdrant_host, - qdrant_port=kb_cfg.qdrant_port, - collection=kb_cfg.qdrant_collection, - tei_host=kb_cfg.tei_host, - tei_port=kb_cfg.tei_port, - sparse_host=kb_cfg.sparse_host, - sparse_port=kb_cfg.sparse_port, - use_sparse=kb_cfg.use_sparse, - top_k=kb_cfg.top_k, - ) - if qdrant.available: - self.knowledge = qdrant - logger.info("Using Qdrant knowledge backend (RECON hybrid)") - except Exception as e: - logger.warning(f"Qdrant knowledge unavailable: {e}") - - # Fall back to SQLite if Qdrant failed or not configured - if not self.knowledge and kb_cfg.backend in ("sqlite", "auto") and kb_cfg.db_path: - try: - from .knowledge import KnowledgeSearch - self.knowledge = KnowledgeSearch( - db_path=kb_cfg.db_path, - top_k=kb_cfg.top_k, - ) - except ImportError as e: - logger.warning(f"SQLite knowledge disabled - missing dependencies: {e}") - - # Command dispatcher (needs mesh_reporter for health commands) - self.dispatcher = create_dispatcher( - prefix=self.config.commands.prefix, - disabled_commands=self.config.commands.disabled_commands, - custom_commands=self.config.commands.custom_commands, - mesh_reporter=self.mesh_reporter, - data_store=self.data_store, - health_engine=self.health_engine, - subscription_manager=self.subscription_manager, - ) - - # Message router - self.router = MessageRouter( - self.config, self.connector, self.history, self.dispatcher, self.llm, - context=self.context, - meshmonitor_sync=self.meshmonitor_sync, - knowledge=self.knowledge, - source_manager=self.data_store, - health_engine=self.health_engine, - mesh_reporter=self.mesh_reporter, - ) - - # Responder - self.responder = Responder(self.config.response, self.connector) - - # Dashboard - if hasattr(self.config, 'dashboard') and self.config.dashboard.enabled: - try: - from .dashboard.server import start_dashboard - self.broadcaster = await start_dashboard(self) - logger.info("Dashboard started on port %d", self.config.dashboard.port) - except Exception as e: - logger.warning("Dashboard failed to start: %s", e) - self.broadcaster = None - else: - self.broadcaster = None - - async def _on_message(self, message: MeshMessage) -> None: - """Handle incoming message.""" - try: - # Passively observe channel broadcasts for context (before filtering) - if self.context and not message.is_dm and message.text: - self.context.observe( - sender_name=message.sender_name, - sender_id=message.sender_id, - text=message.text, - channel=message.channel, - is_dm=False, - ) - - # Check if we should respond - if not self.router.should_respond(message): - return - - logger.info( - f"Processing message from {message.sender_name} ({message.sender_id}): " - f"{message.text[:50]}..." - ) - - # Route the message - # Check for continuation request first - continuation_messages = self.router.check_continuation(message) - if continuation_messages: - await self.responder.send_response( - continuation_messages, - destination=message.sender_id, - channel=message.channel, - ) - return - - result = await self.router.route(message) - - if result.route_type == RouteType.IGNORE: - return - - # Determine response - if result.route_type == RouteType.COMMAND: - if isinstance(result.response, list): - # Command returned pre-split messages — send directly - messages = result.response - else: - # Single string — chunk it - from .chunker import chunk_response - messages, remaining = chunk_response( - result.response, - max_chars=self.config.response.max_length, - max_messages=self.config.response.max_messages, - ) - if remaining: - self.router.continuations.store(message.sender_id, remaining) - elif result.route_type == RouteType.LLM: - messages = await self.router.generate_llm_response(message, result.query) - else: - return - - if not messages: - return - - # Send DM response - await self.responder.send_response( - messages, - destination=message.sender_id, - channel=message.channel, - ) - - except Exception as e: - logger.error(f"Error handling message: {e}", exc_info=True) - - async def _load_summaries(self) -> None: - """Load persisted summaries from database into memory cache.""" - memory = self.llm.get_memory() - if not memory: - return - - if not self.history or not self.history._db: - return - - try: - async with self.history._lock: - cursor = await self.history._db.execute( - "SELECT user_id, summary, message_count, updated_at " - "FROM conversation_summaries" - ) - rows = await cursor.fetchall() - - loaded = 0 - for row in rows: - user_id, summary_text, message_count, updated_at = row - summary = ConversationSummary( - summary=summary_text, - last_updated=updated_at, - message_count=message_count, - ) - memory.load_summary(user_id, summary) - loaded += 1 - - if loaded: - logger.info(f"Loaded {loaded} conversation summaries from database") - - except Exception as e: - logger.warning(f"Failed to load summaries from database: {e}") - - def _write_pid(self) -> None: - """Write PID file.""" - pid_file = Path("/tmp/meshai.pid") - pid_file.write_text(str(os.getpid())) - - def _remove_pid(self) -> None: - """Remove PID file.""" - pid_file = Path("/tmp/meshai.pid") - if pid_file.exists(): - pid_file.unlink() - - async def _dispatch_alerts(self, alerts: list[dict]) -> None: - """Dispatch alerts to subscribers and alert channel.""" - mi = self.config.mesh_intelligence - alert_channel = getattr(mi, 'alert_channel', -1) - - for alert in alerts: - message = alert["message"] - logger.info(f"ALERT: {message}") - - # Send to alert channel if configured - if alert_channel >= 0 and self.connector: - try: - self.connector.send_message( - text=message, - destination=None, # Broadcast - channel=alert_channel, - ) - logger.info(f"Alert sent to channel {alert_channel}") - except Exception as e: - logger.error(f"Failed to send channel alert: {e}") - - # Send DMs to matching subscribers - if self.alert_engine and self.subscription_manager: - subscribers = self.alert_engine.get_subscribers_for_alert(alert) - for sub in subscribers: - user_id = sub["user_id"] - try: - await self._send_sub_dm(user_id, message) - logger.info(f"Alert DM sent to {user_id}: {alert['type']}") - except Exception as e: - logger.error(f"Failed to send alert DM to {user_id}: {e}") - - self.alert_engine.clear_pending() - - async def _check_scheduled_subs(self) -> None: - """Check for and deliver due scheduled reports.""" - from datetime import datetime - from zoneinfo import ZoneInfo - - tz = ZoneInfo("America/Boise") - now = datetime.now(tz) - current_hhmm = now.strftime("%H%M") - current_day = now.strftime("%a").lower() - - due_subs = self.subscription_manager.get_due_subscriptions(current_hhmm, current_day) - - for sub in due_subs: - try: - # Generate report based on scope - report = self._generate_sub_report(sub) - if not report: - continue - - # Send DM to subscriber - user_id = sub["user_id"] - await self._send_sub_dm(user_id, report) - - # Mark as sent - self.subscription_manager.mark_sent(sub["id"]) - logger.info(f"Delivered {sub['sub_type']} report to {user_id}") - - except Exception as e: - logger.error(f"Error delivering subscription {sub['id']}: {e}") - - def _generate_sub_report(self, sub: dict) -> str: - """Generate report content for a subscription.""" - if not self.mesh_reporter: - return None - - sub_type = sub["sub_type"] - scope_type = sub.get("scope_type", "mesh") - scope_value = sub.get("scope_value") - - if scope_type == "region" and scope_value: - # Region-scoped report - region = self.mesh_reporter._find_region(scope_value) - if region: - return self.mesh_reporter.build_region_compact(region.name) - return None - elif scope_type == "node" and scope_value: - # Node-scoped report - return self.mesh_reporter.build_node_compact(scope_value) - else: - # Mesh-wide report - return self.mesh_reporter.build_lora_compact(scope="mesh") - - async def _send_sub_dm(self, node_num: str, message: str) -> None: - """Send a subscription DM to a node.""" - if not self.connector: - return - - # Convert node_num to destination format - try: - dest = int(node_num) - except ValueError: - dest = node_num - - # Send via responder for proper chunking - if self.responder: - await self.responder.send_response( - message, - destination=dest, - channel=0, # DM channel - ) - else: - # Fallback to direct send - self.connector.send_message(message, destination=dest) - - -def setup_logging(verbose: bool = False) -> None: - """Configure logging.""" - level = logging.DEBUG if verbose else logging.INFO - logging.basicConfig( - level=level, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - -def main() -> None: - """Main entry point.""" - parser = argparse.ArgumentParser( - description="MeshAI - LLM-powered Meshtastic assistant", - prog="meshai", - ) - parser.add_argument( - "--version", "-V", action="version", version=f"%(prog)s {__version__}" - ) - parser.add_argument( - "--config", "-c", action="store_true", help="Launch configuration tool" - ) - parser.add_argument( - "--config-file", - "-f", - type=Path, - default=Path("config.yaml"), - help="Path to config file (default: config.yaml)", - ) - parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging") - - args = parser.parse_args() - - setup_logging(args.verbose) - - # Launch configurator if requested - if args.config: - run_configurator(args.config_file) - return - - # Load config - config = load_config(args.config_file) - - # Check if config exists - if not args.config_file.exists(): - logger.warning(f"Config file not found: {args.config_file}") - logger.info("Run 'meshai --config' to create one, or copy config.example.yaml") - sys.exit(1) - - # Create and run bot - bot = MeshAI(config) - - # Handle signals - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - def signal_handler(sig, frame): - logger.info(f"Received signal {sig}") - loop.create_task(bot.stop()) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - try: - loop.run_until_complete(bot.start()) - except KeyboardInterrupt: - pass - finally: - loop.run_until_complete(bot.stop()) - loop.close() - - -if __name__ == "__main__": - main() +"""Main entry point for MeshAI.""" + +import argparse +import asyncio +import logging +import os +import signal +import sys +import time +from pathlib import Path +from typing import Optional + +from . import __version__ +from .backends import AnthropicBackend, GoogleBackend, LLMBackend, OpenAIBackend +from .cli import run_configurator +from .commands import CommandDispatcher +from .commands.dispatcher import create_dispatcher +from .commands.status import set_start_time +from .config import Config, load_config +from .connector import MeshConnector, MeshMessage +from .context import MeshContext +from .history import ConversationHistory +from .memory import ConversationSummary +from .responder import Responder +from .router import MessageRouter, RouteType + +logger = logging.getLogger(__name__) + + +class MeshAI: + """Main application class.""" + + def __init__(self, config: Config): + self.config = config + self.connector: Optional[MeshConnector] = None + self.history: Optional[ConversationHistory] = None + self.dispatcher: Optional[CommandDispatcher] = None + self.llm: Optional[LLMBackend] = None + self.context: Optional[MeshContext] = None + self.meshmonitor_sync = None + self.knowledge = None + self.data_store = None # Replaces source_manager + self.health_engine = None + self.mesh_reporter = None + self.subscription_manager = None + self.alert_engine = None + self.env_store = None # Environmental feeds store + self._last_sub_check: float = 0.0 + self.router: Optional[MessageRouter] = None + self.responder: Optional[Responder] = None + self._running = False + self._loop: Optional[asyncio.AbstractEventLoop] = None + self._last_cleanup: float = 0.0 + self._last_health_compute: float = 0.0 + self.broadcaster = None # Dashboard WebSocket broadcaster + + async def start(self) -> None: + """Start the bot.""" + logger.info(f"Starting MeshAI v{__version__}") + set_start_time(time.time()) + + # Initialize components + await self._init_components() + + # Connect to Meshtastic + self.connector.connect() + self.connector.set_message_callback(self._on_message, asyncio.get_event_loop()) + + # Add own node ID to context ignore list + if self.context and self.connector.my_node_id: + self.context._ignore_nodes.add(self.connector.my_node_id) + + self._running = True + self._loop = asyncio.get_event_loop() + self._last_cleanup = time.time() + self._last_health_compute = 0.0 + + # Write PID file + self._write_pid() + + logger.info("MeshAI started successfully") + + # Keep running + while self._running: + await asyncio.sleep(1) + + # Periodic MeshMonitor refresh + if self.meshmonitor_sync: + self.meshmonitor_sync.maybe_refresh() + + # Periodic data store refresh and health computation + if self.data_store: + refreshed = self.data_store.refresh() + # Recompute health after refresh + if refreshed and self.health_engine: + self.health_engine.compute(self.data_store) + self._last_health_compute = time.time() + + # Broadcast health update to dashboard + if self.broadcaster and self.health_engine.mesh_health: + try: + mh = self.health_engine.mesh_health + health_dict = { + "score": round(mh.score.composite, 1), + "tier": mh.score.tier, + "total_nodes": mh.total_nodes, + "total_regions": mh.total_regions, + "infra_online": mh.score.infra_online, + "infra_total": mh.score.infra_total, + "last_computed": mh.last_computed, + } + await self.broadcaster.broadcast("health_update", health_dict) + except Exception as e: + logger.debug("Dashboard broadcast error: %s", e) + + # Check for alertable conditions + if self.alert_engine: + alerts = self.alert_engine.check() + if alerts: + await self._dispatch_alerts(alerts) + + # Broadcast alerts to dashboard + if self.broadcaster: + for alert in alerts: + try: + await self.broadcaster.broadcast("alert_fired", alert) + except Exception: + pass + + # Environmental feed refresh + if self.env_store: + try: + env_changed = self.env_store.refresh() + if env_changed and self.alert_engine: + env_alerts = self.alert_engine.check_environmental(self.env_store) + if env_alerts: + await self._dispatch_alerts(env_alerts) + if self.broadcaster: + for ea in env_alerts: + await self.broadcaster.broadcast("alert_fired", ea) + + # Broadcast env updates to dashboard + if env_changed and self.broadcaster: + await self.broadcaster.broadcast("env_update", { + "active_count": len(self.env_store.get_active()), + "swpc": self.env_store.get_swpc_status(), + "ducting": self.env_store.get_ducting_status(), + }) + except Exception as e: + logger.debug("Env refresh error: %s", e) + + # Check scheduled subscriptions (every 60 seconds) + if self.subscription_manager and self.mesh_reporter: + if time.time() - self._last_sub_check >= 60: + await self._check_scheduled_subs() + self._last_sub_check = time.time() + + # Periodic cleanup + if time.time() - self._last_cleanup >= 3600: + await self.history.cleanup_expired() + if self.context: + self.context.prune() + self._last_cleanup = time.time() + + async def stop(self) -> None: + """Stop the bot.""" + logger.info("Stopping MeshAI...") + self._running = False + + if self.connector: + self.connector.disconnect() + + if self.history: + await self.history.close() + + if self.llm: + await self.llm.close() + if self.knowledge: + self.knowledge.close() + if self.data_store: + self.data_store.close() + if self.subscription_manager: + self.subscription_manager.close() + + self._remove_pid() + logger.info("MeshAI stopped") + + async def _init_components(self) -> None: + """Initialize all components.""" + # Conversation history + self.history = ConversationHistory(self.config.history) + await self.history.initialize() + + # LLM backend + api_key = self.config.resolve_api_key() + if not api_key: + logger.warning("No API key configured - LLM responses will fail") + + # Memory config + mem_cfg = self.config.memory + window_size = mem_cfg.window_size if mem_cfg.enabled else 0 + summarize_threshold = mem_cfg.summarize_threshold + + # Create backend + backend = self.config.llm.backend.lower() + if backend == "openai": + self.llm = OpenAIBackend( + self.config.llm, api_key, window_size, summarize_threshold + ) + elif backend == "anthropic": + self.llm = AnthropicBackend( + self.config.llm, api_key, window_size, summarize_threshold + ) + elif backend == "google": + self.llm = GoogleBackend( + self.config.llm, api_key, window_size, summarize_threshold + ) + else: + logger.warning(f"Unknown backend '{backend}', defaulting to OpenAI") + self.llm = OpenAIBackend( + self.config.llm, api_key, window_size, summarize_threshold + ) + + # Load persisted summaries into memory cache + await self._load_summaries() + + # Meshtastic connector + self.connector = MeshConnector(self.config.connection) + + # Passive mesh context buffer + ctx_cfg = self.config.context + if ctx_cfg.enabled: + self.context = MeshContext( + observe_channels=ctx_cfg.observe_channels or None, + ignore_nodes=ctx_cfg.ignore_nodes or None, + max_age=ctx_cfg.max_age, + ) + logger.info("Mesh context buffer enabled") + else: + self.context = None + + # MeshMonitor trigger sync + mm_cfg = self.config.meshmonitor + if mm_cfg.enabled and mm_cfg.url: + from .meshmonitor import MeshMonitorSync + self.meshmonitor_sync = MeshMonitorSync( + url=mm_cfg.url, + refresh_interval=mm_cfg.refresh_interval, + ) + count = self.meshmonitor_sync.load() + logger.info(f"MeshMonitor sync enabled, loaded {count} triggers") + else: + self.meshmonitor_sync = None + + # Mesh data store (replaces MeshSourceManager) + # mesh_sources may be dicts or MeshSourceConfig objects depending on config version + enabled_sources = [ + s for s in self.config.mesh_sources + if (s.enabled if hasattr(s, 'enabled') else s.get('enabled', True)) + ] + if enabled_sources: + from .mesh_data_store import MeshDataStore + self.data_store = MeshDataStore( + source_configs=enabled_sources, + db_path="/data/mesh_history.db", + ) + # Initial fetch and backfill + self.data_store.force_refresh() + # Log status + for status in self.data_store.get_status(): + if status["is_loaded"]: + logger.info( + f"Mesh source '{status['name']}' ({status['type']}): " + f"{status['node_count']} nodes" + ) + else: + logger.warning( + f"Mesh source '{status['name']}' ({status['type']}): " + f"failed - {status.get('last_error', 'unknown error')}" + ) + else: + self.data_store = None + + # Mesh health engine + mi_cfg = self.config.mesh_intelligence + if mi_cfg.enabled and self.data_store: + from .mesh_health import MeshHealthEngine + self.health_engine = MeshHealthEngine( + regions=mi_cfg.regions, + locality_radius=mi_cfg.locality_radius_miles, + offline_threshold_hours=mi_cfg.offline_threshold_hours, + packet_threshold=mi_cfg.packet_threshold, + battery_warning_percent=mi_cfg.battery_warning_percent, + ) + # Initial health computation + mesh_health = self.health_engine.compute(self.data_store) + self._last_health_compute = time.time() + logger.info( + f"Mesh intelligence enabled: {mesh_health.total_nodes} nodes, " + f"{mesh_health.total_regions} regions, " + f"score {mesh_health.score.composite:.0f}/100 ({mesh_health.score.tier})" + ) + else: + self.health_engine = None + + # Mesh reporter (for LLM prompt injection and commands) + if self.health_engine and self.data_store: + from .mesh_reporter import MeshReporter + mi_regions = self.config.mesh_intelligence.regions if self.config.mesh_intelligence else [] + self.mesh_reporter = MeshReporter(self.health_engine, self.data_store, region_configs=mi_regions) + logger.info("Mesh reporter enabled") + else: + self.mesh_reporter = None + + # Subscription manager (uses same db as data_store) + if self.data_store: + from .subscriptions import SubscriptionManager + self.subscription_manager = SubscriptionManager(db_path="/data/mesh_history.db") + logger.info("Subscription manager enabled") + else: + self.subscription_manager = None + + # Alert engine (needs health engine, reporter, and subscription manager) + if self.health_engine and self.mesh_reporter and self.subscription_manager: + from .alert_engine import AlertEngine + mi = self.config.mesh_intelligence + self.alert_engine = AlertEngine( + health_engine=self.health_engine, + reporter=self.mesh_reporter, + subscription_manager=self.subscription_manager, + config=mi, + db_path="/data/mesh_history.db", + ) + logger.info(f"Alert engine initialized (critical: {mi.critical_nodes}, channel: {mi.alert_channel})") + + # Environmental feeds + env_cfg = self.config.environmental + if env_cfg.enabled: + from .env.store import EnvironmentalStore + self.env_store = EnvironmentalStore(config=env_cfg) + logger.info(f"Environmental feeds enabled ({len(self.env_store._adapters)} adapters)") + else: + self.env_store = None + + # Knowledge base (optional - Qdrant with SQLite fallback) + kb_cfg = self.config.knowledge + self.knowledge = None + if kb_cfg.enabled: + # Try Qdrant first if configured + if kb_cfg.backend in ("qdrant", "auto") and kb_cfg.qdrant_host: + try: + from .knowledge import QdrantKnowledgeSearch + qdrant = QdrantKnowledgeSearch( + qdrant_host=kb_cfg.qdrant_host, + qdrant_port=kb_cfg.qdrant_port, + collection=kb_cfg.qdrant_collection, + tei_host=kb_cfg.tei_host, + tei_port=kb_cfg.tei_port, + sparse_host=kb_cfg.sparse_host, + sparse_port=kb_cfg.sparse_port, + use_sparse=kb_cfg.use_sparse, + top_k=kb_cfg.top_k, + ) + if qdrant.available: + self.knowledge = qdrant + logger.info("Using Qdrant knowledge backend (RECON hybrid)") + except Exception as e: + logger.warning(f"Qdrant knowledge unavailable: {e}") + + # Fall back to SQLite if Qdrant failed or not configured + if not self.knowledge and kb_cfg.backend in ("sqlite", "auto") and kb_cfg.db_path: + try: + from .knowledge import KnowledgeSearch + self.knowledge = KnowledgeSearch( + db_path=kb_cfg.db_path, + top_k=kb_cfg.top_k, + ) + except ImportError as e: + logger.warning(f"SQLite knowledge disabled - missing dependencies: {e}") + + # Command dispatcher (needs mesh_reporter for health commands) + self.dispatcher = create_dispatcher( + prefix=self.config.commands.prefix, + disabled_commands=self.config.commands.disabled_commands, + custom_commands=self.config.commands.custom_commands, + mesh_reporter=self.mesh_reporter, + data_store=self.data_store, + health_engine=self.health_engine, + subscription_manager=self.subscription_manager, + env_store=self.env_store, + ) + + # Message router + self.router = MessageRouter( + self.config, self.connector, self.history, self.dispatcher, self.llm, + context=self.context, + meshmonitor_sync=self.meshmonitor_sync, + knowledge=self.knowledge, + source_manager=self.data_store, + health_engine=self.health_engine, + mesh_reporter=self.mesh_reporter, + env_store=self.env_store, + ) + + # Responder + self.responder = Responder(self.config.response, self.connector) + + # Dashboard + if hasattr(self.config, 'dashboard') and self.config.dashboard.enabled: + try: + from .dashboard.server import start_dashboard + self.broadcaster = await start_dashboard(self) + logger.info("Dashboard started on port %d", self.config.dashboard.port) + except Exception as e: + logger.warning("Dashboard failed to start: %s", e) + self.broadcaster = None + else: + self.broadcaster = None + + async def _on_message(self, message: MeshMessage) -> None: + """Handle incoming message.""" + try: + # Passively observe channel broadcasts for context (before filtering) + if self.context and not message.is_dm and message.text: + self.context.observe( + sender_name=message.sender_name, + sender_id=message.sender_id, + text=message.text, + channel=message.channel, + is_dm=False, + ) + + # Check if we should respond + if not self.router.should_respond(message): + return + + logger.info( + f"Processing message from {message.sender_name} ({message.sender_id}): " + f"{message.text[:50]}..." + ) + + # Route the message + # Check for continuation request first + continuation_messages = self.router.check_continuation(message) + if continuation_messages: + await self.responder.send_response( + continuation_messages, + destination=message.sender_id, + channel=message.channel, + ) + return + + result = await self.router.route(message) + + if result.route_type == RouteType.IGNORE: + return + + # Determine response + if result.route_type == RouteType.COMMAND: + if isinstance(result.response, list): + # Command returned pre-split messages — send directly + messages = result.response + else: + # Single string — chunk it + from .chunker import chunk_response + messages, remaining = chunk_response( + result.response, + max_chars=self.config.response.max_length, + max_messages=self.config.response.max_messages, + ) + if remaining: + self.router.continuations.store(message.sender_id, remaining) + elif result.route_type == RouteType.LLM: + messages = await self.router.generate_llm_response(message, result.query) + else: + return + + if not messages: + return + + # Send DM response + await self.responder.send_response( + messages, + destination=message.sender_id, + channel=message.channel, + ) + + except Exception as e: + logger.error(f"Error handling message: {e}", exc_info=True) + + async def _load_summaries(self) -> None: + """Load persisted summaries from database into memory cache.""" + memory = self.llm.get_memory() + if not memory: + return + + if not self.history or not self.history._db: + return + + try: + async with self.history._lock: + cursor = await self.history._db.execute( + "SELECT user_id, summary, message_count, updated_at " + "FROM conversation_summaries" + ) + rows = await cursor.fetchall() + + loaded = 0 + for row in rows: + user_id, summary_text, message_count, updated_at = row + summary = ConversationSummary( + summary=summary_text, + last_updated=updated_at, + message_count=message_count, + ) + memory.load_summary(user_id, summary) + loaded += 1 + + if loaded: + logger.info(f"Loaded {loaded} conversation summaries from database") + + except Exception as e: + logger.warning(f"Failed to load summaries from database: {e}") + + def _write_pid(self) -> None: + """Write PID file.""" + pid_file = Path("/tmp/meshai.pid") + pid_file.write_text(str(os.getpid())) + + def _remove_pid(self) -> None: + """Remove PID file.""" + pid_file = Path("/tmp/meshai.pid") + if pid_file.exists(): + pid_file.unlink() + + async def _dispatch_alerts(self, alerts: list[dict]) -> None: + """Dispatch alerts to subscribers and alert channel.""" + mi = self.config.mesh_intelligence + alert_channel = getattr(mi, 'alert_channel', -1) + + for alert in alerts: + message = alert["message"] + logger.info(f"ALERT: {message}") + + # Send to alert channel if configured + if alert_channel >= 0 and self.connector: + try: + self.connector.send_message( + text=message, + destination=None, # Broadcast + channel=alert_channel, + ) + logger.info(f"Alert sent to channel {alert_channel}") + except Exception as e: + logger.error(f"Failed to send channel alert: {e}") + + # Send DMs to matching subscribers + if self.alert_engine and self.subscription_manager: + subscribers = self.alert_engine.get_subscribers_for_alert(alert) + for sub in subscribers: + user_id = sub["user_id"] + try: + await self._send_sub_dm(user_id, message) + logger.info(f"Alert DM sent to {user_id}: {alert['type']}") + except Exception as e: + logger.error(f"Failed to send alert DM to {user_id}: {e}") + + self.alert_engine.clear_pending() + + async def _check_scheduled_subs(self) -> None: + """Check for and deliver due scheduled reports.""" + from datetime import datetime + from zoneinfo import ZoneInfo + + tz = ZoneInfo("America/Boise") + now = datetime.now(tz) + current_hhmm = now.strftime("%H%M") + current_day = now.strftime("%a").lower() + + due_subs = self.subscription_manager.get_due_subscriptions(current_hhmm, current_day) + + for sub in due_subs: + try: + # Generate report based on scope + report = self._generate_sub_report(sub) + if not report: + continue + + # Send DM to subscriber + user_id = sub["user_id"] + await self._send_sub_dm(user_id, report) + + # Mark as sent + self.subscription_manager.mark_sent(sub["id"]) + logger.info(f"Delivered {sub['sub_type']} report to {user_id}") + + except Exception as e: + logger.error(f"Error delivering subscription {sub['id']}: {e}") + + def _generate_sub_report(self, sub: dict) -> str: + """Generate report content for a subscription.""" + if not self.mesh_reporter: + return None + + sub_type = sub["sub_type"] + scope_type = sub.get("scope_type", "mesh") + scope_value = sub.get("scope_value") + + if scope_type == "region" and scope_value: + # Region-scoped report + region = self.mesh_reporter._find_region(scope_value) + if region: + return self.mesh_reporter.build_region_compact(region.name) + return None + elif scope_type == "node" and scope_value: + # Node-scoped report + return self.mesh_reporter.build_node_compact(scope_value) + else: + # Mesh-wide report + return self.mesh_reporter.build_lora_compact(scope="mesh") + + async def _send_sub_dm(self, node_num: str, message: str) -> None: + """Send a subscription DM to a node.""" + if not self.connector: + return + + # Convert node_num to destination format + try: + dest = int(node_num) + except ValueError: + dest = node_num + + # Send via responder for proper chunking + if self.responder: + await self.responder.send_response( + message, + destination=dest, + channel=0, # DM channel + ) + else: + # Fallback to direct send + self.connector.send_message(message, destination=dest) + + +def setup_logging(verbose: bool = False) -> None: + """Configure logging.""" + level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig( + level=level, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + +def main() -> None: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="MeshAI - LLM-powered Meshtastic assistant", + prog="meshai", + ) + parser.add_argument( + "--version", "-V", action="version", version=f"%(prog)s {__version__}" + ) + parser.add_argument( + "--config", "-c", action="store_true", help="Launch configuration tool" + ) + parser.add_argument( + "--config-file", + "-f", + type=Path, + default=Path("config.yaml"), + help="Path to config file (default: config.yaml)", + ) + parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging") + + args = parser.parse_args() + + setup_logging(args.verbose) + + # Launch configurator if requested + if args.config: + run_configurator(args.config_file) + return + + # Load config + config = load_config(args.config_file) + + # Check if config exists + if not args.config_file.exists(): + logger.warning(f"Config file not found: {args.config_file}") + logger.info("Run 'meshai --config' to create one, or copy config.example.yaml") + sys.exit(1) + + # Create and run bot + bot = MeshAI(config) + + # Handle signals + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + def signal_handler(sig, frame): + logger.info(f"Received signal {sig}") + loop.create_task(bot.stop()) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + loop.run_until_complete(bot.start()) + except KeyboardInterrupt: + pass + finally: + loop.run_until_complete(bot.stop()) + loop.close() + + +if __name__ == "__main__": + main() diff --git a/meshai/router.py b/meshai/router.py index 9f11148..3a1fab4 100644 --- a/meshai/router.py +++ b/meshai/router.py @@ -1,837 +1,858 @@ -"""Message routing logic for MeshAI.""" - -import asyncio -import logging -import re -from dataclasses import dataclass -from enum import Enum, auto -from typing import Optional - -from .backends.base import LLMBackend -from .commands import CommandContext, CommandDispatcher -from .config import Config -from .connector import MeshConnector, MeshMessage -from .context import MeshContext -from .history import ConversationHistory -from .chunker import chunk_response, ContinuationState - -logger = logging.getLogger(__name__) - - -class RouteType(Enum): - """Type of message routing.""" - - IGNORE = auto() # Don't respond - COMMAND = auto() # Bang command - LLM = auto() # Route to LLM - - -@dataclass -class RouteResult: - """Result of routing decision.""" - - route_type: RouteType - response: Optional[str] = None # For commands, the response - query: Optional[str] = None # For LLM, the cleaned query - - -# advBBS protocol and notification prefixes to ignore -ADVBBS_PREFIXES = ( - "MAILREQ|", "MAILACK|", "MAILNAK|", "MAILDAT|", "MAILDLV|", - "BOARDREQ|", "BOARDACK|", "BOARDNAK|", "BOARDDAT|", "BOARDDLV|", - "advBBS|", - "[MAIL]", -) - -# Patterns that suggest prompt injection attempts -_INJECTION_PATTERNS = [ - re.compile(r"ignore\s+(all\s+)?previous", re.IGNORECASE), - re.compile(r"ignore\s+your\s+instructions", re.IGNORECASE), - re.compile(r"disregard\s+(all\s+)?previous", re.IGNORECASE), - re.compile(r"you\s+are\s+now\b", re.IGNORECASE), - re.compile(r"new\s+instructions?\s*:", re.IGNORECASE), - re.compile(r"system\s*prompt\s*:", re.IGNORECASE), -] - -# Keywords that indicate mesh-related questions -_MESH_KEYWORDS = { - "mesh", "network", "health", "nodes", "node", "utilization", "signal", - "coverage", "battery", "solar", "offline", "router", "channel", "packet", - "hop", "optimize", "optimization", "infrastructure", "infra", "relay", - "repeater", "region", "locality", "congestion", "collision", "airtime", - "telemetry", "firmware", "subscribe", "alert", "snr", "rssi", - # Additional keywords for better detection - "noisy", "noisiest", "traffic", "packets", "power", "routers", - "repeaters", "regions", "localities", "score", "status", -} - -# Phrases that indicate mesh questions -_MESH_PHRASES = [ - "how's the mesh", - "hows the mesh", - "mesh status", - "what's wrong", - "whats wrong", - "check node", - "node status", - "network health", - "mesh health", - "which node", - "which nodes", - "which infra", - "list nodes", - "list infra", - "tell me about", - "what about", - "how is", - "how are", -] - -# City name to region mapping (hardcoded fallback) -# City/alias mapping now built from config - see _build_alias_map() - -# Mesh awareness instruction for LLM -# Mesh awareness instruction for LLM -_MESH_AWARENESS_PROMPT = """ -MESH DATA RESPONSE RULES (OVERRIDE brevity rules for mesh/network questions): - -The data blocks above contain detailed information about every region, infrastructure node, -coverage gap, and problem node on the mesh. USE THIS DATA in your response. - -RESPONSE STYLE: -- DETAILED, data-driven responses. Reference specific node names, scores, gateway counts. -- Use LOCAL NAMES from the region descriptions (Magic Valley, Treasure Valley, etc.) -- ALWAYS use local region names: say "Treasure Valley" not "South Western ID", say "Magic Valley" not "South Central ID". The code names mean nothing to users. -- When listing nodes, be concise: "BT Base c8d5 — via AIDA" not "BT Base c8d5 (c8d5) is connected via AIDA-MeshMonitor in the South Western ID region." -- Don't repeat the region on every line when listing multiple nodes in the same region. Say the region once at the top, then just list the nodes. -- Don't include shortnames in parentheses when you're already giving the full name — it's noise. -- When discussing infrastructure, name the actual nodes (Mount Harrison Router, not just "5 infra") -- When discussing coverage gaps, explain WHERE and HOW MANY nodes are affected -- When discussing problems, name the node and explain the impact -- You CAN use 3-5 messages. Keep each sentence under 150 characters. -- No markdown formatting - plain text only -- ABSOLUTELY NO markdown. No asterisks, no bold, no bullet points with * or -, no numbered lists with 1. 2. 3. Just plain text sentences. -- NEVER say "Want me to keep going?" — the message system adds this automatically when needed. If you say it yourself, users see it twice. -- When explaining "X/Y gateways" (like 7/7), explain that it means the node is visible to X out of Y data sources (Meshview and MeshMonitor instances that monitor the mesh). It does NOT mean infrastructure routers or regional gateways. -- When reporting packet types, ALWAYS use the name (Position, NodeInfo, Telemetry) not the number. -- Normal position interval: 15-30 minutes (48-96 packets/day). 400+ Position packets in 24h means aggressive position interval, wasting airtime. Tell the user. -- Normal NodeInfo: every 2-3 hours (8-12/day). 50+ is excessive. -- Normal NeighborInfo: every 6 hours (4/day). 20+ is aggressive. -- If a node has high packet volume, explain WHAT the packets are and WHETHER the rate is abnormal compared to normal intervals. - -QUESTION TYPES: -- "How's the mesh?" -> Lead with composite score. Highlight 1-2 biggest issues by name. Summarize each region briefly. -- "Where do we need coverage?" -> Name regions with single-gateway nodes. Name offline infra. Suggest specific locations. -- "Tell me about [node]" -> Give full detail from the data above. -- "How is [region]?" -> Give that region's infrastructure status, coverage, issues. -- "What's wrong?" -> List problem nodes by name with specifics. - -IMPORTANT: Do NOT lump different regions together. Each is a distinct area. -Do NOT recommend infrastructure for "Unlocated" nodes - they have no known position. -""" - - -def _build_region_abbreviations(region_names: list[str]) -> dict[str, str]: - """Build abbreviation to region name mapping. - - Generates abbreviations like: - - "South Central ID" -> "SCID", "SC-ID", "SC ID" - - "South Western ID" -> "SWID", "SW-ID", "SW ID" - - Args: - region_names: List of full region names - - Returns: - Dict mapping lowercase abbreviation to full region name - """ - abbrevs = {} - - for name in region_names: - parts = name.replace("???", "-").replace("???", "-").split() - if not parts: - continue - - # Get first letter of each word (uppercase) - initials = "".join(p[0].upper() for p in parts if p) - abbrevs[initials.lower()] = name - - # If last part is a state abbrev (2 chars), create variants - if len(parts) >= 2: - last = parts[-1] - if len(last) == 2 and last.isupper(): - # "South Central ID" -> prefix is "South Central" - prefix_parts = parts[:-1] - prefix_initials = "".join(p[0].upper() for p in prefix_parts) - - # SC-ID, SC ID, SCID variants - abbrevs[f"{prefix_initials.lower()}-{last.lower()}"] = name - abbrevs[f"{prefix_initials.lower()} {last.lower()}"] = name - abbrevs[f"{prefix_initials.lower()}{last.lower()}"] = name - - return abbrevs - - -class MessageRouter: - """Routes incoming messages to appropriate handlers.""" - - def __init__( - self, - config: Config, - connector: MeshConnector, - history: ConversationHistory, - dispatcher: CommandDispatcher, - llm_backend: LLMBackend, - context: MeshContext = None, - meshmonitor_sync=None, - knowledge=None, - source_manager=None, - health_engine=None, - mesh_reporter=None, - ): - self.config = config - self.connector = connector - self.history = history - self.dispatcher = dispatcher - self.llm = llm_backend - self.context = context - self.meshmonitor_sync = meshmonitor_sync - self.knowledge = knowledge - self.source_manager = source_manager - self.health_engine = health_engine - self.mesh_reporter = mesh_reporter - self.continuations = ContinuationState(max_continuations=3) - - # Per-user mesh context tracking for follow-up handling - # Maps user_id -> {"last_was_mesh": bool, "last_scope": (type, value), "non_mesh_count": int} - self._user_mesh_context: dict[str, dict] = {} - - # Build region abbreviation map - self._region_abbrevs: dict[str, str] = {} - if self.health_engine and self.health_engine.regions: - region_names = [r.name for r in self.health_engine.regions] - self._region_abbrevs = _build_region_abbreviations(region_names) - logger.debug(f"Built region abbreviations: {self._region_abbrevs}") - - # Build city/alias mapping from config - self._alias_map = self._build_alias_map() - if self._alias_map: - logger.debug(f"Built alias map with {len(self._alias_map)} entries") - - def _build_alias_map(self) -> dict[str, str]: - """Build city/alias to region mapping from config.""" - alias_map = {} - if self.config.mesh_intelligence and self.config.mesh_intelligence.regions: - for region in self.config.mesh_intelligence.regions: - # Add aliases - for alias in (getattr(region, 'aliases', []) or []): - alias_map[alias.lower()] = region.name - # Add cities - for city in (getattr(region, 'cities', []) or []): - alias_map[city.lower()] = region.name - # Add local_name - local = getattr(region, 'local_name', '') or '' - if local: - alias_map[local.lower()] = region.name - return alias_map - - def should_respond(self, message: MeshMessage) -> bool: - """Determine if we should respond to this message. - - DM-only bot: ignores all public channel messages. - Commands and conversational LLM responses both work in DMs. - - Args: - message: Incoming message - - Returns: - True if we should process this message - """ - # Always ignore our own messages - if message.sender_id == self.connector.my_node_id: - return False - - # Only respond to DMs - if not message.is_dm: - return False - - if not self.config.bot.respond_to_dms: - return False - - # Ignore advBBS protocol and notification messages - if self.config.bot.filter_bbs_protocols: - if any(message.text.startswith(p) for p in ADVBBS_PREFIXES): - logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...") - return False - - # Ignore messages that MeshMonitor will handle - if self.meshmonitor_sync and self.meshmonitor_sync.matches(message.text): - logger.debug(f"Ignoring MeshMonitor-handled message: {message.text[:40]}...") - return False - - return True - - def check_continuation(self, message) -> list[str] | None: - """Check if this is a continuation request and return messages if so. - - Returns: - List of messages to send, or None if not a continuation - """ - user_id = message.sender_id - text = message.text.strip() - - logger.debug(f"check_continuation: user={user_id}, text='{text[:30]}', has_pending={self.continuations.has_pending(user_id)}") - - if self.continuations.has_pending(user_id): - if self.continuations.is_continuation_request(text): - result = self.continuations.get_continuation(user_id) - if result: - messages, _ = result - return messages - # Max continuations reached, return None to fall through - else: - # User asked something new, clear pending continuation - self.continuations.clear(user_id) - - return None - - async def route(self, message: MeshMessage) -> RouteResult: - """Route a message and generate response. - - Args: - message: Incoming message to route - - Returns: - RouteResult with routing decision and any response - """ - text = message.text.strip() - - # Check for bang command first - if self.dispatcher.is_command(text): - context = self._make_command_context(message) - response = await self.dispatcher.dispatch(text, context) - return RouteResult(RouteType.COMMAND, response=response) - - # Clean up the message (remove @mention) - query = self._clean_query(text) - - if not query: - return RouteResult(RouteType.IGNORE) - - # Route to LLM - return RouteResult(RouteType.LLM, query=query) - - def _is_mesh_question(self, message: str) -> bool: - """Check if message is asking about mesh health/status. - - Args: - message: User message text - - Returns: - True if this is a mesh-related question - """ - msg_lower = message.lower() - - # Check for mesh phrases - for phrase in _MESH_PHRASES: - if phrase in msg_lower: - return True - - # Check for mesh keywords - words = set(re.findall(r'\b\w+\b', msg_lower)) - if words & _MESH_KEYWORDS: - return True - - return False - - def _detect_mesh_scope(self, message: str) -> tuple[str, Optional[str]]: - """Detect the scope of a mesh question. - - Args: - message: User message text - - Returns: - Tuple of (scope_type, scope_value): - - ("node", "{identifier}") if asking about specific node - - ("region", "{region_name}") if asking about specific region - - ("mesh", None) for general mesh questions - """ - msg_lower = message.lower() - - # === NODE MATCHING (check first - more specific) === - if self.health_engine and self.health_engine.mesh_health: - health = self.health_engine.mesh_health - - # 1. Exact shortname match (case-insensitive, word boundary) - for node in health.nodes.values(): - if node.short_name: - pattern = r'\b' + re.escape(node.short_name.lower()) + r'\b' - if re.search(pattern, msg_lower): - return ("node", node.short_name) - - # 2. Longname substring match (case-insensitive) - for node in health.nodes.values(): - if node.long_name and len(node.long_name) > 3: - # Match significant portion of longname - if node.long_name.lower() in msg_lower: - return ("node", node.short_name or node.node_id) - # Also try matching without common suffixes like "Router", "Repeater" - clean_name = node.long_name.lower() - for suffix in [" router", " repeater", " relay", " base", " v2", " - g2"]: - clean_name = clean_name.replace(suffix, "") - if len(clean_name) > 4 and clean_name in msg_lower: - return ("node", node.short_name or node.node_id) - - # 3. NodeId hex match (with or without ! prefix) - hex_pattern = r'!?([0-9a-f]{8})' - hex_match = re.search(hex_pattern, msg_lower) - if hex_match: - hex_id = hex_match.group(1) - for nid, node in health.nodes.items(): - if hex_id in nid.lower(): - return ("node", node.short_name or nid) - - # 4. NodeNum decimal match - num_pattern = r'\b(\d{9,10})\b' - num_match = re.search(num_pattern, message) - if num_match: - node_num = int(num_match.group(1)) - hex_id = format(node_num, 'x') - for nid, node in health.nodes.items(): - if hex_id in nid.lower(): - return ("node", node.short_name or nid) - - # === REGION MATCHING === - if self.health_engine: - # 1. Check abbreviations first (SCID, SWID, etc.) - for abbrev, region_name in self._region_abbrevs.items(): - # Match as word boundary - pattern = r'\b' + re.escape(abbrev) + r'\b' - if re.search(pattern, msg_lower): - return ("region", region_name) - - # 2. Check city names and aliases from config - for alias, region_name in self._alias_map.items(): - if alias in msg_lower: - return ("region", region_name) - - # 3. Full region name matching (SORTED BY LENGTH - longest first) - regions_by_length = sorted( - self.health_engine.regions, - key=lambda r: len(r.name), - reverse=True - ) - - for anchor in regions_by_length: - anchor_lower = anchor.name.lower() - # Check full region name - if anchor_lower in msg_lower: - return ("region", anchor.name) - - # 4. Partial region name matching (also longest first) - for anchor in regions_by_length: - anchor_lower = anchor.name.lower() - # Check significant parts of region name - # Split on common separators - parts = anchor_lower.replace("-", " ").replace("???", " ").replace("???", " ").split() - # Only match on significant words (>3 chars, not state abbrevs) - significant_parts = [p for p in parts if len(p) > 3] - - # Check if ALL significant parts appear in message - if significant_parts and all(p in msg_lower for p in significant_parts): - return ("region", anchor.name) - - return ("mesh", None) - - def _get_user_mesh_context(self, user_id: str) -> dict: - """Get or create mesh context for a user.""" - if user_id not in self._user_mesh_context: - self._user_mesh_context[user_id] = { - "last_was_mesh": False, - "last_scope": ("mesh", None), - "non_mesh_count": 0, - } - return self._user_mesh_context[user_id] - - def _update_user_mesh_context( - self, - user_id: str, - is_mesh: bool, - scope: tuple[str, Optional[str]] = None, - ) -> None: - """Update mesh context tracking for a user.""" - ctx = self._get_user_mesh_context(user_id) - - if is_mesh: - ctx["last_was_mesh"] = True - ctx["non_mesh_count"] = 0 - if scope: - ctx["last_scope"] = scope - else: - ctx["non_mesh_count"] += 1 - # Reset after 2 consecutive non-mesh messages - if ctx["non_mesh_count"] >= 2: - ctx["last_was_mesh"] = False - ctx["last_scope"] = ("mesh", None) - - def _try_compute_distance(self, query: str) -> str: - """Extract two node names from a distance question and compute distance.""" - if not self.mesh_reporter: - return "" - - health = self.mesh_reporter.health_engine.mesh_health - if not health: - return "" - - query_lower = query.lower() - - # Build name -> node lookup (include partial long_name matches) - node_names = {} - for node in health.nodes.values(): - if node.short_name: - node_names[node.short_name.lower()] = node - if node.long_name: - full = node.long_name.lower() - node_names[full] = node - # Add partial matches: "TVM Pearl Relay" also matches "TVM Pearl" - words = full.split() - if len(words) >= 2: - for i in range(2, len(words) + 1): - partial = " ".join(words[:i]) - if partial not in node_names: - node_names[partial] = node - - # AIDA aliases - aida_node = health.nodes.get(0x27780c47) - if aida_node: - for alias in ["aida", "aida-n2", "me", "my node", "yourself", "your position", "you"]: - node_names[alias] = aida_node - - # Find mentioned nodes (longest names first) - found_nodes = [] - - for name in sorted(node_names.keys(), key=len, reverse=True): - if name in query_lower and len(name) >= 2: - node = node_names[name] - if not any(n.node_num == node.node_num for n in found_nodes): - found_nodes.append(node) - if len(found_nodes) >= 2: - break - - # If we only found one or zero nodes, check for ambiguous short terms - if len(found_nodes) < 2: - query_words = query_lower.replace("?", "").replace("!", "").split() - candidate_terms = list(query_words) - for i in range(len(query_words) - 1): - candidate_terms.append(f"{query_words[i]} {query_words[i+1]}") - - skip_words = {"how", "far", "is", "from", "the", "to", "and", "between", "what", - "distance", "away", "are", "apart", "tell", "me", "about", "a", "an"} - - for term in candidate_terms: - if term in skip_words or len(term) < 2: - continue - matches = [] - seen_nums = set() - for node in health.nodes.values(): - if node.node_num in seen_nums: - continue - name_lower = (node.long_name or "").lower() - short_lower = (node.short_name or "").lower() - if term in name_lower or term == short_lower: - matches.append(node) - seen_nums.add(node.node_num) - - if len(matches) > 1: - names = [f" - {n.long_name or n.short_name} ({n.short_name})" - for n in matches[:6]] - return ( - f"AMBIGUOUS: '{term}' matches multiple nodes. " - f"Ask the user which one they mean:\n" + "\n".join(names) - ) - - if len(found_nodes) == 2: - return self.mesh_reporter.build_distance( - str(found_nodes[0].node_num), - str(found_nodes[1].node_num) - ) - elif len(found_nodes) == 1 and aida_node: - return self.mesh_reporter.build_distance( - str(found_nodes[0].node_num), - str(aida_node.node_num) - ) - - return "" - - - async def generate_llm_response(self, message: MeshMessage, query: str) -> str: - """Generate LLM response for a message. - - Args: - message: Original message - query: Cleaned query text - - Returns: - Generated response - """ - # Add user message to history - await self.history.add_message(message.sender_id, "user", query) - - # Get conversation history - history = await self.history.get_history_for_llm(message.sender_id) - - # Build system prompt in order: identity -> static -> meshmonitor -> context -> knowledge -> mesh - - # 1. Dynamic identity from bot config - bot_name = self.config.bot.name or "MeshAI" - bot_owner = self.config.bot.owner or "Unknown" - - identity = ( - f"You are {bot_name}, an LLM-powered assistant on the freq51 Meshtastic mesh network. " - f"Your managing operator is {bot_owner}. " - f"You are open source at github.com/zvx-echo6/meshai.\n\n" - f"IDENTITY: Your name is {bot_name}. You ARE a physical node on the mesh — " - f"node !27780c47 (AIDA-N2). You have a real location, real GPS coordinates, " - f"and real radio connections. When someone asks how far something is from you, " - f"check the mesh data for your node's position and calculate. " - f"You are NOT just software — you are a node that other nodes can see, hear, and route through.\n\n" - ) - - # 2. Static system prompt from config - static_prompt = "" - if getattr(self.config.llm, 'use_system_prompt', True): - static_prompt = self.config.llm.system_prompt - - system_prompt = identity + static_prompt - - # 2b. Dynamic command list (only shows enabled commands) - if self.dispatcher: - commands = self.dispatcher.get_commands() - if commands: - # Deduplicate aliases - seen_names = set() - unique_commands = [] - for cmd in commands: - name_lower = cmd.name.lower() - if name_lower not in seen_names: - seen_names.add(name_lower) - unique_commands.append(cmd) - - cmd_lines = [ - "\nYOUR COMMANDS (only mention these - do NOT mention any commands not listed here):" - ] - for cmd in sorted(unique_commands, key=lambda c: c.name): - cmd_lines.append(f" !{cmd.name} - {cmd.description}") - cmd_lines.append("") - cmd_lines.append( - "CRITICAL: ONLY mention commands in the list above when asked about commands. " - "If a command is not listed here, it does NOT exist. Do not invent commands." - ) - system_prompt += "\n".join(cmd_lines) - - # 3. MeshMonitor info (only when enabled) - if ( - self.meshmonitor_sync - and self.config.meshmonitor.enabled - and self.config.meshmonitor.inject_into_prompt - ): - meshmonitor_intro = ( - "\n\nMESHMONITOR: You run alongside MeshMonitor (by Yeraze) on the same " - "meshtasticd node. MeshMonitor handles web dashboard, maps, telemetry, " - "traceroutes, security scanning, and auto-responder commands. Its trigger " - "commands are listed below ??? if someone asks what commands are available, " - "ONLY list YOUR commands from YOUR COMMANDS above. If someone asks where to get " - "MeshMonitor, direct them to github.com/Yeraze/meshmonitor" - ) - system_prompt += meshmonitor_intro - - commands_summary = self.meshmonitor_sync.get_commands_summary() - if commands_summary: - system_prompt += "\n\n" + commands_summary - - # 4. Inject mesh context if available - if self.context: - max_items = getattr(self.config.context, 'max_context_items', 20) - context_block = self.context.get_context_block(max_items=max_items) - if context_block: - system_prompt += ( - "\n\n--- Recent mesh traffic (for context only, not messages to you) ---\n" - + context_block - ) - else: - system_prompt += ( - "\n\n[No recent mesh traffic observed yet.]" - ) - - # 5. Knowledge base retrieval - if self.knowledge and query: - results = self.knowledge.search(query) - if results: - chunks = "\n\n".join( - f"[{r['title']}]: {r['excerpt']}" for r in results - ) - system_prompt += ( - "\n\nREFERENCE KNOWLEDGE - Answer using this information:\n" - + chunks - ) - - # 6. Mesh Intelligence (inject health data for mesh questions) - user_ctx = self._get_user_mesh_context(message.sender_id) - is_direct_mesh_question = self._is_mesh_question(query) - is_followup = user_ctx["last_was_mesh"] and not is_direct_mesh_question - - should_inject_mesh = is_direct_mesh_question or is_followup - - if self.source_manager and self.mesh_reporter and should_inject_mesh: - # Detect scope from current message - scope_type, scope_value = self._detect_mesh_scope(query) - - # For follow-ups with no detected scope, use previous scope - if is_followup and scope_type == "mesh" and scope_value is None: - prev_scope = user_ctx.get("last_scope", ("mesh", None)) - if prev_scope[0] != "mesh" or prev_scope[1] is not None: - scope_type, scope_value = prev_scope - logger.debug(f"Using previous scope for follow-up: {scope_type}, {scope_value}") - - # Always include Tier 1 summary for mesh questions - tier1 = self.mesh_reporter.build_tier1_summary() - system_prompt += "\n\n" + tier1 - - # Add Tier 2 detail if scoped - if scope_type == "region" and scope_value: - region_detail = self.mesh_reporter.build_region_detail(scope_value) - system_prompt += "\n\n" + region_detail - elif scope_type == "node" and scope_value: - node_detail = self.mesh_reporter.build_node_detail(scope_value) - system_prompt += "\n\n" + node_detail - - # Always include relevant recommendations - recommendations = self.mesh_reporter.build_recommendations(scope_type, scope_value) - if recommendations: - system_prompt += "\n\n" + recommendations - - # Add mesh awareness instructions - system_prompt += _MESH_AWARENESS_PROMPT - - # Build region geography from config dynamically - if self.config.mesh_intelligence and self.config.mesh_intelligence.regions: - geo_lines = ["", "REGION GEOGRAPHY (use local names when discussing these regions):"] - for region in self.config.mesh_intelligence.regions: - local = getattr(region, "local_name", "") or "" - local_str = f' "{local}"' if local else "" - desc = getattr(region, "description", "") or "" - desc_str = f" — {desc}" if desc else "" - aliases = getattr(region, "aliases", []) or [] - alias_str = "" - if aliases: - alias_str = f'\n People may call this: {", ".join(aliases)}' - geo_lines.append(f" - {region.name}{local_str}{desc_str}{alias_str}") - system_prompt += "\n".join(geo_lines) - - # Update mesh context tracking - self._update_user_mesh_context( - message.sender_id, - is_mesh=True, - scope=(scope_type, scope_value), - ) - else: - # Not a mesh question - self._update_user_mesh_context(message.sender_id, is_mesh=False) - - # DEBUG: Log system prompt status - logger.debug(f"System prompt length: {len(system_prompt)} chars") - - # Detect distance questions and inject computed distance - distance_keywords = ["how far", "distance", "how close", "miles from", "km from", "away from"] - if any(kw in query.lower() for kw in distance_keywords): - distance_result = self._try_compute_distance(query) - if distance_result: - system_prompt += f"\n\nDISTANCE CALCULATION:\n{distance_result}\n" - - try: - response = await self.llm.generate( - messages=history, - system_prompt=system_prompt, - max_tokens=self.config.llm.max_response_tokens, - ) - except asyncio.TimeoutError: - logger.error("LLM request timed out") - response = "Sorry, request timed out. Try again." - except Exception as e: - logger.error(f"LLM generation error: {e}") - response = "Sorry, I encountered an error. Please try again." - - # Add assistant response to history - await self.history.add_message(message.sender_id, "assistant", response) - - # Persist summary if one was created/updated - await self._persist_summary(message.sender_id) - - # Strip any markdown the LLM ignored instructions about - from .chunker import strip_markdown - response = strip_markdown(response) - - # Chunk the response with sentence awareness - messages, remaining = chunk_response( - response, - max_chars=self.config.response.max_length, - max_messages=self.config.response.max_messages, - ) - - # Store remaining content for continuation - if remaining: - logger.debug(f"Storing continuation for {message.sender_id}: {len(remaining)} chars remaining") - self.continuations.store(message.sender_id, remaining) - - return messages - - async def _persist_summary(self, user_id: str) -> None: - """Persist any cached summary to the database. - - Args: - user_id: User identifier - """ - memory = self.llm.get_memory() - if not memory: - return - - summary = memory.get_cached_summary(user_id) - if summary: - await self.history.store_summary( - user_id, - summary.summary, - summary.message_count, - ) - logger.debug(f"Persisted summary for {user_id}") - - def _clean_query(self, text: str) -> str: - """Clean up query text and check for prompt injection.""" - cleaned = " ".join(text.split()) - cleaned = cleaned.strip() - - # Check for prompt injection - for pattern in _INJECTION_PATTERNS: - if pattern.search(cleaned): - logger.warning( - f"Possible prompt injection detected: {cleaned[:80]}..." - ) - match = pattern.search(cleaned) - cleaned = cleaned[:match.start()].strip() - if not cleaned: - cleaned = "Hello" - break - - return cleaned - - def _make_command_context(self, message: MeshMessage) -> CommandContext: - """Create command context from message.""" - return CommandContext( - sender_id=message.sender_id, - sender_name=message.sender_name, - channel=message.channel, - is_dm=message.is_dm, - position=message.sender_position, - config=self.config, - connector=self.connector, - history=self.history, - ) - +"""Message routing logic for MeshAI.""" + +import asyncio +import logging +import re +from dataclasses import dataclass +from enum import Enum, auto +from typing import Optional + +from .backends.base import LLMBackend +from .commands import CommandContext, CommandDispatcher +from .config import Config +from .connector import MeshConnector, MeshMessage +from .context import MeshContext +from .history import ConversationHistory +from .chunker import chunk_response, ContinuationState + +logger = logging.getLogger(__name__) + + +class RouteType(Enum): + """Type of message routing.""" + + IGNORE = auto() # Don't respond + COMMAND = auto() # Bang command + LLM = auto() # Route to LLM + + +@dataclass +class RouteResult: + """Result of routing decision.""" + + route_type: RouteType + response: Optional[str] = None # For commands, the response + query: Optional[str] = None # For LLM, the cleaned query + + +# advBBS protocol and notification prefixes to ignore +ADVBBS_PREFIXES = ( + "MAILREQ|", "MAILACK|", "MAILNAK|", "MAILDAT|", "MAILDLV|", + "BOARDREQ|", "BOARDACK|", "BOARDNAK|", "BOARDDAT|", "BOARDDLV|", + "advBBS|", + "[MAIL]", +) + +# Patterns that suggest prompt injection attempts +_INJECTION_PATTERNS = [ + re.compile(r"ignore\s+(all\s+)?previous", re.IGNORECASE), + re.compile(r"ignore\s+your\s+instructions", re.IGNORECASE), + re.compile(r"disregard\s+(all\s+)?previous", re.IGNORECASE), + re.compile(r"you\s+are\s+now\b", re.IGNORECASE), + re.compile(r"new\s+instructions?\s*:", re.IGNORECASE), + re.compile(r"system\s*prompt\s*:", re.IGNORECASE), +] + +# Keywords that indicate mesh-related questions +_MESH_KEYWORDS = { + "mesh", "network", "health", "nodes", "node", "utilization", "signal", + "coverage", "battery", "solar", "offline", "router", "channel", "packet", + "hop", "optimize", "optimization", "infrastructure", "infra", "relay", + "repeater", "region", "locality", "congestion", "collision", "airtime", + "telemetry", "firmware", "subscribe", "alert", "snr", "rssi", + # Additional keywords for better detection + "noisy", "noisiest", "traffic", "packets", "power", "routers", + "repeaters", "regions", "localities", "score", "status", +} + +# Phrases that indicate mesh questions +_MESH_PHRASES = [ + "how's the mesh", + "hows the mesh", + "mesh status", + "what's wrong", + "whats wrong", + "check node", + "node status", + "network health", + "mesh health", + "which node", + "which nodes", + "which infra", + "list nodes", + "list infra", + "tell me about", + "what about", + "how is", + "how are", +] + +# Keywords that indicate environmental/weather/propagation questions +_ENV_KEYWORDS = { + "weather", "alert", "warning", "fire", "smoke", "road", "closure", + "snow", "avalanche", "avy", "solar", "hf", "propagation", "kp", + "aurora", "blackout", "flood", "stream", "river", "ducting", + "tropo", "duct", "uhf", "vhf", "906", "band", "conditions", + "forecast", "sfi", "ionosphere", "geomagnetic", "storm", +} + +# City name to region mapping (hardcoded fallback) +# City/alias mapping now built from config - see _build_alias_map() + +# Mesh awareness instruction for LLM +# Mesh awareness instruction for LLM +_MESH_AWARENESS_PROMPT = """ +MESH DATA RESPONSE RULES (OVERRIDE brevity rules for mesh/network questions): + +The data blocks above contain detailed information about every region, infrastructure node, +coverage gap, and problem node on the mesh. USE THIS DATA in your response. + +RESPONSE STYLE: +- DETAILED, data-driven responses. Reference specific node names, scores, gateway counts. +- Use LOCAL NAMES from the region descriptions (Magic Valley, Treasure Valley, etc.) +- ALWAYS use local region names: say "Treasure Valley" not "South Western ID", say "Magic Valley" not "South Central ID". The code names mean nothing to users. +- When listing nodes, be concise: "BT Base c8d5 — via AIDA" not "BT Base c8d5 (c8d5) is connected via AIDA-MeshMonitor in the South Western ID region." +- Don't repeat the region on every line when listing multiple nodes in the same region. Say the region once at the top, then just list the nodes. +- Don't include shortnames in parentheses when you're already giving the full name — it's noise. +- When discussing infrastructure, name the actual nodes (Mount Harrison Router, not just "5 infra") +- When discussing coverage gaps, explain WHERE and HOW MANY nodes are affected +- When discussing problems, name the node and explain the impact +- You CAN use 3-5 messages. Keep each sentence under 150 characters. +- No markdown formatting - plain text only +- ABSOLUTELY NO markdown. No asterisks, no bold, no bullet points with * or -, no numbered lists with 1. 2. 3. Just plain text sentences. +- NEVER say "Want me to keep going?" — the message system adds this automatically when needed. If you say it yourself, users see it twice. +- When explaining "X/Y gateways" (like 7/7), explain that it means the node is visible to X out of Y data sources (Meshview and MeshMonitor instances that monitor the mesh). It does NOT mean infrastructure routers or regional gateways. +- When reporting packet types, ALWAYS use the name (Position, NodeInfo, Telemetry) not the number. +- Normal position interval: 15-30 minutes (48-96 packets/day). 400+ Position packets in 24h means aggressive position interval, wasting airtime. Tell the user. +- Normal NodeInfo: every 2-3 hours (8-12/day). 50+ is excessive. +- Normal NeighborInfo: every 6 hours (4/day). 20+ is aggressive. +- If a node has high packet volume, explain WHAT the packets are and WHETHER the rate is abnormal compared to normal intervals. + +QUESTION TYPES: +- "How's the mesh?" -> Lead with composite score. Highlight 1-2 biggest issues by name. Summarize each region briefly. +- "Where do we need coverage?" -> Name regions with single-gateway nodes. Name offline infra. Suggest specific locations. +- "Tell me about [node]" -> Give full detail from the data above. +- "How is [region]?" -> Give that region's infrastructure status, coverage, issues. +- "What's wrong?" -> List problem nodes by name with specifics. + +IMPORTANT: Do NOT lump different regions together. Each is a distinct area. +Do NOT recommend infrastructure for "Unlocated" nodes - they have no known position. +""" + + +def _build_region_abbreviations(region_names: list[str]) -> dict[str, str]: + """Build abbreviation to region name mapping. + + Generates abbreviations like: + - "South Central ID" -> "SCID", "SC-ID", "SC ID" + - "South Western ID" -> "SWID", "SW-ID", "SW ID" + + Args: + region_names: List of full region names + + Returns: + Dict mapping lowercase abbreviation to full region name + """ + abbrevs = {} + + for name in region_names: + parts = name.replace("???", "-").replace("???", "-").split() + if not parts: + continue + + # Get first letter of each word (uppercase) + initials = "".join(p[0].upper() for p in parts if p) + abbrevs[initials.lower()] = name + + # If last part is a state abbrev (2 chars), create variants + if len(parts) >= 2: + last = parts[-1] + if len(last) == 2 and last.isupper(): + # "South Central ID" -> prefix is "South Central" + prefix_parts = parts[:-1] + prefix_initials = "".join(p[0].upper() for p in prefix_parts) + + # SC-ID, SC ID, SCID variants + abbrevs[f"{prefix_initials.lower()}-{last.lower()}"] = name + abbrevs[f"{prefix_initials.lower()} {last.lower()}"] = name + abbrevs[f"{prefix_initials.lower()}{last.lower()}"] = name + + return abbrevs + + +class MessageRouter: + """Routes incoming messages to appropriate handlers.""" + + def __init__( + self, + config: Config, + connector: MeshConnector, + history: ConversationHistory, + dispatcher: CommandDispatcher, + llm_backend: LLMBackend, + context: MeshContext = None, + meshmonitor_sync=None, + knowledge=None, + source_manager=None, + health_engine=None, + mesh_reporter=None, + env_store=None, + ): + self.config = config + self.connector = connector + self.history = history + self.dispatcher = dispatcher + self.llm = llm_backend + self.context = context + self.meshmonitor_sync = meshmonitor_sync + self.knowledge = knowledge + self.source_manager = source_manager + self.health_engine = health_engine + self.mesh_reporter = mesh_reporter + self.env_store = env_store + self.continuations = ContinuationState(max_continuations=3) + + # Per-user mesh context tracking for follow-up handling + # Maps user_id -> {"last_was_mesh": bool, "last_scope": (type, value), "non_mesh_count": int} + self._user_mesh_context: dict[str, dict] = {} + + # Build region abbreviation map + self._region_abbrevs: dict[str, str] = {} + if self.health_engine and self.health_engine.regions: + region_names = [r.name for r in self.health_engine.regions] + self._region_abbrevs = _build_region_abbreviations(region_names) + logger.debug(f"Built region abbreviations: {self._region_abbrevs}") + + # Build city/alias mapping from config + self._alias_map = self._build_alias_map() + if self._alias_map: + logger.debug(f"Built alias map with {len(self._alias_map)} entries") + + def _build_alias_map(self) -> dict[str, str]: + """Build city/alias to region mapping from config.""" + alias_map = {} + if self.config.mesh_intelligence and self.config.mesh_intelligence.regions: + for region in self.config.mesh_intelligence.regions: + # Add aliases + for alias in (getattr(region, 'aliases', []) or []): + alias_map[alias.lower()] = region.name + # Add cities + for city in (getattr(region, 'cities', []) or []): + alias_map[city.lower()] = region.name + # Add local_name + local = getattr(region, 'local_name', '') or '' + if local: + alias_map[local.lower()] = region.name + return alias_map + + def should_respond(self, message: MeshMessage) -> bool: + """Determine if we should respond to this message. + + DM-only bot: ignores all public channel messages. + Commands and conversational LLM responses both work in DMs. + + Args: + message: Incoming message + + Returns: + True if we should process this message + """ + # Always ignore our own messages + if message.sender_id == self.connector.my_node_id: + return False + + # Only respond to DMs + if not message.is_dm: + return False + + if not self.config.bot.respond_to_dms: + return False + + # Ignore advBBS protocol and notification messages + if self.config.bot.filter_bbs_protocols: + if any(message.text.startswith(p) for p in ADVBBS_PREFIXES): + logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...") + return False + + # Ignore messages that MeshMonitor will handle + if self.meshmonitor_sync and self.meshmonitor_sync.matches(message.text): + logger.debug(f"Ignoring MeshMonitor-handled message: {message.text[:40]}...") + return False + + return True + + def check_continuation(self, message) -> list[str] | None: + """Check if this is a continuation request and return messages if so. + + Returns: + List of messages to send, or None if not a continuation + """ + user_id = message.sender_id + text = message.text.strip() + + logger.debug(f"check_continuation: user={user_id}, text='{text[:30]}', has_pending={self.continuations.has_pending(user_id)}") + + if self.continuations.has_pending(user_id): + if self.continuations.is_continuation_request(text): + result = self.continuations.get_continuation(user_id) + if result: + messages, _ = result + return messages + # Max continuations reached, return None to fall through + else: + # User asked something new, clear pending continuation + self.continuations.clear(user_id) + + return None + + async def route(self, message: MeshMessage) -> RouteResult: + """Route a message and generate response. + + Args: + message: Incoming message to route + + Returns: + RouteResult with routing decision and any response + """ + text = message.text.strip() + + # Check for bang command first + if self.dispatcher.is_command(text): + context = self._make_command_context(message) + response = await self.dispatcher.dispatch(text, context) + return RouteResult(RouteType.COMMAND, response=response) + + # Clean up the message (remove @mention) + query = self._clean_query(text) + + if not query: + return RouteResult(RouteType.IGNORE) + + # Route to LLM + return RouteResult(RouteType.LLM, query=query) + + def _is_mesh_question(self, message: str) -> bool: + """Check if message is asking about mesh health/status. + + Args: + message: User message text + + Returns: + True if this is a mesh-related question + """ + msg_lower = message.lower() + + # Check for mesh phrases + for phrase in _MESH_PHRASES: + if phrase in msg_lower: + return True + + # Check for mesh keywords + words = set(re.findall(r'\b\w+\b', msg_lower)) + if words & _MESH_KEYWORDS: + return True + + return False + + def _detect_mesh_scope(self, message: str) -> tuple[str, Optional[str]]: + """Detect the scope of a mesh question. + + Args: + message: User message text + + Returns: + Tuple of (scope_type, scope_value): + - ("node", "{identifier}") if asking about specific node + - ("region", "{region_name}") if asking about specific region + - ("mesh", None) for general mesh questions + """ + msg_lower = message.lower() + + # === NODE MATCHING (check first - more specific) === + if self.health_engine and self.health_engine.mesh_health: + health = self.health_engine.mesh_health + + # 1. Exact shortname match (case-insensitive, word boundary) + for node in health.nodes.values(): + if node.short_name: + pattern = r'\b' + re.escape(node.short_name.lower()) + r'\b' + if re.search(pattern, msg_lower): + return ("node", node.short_name) + + # 2. Longname substring match (case-insensitive) + for node in health.nodes.values(): + if node.long_name and len(node.long_name) > 3: + # Match significant portion of longname + if node.long_name.lower() in msg_lower: + return ("node", node.short_name or node.node_id) + # Also try matching without common suffixes like "Router", "Repeater" + clean_name = node.long_name.lower() + for suffix in [" router", " repeater", " relay", " base", " v2", " - g2"]: + clean_name = clean_name.replace(suffix, "") + if len(clean_name) > 4 and clean_name in msg_lower: + return ("node", node.short_name or node.node_id) + + # 3. NodeId hex match (with or without ! prefix) + hex_pattern = r'!?([0-9a-f]{8})' + hex_match = re.search(hex_pattern, msg_lower) + if hex_match: + hex_id = hex_match.group(1) + for nid, node in health.nodes.items(): + if hex_id in nid.lower(): + return ("node", node.short_name or nid) + + # 4. NodeNum decimal match + num_pattern = r'\b(\d{9,10})\b' + num_match = re.search(num_pattern, message) + if num_match: + node_num = int(num_match.group(1)) + hex_id = format(node_num, 'x') + for nid, node in health.nodes.items(): + if hex_id in nid.lower(): + return ("node", node.short_name or nid) + + # === REGION MATCHING === + if self.health_engine: + # 1. Check abbreviations first (SCID, SWID, etc.) + for abbrev, region_name in self._region_abbrevs.items(): + # Match as word boundary + pattern = r'\b' + re.escape(abbrev) + r'\b' + if re.search(pattern, msg_lower): + return ("region", region_name) + + # 2. Check city names and aliases from config + for alias, region_name in self._alias_map.items(): + if alias in msg_lower: + return ("region", region_name) + + # 3. Full region name matching (SORTED BY LENGTH - longest first) + regions_by_length = sorted( + self.health_engine.regions, + key=lambda r: len(r.name), + reverse=True + ) + + for anchor in regions_by_length: + anchor_lower = anchor.name.lower() + # Check full region name + if anchor_lower in msg_lower: + return ("region", anchor.name) + + # 4. Partial region name matching (also longest first) + for anchor in regions_by_length: + anchor_lower = anchor.name.lower() + # Check significant parts of region name + # Split on common separators + parts = anchor_lower.replace("-", " ").replace("???", " ").replace("???", " ").split() + # Only match on significant words (>3 chars, not state abbrevs) + significant_parts = [p for p in parts if len(p) > 3] + + # Check if ALL significant parts appear in message + if significant_parts and all(p in msg_lower for p in significant_parts): + return ("region", anchor.name) + + return ("mesh", None) + + def _get_user_mesh_context(self, user_id: str) -> dict: + """Get or create mesh context for a user.""" + if user_id not in self._user_mesh_context: + self._user_mesh_context[user_id] = { + "last_was_mesh": False, + "last_scope": ("mesh", None), + "non_mesh_count": 0, + } + return self._user_mesh_context[user_id] + + def _update_user_mesh_context( + self, + user_id: str, + is_mesh: bool, + scope: tuple[str, Optional[str]] = None, + ) -> None: + """Update mesh context tracking for a user.""" + ctx = self._get_user_mesh_context(user_id) + + if is_mesh: + ctx["last_was_mesh"] = True + ctx["non_mesh_count"] = 0 + if scope: + ctx["last_scope"] = scope + else: + ctx["non_mesh_count"] += 1 + # Reset after 2 consecutive non-mesh messages + if ctx["non_mesh_count"] >= 2: + ctx["last_was_mesh"] = False + ctx["last_scope"] = ("mesh", None) + + def _try_compute_distance(self, query: str) -> str: + """Extract two node names from a distance question and compute distance.""" + if not self.mesh_reporter: + return "" + + health = self.mesh_reporter.health_engine.mesh_health + if not health: + return "" + + query_lower = query.lower() + + # Build name -> node lookup (include partial long_name matches) + node_names = {} + for node in health.nodes.values(): + if node.short_name: + node_names[node.short_name.lower()] = node + if node.long_name: + full = node.long_name.lower() + node_names[full] = node + # Add partial matches: "TVM Pearl Relay" also matches "TVM Pearl" + words = full.split() + if len(words) >= 2: + for i in range(2, len(words) + 1): + partial = " ".join(words[:i]) + if partial not in node_names: + node_names[partial] = node + + # AIDA aliases + aida_node = health.nodes.get(0x27780c47) + if aida_node: + for alias in ["aida", "aida-n2", "me", "my node", "yourself", "your position", "you"]: + node_names[alias] = aida_node + + # Find mentioned nodes (longest names first) + found_nodes = [] + + for name in sorted(node_names.keys(), key=len, reverse=True): + if name in query_lower and len(name) >= 2: + node = node_names[name] + if not any(n.node_num == node.node_num for n in found_nodes): + found_nodes.append(node) + if len(found_nodes) >= 2: + break + + # If we only found one or zero nodes, check for ambiguous short terms + if len(found_nodes) < 2: + query_words = query_lower.replace("?", "").replace("!", "").split() + candidate_terms = list(query_words) + for i in range(len(query_words) - 1): + candidate_terms.append(f"{query_words[i]} {query_words[i+1]}") + + skip_words = {"how", "far", "is", "from", "the", "to", "and", "between", "what", + "distance", "away", "are", "apart", "tell", "me", "about", "a", "an"} + + for term in candidate_terms: + if term in skip_words or len(term) < 2: + continue + matches = [] + seen_nums = set() + for node in health.nodes.values(): + if node.node_num in seen_nums: + continue + name_lower = (node.long_name or "").lower() + short_lower = (node.short_name or "").lower() + if term in name_lower or term == short_lower: + matches.append(node) + seen_nums.add(node.node_num) + + if len(matches) > 1: + names = [f" - {n.long_name or n.short_name} ({n.short_name})" + for n in matches[:6]] + return ( + f"AMBIGUOUS: '{term}' matches multiple nodes. " + f"Ask the user which one they mean:\n" + "\n".join(names) + ) + + if len(found_nodes) == 2: + return self.mesh_reporter.build_distance( + str(found_nodes[0].node_num), + str(found_nodes[1].node_num) + ) + elif len(found_nodes) == 1 and aida_node: + return self.mesh_reporter.build_distance( + str(found_nodes[0].node_num), + str(aida_node.node_num) + ) + + return "" + + + async def generate_llm_response(self, message: MeshMessage, query: str) -> str: + """Generate LLM response for a message. + + Args: + message: Original message + query: Cleaned query text + + Returns: + Generated response + """ + # Add user message to history + await self.history.add_message(message.sender_id, "user", query) + + # Get conversation history + history = await self.history.get_history_for_llm(message.sender_id) + + # Build system prompt in order: identity -> static -> meshmonitor -> context -> knowledge -> mesh + + # 1. Dynamic identity from bot config + bot_name = self.config.bot.name or "MeshAI" + bot_owner = self.config.bot.owner or "Unknown" + + identity = ( + f"You are {bot_name}, an LLM-powered assistant on the freq51 Meshtastic mesh network. " + f"Your managing operator is {bot_owner}. " + f"You are open source at github.com/zvx-echo6/meshai.\n\n" + f"IDENTITY: Your name is {bot_name}. You ARE a physical node on the mesh — " + f"node !27780c47 (AIDA-N2). You have a real location, real GPS coordinates, " + f"and real radio connections. When someone asks how far something is from you, " + f"check the mesh data for your node's position and calculate. " + f"You are NOT just software — you are a node that other nodes can see, hear, and route through.\n\n" + ) + + # 2. Static system prompt from config + static_prompt = "" + if getattr(self.config.llm, 'use_system_prompt', True): + static_prompt = self.config.llm.system_prompt + + system_prompt = identity + static_prompt + + # 2b. Dynamic command list (only shows enabled commands) + if self.dispatcher: + commands = self.dispatcher.get_commands() + if commands: + # Deduplicate aliases + seen_names = set() + unique_commands = [] + for cmd in commands: + name_lower = cmd.name.lower() + if name_lower not in seen_names: + seen_names.add(name_lower) + unique_commands.append(cmd) + + cmd_lines = [ + "\nYOUR COMMANDS (only mention these - do NOT mention any commands not listed here):" + ] + for cmd in sorted(unique_commands, key=lambda c: c.name): + cmd_lines.append(f" !{cmd.name} - {cmd.description}") + cmd_lines.append("") + cmd_lines.append( + "CRITICAL: ONLY mention commands in the list above when asked about commands. " + "If a command is not listed here, it does NOT exist. Do not invent commands." + ) + system_prompt += "\n".join(cmd_lines) + + # 3. MeshMonitor info (only when enabled) + if ( + self.meshmonitor_sync + and self.config.meshmonitor.enabled + and self.config.meshmonitor.inject_into_prompt + ): + meshmonitor_intro = ( + "\n\nMESHMONITOR: You run alongside MeshMonitor (by Yeraze) on the same " + "meshtasticd node. MeshMonitor handles web dashboard, maps, telemetry, " + "traceroutes, security scanning, and auto-responder commands. Its trigger " + "commands are listed below ??? if someone asks what commands are available, " + "ONLY list YOUR commands from YOUR COMMANDS above. If someone asks where to get " + "MeshMonitor, direct them to github.com/Yeraze/meshmonitor" + ) + system_prompt += meshmonitor_intro + + commands_summary = self.meshmonitor_sync.get_commands_summary() + if commands_summary: + system_prompt += "\n\n" + commands_summary + + # 4. Inject mesh context if available + if self.context: + max_items = getattr(self.config.context, 'max_context_items', 20) + context_block = self.context.get_context_block(max_items=max_items) + if context_block: + system_prompt += ( + "\n\n--- Recent mesh traffic (for context only, not messages to you) ---\n" + + context_block + ) + else: + system_prompt += ( + "\n\n[No recent mesh traffic observed yet.]" + ) + + # 5. Knowledge base retrieval + if self.knowledge and query: + results = self.knowledge.search(query) + if results: + chunks = "\n\n".join( + f"[{r['title']}]: {r['excerpt']}" for r in results + ) + system_prompt += ( + "\n\nREFERENCE KNOWLEDGE - Answer using this information:\n" + + chunks + ) + + # 6. Mesh Intelligence (inject health data for mesh questions) + user_ctx = self._get_user_mesh_context(message.sender_id) + is_direct_mesh_question = self._is_mesh_question(query) + is_followup = user_ctx["last_was_mesh"] and not is_direct_mesh_question + + should_inject_mesh = is_direct_mesh_question or is_followup + + if self.source_manager and self.mesh_reporter and should_inject_mesh: + # Detect scope from current message + scope_type, scope_value = self._detect_mesh_scope(query) + + # For follow-ups with no detected scope, use previous scope + if is_followup and scope_type == "mesh" and scope_value is None: + prev_scope = user_ctx.get("last_scope", ("mesh", None)) + if prev_scope[0] != "mesh" or prev_scope[1] is not None: + scope_type, scope_value = prev_scope + logger.debug(f"Using previous scope for follow-up: {scope_type}, {scope_value}") + + # Always include Tier 1 summary for mesh questions + tier1 = self.mesh_reporter.build_tier1_summary() + system_prompt += "\n\n" + tier1 + + # Add Tier 2 detail if scoped + if scope_type == "region" and scope_value: + region_detail = self.mesh_reporter.build_region_detail(scope_value) + system_prompt += "\n\n" + region_detail + elif scope_type == "node" and scope_value: + node_detail = self.mesh_reporter.build_node_detail(scope_value) + system_prompt += "\n\n" + node_detail + + # Always include relevant recommendations + recommendations = self.mesh_reporter.build_recommendations(scope_type, scope_value) + if recommendations: + system_prompt += "\n\n" + recommendations + + # Add mesh awareness instructions + system_prompt += _MESH_AWARENESS_PROMPT + + # Build region geography from config dynamically + if self.config.mesh_intelligence and self.config.mesh_intelligence.regions: + geo_lines = ["", "REGION GEOGRAPHY (use local names when discussing these regions):"] + for region in self.config.mesh_intelligence.regions: + local = getattr(region, "local_name", "") or "" + local_str = f' "{local}"' if local else "" + desc = getattr(region, "description", "") or "" + desc_str = f" — {desc}" if desc else "" + aliases = getattr(region, "aliases", []) or [] + alias_str = "" + if aliases: + alias_str = f'\n People may call this: {", ".join(aliases)}' + geo_lines.append(f" - {region.name}{local_str}{desc_str}{alias_str}") + system_prompt += "\n".join(geo_lines) + + # Update mesh context tracking + self._update_user_mesh_context( + message.sender_id, + is_mesh=True, + scope=(scope_type, scope_value), + ) + else: + # Not a mesh question + self._update_user_mesh_context(message.sender_id, is_mesh=False) + + # 7. Environmental context injection + if self.env_store: + query_lower = query.lower() if query else "" + env_relevant = any(kw in query_lower for kw in _ENV_KEYWORDS) + # Also inject env context if mesh context is being injected + if env_relevant or should_inject_mesh: + env_summary = self.env_store.get_summary() + if env_summary: + system_prompt += "\n\n" + env_summary + + # DEBUG: Log system prompt status + logger.debug(f"System prompt length: {len(system_prompt)} chars") + + # Detect distance questions and inject computed distance + distance_keywords = ["how far", "distance", "how close", "miles from", "km from", "away from"] + if any(kw in query.lower() for kw in distance_keywords): + distance_result = self._try_compute_distance(query) + if distance_result: + system_prompt += f"\n\nDISTANCE CALCULATION:\n{distance_result}\n" + + try: + response = await self.llm.generate( + messages=history, + system_prompt=system_prompt, + max_tokens=self.config.llm.max_response_tokens, + ) + except asyncio.TimeoutError: + logger.error("LLM request timed out") + response = "Sorry, request timed out. Try again." + except Exception as e: + logger.error(f"LLM generation error: {e}") + response = "Sorry, I encountered an error. Please try again." + + # Add assistant response to history + await self.history.add_message(message.sender_id, "assistant", response) + + # Persist summary if one was created/updated + await self._persist_summary(message.sender_id) + + # Strip any markdown the LLM ignored instructions about + from .chunker import strip_markdown + response = strip_markdown(response) + + # Chunk the response with sentence awareness + messages, remaining = chunk_response( + response, + max_chars=self.config.response.max_length, + max_messages=self.config.response.max_messages, + ) + + # Store remaining content for continuation + if remaining: + logger.debug(f"Storing continuation for {message.sender_id}: {len(remaining)} chars remaining") + self.continuations.store(message.sender_id, remaining) + + return messages + + async def _persist_summary(self, user_id: str) -> None: + """Persist any cached summary to the database. + + Args: + user_id: User identifier + """ + memory = self.llm.get_memory() + if not memory: + return + + summary = memory.get_cached_summary(user_id) + if summary: + await self.history.store_summary( + user_id, + summary.summary, + summary.message_count, + ) + logger.debug(f"Persisted summary for {user_id}") + + def _clean_query(self, text: str) -> str: + """Clean up query text and check for prompt injection.""" + cleaned = " ".join(text.split()) + cleaned = cleaned.strip() + + # Check for prompt injection + for pattern in _INJECTION_PATTERNS: + if pattern.search(cleaned): + logger.warning( + f"Possible prompt injection detected: {cleaned[:80]}..." + ) + match = pattern.search(cleaned) + cleaned = cleaned[:match.start()].strip() + if not cleaned: + cleaned = "Hello" + break + + return cleaned + + def _make_command_context(self, message: MeshMessage) -> CommandContext: + """Create command context from message.""" + return CommandContext( + sender_id=message.sender_id, + sender_name=message.sender_name, + channel=message.channel, + is_dm=message.is_dm, + position=message.sender_position, + config=self.config, + connector=self.connector, + history=self.history, + ) +