From 57125964af8d41b02cdf712771e5d116486b8cd8 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Sat, 11 Jul 2026 04:12:08 +0000 Subject: [PATCH] MeshCore: display per-channel key (PSK) with reveal + copy The Observe MeshCore Channels list now shows each channel's PSK hex so operators can share it with people who want to join. Fetches the /api/meshcore/channels/detail endpoint (name + key) instead of the names-only list; keys are masked by default with a per-row eye reveal toggle and a copy-to-clipboard button. Channels with no retrievable key show a dash. Existing observe (checkbox), remove (trash), and add-row controls are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/pages/MeshCoreConnection.tsx | 95 +++++++++++++++++-- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/work/dashboard-frontend/src/pages/MeshCoreConnection.tsx b/work/dashboard-frontend/src/pages/MeshCoreConnection.tsx index a2054e7..ac6646f 100644 --- a/work/dashboard-frontend/src/pages/MeshCoreConnection.tsx +++ b/work/dashboard-frontend/src/pages/MeshCoreConnection.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react' import { Link } from 'react-router-dom' -import { Save, RotateCcw, RefreshCw, Check, ChevronRight, Trash2 } from 'lucide-react' +import { Save, RotateCcw, RefreshCw, Check, ChevronRight, Trash2, Eye, EyeOff, Copy } from 'lucide-react' import { TextInput, NumberInput, Toggle, ListInput, SelectInput } from './Config' import SerialPortPicker from '@/components/SerialPortPicker' import { notifyRestartRequired } from '@/components/RestartBanner' @@ -8,6 +8,7 @@ import { fetchConfig as apiFetchConfig, updateConfig as apiUpdateConfig, getMeshcoreChannels, + getMeshcoreChannelsDetail, addMeshcoreChannel, removeMeshcoreChannel, sendTestMessage, @@ -61,6 +62,11 @@ export default function MeshCoreConnection() { // Test send state const [channelsActive, setChannelsActive] = useState(false) const [channels, setChannels] = useState([]) + // Per-channel PSK hex (name -> key), captured from the companion so operators + // can share the key with people who want to join. Masked by default. + const [channelKeys, setChannelKeys] = useState>({}) + const [revealedKeys, setRevealedKeys] = useState>(new Set()) + const [copiedKey, setCopiedKey] = useState(null) const [selectedChannel, setSelectedChannel] = useState('') const [testText, setTestText] = useState('') const [testSending, setTestSending] = useState(false) @@ -100,12 +106,26 @@ export default function MeshCoreConnection() { const refreshChannels = useCallback(async () => { try { - const res = await getMeshcoreChannels() - setChannelsActive(res.active) - setChannels(res.channels) - setSelectedChannel((prev) => (prev && res.channels.includes(prev) ? prev : res.channels[0] ?? '')) + // Prefer the detail endpoint (name + PSK key); it carries everything the + // names-only list does. Fall back to names-only if detail is unavailable. + const detail = await getMeshcoreChannelsDetail() + const names = detail.channels.map((c) => c.name) + const keyMap: Record = {} + for (const c of detail.channels) keyMap[c.name] = c.key + setChannelsActive(detail.active) + setChannels(names) + setChannelKeys(keyMap) + setSelectedChannel((prev) => (prev && names.includes(prev) ? prev : names[0] ?? '')) } catch { - setChannelsActive(false) + try { + const res = await getMeshcoreChannels() + setChannelsActive(res.active) + setChannels(res.channels) + setChannelKeys({}) + setSelectedChannel((prev) => (prev && res.channels.includes(prev) ? prev : res.channels[0] ?? '')) + } catch { + setChannelsActive(false) + } } }, []) @@ -113,6 +133,27 @@ export default function MeshCoreConnection() { refreshChannels() }, [refreshChannels]) + const toggleRevealKey = (name: string) => { + setRevealedKeys((prev) => { + const next = new Set(prev) + if (next.has(name)) next.delete(name) + else next.add(name) + return next + }) + } + + const handleCopyKey = async (name: string, key: string) => { + try { + await navigator.clipboard.writeText(key) + setCopiedKey(name) + setTimeout(() => setCopiedKey((prev) => (prev === name ? null : prev)), 1500) + } catch { + // Clipboard API can be blocked (non-secure context); reveal the key so + // the operator can copy it manually instead of failing silently. + setRevealedKeys((prev) => new Set(prev).add(name)) + } + } + const handleAddChannel = async () => { const name = newChannelName.trim() if (!name) return @@ -409,6 +450,8 @@ export default function MeshCoreConnection() {
{channels.map((ch) => { const selected = (mcContext.observe_channels ?? []).includes(ch) + const key = channelKeys[ch] ?? null + const revealed = revealedKeys.has(ch) return (
-

Choose which MeshCore channels feed MeshAI's context. Empty = none are watched — pick channels to include their chatter in what the bot knows about the mesh. Leave busy/public channels out to keep them out of context.

+

Choose which MeshCore channels feed MeshAI's context. Empty = none are watched — pick channels to include their chatter in what the bot knows about the mesh. Leave busy/public channels out to keep them out of context. Each channel's key (PSK) is shown on the right — reveal and copy it to share with people who want to join.

{/* Add a new channel (name + PSK) to the companion's channel table */}