Skip to content

fix: transport key HTTP failure, MQTT tab polling, WS packet recovery, status popout position#28

Merged
rightup merged 3 commits into
pyMC-dev:fix/ui-tidyfrom
zindello:feat/ui-perf
May 17, 2026
Merged

fix: transport key HTTP failure, MQTT tab polling, WS packet recovery, status popout position#28
rightup merged 3 commits into
pyMC-dev:fix/ui-tidyfrom
zindello:feat/ui-perf

Conversation

@zindello
Copy link
Copy Markdown

Bug Fixes

Transport key derivation fails when served over HTTP — Bug

The Edit Key modal derives the region transport key client-side using SHA-256. The original implementation used crypto.subtle.digest, which is a Web Crypto API restricted to secure contexts only (HTTPS or localhost). It works correctly in the dev server (localhost) and silently fails — returning no key — when the compiled UI is served over plain HTTP directly from the repeater.

Fixed by replacing crypto.subtle with @noble/hashes/sha2.js, a pure-JS synchronous implementation with no secure-context restriction.


MQTT polling active on all config tabs — Bug

The Observer (LetsMesh) settings component polls MQTT broker status every 5 seconds while mounted. All 12 Configuration tabs were rendered with v-show, which keeps every tab mounted in the DOM regardless of which is active. This meant MQTT was being polled continuously whenever the Configuration page was open, even when the user was on an unrelated tab.

Changed all 12 tab content divs from v-show to v-if. Tabs now only mount when active, so their polling and lifecycle hooks only run when visible. As a side effect, the unsaved-changes guard now also correctly limits itself to the active tab's component.


Packet view freezes after WebSocket reconnect — Regression introduced on this branch

On main, PacketTable had a useManagedPolling fallback that hit /api/recent_packets every 10 seconds when WebSocket was disconnected. During the DataService centralisation work on this branch, that fallback was removed in favour of a single ensure('recentPackets') call on mount. The ensure call is subject to a 30-second TTL — meaning if the WS dropped after the TTL window, nothing would re-fetch until the page was reloaded.

Fixed with a targeted recovery on reconnect:

  • Disconnect timestamp recorded when WS closes on a path that will attempt reconnect
  • On reconnect, missed packets fetched once via /api/filtered_packets with start_timestamp anchored to the disconnect time, capped at 10 minutes lookback or 1,000 packets — whichever is reached first
  • Results are delta-merged into the store by deduplicating on packet_hash, so any packets that arrived via WS before the close aren't lost
  • No periodic polling — zero API hits during a disconnected period, one targeted catch-up on reconnect

On the recovery limits: The packet view is an operational dashboard, not an analytical tool. Its job is to show what is happening now and what happened recently — not to reconstruct history. If a user needs to go back further than 10 minutes they should be using purpose-built analysis tooling. A 10-minute window covers any realistic reconnect scenario (marginal link drop, brief power interruption, app backgrounded) without risking a large unbounded fetch on a resource-constrained device. The 1,000-packet cap provides a hard memory ceiling consistent with the existing store limit, ensuring recovery on a busy mesh doesn't blow out the browser on hardware that may be running the UI locally on the repeater itself. Both limits were chosen deliberately over a purely count-based approach (which main used) because time-bounded recovery is more meaningful operationally — you care about what you missed, not an arbitrary number of records. It's also what the limit was in main.


System Status popout overlaps the TopBar — Minor regression

The popout was positioned fixed top-14 (56px from the top of the viewport). The TopBar itself is approximately 59px tall on mobile (p-3 padding) and 83px on desktop (sm:p-6), so the panel opened partially behind the icon row it was anchored to.

Fixed with top-16 sm:top-24, derived directly from the TopBar's own padding and button-height classes.


Unsaved changes modal not showing on tab switch, and missing workflow option — Bug + UX gap

The nav bug. Switching Configuration tabs while editing triggered requestCurrentTabLeave, which called the active tab component's requestLeave(callback) and then used ?? to fall back to callback() if the result was nullish. requestLeave returns void — which is undefined. The ?? operator saw undefined and always fired callback() on the right-hand side, switching the tab immediately regardless of whether the modal had been shown. With v-show tabs (old behaviour), the component stayed mounted after the switch so the modal appeared on the wrong tab — an awkward experience the user had already noticed. With the v-if change made earlier in this branch, the component unmounted the moment the tab switched, and the modal disappeared entirely.

Fixed by replacing the ?? pattern with an explicit if/else: if the tab ref exists, call requestLeave and let it own the flow; otherwise fall back to the callback directly.

The missing workflow option. The UnsavedChangesModal previously offered only two paths: Discard Changes (throw away edits and proceed) and Save Settings (save and proceed). There was no way to close the modal and return to editing — if the user accidentally clicked a tab or triggered a navigation they didn't intend, their only options were to lose their work or save prematurely. This gap also meant the Vue Router onBeforeRouteLeave guard had no cancel path: next() was called on save or discard, but if the user somehow closed the modal without choosing, next was never called and the navigation would hang permanently.

Fixed by adding a Keep Editing button that closes the modal and aborts the pending navigation. For the router-leave path, this calls next(false) explicitly. For the tab-switch path, it simply nulls the pending callback. The modal now presents three clear options: keep editing, discard and leave, or save and leave.

Joshua Mesilane and others added 3 commits May 16, 2026 21:30
- EditKeyModal: derive transport key with @noble/hashes/sha2.js instead of
  crypto.subtle.digest — crypto.subtle is restricted to secure contexts (HTTPS
  or localhost) so it silently fails when the app is served over plain HTTP
  directly from the repeater
- Configuration: change all 12 tab content divs from v-show to v-if so that
  per-tab lifecycle hooks (e.g. MQTT polling in LetsMeshSettings) only run
  while the tab is visible, not for the entire time Configuration is open

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- On WS close (reconnect path only): record disconnect timestamp via
  noteDisconnect() so the recovery window is anchored to the actual
  drop time, not an arbitrary lookback from now
- On reconnect: fetch missed packets via /filtered_packets with
  start_timestamp=max(disconnectTime, now-600) and limit=1000 —
  whichever cap is hit first — then merge into the store by dedup on
  packet_hash rather than replacing wholesale
- System Status popout: top-14 (56px) clipped into the topbar; changed
  to top-16 sm:top-24 to clear the bar at both mobile and desktop padding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ting

- Configuration.vue: fix ?? operator bug in requestCurrentTabLeave —
  requestLeave() returns void (undefined), so ?? callback() was always
  firing immediately, switching the tab before the modal could block it;
  with v-if tabs this caused the component to unmount and the modal to
  vanish entirely
- useUnsavedChanges: add handleCancel (close modal, abort nav) and
  pendingCancelFn so the router-leave path calls next(false) on cancel
  rather than leaving the navigation permanently pending
- UnsavedChangesModal: add Cancel emit and Keep Editing button alongside
  existing Discard and Save Settings
- All 7 config tab components: wire @cancel="handleCancel"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rightup rightup merged commit 6f50816 into pyMC-dev:fix/ui-tidy May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants