mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
* 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>
162 lines
5.7 KiB
TypeScript
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>
|
|
)
|
|
}
|