HotFixPerformance

This commit is contained in:
pe1hvh
2026-03-12 13:15:58 +01:00
parent 794f08c780
commit c30eb5a467
5 changed files with 193 additions and 112 deletions

View File

@@ -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.

View File

@@ -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).

View File

@@ -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']:

View File

@@ -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:

View File

@@ -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);
};