diff --git a/neode-ui/src/components/mesh/MeshDeviceSetupModal.vue b/neode-ui/src/components/mesh/MeshDeviceSetupModal.vue index 4b4973bc..895e910a 100644 --- a/neode-ui/src/components/mesh/MeshDeviceSetupModal.vue +++ b/neode-ui/src/components/mesh/MeshDeviceSetupModal.vue @@ -340,7 +340,7 @@ function stopProbeProgress(done = false) { if (done) probeProgress.value = 100 } -const devicePath = computed(() => mesh.undismissedDetectedDevices[0] ?? '') +const devicePath = computed(() => mesh.flashFlowPath ?? mesh.undismissedDetectedDevices[0] ?? '') const show = computed(() => !!devicePath.value) const imageFailed = ref(false) const deviceImage = computed(() => @@ -402,6 +402,23 @@ watch([show, devicePath], async ([visible]) => { stopFlashPoll() return } + if (mesh.flashFlowPath) { + // Manual "Flash LoRa" entry: jump straight to the flash step. Skip the + // read-only probe — the port is usually the live session's, and a second + // opener on the tty corrupts the running connection; status already + // knows the firmware kind for the connected radio. + probe.value = null + probeError.value = '' + probing.value = false + stopProbeProgress() + imageFailed.value = false + openFlashStep() + const t = (mesh.status?.device_type ?? '').toLowerCase() + if (t === 'meshcore' || t === 'meshtastic' || t === 'reticulum') { + flashFamily.value = t as FlashFirmwareFamily + } + return + } step.value = 1 error.value = '' imageFailed.value = false @@ -428,6 +445,10 @@ watch([show, devicePath], async ([visible]) => { }, { immediate: false }) function dismiss() { + if (mesh.flashFlowPath) { + mesh.closeFlashFlow() + return + } if (devicePath.value) mesh.dismissDetectedDevice(devicePath.value) } @@ -598,6 +619,11 @@ async function cancelFlash() { function closeFlashStep() { stopFlashPoll() + if (mesh.flashFlowPath) { + // Manual entry has no detection step to go back to — close the modal. + mesh.closeFlashFlow() + return + } step.value = 1 } diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index 7d9a815a..96788428 100644 --- a/neode-ui/src/stores/mesh.ts +++ b/neode-ui/src/stores/mesh.ts @@ -377,6 +377,16 @@ export const useMeshStore = defineStore('mesh', () => { } localStorage.setItem(DETECT_DISMISS_KEY, JSON.stringify(dismissedDetected.value)) } + // Manual "Flash LoRa" entry (Mesh header button): forces the global setup + // modal open at the flash step for this port — including the live session's + // port, which detection-driven opening deliberately excludes. + const flashFlowPath = ref(null) + function openFlashFlow(path: string) { + flashFlowPath.value = path + } + function closeFlashFlow() { + flashFlowPath.value = null + } /** Read-only firmware/config probe of a detected port (hot-swap modal). */ async function probeDevice(path: string): Promise { return rpcClient.call({ @@ -1066,6 +1076,9 @@ export const useMeshStore = defineStore('mesh', () => { fetchStatus, undismissedDetectedDevices, dismissDetectedDevice, + flashFlowPath, + openFlashFlow, + closeFlashFlow, probeDevice, flashListFirmware, flashDevice, diff --git a/neode-ui/src/views/Mesh.vue b/neode-ui/src/views/Mesh.vue index 05d927eb..923601a7 100644 --- a/neode-ui/src/views/Mesh.vue +++ b/neode-ui/src/views/Mesh.vue @@ -21,6 +21,18 @@ const mesh = useMeshStore() const transport = useTransportStore() const route = useRoute() +// "Flash LoRa" header button target: the connected radio wins, else any +// detected-but-unconnected stick. Opens the global setup modal at its +// flash step (backend stops the listener and frees the port itself). +const flashLoraPath = computed(() => + (mesh.status?.device_connected && mesh.status.device_path) || + mesh.status?.detected_devices?.[0] || + '' +) +function openFlashLora() { + if (flashLoraPath.value) mesh.openFlashFlow(flashLoraPath.value) +} + // Responsive layout breakpoints const isWideDesktop = ref(window.innerWidth >= 1536) const isVeryWideDesktop = ref(window.innerWidth >= 2560 && window.innerHeight >= 1200) @@ -1860,14 +1872,14 @@ async function downloadAttachment(payload: MeshAttachmentPayload) { Mesh device detected

- - Flash Meshcore | Choose Companion USB - + Flash LoRa + diff --git a/neode-ui/src/views/mesh/mesh-styles.css b/neode-ui/src/views/mesh/mesh-styles.css index a6f679c1..7f468f5f 100644 --- a/neode-ui/src/views/mesh/mesh-styles.css +++ b/neode-ui/src/views/mesh/mesh-styles.css @@ -28,6 +28,7 @@ .mesh-subtitle { color: rgba(255, 255, 255, 0.5); font-size: 0.85rem; margin: 2px 0 0; display: flex; align-items: center; gap: 8px; } .mesh-subtitle-badge { font-size: 0.65rem; font-weight: 600; color: #4ade80; background: rgba(74, 222, 128, 0.12); padding: 1px 6px; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.5px; } .mesh-flasher-btn { display: inline-flex; align-items: center; gap: 0; padding: 8px 16px; font-size: 0.9rem; text-decoration: none; white-space: nowrap; flex-shrink: 0; } +.mesh-flasher-btn:disabled { opacity: 0.45; cursor: not-allowed; } .mesh-flasher-sep { margin: 0 8px; color: rgba(255, 255, 255, 0.2); } .mesh-error { color: #ef4444; font-size: 0.85rem; padding: 8px 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border: 1px solid rgba(239, 68, 68, 0.2); flex-shrink: 0; } .mesh-columns { display: flex; gap: 16px; flex: 1; min-height: 0; overflow: hidden; }