From 442c2fad205e548bbc5a049ebe63b8a6faa3ab99 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Fri, 10 Apr 2026 15:43:08 -0700 Subject: [PATCH] Fix some frontend display/quality/doc issues --- CHANGELOG.md | 12 ++++-------- README.md | 4 +++- app/events.py | 4 ++-- app/fanout/bot.py | 2 +- app/fanout/community_mqtt.py | 2 +- app/fanout/manager.py | 2 +- app/fanout/mqtt_base.py | 4 ++-- app/keystore.py | 2 +- app/radio.py | 2 +- app/radio_sync.py | 4 ++-- app/routers/debug.py | 4 ++-- app/routers/radio.py | 4 ++-- app/routers/server_control.py | 4 ++-- app/services/radio_lifecycle.py | 6 +++--- app/version_info.py | 3 +-- app/websocket.py | 2 +- frontend/src/components/TracePane.tsx | 6 ++++-- pkg/aur/remoteterm.env | 3 +++ pyproject.toml | 2 +- tests/test_radio.py | 6 ++---- 20 files changed, 39 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3620a1a..ce4fcc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ * Feature: Add Arch AUR package * Feature: 72hr packet density view in statistics * 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: Fix statistics pane e2e test @@ -145,7 +145,7 @@ * Bugfix: Fix Apprise duplicate names * Bugfix: Be better about identity resolution in the stats pane * 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: Improve community MQTT error bubble-up * Misc: Unread DMs always have a red unread counter @@ -172,7 +172,7 @@ ## [3.3.0] - 2026-03-13 * 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: Debug endpoint * Feature: Force-singleton channel management @@ -235,7 +235,7 @@ * Feature: Massive codebase refactor and overhaul * Bugfix: Fix packet parsing for trace packets * 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: Correct wrong clock sync command * Misc: Improve bot error bubble up @@ -252,10 +252,6 @@ * Bugfix: Don't obscure new integration dropdown on session boundary -## [2.7.8] - 2026-03-08 - - - ## [2.7.8] - 2026-03-08 * Bugfix: Improve frontend asset resolution and fixup the build/push script diff --git a/README.md b/README.md index 83ab993..2b1a3ed 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ For advanced setup and troubleshooting see [README_ADVANCED.md](README_ADVANCED. ## Requirements -- Python 3.10+ +- Python 3.11+ - 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` - MeshCore radio connected via USB serial, TCP, or BLE @@ -135,6 +135,8 @@ sudo docker compose pull 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: ```yaml diff --git a/app/events.py b/app/events.py index 0f9332a..8739c89 100644 --- a/app/events.py +++ b/app/events.py @@ -2,10 +2,10 @@ import json import logging -from typing import Any, Literal +from typing import Any, Literal, NotRequired 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.routers.health import HealthResponse diff --git a/app/fanout/bot.py b/app/fanout/bot.py index 202fe06..727453f 100644 --- a/app/fanout/bot.py +++ b/app/fanout/bot.py @@ -164,7 +164,7 @@ class BotModule(FanoutModule): ), timeout=BOT_EXECUTION_TIMEOUT, ) - except asyncio.TimeoutError: + except TimeoutError: logger.warning("Bot '%s' execution timed out", self.name) return except Exception: diff --git a/app/fanout/community_mqtt.py b/app/fanout/community_mqtt.py index cd9eef1..8aa0d1b 100644 --- a/app/fanout/community_mqtt.py +++ b/app/fanout/community_mqtt.py @@ -538,7 +538,7 @@ class CommunityMqttPublisher(BaseMqttPublisher): self._version_event.clear() try: await asyncio.wait_for(self._version_event.wait(), timeout=30) - except asyncio.TimeoutError: + except TimeoutError: pass return False return True diff --git a/app/fanout/manager.py b/app/fanout/manager.py index cd8d1b5..aff5ee0 100644 --- a/app/fanout/manager.py +++ b/app/fanout/manager.py @@ -225,7 +225,7 @@ class FanoutManager: handler = getattr(module, handler_name) await asyncio.wait_for(handler(data), timeout=_DISPATCH_TIMEOUT_SECONDS) self._clear_module_error(config_id) - except asyncio.TimeoutError: + except TimeoutError: timeout_error = f"{handler_name} timed out after {_DISPATCH_TIMEOUT_SECONDS:.1f}s" self._set_module_error(config_id, timeout_error) logger.error( diff --git a/app/fanout/mqtt_base.py b/app/fanout/mqtt_base.py index 656bbf8..4c7fc99 100644 --- a/app/fanout/mqtt_base.py +++ b/app/fanout/mqtt_base.py @@ -196,7 +196,7 @@ class BaseMqttPublisher(ABC): self._version_event.wait(), timeout=self._not_configured_timeout, ) - except asyncio.TimeoutError: + except TimeoutError: continue except asyncio.CancelledError: return @@ -231,7 +231,7 @@ class BaseMqttPublisher(ABC): self._version_event.clear() try: await asyncio.wait_for(self._version_event.wait(), timeout=60) - except asyncio.TimeoutError: + except TimeoutError: elapsed = time.monotonic() - connect_time await self._on_periodic_wake(elapsed) if self._should_break_wait(elapsed): diff --git a/app/keystore.py b/app/keystore.py index 28031e9..26f3240 100644 --- a/app/keystore.py +++ b/app/keystore.py @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) NO_EVENT_RECEIVED_GUIDANCE = ( "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 " "issue commands to the radio." ) diff --git a/app/radio.py b/app/radio.py index 6f12d1f..ac58834 100644 --- a/app/radio.py +++ b/app/radio.py @@ -118,7 +118,7 @@ async def test_serial_device(port: str, baudrate: int, timeout: float = 3.0) -> return True return False - except asyncio.TimeoutError: + except TimeoutError: logger.debug("Device %s timed out", port) return False except Exception as e: diff --git a/app/radio_sync.py b/app/radio_sync.py index 822a046..f65e789 100644 --- a/app/radio_sync.py +++ b/app/radio_sync.py @@ -480,7 +480,7 @@ async def drain_pending_messages(mc: MeshCore) -> int: # Small delay between fetches await asyncio.sleep(0.1) - except asyncio.TimeoutError: + except TimeoutError: break except Exception as e: 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 count += await drain_pending_messages(mc) - except asyncio.TimeoutError: + except TimeoutError: pass except Exception as e: logger.warning("Message poll exception: %s", e, exc_info=True) diff --git a/app/routers/debug.py b/app/routers/debug.py index e309f92..ac67b67 100644 --- a/app/routers/debug.py +++ b/app/routers/debug.py @@ -4,7 +4,7 @@ import os import platform import struct import sys -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, Literal from fastapi import APIRouter @@ -390,7 +390,7 @@ async def debug_support_snapshot() -> DebugSnapshotResponse: is_reconnecting=is_reconnecting, ) return DebugSnapshotResponse( - captured_at=datetime.now(timezone.utc).isoformat(), + captured_at=datetime.now(UTC).isoformat(), system=_build_system_info(), application=_build_application_info(), health=_build_debug_health_summary(health_data, radio_state=radio_state), diff --git a/app/routers/radio.py b/app/routers/radio.py index 0bd4ce5..f468da0 100644 --- a/app/routers/radio.py +++ b/app/routers/radio.py @@ -473,7 +473,7 @@ async def discover_mesh(request: RadioDiscoveryRequest) -> RadioDiscoveryRespons break try: event = await asyncio.wait_for(events.get(), timeout=remaining) - except asyncio.TimeoutError: + except TimeoutError: break merged = _merge_discovery_result( @@ -536,7 +536,7 @@ async def trace_path(request: RadioTraceRequest) -> RadioTraceResponse: timeout_seconds = _trace_timeout_seconds(send_result) try: 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 finally: if not response_task.done(): diff --git a/app/routers/server_control.py b/app/routers/server_control.py index c348638..ca6c502 100644 --- a/app/routers/server_control.py +++ b/app/routers/server_control.py @@ -94,7 +94,7 @@ async def fetch_contact_cli_response( while _monotonic() < deadline: try: result = await mc.commands.get_msg(timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: continue except Exception as exc: logger.debug("get_msg() exception: %s", exc) @@ -196,7 +196,7 @@ async def prepare_authenticated_contact_connection( login_future, timeout=response_timeout, ) - except asyncio.TimeoutError: + except TimeoutError: logger.warning( "No login response from %s %s within %.1fs", contact_label, diff --git a/app/services/radio_lifecycle.py b/app/services/radio_lifecycle.py index 212a545..afcaa90 100644 --- a/app/services/radio_lifecycle.py +++ b/app/services/radio_lifecycle.py @@ -1,6 +1,6 @@ import asyncio import logging -from datetime import datetime, timezone +from datetime import UTC, datetime logger = logging.getLogger(__name__) @@ -193,7 +193,7 @@ async def run_post_connect_setup(radio_manager) -> None: logger.info( "Radio clock at connect: epoch=%d utc=%s", radio_time, - datetime.fromtimestamp(radio_time, timezone.utc).strftime( + datetime.fromtimestamp(radio_time, UTC).strftime( "%Y-%m-%d %H:%M:%S UTC" ), ) @@ -274,7 +274,7 @@ async def prepare_connected_radio(radio_manager, *, broadcast_on_success: bool = try: await radio_manager.post_connect_setup() break - except asyncio.TimeoutError as exc: + except TimeoutError as exc: if attempt < POST_CONNECT_SETUP_MAX_ATTEMPTS: logger.warning( "Post-connect setup timed out after %ds on attempt %d/%d; retrying once", diff --git a/app/version_info.py b/app/version_info.py index 91c5ece..798274d 100644 --- a/app/version_info.py +++ b/app/version_info.py @@ -13,13 +13,12 @@ import importlib.metadata import json import os import subprocess +import tomllib from dataclasses import dataclass from functools import lru_cache from pathlib import Path from typing import Any -import tomllib - RELEASE_BUILD_INFO_FILENAME = "build_info.json" PROJECT_NAME = "remoteterm-meshcore" diff --git a/app/websocket.py b/app/websocket.py index 399678d..ba08694 100644 --- a/app/websocket.py +++ b/app/websocket.py @@ -59,7 +59,7 @@ class WebSocketManager: try: # Timeout prevents blocking on slow/unresponsive clients 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") disconnected.append(connection) except Exception as e: diff --git a/frontend/src/components/TracePane.tsx b/frontend/src/components/TracePane.tsx index 06ef8b5..08589db 100644 --- a/frontend/src/components/TracePane.tsx +++ b/frontend/src/components/TracePane.tsx @@ -9,7 +9,8 @@ import type { RadioTraceResponse, } 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 { handleKeyboardActivate } from '../utils/a11y'; import { ContactAvatar } from './ContactAvatar'; @@ -186,6 +187,7 @@ function TraceNodeRow({ } export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps) { + const distanceUnit = useDistanceUnit(); const [searchQuery, setSearchQuery] = useState(''); const [sortMode, setSortMode] = useState('alpha'); const [draftHops, setDraftHops] = useState([]); @@ -536,7 +538,7 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps) {sortMode === 'distance' && distanceKm !== null ? (
- {distanceKm.toFixed(1)} km away + {formatDistance(distanceKm, distanceUnit)} away
) : null} {selectedCount > 0 ? ( diff --git a/pkg/aur/remoteterm.env b/pkg/aur/remoteterm.env index ecc80d2..f4c4601 100644 --- a/pkg/aur/remoteterm.env +++ b/pkg/aur/remoteterm.env @@ -13,6 +13,9 @@ #MESHCORE_TCP_PORT=5000 # 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_PIN=123456 diff --git a/pyproject.toml b/pyproject.toml index 5bab5f5..4d6b032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ testpaths = ["tests"] addopts = "-n auto --dist worksteal" [tool.ruff] -target-version = "py310" +target-version = "py311" line-length = 100 [tool.ruff.lint] diff --git a/tests/test_radio.py b/tests/test_radio.py index 89a7e97..ef8a9df 100644 --- a/tests/test_radio.py +++ b/tests/test_radio.py @@ -1073,9 +1073,7 @@ class TestPostConnectSetupOrdering: rm = RadioManager() rm._connection_info = "Serial: /dev/ttyUSB0" - rm.post_connect_setup = AsyncMock( - side_effect=[asyncio.TimeoutError(), asyncio.TimeoutError()] - ) + rm.post_connect_setup = AsyncMock(side_effect=[TimeoutError(), TimeoutError()]) with ( patch("app.websocket.broadcast_error") as mock_broadcast_error, @@ -1099,7 +1097,7 @@ class TestPostConnectSetupOrdering: rm = RadioManager() 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 ( patch("app.websocket.broadcast_error") as mock_broadcast_error,