Fix some frontend display/quality/doc issues

This commit is contained in:
Jack Kingsman
2026-04-10 15:43:08 -07:00
parent 8cc542ce23
commit 442c2fad20
20 changed files with 39 additions and 39 deletions
+4 -8
View File
@@ -3,7 +3,7 @@
* Feature: Add Arch AUR package * Feature: Add Arch AUR package
* Feature: 72hr packet density view in statistics * Feature: 72hr packet density view in statistics
* Feature: Add warnings for event loop selection for MQTT on Windows startup * Feature: Add warnings for event loop selection for MQTT on Windows startup
* Bufix: Bump Apprise to 1.9.9 to fix Matrix bug * Bugfix: Bump Apprise to 1.9.9 to fix Matrix bug
* Misc: More memory-conscious on recent contact fetch * Misc: More memory-conscious on recent contact fetch
* Misc: Fix statistics pane e2e test * Misc: Fix statistics pane e2e test
@@ -145,7 +145,7 @@
* Bugfix: Fix Apprise duplicate names * Bugfix: Fix Apprise duplicate names
* Bugfix: Be better about identity resolution in the stats pane * Bugfix: Be better about identity resolution in the stats pane
* Misc: Docs, test, and performance enhancements * Misc: Docs, test, and performance enhancements
* Misc: Don't prompt "Are you sure" when leaving an unedited interation * Misc: Don't prompt "Are you sure" when leaving an unedited integration
* Misc: Log node time on startup * Misc: Log node time on startup
* Misc: Improve community MQTT error bubble-up * Misc: Improve community MQTT error bubble-up
* Misc: Unread DMs always have a red unread counter * Misc: Unread DMs always have a red unread counter
@@ -172,7 +172,7 @@
## [3.3.0] - 2026-03-13 ## [3.3.0] - 2026-03-13
* Feature: Use dashed lines to show collapsed ambiguous router results * Feature: Use dashed lines to show collapsed ambiguous router results
* Feature: Jump to unred * Feature: Jump to unread
* Feature: Local channel management to prevent need to reload channel every time * Feature: Local channel management to prevent need to reload channel every time
* Feature: Debug endpoint * Feature: Debug endpoint
* Feature: Force-singleton channel management * Feature: Force-singleton channel management
@@ -235,7 +235,7 @@
* Feature: Massive codebase refactor and overhaul * Feature: Massive codebase refactor and overhaul
* Bugfix: Fix packet parsing for trace packets * Bugfix: Fix packet parsing for trace packets
* Bugfix: Refetch channels on reconnect * Bugfix: Refetch channels on reconnect
* Bugfix: Load All on repeater pane on mobile doesn't etend into lower text * Bugfix: Load All on repeater pane on mobile doesn't extend into lower text
* Bugfix: Timestamps in logs * Bugfix: Timestamps in logs
* Bugfix: Correct wrong clock sync command * Bugfix: Correct wrong clock sync command
* Misc: Improve bot error bubble up * Misc: Improve bot error bubble up
@@ -252,10 +252,6 @@
* Bugfix: Don't obscure new integration dropdown on session boundary * Bugfix: Don't obscure new integration dropdown on session boundary
## [2.7.8] - 2026-03-08
## [2.7.8] - 2026-03-08 ## [2.7.8] - 2026-03-08
* Bugfix: Improve frontend asset resolution and fixup the build/push script * Bugfix: Improve frontend asset resolution and fixup the build/push script
+3 -1
View File
@@ -23,7 +23,7 @@ For advanced setup and troubleshooting see [README_ADVANCED.md](README_ADVANCED.
## Requirements ## Requirements
- Python 3.10+ - Python 3.11+
- Node.js LTS or current (20, 22, 24, 25) if you're not using a prebuilt release - Node.js LTS or current (20, 22, 24, 25) if you're not using a prebuilt release
- [UV](https://astral.sh/uv) package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh` - [UV](https://astral.sh/uv) package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh`
- MeshCore radio connected via USB serial, TCP, or BLE - MeshCore radio connected via USB serial, TCP, or BLE
@@ -135,6 +135,8 @@ sudo docker compose pull
sudo docker compose up -d sudo docker compose up -d
``` ```
> If you switched to a local build (`build: .` instead of `image:`), use `sudo docker compose up -d --build` instead — `pull` only fetches remote images.
The example file and setup script default to the published Docker Hub image. To build locally from your checkout instead, replace: The example file and setup script default to the published Docker Hub image. To build locally from your checkout instead, replace:
```yaml ```yaml
+2 -2
View File
@@ -2,10 +2,10 @@
import json import json
import logging import logging
from typing import Any, Literal from typing import Any, Literal, NotRequired
from pydantic import TypeAdapter from pydantic import TypeAdapter
from typing_extensions import NotRequired, TypedDict from typing_extensions import TypedDict
from app.models import Channel, Contact, Message, MessagePath, RawPacketBroadcast from app.models import Channel, Contact, Message, MessagePath, RawPacketBroadcast
from app.routers.health import HealthResponse from app.routers.health import HealthResponse
+1 -1
View File
@@ -164,7 +164,7 @@ class BotModule(FanoutModule):
), ),
timeout=BOT_EXECUTION_TIMEOUT, timeout=BOT_EXECUTION_TIMEOUT,
) )
except asyncio.TimeoutError: except TimeoutError:
logger.warning("Bot '%s' execution timed out", self.name) logger.warning("Bot '%s' execution timed out", self.name)
return return
except Exception: except Exception:
+1 -1
View File
@@ -538,7 +538,7 @@ class CommunityMqttPublisher(BaseMqttPublisher):
self._version_event.clear() self._version_event.clear()
try: try:
await asyncio.wait_for(self._version_event.wait(), timeout=30) await asyncio.wait_for(self._version_event.wait(), timeout=30)
except asyncio.TimeoutError: except TimeoutError:
pass pass
return False return False
return True return True
+1 -1
View File
@@ -225,7 +225,7 @@ class FanoutManager:
handler = getattr(module, handler_name) handler = getattr(module, handler_name)
await asyncio.wait_for(handler(data), timeout=_DISPATCH_TIMEOUT_SECONDS) await asyncio.wait_for(handler(data), timeout=_DISPATCH_TIMEOUT_SECONDS)
self._clear_module_error(config_id) self._clear_module_error(config_id)
except asyncio.TimeoutError: except TimeoutError:
timeout_error = f"{handler_name} timed out after {_DISPATCH_TIMEOUT_SECONDS:.1f}s" timeout_error = f"{handler_name} timed out after {_DISPATCH_TIMEOUT_SECONDS:.1f}s"
self._set_module_error(config_id, timeout_error) self._set_module_error(config_id, timeout_error)
logger.error( logger.error(
+2 -2
View File
@@ -196,7 +196,7 @@ class BaseMqttPublisher(ABC):
self._version_event.wait(), self._version_event.wait(),
timeout=self._not_configured_timeout, timeout=self._not_configured_timeout,
) )
except asyncio.TimeoutError: except TimeoutError:
continue continue
except asyncio.CancelledError: except asyncio.CancelledError:
return return
@@ -231,7 +231,7 @@ class BaseMqttPublisher(ABC):
self._version_event.clear() self._version_event.clear()
try: try:
await asyncio.wait_for(self._version_event.wait(), timeout=60) await asyncio.wait_for(self._version_event.wait(), timeout=60)
except asyncio.TimeoutError: except TimeoutError:
elapsed = time.monotonic() - connect_time elapsed = time.monotonic() - connect_time
await self._on_periodic_wake(elapsed) await self._on_periodic_wake(elapsed)
if self._should_break_wait(elapsed): if self._should_break_wait(elapsed):
+1 -1
View File
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
NO_EVENT_RECEIVED_GUIDANCE = ( NO_EVENT_RECEIVED_GUIDANCE = (
"Radio command channel is unresponsive (no_event_received). Ensure that your firmware is not " "Radio command channel is unresponsive (no_event_received). Ensure that your firmware is not "
"incompatible, outdated, or wrong-mode (e.g. repeater, not client), and that" "incompatible, outdated, or wrong-mode (e.g. repeater, not client), and that "
"serial/TCP/BLE connectivity is successful (try another app and see if that one works?). The app cannot proceed because it cannot " "serial/TCP/BLE connectivity is successful (try another app and see if that one works?). The app cannot proceed because it cannot "
"issue commands to the radio." "issue commands to the radio."
) )
+1 -1
View File
@@ -118,7 +118,7 @@ async def test_serial_device(port: str, baudrate: int, timeout: float = 3.0) ->
return True return True
return False return False
except asyncio.TimeoutError: except TimeoutError:
logger.debug("Device %s timed out", port) logger.debug("Device %s timed out", port)
return False return False
except Exception as e: except Exception as e:
+2 -2
View File
@@ -480,7 +480,7 @@ async def drain_pending_messages(mc: MeshCore) -> int:
# Small delay between fetches # Small delay between fetches
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
except asyncio.TimeoutError: except TimeoutError:
break break
except Exception as e: except Exception as e:
logger.warning("Error draining messages: %s", e, exc_info=True) logger.warning("Error draining messages: %s", e, exc_info=True)
@@ -518,7 +518,7 @@ async def poll_for_messages(mc: MeshCore) -> int:
# If we got a message, there might be more - drain them # If we got a message, there might be more - drain them
count += await drain_pending_messages(mc) count += await drain_pending_messages(mc)
except asyncio.TimeoutError: except TimeoutError:
pass pass
except Exception as e: except Exception as e:
logger.warning("Message poll exception: %s", e, exc_info=True) logger.warning("Message poll exception: %s", e, exc_info=True)
+2 -2
View File
@@ -4,7 +4,7 @@ import os
import platform import platform
import struct import struct
import sys import sys
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Any, Literal from typing import Any, Literal
from fastapi import APIRouter from fastapi import APIRouter
@@ -390,7 +390,7 @@ async def debug_support_snapshot() -> DebugSnapshotResponse:
is_reconnecting=is_reconnecting, is_reconnecting=is_reconnecting,
) )
return DebugSnapshotResponse( return DebugSnapshotResponse(
captured_at=datetime.now(timezone.utc).isoformat(), captured_at=datetime.now(UTC).isoformat(),
system=_build_system_info(), system=_build_system_info(),
application=_build_application_info(), application=_build_application_info(),
health=_build_debug_health_summary(health_data, radio_state=radio_state), health=_build_debug_health_summary(health_data, radio_state=radio_state),
+2 -2
View File
@@ -473,7 +473,7 @@ async def discover_mesh(request: RadioDiscoveryRequest) -> RadioDiscoveryRespons
break break
try: try:
event = await asyncio.wait_for(events.get(), timeout=remaining) event = await asyncio.wait_for(events.get(), timeout=remaining)
except asyncio.TimeoutError: except TimeoutError:
break break
merged = _merge_discovery_result( merged = _merge_discovery_result(
@@ -536,7 +536,7 @@ async def trace_path(request: RadioTraceRequest) -> RadioTraceResponse:
timeout_seconds = _trace_timeout_seconds(send_result) timeout_seconds = _trace_timeout_seconds(send_result)
try: try:
event = await asyncio.wait_for(response_task, timeout=timeout_seconds) event = await asyncio.wait_for(response_task, timeout=timeout_seconds)
except asyncio.TimeoutError as exc: except TimeoutError as exc:
raise HTTPException(status_code=504, detail="No trace response heard") from exc raise HTTPException(status_code=504, detail="No trace response heard") from exc
finally: finally:
if not response_task.done(): if not response_task.done():
+2 -2
View File
@@ -94,7 +94,7 @@ async def fetch_contact_cli_response(
while _monotonic() < deadline: while _monotonic() < deadline:
try: try:
result = await mc.commands.get_msg(timeout=2.0) result = await mc.commands.get_msg(timeout=2.0)
except asyncio.TimeoutError: except TimeoutError:
continue continue
except Exception as exc: except Exception as exc:
logger.debug("get_msg() exception: %s", exc) logger.debug("get_msg() exception: %s", exc)
@@ -196,7 +196,7 @@ async def prepare_authenticated_contact_connection(
login_future, login_future,
timeout=response_timeout, timeout=response_timeout,
) )
except asyncio.TimeoutError: except TimeoutError:
logger.warning( logger.warning(
"No login response from %s %s within %.1fs", "No login response from %s %s within %.1fs",
contact_label, contact_label,
+3 -3
View File
@@ -1,6 +1,6 @@
import asyncio import asyncio
import logging import logging
from datetime import datetime, timezone from datetime import UTC, datetime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -193,7 +193,7 @@ async def run_post_connect_setup(radio_manager) -> None:
logger.info( logger.info(
"Radio clock at connect: epoch=%d utc=%s", "Radio clock at connect: epoch=%d utc=%s",
radio_time, radio_time,
datetime.fromtimestamp(radio_time, timezone.utc).strftime( datetime.fromtimestamp(radio_time, UTC).strftime(
"%Y-%m-%d %H:%M:%S UTC" "%Y-%m-%d %H:%M:%S UTC"
), ),
) )
@@ -274,7 +274,7 @@ async def prepare_connected_radio(radio_manager, *, broadcast_on_success: bool =
try: try:
await radio_manager.post_connect_setup() await radio_manager.post_connect_setup()
break break
except asyncio.TimeoutError as exc: except TimeoutError as exc:
if attempt < POST_CONNECT_SETUP_MAX_ATTEMPTS: if attempt < POST_CONNECT_SETUP_MAX_ATTEMPTS:
logger.warning( logger.warning(
"Post-connect setup timed out after %ds on attempt %d/%d; retrying once", "Post-connect setup timed out after %ds on attempt %d/%d; retrying once",
+1 -2
View File
@@ -13,13 +13,12 @@ import importlib.metadata
import json import json
import os import os
import subprocess import subprocess
import tomllib
from dataclasses import dataclass from dataclasses import dataclass
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import tomllib
RELEASE_BUILD_INFO_FILENAME = "build_info.json" RELEASE_BUILD_INFO_FILENAME = "build_info.json"
PROJECT_NAME = "remoteterm-meshcore" PROJECT_NAME = "remoteterm-meshcore"
+1 -1
View File
@@ -59,7 +59,7 @@ class WebSocketManager:
try: try:
# Timeout prevents blocking on slow/unresponsive clients # Timeout prevents blocking on slow/unresponsive clients
await asyncio.wait_for(connection.send_text(message), timeout=SEND_TIMEOUT_SECONDS) await asyncio.wait_for(connection.send_text(message), timeout=SEND_TIMEOUT_SECONDS)
except asyncio.TimeoutError: except TimeoutError:
logger.debug("Timeout sending to WebSocket client, marking disconnected") logger.debug("Timeout sending to WebSocket client, marking disconnected")
disconnected.append(connection) disconnected.append(connection)
except Exception as e: except Exception as e:
+4 -2
View File
@@ -9,7 +9,8 @@ import type {
RadioTraceResponse, RadioTraceResponse,
} from '../types'; } from '../types';
import { CONTACT_TYPE_REPEATER } from '../types'; import { CONTACT_TYPE_REPEATER } from '../types';
import { calculateDistance, isValidLocation } from '../utils/pathUtils'; import { calculateDistance, formatDistance, isValidLocation } from '../utils/pathUtils';
import { useDistanceUnit } from '../contexts/DistanceUnitContext';
import { getContactDisplayName } from '../utils/pubkey'; import { getContactDisplayName } from '../utils/pubkey';
import { handleKeyboardActivate } from '../utils/a11y'; import { handleKeyboardActivate } from '../utils/a11y';
import { ContactAvatar } from './ContactAvatar'; import { ContactAvatar } from './ContactAvatar';
@@ -186,6 +187,7 @@ function TraceNodeRow({
} }
export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps) { export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps) {
const distanceUnit = useDistanceUnit();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [sortMode, setSortMode] = useState<TraceSortMode>('alpha'); const [sortMode, setSortMode] = useState<TraceSortMode>('alpha');
const [draftHops, setDraftHops] = useState<TraceDraftHop[]>([]); const [draftHops, setDraftHops] = useState<TraceDraftHop[]>([]);
@@ -536,7 +538,7 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
</div> </div>
{sortMode === 'distance' && distanceKm !== null ? ( {sortMode === 'distance' && distanceKm !== null ? (
<div className="mt-1 text-[0.6875rem] text-muted-foreground"> <div className="mt-1 text-[0.6875rem] text-muted-foreground">
{distanceKm.toFixed(1)} km away {formatDistance(distanceKm, distanceUnit)} away
</div> </div>
) : null} ) : null}
{selectedCount > 0 ? ( {selectedCount > 0 ? (
+3
View File
@@ -13,6 +13,9 @@
#MESHCORE_TCP_PORT=5000 #MESHCORE_TCP_PORT=5000
# BLE (also requires the optional `bluez` package) # BLE (also requires the optional `bluez` package)
# NOTE: The systemd service sets ProtectHome=yes, which may block the D-Bus
# session bus at /run/user/. If BLE fails to connect, try overriding with
# ProtectHome=no in a systemd drop-in.
#MESHCORE_BLE_ADDRESS=AA:BB:CC:DD:EE:FF #MESHCORE_BLE_ADDRESS=AA:BB:CC:DD:EE:FF
#MESHCORE_BLE_PIN=123456 #MESHCORE_BLE_PIN=123456
+1 -1
View File
@@ -32,7 +32,7 @@ testpaths = ["tests"]
addopts = "-n auto --dist worksteal" addopts = "-n auto --dist worksteal"
[tool.ruff] [tool.ruff]
target-version = "py310" target-version = "py311"
line-length = 100 line-length = 100
[tool.ruff.lint] [tool.ruff.lint]
+2 -4
View File
@@ -1073,9 +1073,7 @@ class TestPostConnectSetupOrdering:
rm = RadioManager() rm = RadioManager()
rm._connection_info = "Serial: /dev/ttyUSB0" rm._connection_info = "Serial: /dev/ttyUSB0"
rm.post_connect_setup = AsyncMock( rm.post_connect_setup = AsyncMock(side_effect=[TimeoutError(), TimeoutError()])
side_effect=[asyncio.TimeoutError(), asyncio.TimeoutError()]
)
with ( with (
patch("app.websocket.broadcast_error") as mock_broadcast_error, patch("app.websocket.broadcast_error") as mock_broadcast_error,
@@ -1099,7 +1097,7 @@ class TestPostConnectSetupOrdering:
rm = RadioManager() rm = RadioManager()
rm._connection_info = "Serial: /dev/ttyUSB0" rm._connection_info = "Serial: /dev/ttyUSB0"
rm.post_connect_setup = AsyncMock(side_effect=[asyncio.TimeoutError(), None]) rm.post_connect_setup = AsyncMock(side_effect=[TimeoutError(), None])
with ( with (
patch("app.websocket.broadcast_error") as mock_broadcast_error, patch("app.websocket.broadcast_error") as mock_broadcast_error,