meshai/work/dashboard-frontend/src/pages/MeshtasticSources.tsx
malice de1e58aa71
feat(routing): MeshCore as first-class delivery types (meshcore_broadcast/dm) (#11)
* feat(routing): MeshCore as first-class delivery types (meshcore_broadcast/dm)

Replace the composite auto-fan with explicit per-mesh delivery types so
each family independently controls broadcast/DM per severity on Meshtastic
AND MeshCore. mesh_broadcast->Meshtastic only, meshcore_broadcast->MeshCore
(by channel name), mesh_dm/meshcore_dm likewise; routing via the existing
transport hint. Adds meshcore_dm_contacts. Meshtastic-only configs
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(routing): deliver meshcore_broadcast via meshcore_channel through CompositeTransport

The hinted _broadcast path passed the channel NAME on the `channel` kwarg,
which MeshCoreTransport ignores (it reads meshcore_channel), so
meshcore_broadcast silently no-op'd on transport=both configs. Route the
meshcore child via meshcore_channel and the meshtastic child via channel.
Fix the test that asserted the broken kwarg layout. Add the new delivery
types to the remaining enumeration/validation sites (channel-test endpoint,
scheduler digest chunking, danger-zone valid set).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): split Notifications into Meshtastic and MeshCore sections

Delineate per-mesh routing: each family configures Meshtastic delivery
(mesh_broadcast/mesh_dm, channel index, node IDs) and MeshCore delivery
(meshcore_broadcast/meshcore_dm, channel name, contacts) in separate
sections; shared settings (enable/severity/regions/email/webhook) once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): first-class MeshCore nav section + dedicated pages

Group the sidebar into Meshtastic and MeshCore sections. Promote MeshCore
routing and connection to their own pages; move MeshCore routing out of
Notifications (which stays Meshtastic + shared family settings). Add
placeholder Contacts and Companion pages for the follow-on companion data
API. No backend change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): parallel MT/MC nav order + symmetric connection links

Order both nav groups Connection/Routing/Mesh(Contacts)/Sources(Companion).
Replace the prominent MeshCore block on the Meshtastic Connection page with
a single subtle cross-link, mirrored on the MeshCore Connection page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dashboard): focused Meshtastic Connection/Sources pages for MT/MC parity

Meshtastic Connection and Sources are now their own focused pages
(mirroring MeshCore), instead of deep-linking into the full Config page.
Global settings move to a restored top-level Config item. No duplicate
editors; connection cross-links are mirror-image between the two pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:42:14 -06:00

162 lines
5.7 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react'
import { Save, RotateCcw, RefreshCw, Check } from 'lucide-react'
import {
MeshMonitorSection,
MeshSourcesSection,
type MeshMonitorConfig,
type MeshSourceConfig,
} from './Config'
import { notifyRestartRequired } from '@/components/RestartBanner'
import { fetchConfig as apiFetchConfig, updateConfig as apiUpdateConfig } from '@/lib/api'
export default function MeshtasticSources() {
const [meshmonitor, setMeshmonitor] = useState<MeshMonitorConfig | null>(null)
const [originalMeshmonitor, setOriginalMeshmonitor] = useState<MeshMonitorConfig | null>(null)
const [meshSources, setMeshSources] = useState<MeshSourceConfig[] | null>(null)
const [originalMeshSources, setOriginalMeshSources] = useState<MeshSourceConfig[] | null>(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [hasChanges, setHasChanges] = useState(false)
const fetchData = useCallback(async () => {
setLoading(true)
try {
const [mm, ms] = await Promise.all([
apiFetchConfig('meshmonitor') as Promise<MeshMonitorConfig>,
apiFetchConfig('mesh_sources') as Promise<MeshSourceConfig[]>,
])
setMeshmonitor(mm)
setOriginalMeshmonitor(JSON.parse(JSON.stringify(mm)))
setMeshSources(ms)
setOriginalMeshSources(JSON.parse(JSON.stringify(ms)))
setHasChanges(false)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
document.title = 'Meshtastic Sources - MeshAI'
fetchData()
}, [fetchData])
useEffect(() => {
if (meshmonitor && originalMeshmonitor && meshSources && originalMeshSources) {
const mmChanged = JSON.stringify(meshmonitor) !== JSON.stringify(originalMeshmonitor)
const msChanged = JSON.stringify(meshSources) !== JSON.stringify(originalMeshSources)
setHasChanges(mmChanged || msChanged)
}
}, [meshmonitor, originalMeshmonitor, meshSources, originalMeshSources])
const saveConfig = async () => {
if (!meshmonitor || !meshSources) return
setSaving(true)
setError(null)
setSuccess(null)
try {
const [mmResult, msResult] = await Promise.all([
apiUpdateConfig('meshmonitor', meshmonitor),
apiUpdateConfig('mesh_sources', meshSources),
])
setOriginalMeshmonitor(JSON.parse(JSON.stringify(meshmonitor)))
setOriginalMeshSources(JSON.parse(JSON.stringify(meshSources)))
setHasChanges(false)
setSuccess('Meshtastic sources saved successfully')
if (mmResult.restart_required || msResult.restart_required) {
notifyRestartRequired([])
}
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError(err instanceof Error ? err.message : 'Save failed')
} finally {
setSaving(false)
}
}
const discardChanges = () => {
if (originalMeshmonitor) setMeshmonitor(JSON.parse(JSON.stringify(originalMeshmonitor)))
if (originalMeshSources) setMeshSources(JSON.parse(JSON.stringify(originalMeshSources)))
setHasChanges(false)
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-slate-400">Loading Meshtastic sources...</div>
</div>
)
}
if (!meshmonitor || !meshSources) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-red-400">Failed to load sources config</div>
</div>
)
}
return (
<div className="max-w-2xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">
MeshMonitor integration and mesh awareness data sources.
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={fetchData}
className="p-2 text-slate-400 hover:text-slate-200 hover:bg-bg-hover rounded transition-colors"
title="Refresh"
>
<RefreshCw size={18} />
</button>
<button
onClick={discardChanges}
disabled={!hasChanges}
className="flex items-center gap-2 px-3 py-2 text-slate-400 hover:text-slate-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<RotateCcw size={16} />
Discard
</button>
<button
onClick={saveConfig}
disabled={saving || !hasChanges}
className="flex items-center gap-2 px-4 py-2 bg-accent hover:bg-accent/80 disabled:bg-slate-700 disabled:cursor-not-allowed rounded text-white transition-colors"
>
<Save size={16} />
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
{/* Status messages */}
{error && (
<div className="p-3 text-sm bg-red-500/10 text-red-400 border border-red-500/20">{error}</div>
)}
{success && (
<div className="p-3 text-sm bg-green-500/10 text-green-400 border border-green-500/20">
<Check size={14} className="inline mr-2" />
{success}
</div>
)}
{/* MeshMonitor card */}
<div className="bg-bg-card border border-border p-6">
<MeshMonitorSection data={meshmonitor} onChange={setMeshmonitor} />
</div>
{/* Mesh Sources card */}
<div className="bg-bg-card border border-border p-6">
<MeshSourcesSection data={meshSources} onChange={setMeshSources} />
</div>
</div>
)
}