62 KiB
[1.13.4] - 2026-03-12 — Room Server message classification fix
Fixed
- 🛠 Incoming room messages from other participants could be misclassified as normal DMs —
CONTACT_MSG_RECVroom detection now keys ontxt_type == 2instead of requiringsignature. - 🛠 Incoming room traffic could be attached to the wrong key — room message handling now prefers
room_pubkey/ receiver-style payload keys before falling back topubkey_prefix. - 🛠 Room login UI could stay out of sync with the actual server-confirmed state —
LOGIN_SUCCESSnow updatesroom_login_statesand refreshes room history using the resolved room key.
Changed
- 🔄
meshcore_gui/ble/events.py— Broadened room payload parsing and added payload-key debug logging for incoming room traffic. - 🔄
meshcore_gui/ble/worker.py—LOGIN_SUCCESShandler now updates per-room login state and refreshes cached room history. - 🔄
meshcore_gui/config.py— Version kept at1.13.4.
Impact
- Keeps the existing Room Server panel logic intact.
- Fix is limited to room event classification and room login confirmation handling.
- No intended behavioural change for ordinary DMs or channel messages.
CHANGELOG
All notable changes to MeshCore GUI are documented in this file. Format follows Keep a Changelog and Semantic Versioning.
<<<<<<< HEAD
[1.13.3] - 2026-03-12 — Active Panel Timer Gating
Changed
- 🔄
meshcore_gui/gui/dashboard.py— The 500 ms dashboard timer now keeps only lightweight global state updates running continuously (status label, channel filters/options, drawer submenu consistency). Expensive panel refreshes are now gated to the currently active panel only - 🔄
meshcore_gui/gui/dashboard.py— Added immediate active-panel refresh on panel switch so newly opened panels populate at once instead of waiting for the next timer tick - 🔄
meshcore_gui/gui/panels/map_panel.py— Removed eager hiddenensure_mapbootstrap fromrender(); the browser map now starts only when real snapshot work exists or when a live map already exists - 🔄
meshcore_gui/static/leaflet_map_panel.js— Theme-only calls without snapshot work no longer start hidden host retry processing before a real map exists - 🔄
meshcore_gui/config.py— Version bumped to1.13.3
Fixed
- 🛠 Hidden panels still refreshed every 500 ms — Device, actions, contacts, messages, rooms and RX log are no longer needlessly updated while another panel is active
- 🛠 Map bootstrap activity while panel is not visible — Removed one source of
MeshCoreLeafletBoot timeout waiting for visible map hostcaused by eager hidden startup traffic - 🛠 Slow navigation over VPN — Reduced unnecessary dashboard-side UI churn by limiting timer-driven work to the active panel
Impact
- Faster panel switching because the selected panel gets one direct refresh immediately
- Lower background UI/update load on dashboard level, especially when the map panel is not active
- Smaller chance of Leaflet hidden-host retries and related console noise outside active map usage
- No intended functional regression for route maps or visible panel behaviour
[1.13.2] - 2026-03-11 — Map Display Bugfix
Fixed
- 🛠 MAP panel blank when contacts list is empty at startup — dashboard update loop
had two separate conditional map-update blocks that both silently stopped firing after
tick 1 when
data['contacts']was empty. Map panel received no further snapshots and remained blank indefinitely. - 🛠 Leaflet map initialized on hidden (zero-size) container —
processPendingin the browser runtime calledL.map()on the host element while it was stilldisplay:none(Vue v-show, panel not yet visible). This produced a broken 0×0 map that never recovered becauseensureMapreturned the cached broken state on all subsequent calls. Fixed by adding aclientWidth/clientHeightguard inensureMap: initialization is deferred until the host has real dimensions. - 🛠 Route map container had no height —
route_page.pyused the Tailwind classh-96for the Leaflet host<div>. NiceGUI/Quasar does not include Tailwind CSS, soh-96had no effect and the container rendered at height 0. Leaflet initialized on a zero-height element and produced a blank map. - 🛠 Route map not rendered when no node has GPS coordinates —
_render_mapreturned early before creating the Leaflet container whenpayload['nodes']was empty. Fixed: container is always created; a notice label is shown instead.
Changed
- 🔄
meshcore_gui/static/leaflet_map_panel.js— Added size guard inensureMap: returnsnullwhen host hasclientWidth === 0 && clientHeight === 0and no map state exists yet.processPendingretries on the next tick once the panel is visible. - 🔄
meshcore_gui/gui/dashboard.py— Consolidated two conditional map-update blocks into a single unconditional update while the MAP panel is active. Addedh-96to the DOMCA CSS height overrides for consistency with the route page map container. - 🔄
meshcore_gui/gui/route_page.py— Replacedh-96Tailwind class on the route map host<div>with an explicit inlinestyle(height: 24rem). Removed earlyreturnguard so the Leaflet container is always created.
Impact
- MAP panel now renders reliably on first open regardless of contact/GPS availability
- Route map now always shows with correct height even when route nodes have no GPS
- No breaking changes outside the three files listed above
b76eacf1119026c49c25d2811a6d713da8f8e01b
[1.13.0] - 2026-03-09 — Leaflet Map Runtime Stabilization
Added
- ✅
meshcore_gui/static/leaflet_map_panel.js— Dedicated browser-side Leaflet runtime responsible for map lifecycle, marker registry, clustering and theme handling independent from NiceGUI redraw cycles - ✅
meshcore_gui/static/leaflet_map_panel.css— Styling for browser-side node markers, cluster icons and map container - ✅
meshcore_gui/services/map_snapshot_service.py— Snapshot service that normalizes device/contact map data into a compact payload for the browser runtime - ✅ Browser-side map state management for center, zoom and theme
- ✅ Theme persistence across reconnect events via browser storage fallback
- ✅ Browser-side contact clustering via
Leaflet.markercluster - ✅ Separate non-clustered device marker layer so the own device remains individually visible
Changed
- 🔄
meshcore_gui/gui/panels/map_panel.py— Replaced NiceGUI Leaflet wrapper usage with a pure browser-managed Leaflet container while preserving the existing card layout, theme toggle and center-on-device control - 🔄 Leaflet bootstrap moved out of inline Python into a dedicated browser runtime loaded from
/static - 🔄 Asset loading order is now explicit: Leaflet first, then
Leaflet.markercluster, then the MeshCore panel runtime - 🔄 Map initialization now occurs only once per container; NiceGUI refresh cycles no longer recreate the map
- 🔄 Dashboard update loop now sends compact map snapshots instead of triggering redraws
- 🔄 Snapshot processing in the browser is coalesced so only the newest payload is applied
- 🔄 Map markers are managed in separate device/contact layers and updated incrementally by stable node id
- 🔄 Contact markers are rendered inside a persistent cluster layer while the device marker remains outside clustering
- 🔄 Theme switching moved to a dedicated theme channel instead of being embedded in snapshot data
Fixed
- 🛠 Map disappearing during dashboard refresh cycles — prevented repeated map reinitialization caused by the 500 ms NiceGUI update loop
- 🛠 Markers disappearing between refreshes — marker updates are now incremental and keyed by node id
- 🛠 Blank map container on load — browser bootstrap now waits for DOM host, Leaflet runtime and panel runtime before initialization
- 🛠 Leaflet clustering bootstrap failure (
L is not defined) — resolved by enforcing correct script dependency order before the panel runtime starts - 🛠 MarkerClusterGroup failure (
Map has no maxZoom specified) — the map now definesmaxZoomduring initial creation before the cluster layer is attached - 🛠 Half-initialized map retry cascade (
Map container is already initialized) — map state is now registered safely during initialization so a failed attempt cannot trigger a secondL.map(...)on the same container - 🛠 Race condition between queued snapshot and theme selection — explicit theme changes can no longer be overwritten by stale snapshot payloads
- 🛠 Viewport jumping back to default center/zoom — stored viewport is no longer reapplied on each snapshot update
- 🛠 Theme reverting to default during reconnect — effective map theme is restored before snapshot processing resumes
Impact
- Leaflet map is now managed entirely in the browser and is no longer recreated on each dashboard refresh
- Node markers remain stable and no longer flicker or disappear during the 500 ms update cycle
- Dense contact sets can now be rendered with clustering without violating the browser-owned map lifecycle
- Theme switching and viewport state persist reliably across reconnect events
- No breaking changes outside the map subsystem
[1.12.1] - 2026-03-08 — Minor change bot
Changed
- 🔄
meshcore_gui/services/bot.py: remove path id's
Impact
- No breaking changes — all existing functionality preserved serial.
[1.12.0] - 2026-02-26 — MeshCore Observer Fase 1
Added
- ✅ MeshCore Observer daemon — New standalone read-only daemon (
meshcore_observer.py) that reads archive JSON files produced by meshcore_gui and meshcore_bridge, aggregates them, and presents a unified NiceGUI monitoring dashboard on port 9093. - ✅ ArchiveWatcher — Core component that polls
~/.meshcore-gui/archive/for*_messages.jsonand*_rxlog.jsonfiles, tracks mtime changes, and returns only new entries since previous poll. Thread-safe, zero writes, graceful on corrupt JSON. - ✅ Observer dashboard panels — Sources overview, aggregated messages feed (sorted by timestamp), aggregated RX log table, and statistics panel with uptime/counters/per-source breakdown. Full DOMCA theme (dark + light mode).
- ✅ Source filter — Dropdown to filter messages and RX log by archive source.
- ✅ Channel filter — Dropdown to filter messages by channel name.
- ✅ ObserverConfig — YAML-based configuration with
from_yaml()classmethod, defaults work without config file. - ✅ observer_config.yaml — Documented config template with all options.
- ✅ install_observer.sh — systemd installer (
/opt/meshcore-observer/,/etc/meshcore/observer_config.yaml), with--uninstalloption. - ✅ RxLogEntry raw packet fields — 5 new fields on
RxLogEntrydataclass:raw_payload,packet_len,payload_len,route_type,packet_type_num(all with defaults, backward compatible). - ✅ EventHandler.on_rx_log() metadata — Raw payload hex and packet metadata now passed through to RxLogEntry and archived (preparation for Fase 2 LetsMesh uplink).
Changed
- 🔄
meshcore_gui/core/models.py: RxLogEntry +5 fields with defaults (backward compatible). - 🔄
meshcore_gui/ble/events.py: on_rx_log() fills raw_payload and metadata (~10 lines added). - 🔄
meshcore_gui/services/message_archive.py: add_rx_log() serializes the 5 new RxLogEntry fields. - 🔄
meshcore_gui/config.py: Version bumped to1.12.0.
Impact
- No breaking changes — All new RxLogEntry fields have defaults; existing archives and code work identically.
- New daemon — meshcore_observer is fully standalone; no imports from meshcore_gui (reads only JSON files).
Added
- ✅ Serial CLI flags —
--baud=BAUDand--serial-cx-dly=SECONDSfor serial configuration at startup.
Changed
- 🔄 Connection layer — Switched from BLE to serial (
MeshCore.create_serial) with serial reconnect handling. - 🔄
config.py: AddedSERIAL_BAUDRATE,SERIAL_CX_DELAY,DEFAULT_TIMEOUT,MESHCORE_LIB_DEBUG; removed BLE PIN settings; version bumped to1.10.0. - 🔄
meshcore_gui.py/meshcore_gui/__main__.py: Updated usage, banners and defaults for serial ports. - 🔄 Docs: Updated README and core docs for serial usage; BLE documents marked as legacy.
Impact
- No breaking changes — all existing functionality preserved serial.
[1.9.11] - 2026-02-19 — Message Dedup Hotfix
Fixed
- 🛠 Duplicate messages after (re)connect —
load_recent_from_archive()appended archived messages on every connect attempt without clearing existing entries; after N failed connects, each message appeared N times. Method is now idempotent: clears the in-memory list before loading. - 🛠 Persistent duplicate messages — Live BLE events for messages already loaded from archive were not suppressed because the
DualDeduplicatorwas never seeded with archived content. Added_seed_dedup_from_messages()inBLEWorkerafter cache/archive load and after reconnect. - 🛠 Last-line-of-defence dedup in SharedData —
add_message()now maintains a fingerprint set (message_hashorchannel:sender:text) and silently skips messages whose fingerprint is already tracked. This guards against duplicates regardless of their source. - 🛠 Messages panel empty on first click —
_show_panel()made the container visible but relied on the next 500 ms timer tick to populate it. Added an immediate_messages.update()call so content is rendered the moment the panel becomes visible.
Changed
- 🔄
core/shared_data.py: Added_message_fingerprintsset and_message_fingerprint()static method;add_message()checks fingerprint before insert and evicts fingerprints when messages are rotated out;load_recent_from_archive()clears messages and fingerprints before loading (idempotent) - 🔄
ble/worker.py: Added_seed_dedup_from_messages()helper; called after_apply_cache()and after reconnect_load_data()to seedDualDeduplicatorwith existing messages - 🔄
gui/dashboard.py:_show_panel()now forces an immediate_messages.update()when the messages panel is shown, eliminating the stale-content flash - 🔄
config.py: Version bumped to1.9.11
Impact
- Eliminates all duplicate message display scenarios: initial connect, failed retries, reconnect, and BLE event replay
- No breaking changes — all existing functionality preserved
- Fingerprint set is bounded to the same 100-message cap as the message list
[1.9.10] - 2026-02-19 — Map Tooltips & Separate Own-Position Marker
Added
- ✅ Map marker tooltips — All markers on the Leaflet map now show a tooltip on hover with the node name and type icon (📱, 📡, 🏠) from
TYPE_ICONS - ✅ Separate own-position marker — The device's own position is now tracked as a dedicated
_own_marker, independent from contact markers. This prevents the own marker from being removed/recreated on every contact update cycle
Changed
- 🔄
gui/panels/map_panel.py: Renamed_markersto_contacts_markers; added_own_markerattribute; own position marker is only updated whendevice_updatedflag is set (not every timer tick); contact markers are only rebuilt whencontacts_updatedis set; addedTYPE_ICONSimport for tooltip icons - 🔄
gui/dashboard.py: Addedself._map.update(data)call in thedevice_updatedblock so the own-position marker updates when device info changes (e.g. GPS position update) - 🔄
config.py: Version bumped to1.9.10
Impact
- Map centering on own device now works correctly and updates only when position actually changes
- Contact markers are no longer needlessly destroyed and recreated on every UI timer tick — only on actual contact data changes
- Tooltips make it easy to identify nodes on the map without clicking
- No breaking changes — all existing map functionality preserved
Credits
[1.9.9] - 2026-02-18 — Variable Landing Page & Operator Callsign
Added
- ✅ Configurable operator callsign — New
OPERATOR_CALLSIGNconstant inconfig.py(default:"PE1HVH"). Used in the landing page SVG and the drawer footer copyright label. Change this single value to personalize the entire GUI for a different operator - ✅ External landing page SVG — The DOMCA splash screen is now loaded from a standalone file (
static/landing_default.svg) instead of being hardcoded indashboard.py. NewLANDING_SVG_PATHconstant inconfig.pypoints to the SVG file. The placeholder{callsign}in the SVG is replaced at runtime withOPERATOR_CALLSIGN - ✅ Landing page customization — To use a custom landing page: copy
landing_default.svg(or create your own SVG), use{callsign}wherever the operator callsign should appear, and pointLANDING_SVG_PATHto your file. The default SVG includes an instructive comment block explaining the placeholder mechanism
Changed
- 🔄
config.py: AddedOPERATOR_CALLSIGNandLANDING_SVG_PATHconstants in new OPERATOR / LANDING PAGE section; version bumped to1.9.9 - 🔄
gui/dashboard.py: Removed hardcoded_DOMCA_SVGstring (~70 lines); added_load_landing_svg()helper that reads SVG from disk and replaces{callsign}placeholder; CSS variable--pe1hvhrenamed to--callsign; drawer footer copyright label now usesconfig.OPERATOR_CALLSIGN
Added (files)
- ✅
static/landing_default.svg— The original DOMCA splash SVG extracted as a standalone file, with{callsign}placeholder and--callsignCSS variable. Serves as both the default landing page and a reference template for custom SVGs
Impact
- Out-of-the-box behavior is identical to v1.9.8 (same DOMCA branding, same PE1HVH callsign)
- Operators personalize by changing 1–2 lines in
config.py— no code modifications needed - Fallback: if the SVG file is missing, a minimal placeholder text is shown instead of a crash
- No breaking changes — all existing dashboard functionality (panels, menus, timer, theming) unchanged
[1.9.8] - 2026-02-17 — Bugfix: Route Page Sender ID, Type & Location Not Populated
Fixed
- 🛠 Sender ID, Type and Location empty in Route Page — After the v4.1 refactoring to
RouteBuilder/RouteNode, the sender contact lookup relied solely onSharedData.get_contact_by_prefix()(live lock-based) andget_contact_by_name(). When both failed (e.g. emptysender_pubkeyfrom RX_LOG decode, or name mismatch),route['sender']remainedNoneand the route table fell through to a hardcoded fallback withtype: '-',location: '-'. The contact data was available in the snapshotdata['contacts']but was never searched - 🛠 Route table fallback row ignored available contact data — When
route['sender']wasNone, the_render_route_tablemethod used a static fallback row without attempting to find the contact in the data snapshot. Even when the contact was present indata['contacts']with valid type and location, these fields showed as'-'
Changed
- 🔄
services/route_builder.py: Added two additional fallback strategies inbuild()after the existing SharedData lookups: (3) bidirectional pubkey prefix match againstdata['contacts']snapshot, (4) case-insensitiveadv_namematch againstdata['contacts']snapshot. Added helper methods_find_contact_by_pubkey()and_find_contact_by_adv_name()for snapshot-based lookups - 🔄
gui/route_page.py: Added defensive fallback in_render_route_table()sender section — whenroute['sender']isNone, attempts to find the contact in the snapshot via_find_sender_contact()before falling back to the static'-'row. Added_find_sender_contact()helper method
Impact
- Sender ID (hash), Type and Location are now populated correctly in the route table when the contact is known
- Four-layer lookup chain ensures maximum resolution: (1) SharedData pubkey lookup, (2) SharedData name lookup, (3) snapshot pubkey lookup, (4) snapshot name lookup
- Defensive fallback in route_page guarantees data is shown even if RouteBuilder misses it
- No breaking changes — all existing route page behavior, styling and data flows unchanged
[1.9.7] - 2026-02-17 — Layout Fix: Archive Filter Toggle & Route Page Styling
Changed
- 🔄
gui/archive_page.py: Archive filter card now hidden by default; toggle visibility via afilter_listicon button placed right-aligned on the same row as the "📚 Archive" title. Header restructured from single label toui.row()withjustify-betweenlayout - 🔄
gui/route_page.py: Route page now uses DOMCA theme (imported fromdashboard.py) with dark mode as default, consistent with the main dashboard. Header restyled frombg-blue-600to Quasar-themed header with JetBrains Mono font. Content container changed fromw-full max-w-4xl mx-autotodomca-panelclass for consistent responsive sizing - 🔄
gui/dashboard.py: Addeddomca-header-textCSS class with@media (max-width: 599px)rule to hide header text on narrow viewports; applied to version label and status label - 🔄
gui/route_page.py: Header label also usesdomca-header-textclass for consistent responsive behaviour
Added
- ✅ Archive filter toggle —
filter_listicon button in archive header row toggles the filter card visibility on click - ✅ Route page close button —
X(close) icon button added right-aligned in the route page header; callswindow.close()to close the browser tab - ✅ Responsive header — On viewports < 600px, header text labels are hidden; only icon buttons (menu, dark mode toggle, close) remain visible
Impact
- Archive page is cleaner by default — filters only shown when needed
- Route page visually consistent with the main dashboard (DOMCA theme, dark mode, responsive panel width)
- Headers degrade gracefully on mobile (< 600px): only icon buttons visible, no text overflow
- No functional changes — all event handlers, callbacks, data bindings, logic and imports are identical to the input
[1.9.6] - 2026-02-17 — Bugfix: Channel Discovery Reliability
Fixed
- 🛠 Channels not appearing (especially on mobile) — Channel discovery aborted too early on slow BLE connections. The
_discover_channels()probe used a single attempt per channel slot and stopped after just 2 consecutive empty responses. On mobile BLE stacks (WebBluetooth via NiceGUI) where GATT responses are slower, this caused discovery to abort before finding any channels, falling back to only[0] Public - 🛠 Race condition: channel update flag lost between threads —
get_snapshot()andclear_update_flags()were two separate calls, each acquiring the lock independently. If the BLE worker setchannels_updated = Truebetween these two calls, the GUI consumed the flag viaget_snapshot()but thenclear_update_flags()reset it — causing the channel submenu and dropdown to never populate - 🛠 Channels disappear on browser reconnect — When a browser tab is closed and reopened,
render()creates new (empty) NiceGUI containers for the drawer submenus, but did not reset_last_channel_fingerprint. The_update_submenus()method compared the new fingerprint against the stale one, found them equal, and skipped the rebuild — leaving the new containers permanently empty. Fixed by resetting both_last_channel_fingerprintand_last_rooms_fingerprintinrender()
Changed
- 🔄
core/shared_data.py: New atomic methodget_snapshot_and_clear_flags()that reads the snapshot and resets all update flags in a single lock acquisition. Internally refactored to_build_snapshot_unlocked()helper. Existingget_snapshot()andclear_update_flags()retained for backward compatibility - 🔄
ble/worker.py:_discover_channels()—max_attemptsincreased from 1 to 2 per channel slot; inter-attemptdelayincreased from 0.5s to 1.0s; consecutive error threshold raised from 2 to 3; inter-channel pause increased from 0.15s to 0.3s for mobile BLE stack breathing room - 🔄
gui/dashboard.py:_update_ui()now usesget_snapshot_and_clear_flags()instead of separateget_snapshot()+clear_update_flags();render()now resets_last_channel_fingerprintand_last_rooms_fingerprinttoNoneso that_update_submenus()rebuilds into the freshly created containers; channel-dependent updates (update_filters,update_channel_options,_update_submenus) now run unconditionally when channel data exists — safe because each method has internal idempotency checks - 🔄
gui/panels/messages_panel.py:update_channel_options()now includes an equality check on options dict to skip redundant.update()calls to the NiceGUI client on every 500ms timer tick
Impact
- Channel discovery now survives transient BLE timeouts that are common on mobile connections
- Atomic snapshot eliminates the threading race condition that caused channels to silently never appear
- Browser close+reopen no longer loses channels — the single-instance timer race on the shared
DashboardPageis fully mitigated - No breaking changes — all existing API methods retained, all other functionality unchanged
[1.9.5] - 2026-02-16 — Layout Fix: RX Log Table Responsive Sizing
Fixed
- 🛠 RX Log table did not adapt to panel/card size — The table used
max-h-48(a maximum height cap) instead of a responsive fixed height, causing it to remain small regardless of available space. Changed toh-40which is overridden by the existing dashboard CSS tocalc(100vh - 20rem)— the same responsive pattern used by the Messages panel - 🛠 RX Log table did not fill card width — Added
w-fullclass to the table element so it stretches to the full width of the parent card - 🛠 RX Log card did not fill panel height — Added
flex-growclass to the card container so it expands to fill the available panel space
Changed
- 🔄
gui/panels/rxlog_panel.py: Card classes'w-full'→'w-full flex-grow'(line 45); table classes'text-xs max-h-48 overflow-y-auto'→'w-full text-xs h-40 overflow-y-auto'(line 65)
Impact
- RX Log table now fills the panel consistently on both desktop and mobile viewports
- Layout is consistent with other panels (Messages, Contacts) that use the same
h-40responsive height pattern - No functional changes — all event handlers, callbacks, data bindings, logica and imports are identical to the input
[1.9.4] - 2026-02-16 — BLE Address Log Prefix & Entry Point Cleanup
Added
- ✅ BLE address prefix in log filename — Log file is now named
<BLE_ADDRESS>_meshcore_gui.log(e.g.AA_BB_CC_DD_EE_FF_meshcore_gui.log) instead of the genericmeshcore_gui.log. Makes it easy to identify which device produced which log file when running multiple instances- New helper
_sanitize_ble_address()stripsliteral:prefix and replaces colons with underscores - New function
configure_log_file(ble_address)updatesLOG_FILEat runtime before the logger is initialised - Rotated backups follow the same naming pattern automatically
- New helper
Removed
- ❌
meshcore_gui/meshcore_gui.py— Redundant copy ofmain()that was never imported. All three entry points (meshcore_gui.pyroot,__main__.py, andmeshcore_gui/meshcore_gui.py) contained near-identical copies of the same logic, causing changes to be missed (as demonstrated by this fix).__main__.pyis now the single source of truth; rootmeshcore_gui.pyis a thin wrapper that imports from it
Changed
- 🔄
config.py: Added_sanitize_ble_address()andconfigure_log_file(); version bumped to1.9.4 - 🔄
__main__.py: Addedconfig.configure_log_file(ble_address)call before any debug output - 🔄
meshcore_gui.py(root): Reduced to 4-line wrapper importingmainfrom__main__
Impact
- Log files are now identifiable per BLE device
- Single source of truth for
main()eliminates future sync issues between entry points - Both startup methods (
python meshcore_gui.pyandpython -m meshcore_gui) remain functional - No breaking changes — defaults and all existing behaviour unchanged
[1.9.3] - 2026-02-16 — Bugfix: Map Default Location & Payload Type Decoding
Fixed
- 🛠 Map centred on hardcoded Zwolle instead of device location — All Leaflet maps used magic-number coordinates
(52.5, 6.0)as initial centre and fallback. These are now replaced by a single configurable constantDEFAULT_MAP_CENTERinconfig.py. Once the device reports a validadv_lat/adv_lon, maps re-centre on the actual device position (existing behaviour, unchanged) - 🛠 Payload type shown as raw integer — Payload type is now retrieved from the decoded payload and translated to human-readable text using MeshCoreDecoder functions, instead of displaying the raw numeric type value
Changed
- 🔄
config.py: AddedDEFAULT_MAP_CENTER(default:(52.5168, 6.0830)) andDEFAULT_MAP_ZOOM(default:9) constants in new MAP DEFAULTS section. Version bumped to1.9.2 - 🔄
gui/panels/map_panel.py: ImportsDEFAULT_MAP_CENTERandDEFAULT_MAP_ZOOMfrom config;ui.leaflet(center=...)uses config constants instead of hardcoded values - 🔄
gui/route_page.py: ImportsDEFAULT_MAP_CENTERandDEFAULT_MAP_ZOOMfrom config; fallback coordinates (or 52.5/or 6.0) replaced byDEFAULT_MAP_CENTER[0]/[1]; zoom usesDEFAULT_MAP_ZOOM
Impact
- Map default location is now a single-point-of-change in
config.py - Payload type is displayed as readable text instead of a raw number
- No breaking changes — all existing map behaviour (re-centre on device position, contact markers) unchanged
[1.9.2] - 2026-02-15 — CLI Parameters & Cleanup
Added
- ✅
--port=PORTCLI parameter — Web server port is now configurable at startup (default:8081). Allows running multiple instances simultaneously on different ports - ✅
--ble-pin=PINCLI parameter — BLE pairing PIN is now configurable at startup (default:123456). Eliminates the need to editconfig.pyfor devices with a non-default PIN, and works in systemd service files - ✅ Per-device log file — Debug log file now includes the BLE address in its filename (e.g.
F0_9E_9E_75_A3_01_meshcore_gui.log), so multiple instances log to separate files
Fixed
- 🛠 BLE PIN not applied from CLI —
ble/worker.pyimportedBLE_PINas a constant at module load time (from config import BLE_PIN), capturing the default value"123456"before CLI parsing could overrideconfig.BLE_PIN. Changed to runtime access viaconfig.BLE_PINso the--ble-pinparameter is correctly passed to the BLE agent
Removed
- ❌ Redundant
meshcore_gui/meshcore_gui.py— This file was a near-identical copy of bothmeshcore_gui.py(top-level) andmeshcore_gui/__main__.py, but was never imported or referenced. Removed to eliminate maintenance risk. The two remaining entry points cover all startup methods:python meshcore_gui.pyandpython -m meshcore_gui
Impact
- Multiple instances can run side-by-side with different ports, PINs and log files
- Service deployments no longer require editing
config.py— all runtime settings via CLI - No breaking changes — all defaults are unchanged
[1.9.1] - 2026-02-14 — Bugfix: Dual Reconnect Conflict
Fixed
- 🛠 Library reconnect interfered with application reconnect — The meshcore library's internal
auto_reconnect(visible in logs as"Attempting reconnection 1/3") ran a fast 3-attempt reconnect cycle without bond cleanup. This prevented the application's ownreconnect_loop(which doesremove_bond()+ backoff) from succeeding, because BlueZ retained a stale bond →"failed to discover service"
Changed
- 🔄
ble/worker.py: Setauto_reconnect=Falsein bothMeshCore.create_ble()call sites (_connect()and_create_fresh_connection()), so only the application's bond-awarereconnect_loophandles reconnection - 🔄
ble/worker.py: Added"failed to discover"and"service discovery"to disconnect detection keywords for defensive coverage
Impact
- Eliminates the ~9 second wasted library reconnect cycle after every BLE disconnect
- Application's
reconnect_loop(with bond cleanup) now runs immediately after disconnect detection - No breaking changes — the application reconnect logic was already fully functional
[1.9.0] - 2026-02-14 — BLE Connection Stability
Added
- ✅ Built-in BLE PIN agent — New
ble/ble_agent.pyregisters a D-Bus agent with BlueZ to handle PIN pairing requests automatically. Eliminates the need for externalbt-agent.serviceandbluez-toolspackage- Uses
dbus_fast(already a dependency ofbleak, no new packages) - Supports
RequestPinCode,RequestPasskey,DisplayPasskey,RequestConfirmation,AuthorizeServicecallbacks - Configurable PIN via
BLE_PINinconfig.py(default:123456)
- Uses
- ✅ Automatic bond cleanup — New
ble/ble_reconnect.pyprovidesremove_bond()function that removes stale BLE bonds via D-Bus, equivalent tobluetoothctl remove <address>. Called automatically on startup and before each reconnect attempt - ✅ Automatic reconnect after disconnect — BLEWorker main loop now detects BLE disconnects (via connection error exceptions) and automatically triggers a reconnect sequence: bond removal → linear backoff wait → fresh connection → re-wire handlers → reload device data
- Configurable via
RECONNECT_MAX_RETRIES(default: 5) andRECONNECT_BASE_DELAY(default: 5.0s) - After all retries exhausted: waits 60s then starts a new retry cycle (infinite recovery)
- Configurable via
- ✅ Generic install script —
install_ble_stable.shauto-detects user, project directory, venv path and entry point to generate systemd service and D-Bus policy. Supports--uninstallflag
Changed
- 🔄
ble/worker.py—_async_main()rewritten with three phases: (1) start PIN agent, (2) remove stale bond, (3) connect + main loop with disconnect detection. Reconnect logic re-wires all event handlers and reloads device data after successful reconnection - 🔄
config.py— AddedBLE_PIN,RECONNECT_MAX_RETRIES,RECONNECT_BASE_DELAYconstants
Removed
- ❌
bt-agent.servicedependency — No longer needed; PIN pairing is handled by the built-in agent - ❌
bluez-toolssystem package — No longer needed - ❌
~/.meshcore-ble-pinfile — No longer needed - ❌ Manual
bluetoothctl removebefore startup — Handled automatically - ❌
ExecStartPrein systemd service — Bond cleanup is internal
Impact
- Zero external dependencies for BLE pairing on Linux
- Automatic recovery from the T1000e ~2 hour BLE disconnect issue
- No manual intervention needed after BLE connection loss
- Single systemd service (
meshcore-gui.service) manages everything - No breaking changes to existing functionality
[1.8.0] - 2026-02-14 — DRY Message Construction & Archive Layout Unification
Fixed
- 🛠 Case-sensitive prefix matching —
get_contact_name_by_prefix()andget_contact_by_prefix()inshared_data.pyfailed to match path hashes (uppercase, e.g.'B8') against contact pubkeys (lowercase, e.g.'b8a3f2...'). Added.lower()to both sides of the comparison, consistent with_resolve_path_names()which already had it - 🛠 Route page 404 from archive — Archive page linked to
/route/{hash}but route was registered as/route/{msg_index:int}, causing a JSON parse error for hex hash strings. Route parameter changed tostrwith 3-strategy lookup (index → memory hash → archive fallback) - 🛠 Three entry points out of sync —
meshcore_gui.py(root),meshcore_gui/meshcore_gui.py(inner) andmeshcore_gui/__main__.pyhad diverging route registrations. All three now use identical/route/{msg_key}withstrparameter
Changed
- 🔄
core/models.py— DRY factory methods and formattingMessage.now_timestamp(): static method replacing 7× hardcodeddatetime.now().strftime('%H:%M:%S')acrossevents.pyandcommands.pyMessage.incoming(): classmethod factory for received messages (direction='in', auto-timestamp)Message.outgoing(): classmethod factory for sent messages (sender='Me',direction='out', auto-timestamp)Message.format_line(channel_names): single-line display formatting ("12:34:56 ← [Public] [2h✓] PE1ABC: Hello mesh!"), replacing duplicate inline formatting inmessages_panel.pyandarchive_page.py
- 🔄
ble/events.py— 4×Message(...)constructors replaced byMessage.incoming();datetimeimport removed - 🔄
ble/commands.py— 3×Message(...)constructors replaced byMessage.outgoing();datetimeimport removed - 🔄
gui/panels/messages_panel.py— 15 lines inline formatting replaced by singlemsg.format_line(channel_names)call - 🔄
gui/archive_page.py— Layout unified with main page- Multi-row card layout replaced by single-line
msg.format_line()in monospace container (same style as main page) - DM added to channel filter dropdown (post-filter on
channel is None) - Message click opens
/route/{message_hash}in new tab (was: no click handler on archive messages) - Removed
_render_message_card()(98 lines) and_render_archive_route()(75 lines) - Removed
RouteBuilderdependency andTYPE_LABELSimport - File reduced from 445 to 267 lines
- Multi-row card layout replaced by single-line
- 🔄
gui/route_page.py—render(msg_index: int)→render(msg_key: str)with 3-strategy message lookup: (1) numeric index from in-memory list, (2) hash match in memory, (3)archive.get_message_by_hash()fallback - 🔄
services/message_archive.py— New methodget_message_by_hash(hash)for single-message lookup by packet hash - 🔄
__main__.py+meshcore_gui.py(both) — Route changed from/route/{msg_index}(int) to/route/{msg_key}(str)
Impact
- DRY: timestamp formatting 7→1 definition, message construction 7→2 factories, line formatting 2→1 method
- Archive page visually consistent with main messages panel (single-line, monospace)
- Archive messages now clickable to open route visualization (was: only in-memory messages)
- Case-insensitive prefix matching fixes path name resolution for contacts with uppercase path hashes
- No breaking changes to BLE protocol handling, dedup, bot, or data storage
Known Limitations
- DM filter in archive uses post-filtering (query without channel filter + filter on
channel is None); becomes exact whenquery_messages()gets native DM support
Parked for later
- Multi-path tracking (enrich RxLogEntry with multiple path observations)
- Events correlation improvements (only if proven data loss after
.lower()fix)
[1.7.0] - 2026-02-13 — Archive Channel Name Persistence
Added
- ✅ Channel name stored in archive — Messages now persist
channel_namealongside the numericchannelindex in<ADDRESS>_messages.json, so archived messages retain their human-readable channel name even when the device is not connectedMessagedataclass: new fieldchannel_name: str(default"", backward compatible)SharedData.add_message(): automatically resolveschannel_namefrom the live channels list when not already set (new helper_resolve_channel_name())MessageArchive.add_message(): writeschannel_nameto the JSON dict
- ✅ Archive channel selector built from archived data — Channel filter dropdown on
/archivenow populated viaSELECT DISTINCT channel_nameon the archive instead of the live BLE channels list- New method
MessageArchive.get_distinct_channel_names()returns sorted unique channel names from stored messages - Selector shows only channels that actually have archived messages
- New method
- ✅ Archive filter on channel name —
MessageArchive.query_messages()parameter changed fromchannel: Optional[int]tochannel_name: Optional[str](exact match on name string)
Changed
- 🔄
core/models.py: Addedchannel_namefield toMessagedataclass andfrom_dict() - 🔄
core/shared_data.py:add_message()resolves channel name; added_resolve_channel_name()helper - 🔄
services/message_archive.py:channel_namepersisted in JSON;query_messages()filters by name; newget_distinct_channel_names()method - 🔄
gui/archive_page.py: Channel selector built fromarchive.get_distinct_channel_names(); filter state changed from_channel_filter(int) to_channel_name_filter(str); message cards showchannel_namedirectly from archive
Fixed
- 🛠 Main page empty after startup — After a restart the messages panel showed no messages until new live BLE traffic arrived.
SharedData.load_recent_from_archive()now loads up to 100 recent archived messages during the cache-first startup phase, so historical messages are immediately visible- New method
SharedData.load_recent_from_archive(limit)— reads fromMessageArchive.query_messages()and populates the in-memory list without re-archiving BLEWorker._apply_cache()callsload_recent_from_archive()at the end of cache loading
- New method
Impact
- Archived messages now self-contained — channel name visible without live BLE connection
- Main page immediately shows historical messages after startup (no waiting for live BLE traffic)
- Backward compatible — old archive entries without
channel_namefall back to"Ch <idx>" - No breaking changes to existing functionality
[1.6.0] - 2026-02-13 — Dashboard Layout Consolidation
Changed
- 🔄 Messages panel consolidated — Filter checkboxes (DM + channels) and message input (text field, channel selector, Send button) are now integrated into the Messages panel, replacing the separate Filter and Input panels
- DM + channel checkboxes displayed centered in the Messages header row, between the "💬 Messages" label and the "📚 Archive" button
- Message input row (text field, channel selector, Send button) placed below the message list within the same card
messages_panel.py: Constructor now acceptsput_commandcallable; addedupdate_filters(data),update_channel_options(channels)methods andchannel_filters,last_channelsproperties (all logic 1:1 from FilterPanel/InputPanel);update()signature unchanged
- 🔄 Actions panel expanded — BOT toggle checkbox moved from Filter panel to Actions panel, below the Refresh/Advert buttons
actions_panel.py: Constructor now acceptsset_bot_enabledcallable; addedupdate(data)method for BOT state sync;_on_bot_toggle()logic 1:1 from FilterPanel
- 🔄 Dashboard layout simplified — Centre column reduced from 4 panels (Map → Input → Filter → Messages) to 2 panels (Map → Messages)
dashboard.py: FilterPanel and InputPanel no longer rendered; all dependencies rerouted to MessagesPanel and ActionsPanel;_update_ui()call-sites updated accordingly
Removed (from layout, files retained)
- ❌ Filter panel no longer rendered as separate panel —
filter_panel.pyretained in codebase but not instantiated in dashboard - ❌ Input panel no longer rendered as separate panel —
input_panel.pyretained in codebase but not instantiated in dashboard
Impact
- Cleaner, more compact dashboard: 2 fewer panels in the centre column
- All functionality preserved — message filtering, send, BOT toggle, archive all work identically
- No breaking changes to BLE, services, core or other panels
[1.5.0] - 2026-02-11 — Room Server Support, Dynamic Channel Discovery & Contact Management
Added
- ✅ Room Server panel — Dedicated per-room-server message panel in the centre column below Messages. Each Room Server (type=3 contact) gets its own
ui.card()with login/logout controls and message display- Click a Room Server contact to open an add/login dialog with password field
- After login: messages are displayed in the room card; send messages directly from the room panel
- Password row + login button automatically replaced by Logout button after successful login
- Room Server author attribution via
signaturefield (txt_type=2) — real message author is resolved from the 4-byte pubkey prefix, not the room server pubkey - New panel:
gui/panels/room_server_panel.py— per-room card management with login state tracking
- ✅ Room Server password store — Passwords stored outside the repository in
~/.meshcore-gui/room_passwords/<ADDRESS>.json- New service:
services/room_password_store.py— JSON-backed persistent password storage per BLE device, analogous toPinStore - Room panels are restored from stored passwords on app restart
- New service:
- ✅ Dynamic channel discovery — Channels are now auto-discovered from the device at startup via
get_channel()BLE probing, replacing the hardcodedCHANNELS_CONFIG- Single-attempt probe per channel slot with early stop after 2 consecutive empty slots
- Channel name and encryption key extracted in a single pass (combined discovery + key loading)
- Configurable channel caching via
CHANNEL_CACHE_ENABLED(default:False— always fresh from device) MAX_CHANNELSsetting (default: 8) controls how many slots are probed
- ✅ Individual contact deletion — 🗑️ delete button per unpinned contact in the contacts list, with confirmation dialog
- New command:
remove_single_contactin BLE command handler - Pinned contacts are protected (no delete button shown)
- New command:
- ✅ "Also delete from history" option — Checkbox in the Clean up confirmation dialog to also remove locally cached contact data
- ✅ Room Server protocol research —
RoomServer_Companion_App_Onderzoek.mddocuments the full companion app message flow (login, push protocol, signature mechanism, auto_message_fetching)
Changed
- 🔄
config.py: RemovedCHANNELS_CONFIGconstant; addedMAX_CHANNELS(default: 8) andCHANNEL_CACHE_ENABLED(default:False) - 🔄
ble/worker.py: Replaced hardcoded channel loading with_discover_channels()method; added_try_get_channel_info()helper;_apply_cache()respectsCHANNEL_CACHE_ENABLEDsetting; removed_load_channel_keys()(integrated into discovery pass) - 🔄
ble/commands.py: Addedlogin_room,send_room_msgandremove_single_contactcommand handlers - 🔄
gui/panels/contacts_panel.py: Contact click now dispatches by type — type=3 (Room Server) opens room dialog, others open DM dialog; addedon_add_roomcallback parameter; added 🗑️ delete button per unpinned contact - 🔄
gui/panels/messages_panel.py: Room Server messages filtered from general message view via_is_room_message()with prefix matching;update()acceptsroom_pubkeysparameter - 🔄
gui/dashboard.py: AddedRoomServerPanelin centre column;_update_ui()passesroom_pubkeysto Messages panel; added_on_add_room_servercallback - 🔄
gui/panels/filter_panel.py: Channel filter checkboxes now built dynamically from discovered channels (no hardcoded references) - 🔄
services/bot.py: Removed stale comment referencing hardcoded channels
Fixed
- 🛠 Room Server messages appeared as DM — Messages from Room Servers (txt_type=2) were displayed in the general Messages panel as direct messages. They are now filtered out and shown exclusively in the Room Server panel
- 🛠 Historical room messages not shown after login — Post-login fetch loop was polling
get_msg()before room server had time to push messages over LoRa RF (10–75s per message). Removed redundant fetch loop; the library'sauto_message_fetchinghandlesMESSAGES_WAITINGevents correctly and event-driven - 🛠 Author attribution incorrect for room messages — Room server messages showed the room server name as sender instead of the actual message author. Now correctly resolved from the
signaturefield (4-byte pubkey prefix) via contact lookup
Impact
- Room Servers are now first-class citizens in the GUI with dedicated panels
- Channel configuration no longer requires manual editing of
config.py - Contact list management is more granular with per-contact deletion
- No breaking changes to existing functionality (messages, DM, map, archive, bot, etc.)
[1.4.0] - 2026-02-09 — SDK Event Race Condition Fix
Fixed
- 🛠 BLE startup delay of ~2 minutes eliminated — The meshcore Python SDK (
commands/base.py) dispatched device response events beforewait_for_events()registered its subscription. On busy networks with frequentRX_LOG_DATAevents, this causedsend_device_query()andget_channel()to fail repeatedly withno_event_received, wasting 110+ seconds in timeouts
Changed
- 📄
meshcoreSDKcommands/base.py: Rewrittensend()method to subscribe to expected events before transmitting the BLE command (subscribe-before-send pattern), matching the approach used by the companion apps (meshcore.js, iOS, Android). Submitted upstream as meshcore_py PR #52
Impact
- Startup time reduced from ~2+ minutes to ~10 seconds on busy networks
- All BLE commands (
send_device_query,get_channel,get_bat,send_appstart, etc.) now succeed on first attempt instead of requiring multiple retries - No changes to meshcore_gui code required — the fix is entirely in the meshcore SDK
Temporary Installation
Until the fix is merged upstream, install the patched meshcore SDK:
pip install --force-reinstall git+https://github.com/PE1HVH/meshcore_py.git@fix/event-race-condition
[1.3.2] - 2026-02-09 — Bugfix: Bot Device Name Restoration After Restart
Fixed
- 🛠 Bot device name not properly restored after restart/crash — After a restart or crash with bot mode previously active, the original device name was incorrectly stored as the bot name (e.g.
NL-OV-ZWL-STDSHGN-WKC Bot) instead of the real device name (e.g.PE1HVH T1000e). The original device name is now correctly preserved and restored when bot mode is disabled
Changed
- 🔄
commands.py:set_bot_namehandler now verifies that the stored original name is not already the bot name before saving - 🔄
shared_data.py:original_device_nameis only written when it differs fromBOT_DEVICE_NAMEto prevent overwriting with the bot name on restart
[1.3.1] - 2026-02-09 — Bugfix: Auto-add AttributeError
Fixed
- 🛠 Auto-add error on first toggle — Setting auto-add for the first time raised
AttributeError: 'telemetry_mode_base'. Theset_manual_add_contacts()SDK call now handles missingtelemetry_mode_baseattribute gracefully
Changed
- 🔄
commands.py:set_auto_addhandler wrapsset_manual_add_contacts()call with attribute check and error handling for missingtelemetry_mode_base
[1.3.0] - 2026-02-08 — Bot Device Name Management
Added
- ✅ Bot device name switching — When the BOT checkbox is enabled, the device name is automatically changed to a configurable bot name; when disabled, the original name is restored
- Original device name is saved before renaming so it can be restored on BOT disable
- Device name written to device via BLE
set_name()SDK call - Graceful handling of BLE failures during name change
- ✅
BOT_DEVICE_NAMEconstant inconfig.py— Configurable fixed device name used when bot mode is active (default:;NL-OV-ZWL-STDSHGN-WKC Bot)
Changed
- 🔄
config.py: AddedBOT_DEVICE_NAMEconstant for bot mode device name - 🔄
bot.py: Removed hardcodedBOT_NAMEprefix ("Zwolle Bot") from bot reply messages — bot replies no longer include a name prefix - 🔄
filter_panel.py: BOT checkbox toggle now triggers device name save/rename via command queue - 🔄
commands.py: Addedset_bot_nameandrestore_namecommand handlers for device name switching - 🔄
shared_data.py: Addedoriginal_device_namefield for storing the pre-bot device name
Removed
- ❌
BOT_NAMEconstant frombot.py— bot reply prefix removed; replies no longer prepend a bot display name
[1.2.0] - 2026-02-08 — Contact Maintenance Feature
Added
-
✅ Pin/Unpin contacts (Iteration A) — Toggle to pin individual contacts, protecting them from bulk deletion
- Persistent pin state stored in
~/.meshcore-gui/cache/<ADDRESS>_pins.json - Pinned contacts visually marked with yellow background
- Pinned contacts sorted to top of contact list
- Pin state survives app restart
- New service:
services/pin_store.py— JSON-backed persistent pin storage
- Persistent pin state stored in
-
✅ Bulk delete unpinned contacts (Iteration B) — Remove all unpinned contacts from device in one action
- "🧹 Clean up" button in contacts panel with confirmation dialog
- Shows count of contacts to be removed vs. pinned contacts kept
- Progress status updates during removal
- Automatic device resync after completion
- New service:
services/contact_cleaner.py— ContactCleanerService with purge statistics
-
✅ Auto-add contacts toggle (Iteration C) — Control whether device automatically adds new contacts from mesh adverts
- "📥 Auto-add" checkbox in contacts panel (next to Clean up button)
- Syncs with device via
set_manual_add_contacts()SDK call - Inverted logic handled internally (UI "Auto-add ON" =
set_manual_add_contacts(false)) - Optimistic update with automatic rollback on BLE failure
- State synchronized from device on each GUI update cycle
Changed
- 🔄
contacts_panel.py: Added pin checkbox per contact, purge button, auto-add toggle, DM dialog (all existing functionality preserved) - 🔄
commands.py: Addedpurge_unpinnedandset_auto_addcommand handlers - 🔄
shared_data.py: Addedauto_add_enabledfield with thread-safe getter/setter - 🔄
protocols.py: Addedset_auto_add_enabledandis_auto_add_enabledto Writer and Reader protocols - 🔄
dashboard.py: PassesPinStoreandset_auto_add_enabledcallback to ContactsPanel - 🔄 UI language: All Dutch strings in
contacts_panel.pyandcommands.pytranslated to English
Fixed
- 🛠 Route table names and IDs not displayed — Route tables in both current messages (RoutePage) and archive messages (ArchivePage) now correctly show node names and public key IDs for sender, repeaters and receiver
Changed
- 🔄 CHANGELOG.md: Corrected version numbering to semantic versioning, fixed inaccurate references (archive button location, filter state persistence)
- 🔄 README.md: Added Message Archive feature, updated project structure, configuration table and architecture diagram
- 🔄 MeshCore_GUI_Design.docx: Added ArchivePage, MessageArchive, Models components; updated project structure, protocols, configuration and version history
[1.1.0] - 2026-02-07 — Archive Viewer Feature
Added
- ✅ Archive Viewer Page (
/archive) — Full-featured message archive browser- Pagination (50 messages per page, configurable)
- Channel filter dropdown (All + configured channels)
- Time range filter (24h, 7d, 30d, 90d, All time)
- Text search (case-insensitive)
- Filter state stored in instance variables (reset on page reload)
- Message cards with same styling as main messages panel
- Clickable messages for route visualization (where available)
- 💬 Reply functionality — Expandable reply panel per message
- 🗺️ Inline route table — Expandable route display per archive message with sender, repeaters and receiver (names, IDs, node types)
- (Note: Reply panels and inline route tables removed in v1.8.0, replaced by click-to-route navigation via message hash)
-
✅ MessageArchive.query_messages() method
- Filter by: time range, channel, text search, sender
- Pagination support (limit, offset)
- Returns tuple: (messages, total_count)
- Sorting: Newest first
-
✅ UI Integration
- "📚 Archive" button in Messages panel header (opens in new tab)
- Back to Dashboard button in archive page
- ✅ Reply Panel
- Expandable reply per message (💬 Reply button)
- Pre-filled with @sender mention
- Channel selector
- Send button with success notification
- Auto-close expansion after send
Changed
- 🔄
SharedData.get_snapshot(): Now includes'archive'field - 🔄
MessagesPanel: Added archive button in header row - 🔄 Both entry points (
__main__.pyandmeshcore_gui.py): Register/archiveroute
Performance
- Query: ~10ms for 10k messages with filters
- Memory: ~10KB per page (50 messages)
- No impact on main UI (separate page)
Known Limitations
Route visualization only works for messages in recent buffer (last 100)— Fixed in v1.8.0: archive messages now support click-to-route viaget_message_by_hash()fallback- Text search is linear scan (no indexing yet)
- Sender filter exists in API but not in UI yet
[1.0.3] - 2026-02-07 — Critical Bugfix: Archive Overwrite Prevention
Fixed
- 🛠 CRITICAL: Fixed bug where archive was overwritten instead of appended on restart
- 🛠 Archive now preserves existing data when read errors occur
- 🛠 Buffer is retained for retry if existing archive cannot be read
Changed
- 🔄
_flush_messages(): Early return on read error instead of overwriting - 🔄
_flush_rxlog(): Early return on read error instead of overwriting - 🔄 Better error messages for version mismatch and JSON decode errors
Details
Problem: If the existing archive file had a JSON parse error or version mismatch,
the flush operation would proceed with existing_messages = [], effectively
overwriting all historical data with only the new buffered messages.
Solution: The flush methods now:
- Try to read existing archive first
- If read fails (JSON error, version mismatch, IO error), abort the flush
- Keep buffer intact for next retry
- Only clear buffer after successful write
Impact: No data loss on restart or when archive files have issues.
Testing
- ✅ Added
test_append_on_restart_not_overwrite()integration test - ✅ Verifies data is appended across multiple sessions
- ✅ All existing tests still pass
[1.0.2] - 2026-02-07 — RxLog message_hash Enhancement
Added
- ✅
message_hashfield added toRxLogEntrymodel - ✅ RxLog entries now include message_hash for correlation with messages
- ✅ Archive JSON includes message_hash in rxlog entries
Changed
- 🔄
events.py: Restructuredon_rx_log()to extract message_hash before creating RxLogEntry - 🔄
message_archive.py: Updated rxlog archiving to include message_hash field - 🔄 Tests updated to verify message_hash persistence
Benefits
- Correlation: Link RX log entries to their corresponding messages
- Analysis: Track which packets resulted in messages
- Debugging: Better troubleshooting of packet processing
[1.0.1] - 2026-02-07 — Entry Point Fix
Fixed
- ✅
meshcore_gui.py(root entry point) now passes ble_address to SharedData - ✅ Archive works correctly regardless of how application is started
Changed
- 🔄 Both entry points (
meshcore_gui.pyandmeshcore_gui/__main__.py) updated
[1.0.0] - 2026-02-07 — Message & Metadata Persistence
Added
- ✅ MessageArchive class for persistent storage
- ✅ Configurable retention periods (MESSAGE_RETENTION_DAYS, RXLOG_RETENTION_DAYS, CONTACT_RETENTION_DAYS)
- ✅ Automatic daily cleanup of old data
- ✅ Batch writes for performance
- ✅ Thread-safe with separate locks
- ✅ Atomic file writes
- ✅ Contact retention in DeviceCache
- ✅ Archive statistics API
- ✅ Comprehensive tests (20+ unit, 8+ integration)
- ✅ Full documentation
Storage Locations
~/.meshcore-gui/archive/<ADDRESS>_messages.json~/.meshcore-gui/archive/<ADDRESS>_rxlog.json
Requirements Completed
-
R1: All incoming messages persistent ✅
-
R2: All incoming RxLog entries persistent ✅
-
R3: Configurable retention ✅
-
R4: Automatic cleanup ✅
-
R5: Backward compatibility ✅
-
R6: Contact retention ✅
-
R7: Archive stats API ✅
-
Fix3: Leaflet asset injection is now per page render instead of process-global, and browser bootstrap now retries until the host element, Leaflet runtime, and MeshCore panel runtime are all available. This fixes blank map containers caused by missing or late-loaded JS/CSS assets.
-
Fix5: Removed per-snapshot map invalidate calls, stopped forcing a default dark theme during map bootstrap, and added client-side interaction/resize guards so zooming stays responsive and the theme no longer jumps back during status-loop updates.
2026-03-09 map hotfix v2
- regular map snapshots no longer carry theme state
- explicit theme changes are now handled only via the dedicated theme channel
- initial map render now sends an ensure_map command plus an immediate theme sync
- added no-op ensure_map handling in the Leaflet runtime to avoid accidental fallback behaviour