diff --git a/work/dashboard-frontend/src/lib/api.ts b/work/dashboard-frontend/src/lib/api.ts index 8323e42..50638ee 100644 --- a/work/dashboard-frontend/src/lib/api.ts +++ b/work/dashboard-frontend/src/lib/api.ts @@ -32,7 +32,13 @@ export interface MeshHealth { total_regions: number unlocated_count: number last_computed: string - recommendations: string[] + // Only populated by the REST /api/health response; the websocket + // health_update push includes neither of these (see mesh_routes.py). + recommendations?: string[] + // false when the recommendations engine couldn't run (unwired reporter or + // an exception) — distinct from a genuinely empty `recommendations` list. + // Must NOT be treated the same as "mesh is healthy". + recommendations_available?: boolean } export interface NodeInfo { diff --git a/work/dashboard-frontend/src/pages/MeshtasticNodes.tsx b/work/dashboard-frontend/src/pages/MeshtasticNodes.tsx index 29a972f..8d5e6ee 100644 --- a/work/dashboard-frontend/src/pages/MeshtasticNodes.tsx +++ b/work/dashboard-frontend/src/pages/MeshtasticNodes.tsx @@ -1,11 +1,9 @@ import { useState, useEffect, useCallback } from 'react' -import { Save, RotateCcw, RefreshCw, Check } from 'lucide-react' +import { Link } from 'react-router-dom' +import { RefreshCw, Lightbulb, CheckCircle, AlertTriangle, ExternalLink } from 'lucide-react' import Mesh from './Mesh' import MeshtasticSources from './MeshtasticSources' -import { MeshIntelligenceSection, type MeshIntelligenceConfig } from './Config' -import { fetchConfig as apiFetchConfig, updateConfig as apiUpdateConfig } from '@/lib/api' -import { useDirty } from '@/context/DirtyContext' -import { notifyRestartRequired } from '@/components/RestartBanner' +import { fetchHealth, type MeshHealth } from '@/lib/api' const TABS = [ { key: 'nodes', label: 'Nodes' }, @@ -19,14 +17,9 @@ export default function MeshtasticNodes() { const [activeTab, setActiveTab] = useState('nodes') // Health tab state - const { setDirty } = useDirty() - const [intelligence, setIntelligence] = useState(null) - const [originalIntelligence, setOriginalIntelligence] = useState(null) + const [health, setHealth] = useState(null) const [loading, setLoading] = useState(false) - const [saving, setSaving] = useState(false) const [error, setError] = useState(null) - const [success, setSuccess] = useState(null) - const [hasChanges, setHasChanges] = useState(false) useEffect(() => { document.title = 'Nodes & Health - MeshAI' @@ -36,12 +29,10 @@ export default function MeshtasticNodes() { setLoading(true) setError(null) try { - const data = (await apiFetchConfig('mesh_intelligence')) as MeshIntelligenceConfig - setIntelligence(data) - setOriginalIntelligence(JSON.parse(JSON.stringify(data))) - setHasChanges(false) + const data = await fetchHealth() + setHealth(data) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load mesh intelligence config') + setError(err instanceof Error ? err.message : 'Failed to load mesh health') } finally { setLoading(false) } @@ -49,46 +40,10 @@ export default function MeshtasticNodes() { // Load when Health tab becomes active useEffect(() => { - if (activeTab === 'health' && intelligence === null && !loading) { + if (activeTab === 'health' && health === null && !loading) { fetchData() } - }, [activeTab, intelligence, loading, fetchData]) - - useEffect(() => { - if (intelligence && originalIntelligence) { - setHasChanges(JSON.stringify(intelligence) !== JSON.stringify(originalIntelligence)) - } - }, [intelligence, originalIntelligence]) - - useEffect(() => { - setDirty(hasChanges) - return () => setDirty(false) - }, [hasChanges, setDirty]) - - const saveConfig = async () => { - if (!intelligence) return - setSaving(true) - setError(null) - setSuccess(null) - try { - const result = await apiUpdateConfig('mesh_intelligence', intelligence) - setOriginalIntelligence(JSON.parse(JSON.stringify(intelligence))) - setHasChanges(false) - setDirty(false) - setSuccess('Mesh intelligence saved successfully') - if (result.restart_required) notifyRestartRequired([]) - setTimeout(() => setSuccess(null), 3000) - } catch (err) { - setError(err instanceof Error ? err.message : 'Save failed') - } finally { - setSaving(false) - } - } - - const discardChanges = () => { - if (originalIntelligence) setIntelligence(JSON.parse(JSON.stringify(originalIntelligence))) - setHasChanges(false) - } + }, [activeTab, health, loading, fetchData]) return (
@@ -113,64 +68,78 @@ export default function MeshtasticNodes() { {activeTab === 'nodes' && } {activeTab === 'sources' && } {activeTab === 'health' && ( -
- {/* Save bar */} +
+ {/* Header */}
-
-

- Mesh health scoring, region management, and automated alerting. -

-
-
- - - -
+

+ Optimization recommendations generated from current mesh health. +

+
{/* Status messages */} {error && (
{error}
)} - {success && ( -
- {success} -
- )} {loading ? (
Loading...
- ) : intelligence ? ( -
- + ) : health ? ( +
+

+ Optimization Recommendations +

+ {health.recommendations && health.recommendations.length > 0 ? ( +
+ {health.recommendations.map((rec, i) => ( +
+ + {rec} +
+ ))} +
+ ) : health.recommendations_available === false ? ( +
+ + + Couldn't compute recommendations right now. Check server logs. + +
+ ) : ( +
+ + No recommendations — mesh is healthy. +
+ )}
) : (
-
Failed to load config
+
Failed to load mesh health
)} + + {/* Cross-link note */} +
+ +
+ Health scoring, region management, and alerting thresholds are configured on{' '} + + Settings → Intelligence + + . +
+
)}