feat(dashboard): MeshCore transport + per-family routing GUI controls (#10)

* feat(dashboard): MeshCore transport + per-family routing GUI controls

Add Transport mode selector (Meshtastic/MeshCore/Both) and MeshCore
host/port fields to the Config Connection section, and an independent
per-family "MeshCore channel" number input in Notifications (blank = not
broadcast on MeshCore, sends null). Extends the ConnectionConfig and
per-family toggle TS types.

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

* refactor(routing): MeshCore routing by channel name, not index

MeshCore channels are {name,PSK} (up to 40+ slots, not Meshtastic's 0-7).
The send index is a fragile slot position, so store the channel NAME per
family and resolve name->slot against the companion's live channel table
at send time; never blind-send to an unresolved slot. GUI field becomes a
channel-name text box. meshtastic path unchanged.

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

* fix(routing): thread per-family meshcore_channel through the broadcast send path

MeshBroadcastChannel now carries the rule's meshcore_channel name and
passes it to send_message, so per-family MeshCore routing actually fires
end-to-end (dispatcher -> channel -> composite -> MeshCoreTransport).
Meshtastic path unchanged.

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>
This commit is contained in:
malice 2026-07-02 16:49:59 -06:00 committed by GitHub
commit 24cb6a31df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 358 additions and 45 deletions

View file

@ -23,6 +23,9 @@ interface ConnectionConfig {
serial_port: string
tcp_host: string
tcp_port: number
transport?: string
meshcore_host?: string
meshcore_port?: number
}
interface ResponseConfig {
@ -707,9 +710,23 @@ function BotSection({ data, onChange }: { data: BotConfig; onChange: (d: BotConf
}
function ConnectionSection({ data, onChange }: { data: ConnectionConfig; onChange: (d: ConnectionConfig) => void }) {
const transport = data.transport ?? 'meshtastic'
const showMeshCore = transport === 'meshcore' || transport === 'both'
return (
<div className="space-y-4">
<SectionDescription text={SECTION_DESCRIPTIONS.connection} />
<SelectInput
label="Transport Mode"
value={transport}
onChange={(v) => onChange({ ...data, transport: v })}
options={[
{ value: 'meshtastic', label: 'Meshtastic' },
{ value: 'meshcore', label: 'MeshCore' },
{ value: 'both', label: 'Both' },
]}
helper="Which radio transport(s) MeshAI uses"
info="Meshtastic: connect to a Meshtastic radio only. MeshCore: connect to a MeshCore node only. Both: connect to both simultaneously for dual-transport operation."
/>
<SelectInput
label="Connection Type"
value={data.type}
@ -749,6 +766,29 @@ function ConnectionSection({ data, onChange }: { data: ConnectionConfig; onChang
/>
</div>
)}
{showMeshCore && (
<div className="space-y-4 pt-2 border-t border-[#1e2a3a]">
<div className="text-xs text-slate-500 uppercase tracking-wide">MeshCore Connection</div>
<div className="grid grid-cols-2 gap-4">
<TextInput
label="MeshCore Host"
value={data.meshcore_host ?? ''}
onChange={(v) => onChange({ ...data, meshcore_host: v })}
placeholder="192.168.1.100"
helper="IP or hostname of the MeshCore node"
info="Address of the MeshCore node to connect to."
/>
<NumberInput
label="MeshCore Port"
value={data.meshcore_port ?? 5525}
onChange={(v) => onChange({ ...data, meshcore_port: v })}
min={1}
max={65535}
helper="MeshCore TCP port (default 5525)"
/>
</div>
</div>
)}
</div>
)
}

View file

@ -46,6 +46,7 @@ interface NotificationToggle {
regions: string[]
severity_channels: Record<string, string[]>
broadcast_channel: number | null
meshcore_channel?: string | null
node_ids: string[]
smtp_host: string
smtp_port: number
@ -1611,6 +1612,17 @@ function MasterToggles({ toggles, onChange }: {
</table>
<ListInput label="Regions (empty = all)" value={t.regions || []} onChange={(v) => upd(key, { regions: v })} placeholder="Add region..." /> <div className="text-xs text-slate-500 pt-1">Channel config</div>
<NumberInput label="Broadcast channel" value={t.broadcast_channel ?? 0} onChange={(v) => upd(key, { broadcast_channel: v })} />
<div className="space-y-1">
<label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">MeshCore channel</label>
<input
type="text"
value={t.meshcore_channel != null ? t.meshcore_channel : ''}
onChange={(e) => upd(key, { meshcore_channel: e.target.value === '' ? null : e.target.value })}
placeholder=""
className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 font-mono focus:outline-none focus:border-accent"
/>
<p className="text-xs text-slate-600">MeshCore channel name on your companion (e.g. AIDA); blank = not broadcast on MeshCore.</p>
</div>
<ListInput label="DM node IDs" value={t.node_ids || []} onChange={(v) => upd(key, { node_ids: v })} placeholder="!nodeid" />
<ListInput label="Email recipients" value={t.recipients || []} onChange={(v) => upd(key, { recipients: v })} placeholder="ops@example.com" />
<TextInput label="SMTP host" value={t.smtp_host || ''} onChange={(v) => upd(key, { smtp_host: v })} placeholder="smtp.example.com" />