From 494f2728150b19138c08792677260aef9ef11759 Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 30 Jun 2026 23:03:09 -0400 Subject: [PATCH] feat(mesh): Device settings tab (backlog #8) New MeshDevicePanel.vue, added as a 4th/5th tab entry to activeTab/toolsTab/ mobileTab following the exact existing pattern (chat/bitcoin/deadman/ assistant/map). Shows firmware version, node ID, advert name, LoRa region, channel, and device type -- firmware_version/self_node_id were already server-side but never rendered; region is new (composed into MeshStatus from MeshConfig.lora_region at read time, not part of the live session state). Reboot button wired to the already-working mesh.reboot-radio RPC. Co-Authored-By: Claude Sonnet 5 --- core/archipelago/src/mesh/listener/mod.rs | 1 + core/archipelago/src/mesh/mod.rs | 8 ++- core/archipelago/src/mesh/types.rs | 5 ++ neode-ui/src/stores/mesh.ts | 10 +++ neode-ui/src/views/Mesh.vue | 22 +++++-- neode-ui/src/views/mesh/MeshDevicePanel.vue | 68 +++++++++++++++++++++ neode-ui/src/views/mesh/mesh-styles.css | 6 ++ 7 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 neode-ui/src/views/mesh/MeshDevicePanel.vue diff --git a/core/archipelago/src/mesh/listener/mod.rs b/core/archipelago/src/mesh/listener/mod.rs index 85b416ad..fdf61eb6 100644 --- a/core/archipelago/src/mesh/listener/mod.rs +++ b/core/archipelago/src/mesh/listener/mod.rs @@ -275,6 +275,7 @@ impl MeshState { channel_name: channel_name.to_string(), messages_sent: 0, messages_received: 0, + region: None, }), event_tx: tx, next_message_id: RwLock::new(1), diff --git a/core/archipelago/src/mesh/mod.rs b/core/archipelago/src/mesh/mod.rs index f129b194..5239563c 100644 --- a/core/archipelago/src/mesh/mod.rs +++ b/core/archipelago/src/mesh/mod.rs @@ -939,7 +939,13 @@ impl MeshService { /// Get current mesh status. pub async fn status(&self) -> MeshStatus { - self.state.status.read().await.clone() + let mut status = self.state.status.read().await.clone(); + // The operator-configured LoRa region isn't part of the live session + // state (it's config, read once at session start) — compose it in + // here rather than threading it through the session's shared status + // writes, for the Device tab (#8) to display. + status.region = self.config.lora_region.clone(); + status } /// Get a reference to the shared mesh state. diff --git a/core/archipelago/src/mesh/types.rs b/core/archipelago/src/mesh/types.rs index 09733cad..c7562efc 100644 --- a/core/archipelago/src/mesh/types.rs +++ b/core/archipelago/src/mesh/types.rs @@ -178,6 +178,11 @@ pub struct MeshStatus { pub channel_name: String, pub messages_sent: u64, pub messages_received: u64, + /// Operator-configured LoRa region (e.g. "EU_868"), for the Device tab + /// (#8) — composed in by `MeshService::status()` from `MeshConfig`, not + /// part of the live session state itself. + #[serde(default)] + pub region: Option, } /// Information returned from device during initialization. diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index 6c0e7570..3152c70a 100644 --- a/neode-ui/src/stores/mesh.ts +++ b/neode-ui/src/stores/mesh.ts @@ -19,6 +19,8 @@ export interface MeshStatus { /** Bitcoin block-header send/receive prefs (issue #28). */ announce_block_headers?: boolean receive_block_headers?: boolean + /** Operator-configured LoRa region (e.g. "EU_868"), for the Device tab. */ + region?: string | null } export interface MeshPeer { @@ -555,6 +557,13 @@ export const useMeshStore = defineStore('mesh', () => { } } + async function rebootRadio(seconds = 2) { + return rpcClient.call<{ reboot: boolean; seconds: number }>({ + method: 'mesh.reboot-radio', + params: { seconds }, + }) + } + async function getOutbox() { try { return await rpcClient.call<{ count: number; messages?: unknown[] }>({ method: 'mesh.outbox' }) @@ -812,6 +821,7 @@ export const useMeshStore = defineStore('mesh', () => { fetchContent, sendReply, sendReaction, + rebootRadio, getOutbox, sendReadReceipt, forwardMessage, diff --git a/neode-ui/src/views/Mesh.vue b/neode-ui/src/views/Mesh.vue index f2d282c5..4523e49b 100644 --- a/neode-ui/src/views/Mesh.vue +++ b/neode-ui/src/views/Mesh.vue @@ -8,6 +8,7 @@ import AnimatedLogo from '@/components/AnimatedLogo.vue' import MeshMap from '@/components/MeshMap.vue' import MeshBitcoinPanel from '@/views/mesh/MeshBitcoinPanel.vue' import MeshDeadmanPanel from '@/views/mesh/MeshDeadmanPanel.vue' +import MeshDevicePanel from '@/views/mesh/MeshDevicePanel.vue' import MeshAssistantPanel from '@/views/mesh/MeshAssistantPanel.vue' import { rpcClient } from '@/api/rpc-client' import { wsClient } from '@/api/websocket' @@ -259,16 +260,16 @@ async function clearAllMesh() { } // Phase 4: Off-grid Bitcoin + Dead Man's Switch -const activeTab = ref<'chat' | 'bitcoin' | 'deadman' | 'assistant' | 'map'>('chat') +const activeTab = ref<'chat' | 'bitcoin' | 'deadman' | 'assistant' | 'map' | 'device'>('chat') // Tools tab for 3rd column on wide desktop and mobile below-chat -const toolsTab = ref<'bitcoin' | 'deadman' | 'assistant' | 'map'>('bitcoin') +const toolsTab = ref<'bitcoin' | 'deadman' | 'assistant' | 'map' | 'device'>('bitcoin') // Mobile: a single set of floating tabs drives the whole pane (Chat + tools). // 'chat' shows the peers list / active conversation; the rest swap the pane to // that tool. Selecting a tool leaves any open conversation. -const mobileTab = ref<'chat' | 'bitcoin' | 'deadman' | 'assistant' | 'map'>('chat') -function selectMobileTab(tab: 'chat' | 'bitcoin' | 'deadman' | 'assistant' | 'map') { +const mobileTab = ref<'chat' | 'bitcoin' | 'deadman' | 'assistant' | 'map' | 'device'>('chat') +function selectMobileTab(tab: 'chat' | 'bitcoin' | 'deadman' | 'assistant' | 'map' | 'device') { mobileTab.value = tab if (tab !== 'chat') mobileShowChat.value = false } @@ -301,6 +302,12 @@ const showMapPanel = computed(() => { if (isMobile.value) return mobileTab.value === 'map' return activeTab.value === 'map' }) +const showDevicePanel = computed(() => { + if (isVeryWideDesktop.value) return true + if (isWideDesktop.value) return toolsTab.value === 'device' + if (isMobile.value) return mobileTab.value === 'device' + return activeTab.value === 'device' +}) // Mobile: tool pane shows whenever a non-chat tab is active. const showMobileTools = computed(() => isMobile.value && mobileTab.value !== 'chat') const showTabBar = computed(() => !isWideDesktop.value && !isMobile.value) @@ -1860,6 +1867,9 @@ function isImageMime(mime?: string): boolean { + @@ -2203,11 +2213,13 @@ function isImageMime(mime?: string): boolean { AI +
+ @@ -2217,6 +2229,7 @@ function isImageMime(mime?: string): boolean { + @@ -2234,6 +2247,7 @@ function isImageMime(mime?: string): boolean { + diff --git a/neode-ui/src/views/mesh/MeshDevicePanel.vue b/neode-ui/src/views/mesh/MeshDevicePanel.vue new file mode 100644 index 00000000..0d6d38d8 --- /dev/null +++ b/neode-ui/src/views/mesh/MeshDevicePanel.vue @@ -0,0 +1,68 @@ + + + diff --git a/neode-ui/src/views/mesh/mesh-styles.css b/neode-ui/src/views/mesh/mesh-styles.css index 3061b6f7..2baabdce 100644 --- a/neode-ui/src/views/mesh/mesh-styles.css +++ b/neode-ui/src/views/mesh/mesh-styles.css @@ -368,6 +368,12 @@ .mesh-assistant-addkey input { flex: 1; min-width: 0; } .mesh-panel-title { font-size: 1rem; font-weight: 700; color: rgba(255,255,255,0.95); margin: 0; } .mesh-panel-sub { font-size: 0.8rem; color: rgba(255,255,255,0.45); margin: -4px 0 0; } +.mesh-device-panel { display: flex; flex-direction: column; gap: 12px; } +.mesh-device-panel-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } +.mesh-device-panel-actions { display: flex; flex-direction: column; gap: 6px; padding-top: 4px; border-top: 1px solid rgba(255,255,255,0.08); } +.mesh-device-reboot-btn { align-self: flex-start; padding: 8px 16px; font-size: 0.85rem; } +.mesh-device-reboot-hint { font-size: 0.75rem; color: rgba(255,255,255,0.4); margin: 0; } +.mesh-device-reboot-error { font-size: 0.8rem; color: #ef4444; margin: 0; } .mesh-bitcoin-section { display: flex; flex-direction: column; gap: 8px; } .mesh-bitcoin-section-header { display: flex; justify-content: space-between; align-items: center; } .mesh-bitcoin-label { font-size: 0.75rem; font-weight: 600; color: rgba(255,255,255,0.5); text-transform: uppercase; letter-spacing: 0.5px; }