mirror of
https://github.com/pe1hvh/meshcore-gui.git
synced 2026-03-28 17:42:38 +01:00
HotFixPerformance
This commit is contained in:
80
CHANGELOG.md
80
CHANGELOG.md
@@ -6,6 +6,28 @@
|
||||
All notable changes to MeshCore GUI are documented in this file.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
<<<<<<< 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 hidden `ensure_map` bootstrap from `render()`; 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 to `1.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 host` caused 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
|
||||
|
||||
@@ -45,47 +67,36 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver
|
||||
- No breaking changes outside the three files listed above
|
||||
|
||||
---
|
||||
## [1.13.1] - 2026-03-09 — Message Icon Consistency
|
||||
|
||||
### Fixed
|
||||
- Route map markers now use the same JS-rendered node icons as the main MAP instead of NiceGUI default blue markers.
|
||||
- Route detail pages now bootstrap their Leaflet assets explicitly so the shared map icon runtime is available there too
|
||||
- Route maps are now rendered browser-side through the shared Leaflet JS runtime for icon consistency with MAP, Messages, and Archive.
|
||||
|
||||
### Changed
|
||||
- 🔄 `meshcore_gui/gui/constants.py` — Added shared helper functions to resolve node-type icons and labels from the same contact type mapping used by the map and contacts panel
|
||||
- 🔄 `meshcore_gui/core/models.py` — `Message.format_line()` now supports an optional sender prefix so message-related views can prepend the same node icon set without changing existing formatting logic
|
||||
- 🔄 `meshcore_gui/gui/panels/messages_panel.py` — Message rows now prepend the sender with the same node icon mapping as the map/contact views
|
||||
- 🔄 `meshcore_gui/gui/archive_page.py` — Archive rows now use the same sender icon mapping as the live messages panel and map/contact views
|
||||
- 🔄 `meshcore_gui/gui/route_page.py` — Route header and route detail table now show node-type icons derived from the shared contact type mapping instead of generic hardcoded role icons
|
||||
|
||||
### Impact
|
||||
- Message-driven views now use one consistent icon language across map, contacts, messages, archive and route detail
|
||||
- Existing map runtime and panel behavior remain unchanged
|
||||
- No breaking changes outside icon rendering
|
||||
|
||||
>>>>>>> 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 and theme handling independent from NiceGUI redraw cycles
|
||||
- ✅ `meshcore_gui/static/leaflet_map_panel.css` — Styling for browser-side node markers and map container
|
||||
- ✅ `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 defines `maxZoom` during 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 second `L.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
|
||||
@@ -93,6 +104,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver
|
||||
### 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
|
||||
---
|
||||
@@ -820,29 +832,3 @@ overwriting all historical data with only the new buffered messages.
|
||||
- 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
|
||||
|
||||
## [1.13.0] - 2026-03-09
|
||||
|
||||
### Added
|
||||
- Leaflet marker clustering using Leaflet.markercluster for contact nodes.
|
||||
- Browser-side cluster rendering with the device marker kept outside the cluster layer.
|
||||
- Cluster performance tuning with `chunkedLoading: true`.
|
||||
- Spiderfy support at max zoom for overlapping markers.
|
||||
|
||||
### Fixed
|
||||
- Wrong asset load order causing `L is not defined` in MarkerClusterGroup.
|
||||
- Cluster initialization failure caused by missing `maxZoom` on map startup.
|
||||
- Retry cascade causing `Map container is already initialized`.
|
||||
|
||||
### Changed
|
||||
- Map lifecycle is browser-owned: NiceGUI hosts the container, Leaflet owns map state.
|
||||
- Contact markers are updated incrementally in the existing cluster layer.
|
||||
|
||||
## 2026-03-12 map host bootstrap fix
|
||||
- Fixed dashboard MAP bootstrap for hidden/inactive panels by rendering the Leaflet host as a real NiceGUI `div` element instead of injecting raw HTML inside a hidden container.
|
||||
- Fixed browser bootstrap retries so a zero-size hidden host is retried instead of being dropped permanently.
|
||||
- Simplified host waiting logic to polling-based retries, which avoids missing late NiceGUI DOM inserts while the MAP panel is still being mounted.
|
||||
|
||||
## 2026-03-12 route map host mount fix
|
||||
- Fixed Message and Archive route pages so the Leaflet route map host is rendered as a real NiceGUI DOM element instead of injected raw HTML.
|
||||
- This aligns the route page bootstrap with the dashboard MAP fix and prevents route maps from staying blank after clicking a message.
|
||||
|
||||
@@ -25,7 +25,7 @@ from typing import Any, Dict, List
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
VERSION: str = "1.13.2"
|
||||
VERSION: str = "1.13.3"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@@ -293,7 +293,7 @@ CHANNEL_CACHE_ENABLED: bool = False
|
||||
|
||||
# Fixed device name applied when the BOT checkbox is enabled.
|
||||
# The original device name is saved and restored when BOT is disabled.
|
||||
BOT_DEVICE_NAME: str = "NL-OV-ZWL-STDSHGN-WKC Bot"
|
||||
BOT_DEVICE_NAME: str = "ZwolsBotje"
|
||||
|
||||
# Default device name used as fallback when restoring from BOT mode
|
||||
# and no original name was saved (e.g. after a restart).
|
||||
|
||||
@@ -677,34 +677,17 @@ class DashboardPage:
|
||||
container.set_visibility(pid == panel_id)
|
||||
self._active_panel = panel_id
|
||||
|
||||
# Apply channel filter to messages panel
|
||||
if panel_id == 'messages' and self._messages:
|
||||
self._messages.set_active_channel(channel)
|
||||
# Force immediate rebuild so the panel is populated the
|
||||
# moment it becomes visible, instead of waiting for the
|
||||
# next 500 ms timer tick (which caused the "empty on first
|
||||
# click, populated on second click" symptom).
|
||||
data = self._shared.get_snapshot()
|
||||
self._messages.update(
|
||||
data,
|
||||
self._messages.channel_filters,
|
||||
self._messages.last_channels,
|
||||
room_pubkeys=(
|
||||
self._room_server.get_room_pubkeys()
|
||||
if self._room_server else None
|
||||
),
|
||||
)
|
||||
|
||||
# Apply channel filter to archive panel
|
||||
if panel_id == 'archive' and self._archive_page:
|
||||
self._archive_page.set_channel_filter(channel)
|
||||
|
||||
# Force map recenter when opening map panel (Leaflet may be hidden on load)
|
||||
if panel_id == 'map' and self._map:
|
||||
data = self._shared.get_snapshot()
|
||||
data['force_center'] = True
|
||||
self._map.update(data)
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
self._refresh_active_panel_now(force_map_center=(panel_id == 'map'))
|
||||
|
||||
>>>>>>> b76eacf1119026c49c25d2811a6d713da8f8e01b
|
||||
# Update active menu highlight (standalone buttons only)
|
||||
for pid, btn in self._menu_buttons.items():
|
||||
if pid == panel_id:
|
||||
@@ -712,10 +695,89 @@ class DashboardPage:
|
||||
else:
|
||||
btn.classes(remove='domca-menu-active')
|
||||
|
||||
# Refresh only the selected panel immediately so the user does not
|
||||
# need to wait for the next 500 ms dashboard tick.
|
||||
self._refresh_active_panel_now()
|
||||
|
||||
# Close drawer after selection
|
||||
if self._drawer:
|
||||
self._drawer.hide()
|
||||
|
||||
<<<<<<< HEAD
|
||||
def _refresh_active_panel_now(self) -> None:
|
||||
"""Refresh exactly the active panel once, outside the timer tick."""
|
||||
data = self._shared.get_snapshot()
|
||||
|
||||
if self._active_panel == 'device' and self._device:
|
||||
self._device.update(data)
|
||||
elif self._active_panel == 'map' and self._map:
|
||||
data = dict(data)
|
||||
data['force_center'] = True
|
||||
self._map.update(data)
|
||||
elif self._active_panel == 'actions' and self._actions:
|
||||
self._actions.update(data)
|
||||
elif self._active_panel == 'contacts' and self._contacts:
|
||||
self._contacts.update(data)
|
||||
elif self._active_panel == 'messages' and self._messages:
|
||||
if data.get('channels'):
|
||||
self._messages.update_filters(data)
|
||||
self._messages.update_channel_options(data['channels'])
|
||||
self._update_submenus(data)
|
||||
=======
|
||||
def _refresh_active_panel_now(self, force_map_center: bool = False) -> None:
|
||||
"""Refresh only the currently visible panel.
|
||||
|
||||
This is used directly after a panel switch so the user does not
|
||||
need to wait for the next 500 ms dashboard tick.
|
||||
"""
|
||||
data = self._shared.get_snapshot()
|
||||
|
||||
if data.get('channels'):
|
||||
self._messages.update_filters(data)
|
||||
self._messages.update_channel_options(data['channels'])
|
||||
self._update_submenus(data)
|
||||
|
||||
if self._active_panel == 'device':
|
||||
self._device.update(data)
|
||||
elif self._active_panel == 'map':
|
||||
if force_map_center:
|
||||
data['force_center'] = True
|
||||
self._map.update(data)
|
||||
elif self._active_panel == 'actions':
|
||||
self._actions.update(data)
|
||||
elif self._active_panel == 'contacts':
|
||||
self._contacts.update(data)
|
||||
elif self._active_panel == 'messages':
|
||||
>>>>>>> b76eacf1119026c49c25d2811a6d713da8f8e01b
|
||||
self._messages.update(
|
||||
data,
|
||||
self._messages.channel_filters,
|
||||
self._messages.last_channels,
|
||||
<<<<<<< HEAD
|
||||
room_pubkeys=self._room_server.get_room_pubkeys() if self._room_server else None,
|
||||
)
|
||||
elif self._active_panel == 'rooms' and self._room_server:
|
||||
if data.get('channels'):
|
||||
self._messages.update_filters(data)
|
||||
self._messages.update_channel_options(data['channels'])
|
||||
self._update_submenus(data)
|
||||
self._room_server.update(data)
|
||||
elif self._active_panel == 'rxlog' and self._rxlog:
|
||||
self._rxlog.update(data)
|
||||
elif self._active_panel == 'archive' and self._archive_page:
|
||||
self._archive_page.refresh()
|
||||
=======
|
||||
room_pubkeys=(
|
||||
self._room_server.get_room_pubkeys()
|
||||
if self._room_server else None
|
||||
),
|
||||
)
|
||||
elif self._active_panel == 'rooms':
|
||||
self._room_server.update(data)
|
||||
elif self._active_panel == 'rxlog':
|
||||
self._rxlog.update(data)
|
||||
>>>>>>> b76eacf1119026c49c25d2811a6d713da8f8e01b
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Room Server callback (from ContactsPanel)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -753,56 +815,78 @@ class DashboardPage:
|
||||
# Always update status
|
||||
self._status_label.text = data['status']
|
||||
|
||||
# Device info
|
||||
if data['device_updated'] or is_first:
|
||||
self._device.update(data)
|
||||
|
||||
# Map: always send a snapshot while the panel is active.
|
||||
# The JS runtime coalesces pending payloads — only the newest
|
||||
# is ever applied — so calling update() on every tick is cheap.
|
||||
# This ensures the Leaflet runtime always gets at least one
|
||||
# valid snapshot after it finishes loading, regardless of
|
||||
# whether device_updated or is_first happened to be True
|
||||
# on the tick that fired before MeshCoreLeafletBoot was defined.
|
||||
if self._active_panel == 'map':
|
||||
self._map.update(data)
|
||||
|
||||
# Channel-dependent UI: always ensure consistency when
|
||||
# channels exist. Because a single DashboardPage instance
|
||||
# is shared across browser sessions (render() is called on
|
||||
# each new connection), the old session's timer can steal
|
||||
# the is_first flag before the new timer fires. Running
|
||||
# these unconditionally is safe because each method has an
|
||||
# internal fingerprint/equality check that prevents
|
||||
# unnecessary DOM updates.
|
||||
<<<<<<< HEAD
|
||||
# Channel-dependent navigation/filter state remains global, but
|
||||
# expensive panel refreshes must only run for the active panel.
|
||||
=======
|
||||
# Channel-dependent drawer/submenu state may stay global.
|
||||
# The helpers below already contain equality checks, so this
|
||||
# remains cheap while keeping navigation consistent.
|
||||
>>>>>>> b76eacf1119026c49c25d2811a6d713da8f8e01b
|
||||
if data['channels']:
|
||||
self._messages.update_filters(data)
|
||||
self._messages.update_channel_options(data['channels'])
|
||||
self._update_submenus(data)
|
||||
|
||||
# BOT checkbox state (only on actual change or first render
|
||||
# to avoid overwriting user interaction mid-toggle)
|
||||
if data['channels_updated'] or is_first:
|
||||
self._actions.update(data)
|
||||
if self._active_panel == 'device':
|
||||
if data['device_updated'] or is_first:
|
||||
self._device.update(data)
|
||||
<<<<<<< HEAD
|
||||
elif self._active_panel == 'map':
|
||||
self._map.update(data)
|
||||
elif self._active_panel == 'actions':
|
||||
if data['channels_updated'] or is_first:
|
||||
self._actions.update(data)
|
||||
elif self._active_panel == 'contacts':
|
||||
if data['contacts_updated'] or is_first:
|
||||
self._contacts.update(data)
|
||||
=======
|
||||
|
||||
# Contacts
|
||||
if data['contacts_updated'] or is_first:
|
||||
self._contacts.update(data)
|
||||
elif self._active_panel == 'map':
|
||||
# Keep sending snapshots while the map panel is active.
|
||||
# The browser runtime coalesces pending payloads, so only
|
||||
# the newest snapshot is applied.
|
||||
self._map.update(data)
|
||||
|
||||
# Messages (always — for live filter changes)
|
||||
self._messages.update(
|
||||
data,
|
||||
self._messages.channel_filters,
|
||||
self._messages.last_channels,
|
||||
room_pubkeys=self._room_server.get_room_pubkeys() if self._room_server else None,
|
||||
)
|
||||
elif self._active_panel == 'actions':
|
||||
if data['channels_updated'] or is_first:
|
||||
self._actions.update(data)
|
||||
|
||||
# Room Server panels (always — for live messages + contact changes)
|
||||
self._room_server.update(data)
|
||||
elif self._active_panel == 'contacts':
|
||||
if data['contacts_updated'] or is_first:
|
||||
self._contacts.update(data)
|
||||
|
||||
# RX Log
|
||||
if data['rxlog_updated']:
|
||||
self._rxlog.update(data)
|
||||
>>>>>>> b76eacf1119026c49c25d2811a6d713da8f8e01b
|
||||
elif self._active_panel == 'messages':
|
||||
self._messages.update(
|
||||
data,
|
||||
self._messages.channel_filters,
|
||||
self._messages.last_channels,
|
||||
<<<<<<< HEAD
|
||||
room_pubkeys=self._room_server.get_room_pubkeys() if self._room_server else None,
|
||||
)
|
||||
elif self._active_panel == 'rooms':
|
||||
self._room_server.update(data)
|
||||
elif self._active_panel == 'rxlog':
|
||||
if data['rxlog_updated'] or is_first:
|
||||
self._rxlog.update(data)
|
||||
elif self._active_panel == 'archive' and self._archive_page:
|
||||
if data.get('messages_updated') or data.get('channels_updated') or is_first:
|
||||
self._archive_page.refresh()
|
||||
=======
|
||||
room_pubkeys=(
|
||||
self._room_server.get_room_pubkeys()
|
||||
if self._room_server else None
|
||||
),
|
||||
)
|
||||
|
||||
elif self._active_panel == 'rooms':
|
||||
self._room_server.update(data)
|
||||
|
||||
elif self._active_panel == 'rxlog':
|
||||
if data['rxlog_updated'] or is_first:
|
||||
self._rxlog.update(data)
|
||||
>>>>>>> b76eacf1119026c49c25d2811a6d713da8f8e01b
|
||||
|
||||
# Signal worker that GUI is ready for data
|
||||
if is_first and data['channels'] and data['contacts']:
|
||||
|
||||
@@ -48,7 +48,6 @@ class MapPanel:
|
||||
ui.element('div').props(f'id={self._container_id}').classes(
|
||||
'meshcore-leaflet-host w-full h-72'
|
||||
)
|
||||
self._dispatch_to_browser(snapshot={'__command__': 'ensure_map'})
|
||||
self._apply_theme_only()
|
||||
|
||||
def set_ui_dark_mode(self, value: bool | None) -> None:
|
||||
|
||||
@@ -634,6 +634,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
const hasSnapshotWork = Boolean(current.snapshot);
|
||||
const hasLiveMap = maps.has(containerId);
|
||||
|
||||
if (!hasSnapshotWork && !hasLiveMap) {
|
||||
=======
|
||||
if (!current.snapshot && current.theme && !maps.has(containerId)) {
|
||||
pending.set(containerId, current);
|
||||
>>>>>>> b76eacf1119026c49c25d2811a6d713da8f8e01b
|
||||
return;
|
||||
}
|
||||
|
||||
pending.set(containerId, current);
|
||||
scheduleProcess(containerId, 0);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user