mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
feat(dashboard): Add dynamic channel and node pickers
- Add GET /api/channels endpoint for live radio channel data - Create ChannelPicker component (single/multi-select from live channels) - Create NodePicker component (searchable multi-select from mesh nodes) - Replace manual inputs in Config with data-driven pickers - Update Notifications to use pickers for mesh broadcast/DM - Resolve node names in Alerts subscriptions display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
10328686e2
commit
3fa7b9fe5e
10 changed files with 551 additions and 107 deletions
|
|
@ -26,6 +26,13 @@ import {
|
|||
type AlertHistoryItem,
|
||||
type Subscription,
|
||||
} from '@/lib/api'
|
||||
|
||||
interface Node {
|
||||
node_num: number
|
||||
node_id_hex: string
|
||||
short_name: string
|
||||
long_name: string
|
||||
}
|
||||
import { useWebSocket } from '@/hooks/useWebSocket'
|
||||
|
||||
// Alert type icons mapping
|
||||
|
|
@ -308,7 +315,20 @@ function AlertHistoryTable({
|
|||
}
|
||||
|
||||
// Subscription Card Component
|
||||
function SubscriptionCard({ subscription }: { subscription: Subscription }) {
|
||||
function SubscriptionCard({ subscription, nodes }: { subscription: Subscription; nodes: Node[] }) {
|
||||
const resolveNodeName = (userId: string): string => {
|
||||
const node = nodes.find(n =>
|
||||
n.node_id_hex === userId ||
|
||||
String(n.node_num) === userId ||
|
||||
n.short_name === userId
|
||||
)
|
||||
if (node) {
|
||||
return node.long_name && node.long_name !== node.short_name
|
||||
? `${node.short_name} (${node.long_name})`
|
||||
: node.short_name
|
||||
}
|
||||
return userId
|
||||
}
|
||||
const formatSchedule = () => {
|
||||
if (subscription.sub_type === 'alerts') {
|
||||
return 'Real-time'
|
||||
|
|
@ -356,7 +376,7 @@ function SubscriptionCard({ subscription }: { subscription: Subscription }) {
|
|||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">
|
||||
{formatSchedule()} • Node {subscription.user_id}
|
||||
{formatSchedule()} • {resolveNodeName(subscription.user_id)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-2 h-2 rounded-full ${subscription.enabled ? 'bg-green-500' : 'bg-slate-500'}`} />
|
||||
|
|
@ -369,6 +389,7 @@ export default function Alerts() {
|
|||
const [activeAlerts, setActiveAlerts] = useState<Alert[]>([])
|
||||
const [history, setHistory] = useState<AlertHistoryItem[]>([])
|
||||
const [subscriptions, setSubscriptions] = useState<Subscription[]>([])
|
||||
const [nodes, setNodes] = useState<Node[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
|
|
@ -395,8 +416,9 @@ export default function Alerts() {
|
|||
fetchAlerts().catch(() => []),
|
||||
fetchAlertHistory(pageSize, 0).catch(() => ({ items: [], total: 0 })),
|
||||
fetchSubscriptions().catch(() => []),
|
||||
fetch('/api/nodes').then(r => r.json()).catch(() => []),
|
||||
])
|
||||
.then(([alerts, historyData, subs]) => {
|
||||
.then(([alerts, historyData, subs, nodeData]) => {
|
||||
setActiveAlerts(alerts)
|
||||
if (Array.isArray(historyData)) {
|
||||
setHistory(historyData)
|
||||
|
|
@ -406,6 +428,7 @@ export default function Alerts() {
|
|||
setTotalPages(Math.ceil((historyData.total || 0) / pageSize))
|
||||
}
|
||||
setSubscriptions(subs)
|
||||
setNodes(nodeData)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
@ -532,7 +555,7 @@ export default function Alerts() {
|
|||
{subscriptions.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{subscriptions.map((sub) => (
|
||||
<SubscriptionCard key={sub.id} subscription={sub} />
|
||||
<SubscriptionCard key={sub.id} subscription={sub} nodes={nodes} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import NodePicker from '@/components/NodePicker'
|
||||
import ChannelPicker from '@/components/ChannelPicker'
|
||||
import {
|
||||
Settings, Bot, Wifi, MessageSquare, Database, Brain, Eye,
|
||||
Terminal, Cpu, Cloud, Radio, BookOpen, Layers, Activity,
|
||||
|
|
@ -882,18 +884,19 @@ function ContextSection({ data, onChange }: { data: ContextConfig; onChange: (d:
|
|||
/>
|
||||
{data.enabled && (
|
||||
<>
|
||||
<NumberListInput
|
||||
<ChannelPicker
|
||||
label="Observe Channels"
|
||||
value={data.observe_channels}
|
||||
onChange={(v) => onChange({ ...data, observe_channels: v })}
|
||||
helper="Channel indexes to monitor (empty = all)"
|
||||
info="Meshtastic channel numbers to listen on. Channel 0 is the default primary channel. Leave empty to monitor all channels."
|
||||
helper="Channels to monitor (empty = all)"
|
||||
info="Meshtastic channels to listen on. Leave empty to monitor all channels."
|
||||
mode="multi"
|
||||
/>
|
||||
<ListInput
|
||||
<NodePicker
|
||||
label="Ignore Nodes"
|
||||
value={data.ignore_nodes}
|
||||
onChange={(v) => onChange({ ...data, ignore_nodes: v })}
|
||||
helper="Node IDs to exclude from context"
|
||||
helper="Nodes to exclude from context"
|
||||
info="Messages from these nodes won't be included in passive context. Useful for filtering out noisy automated nodes."
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
|
@ -1439,22 +1442,24 @@ function MeshIntelligenceSection({ data, onChange }: { data: MeshIntelligenceCon
|
|||
/>
|
||||
</div>
|
||||
|
||||
<ListInput
|
||||
<NodePicker
|
||||
label="Critical Nodes"
|
||||
value={data.critical_nodes}
|
||||
onChange={(v) => onChange({ ...data, critical_nodes: v })}
|
||||
helper="Short names of critical infrastructure"
|
||||
info="Nodes that get priority alerting when they go offline. Use the node's short name (e.g., MHR, HPR)."
|
||||
helper="Critical infrastructure nodes"
|
||||
info="Nodes that get priority alerting when they go offline."
|
||||
roleFilter="infrastructure"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<NumberInput
|
||||
<ChannelPicker
|
||||
label="Alert Channel"
|
||||
value={data.alert_channel}
|
||||
onChange={(v) => onChange({ ...data, alert_channel: v })}
|
||||
min={-1}
|
||||
helper="-1 = disabled"
|
||||
info="Meshtastic channel number for broadcast alerts. Set to -1 to disable channel broadcasting."
|
||||
helper="Channel for broadcast alerts"
|
||||
info="Meshtastic channel for broadcast alerts. Select Disabled to turn off channel broadcasting."
|
||||
mode="single"
|
||||
includeDisabled
|
||||
/>
|
||||
<NumberInput
|
||||
label="Alert Cooldown (min)"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import {
|
|||
Save, RotateCcw, RefreshCw, Plus, Trash2, ChevronDown, ChevronRight,
|
||||
Check, X, Eye as EyeIcon, EyeOff, ExternalLink, Send
|
||||
} from 'lucide-react'
|
||||
import ChannelPicker from '@/components/ChannelPicker'
|
||||
import NodePicker from '@/components/NodePicker'
|
||||
|
||||
// Types
|
||||
interface NotificationChannelConfig {
|
||||
|
|
@ -375,24 +377,24 @@ function NotificationChannelCard({
|
|||
/>
|
||||
|
||||
{channel.type === 'mesh_broadcast' && (
|
||||
<NumberInput
|
||||
label="Channel Index"
|
||||
<ChannelPicker
|
||||
label="Broadcast Channel"
|
||||
value={channel.channel_index}
|
||||
onChange={(v) => onChange({ ...channel, channel_index: v })}
|
||||
min={0}
|
||||
max={7}
|
||||
helper="Mesh channel number (0-7)"
|
||||
info="The mesh channel to broadcast alerts on. Channel 0 is typically the default channel."
|
||||
helper="Channel for broadcast alerts"
|
||||
info="The mesh channel to broadcast alerts on."
|
||||
mode="single"
|
||||
/>
|
||||
)}
|
||||
|
||||
{channel.type === 'mesh_dm' && (
|
||||
<ListInput
|
||||
label="Node IDs"
|
||||
<NodePicker
|
||||
label="Recipient Nodes"
|
||||
value={channel.node_ids}
|
||||
onChange={(v) => onChange({ ...channel, node_ids: v })}
|
||||
helper="Node IDs to receive DM alerts"
|
||||
info="Node IDs that receive direct message alerts. Enter the full node ID (e.g., '!a1b2c3d4') for each recipient."
|
||||
helper="Nodes to receive DM alerts"
|
||||
info="Nodes that receive direct message alerts."
|
||||
valueType="node_id_hex"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue