diff --git a/app/radio.py b/app/radio.py index bd9ab028..70710e0e 100644 --- a/app/radio.py +++ b/app/radio.py @@ -131,6 +131,11 @@ class RadioManager: self._setup_lock: asyncio.Lock | None = None self._setup_in_progress: bool = False self._setup_complete: bool = False + self.device_info_loaded: bool = False + self.max_contacts: int | None = None + self.device_model: str | None = None + self.firmware_build: str | None = None + self.firmware_version: str | None = None self.max_channels: int = 40 self.path_hash_mode: int = 0 self.path_hash_mode_supported: bool = False @@ -488,6 +493,11 @@ class RadioManager: await self._disable_meshcore_auto_reconnect(mc) self._meshcore = None self._setup_complete = False + self.device_info_loaded = False + self.max_contacts = None + self.device_model = None + self.firmware_build = None + self.firmware_version = None self.max_channels = 40 self.path_hash_mode = 0 self.path_hash_mode_supported = False diff --git a/app/routers/health.py b/app/routers/health.py index 0fa6fc05..226202a1 100644 --- a/app/routers/health.py +++ b/app/routers/health.py @@ -11,18 +11,34 @@ from app.services.radio_runtime import radio_runtime as radio_manager router = APIRouter(tags=["health"]) +class RadioDeviceInfoResponse(BaseModel): + model: str | None = None + firmware_build: str | None = None + firmware_version: str | None = None + max_contacts: int | None = None + max_channels: int | None = None + + class HealthResponse(BaseModel): status: str radio_connected: bool radio_initializing: bool = False radio_state: str = "disconnected" connection_info: str | None + radio_device_info: RadioDeviceInfoResponse | None = None database_size_mb: float oldest_undecrypted_timestamp: int | None fanout_statuses: dict[str, dict[str, str]] = {} bots_disabled: bool = False +def _clean_optional_str(value: object) -> str | None: + if not isinstance(value, str): + return None + cleaned = value.strip() + return cleaned or None + + async def build_health_data(radio_connected: bool, connection_info: str | None) -> dict: """Build the health status payload used by REST endpoint and WebSocket broadcasts.""" db_size_mb = 0.0 @@ -48,22 +64,12 @@ async def build_health_data(radio_connected: bool, connection_info: str | None) pass setup_in_progress = getattr(radio_manager, "is_setup_in_progress", False) - if not isinstance(setup_in_progress, bool): - setup_in_progress = False - setup_complete = getattr(radio_manager, "is_setup_complete", radio_connected) - if not isinstance(setup_complete, bool): - setup_complete = radio_connected if not radio_connected: setup_complete = False connection_desired = getattr(radio_manager, "connection_desired", True) - if not isinstance(connection_desired, bool): - connection_desired = True - is_reconnecting = getattr(radio_manager, "is_reconnecting", False) - if not isinstance(is_reconnecting, bool): - is_reconnecting = False radio_initializing = bool(radio_connected and (setup_in_progress or not setup_complete)) if not connection_desired: @@ -77,12 +83,26 @@ async def build_health_data(radio_connected: bool, connection_info: str | None) else: radio_state = "disconnected" + radio_device_info = None + device_info_loaded = getattr(radio_manager, "device_info_loaded", False) + if radio_connected and device_info_loaded: + radio_device_info = { + "model": _clean_optional_str(getattr(radio_manager, "device_model", None)), + "firmware_build": _clean_optional_str(getattr(radio_manager, "firmware_build", None)), + "firmware_version": _clean_optional_str( + getattr(radio_manager, "firmware_version", None) + ), + "max_contacts": getattr(radio_manager, "max_contacts", None), + "max_channels": getattr(radio_manager, "max_channels", None), + } + return { "status": "ok" if radio_connected and not radio_initializing else "degraded", "radio_connected": radio_connected, "radio_initializing": radio_initializing, "radio_state": radio_state, "connection_info": connection_info, + "radio_device_info": radio_device_info, "database_size_mb": db_size_mb, "oldest_undecrypted_timestamp": oldest_ts, "fanout_statuses": fanout_statuses, diff --git a/app/services/radio_lifecycle.py b/app/services/radio_lifecycle.py index c601a9ac..f5b630f0 100644 --- a/app/services/radio_lifecycle.py +++ b/app/services/radio_lifecycle.py @@ -7,6 +7,21 @@ POST_CONNECT_SETUP_TIMEOUT_SECONDS = 300 POST_CONNECT_SETUP_MAX_ATTEMPTS = 2 +def _clean_device_string(value: object) -> str | None: + if not isinstance(value, str): + return None + cleaned = value.strip() + return cleaned or None + + +def _decode_fixed_string(raw: bytes, start: int, length: int) -> str | None: + if len(raw) < start: + return None + return _clean_device_string( + raw[start : start + length].decode("utf-8", "ignore").replace("\0", "") + ) + + async def run_post_connect_setup(radio_manager) -> None: """Run shared radio initialization after a transport connection succeeds.""" from app.event_handlers import register_event_handlers @@ -78,26 +93,66 @@ async def run_post_connect_setup(radio_manager) -> None: return await _original_handle_rx(data) reader.handle_rx = _capture_handle_rx + radio_manager.device_info_loaded = False + radio_manager.max_contacts = None + radio_manager.device_model = None + radio_manager.firmware_build = None + radio_manager.firmware_version = None radio_manager.max_channels = 40 radio_manager.path_hash_mode = 0 radio_manager.path_hash_mode_supported = False try: device_query = await mc.commands.send_device_query() - if device_query and "max_channels" in device_query.payload: - radio_manager.max_channels = max( - 1, int(device_query.payload["max_channels"]) - ) - if device_query and "path_hash_mode" in device_query.payload: - radio_manager.path_hash_mode = device_query.payload["path_hash_mode"] + payload = ( + device_query.payload + if device_query is not None and isinstance(device_query.payload, dict) + else {} + ) + + payload_max_contacts = payload.get("max_contacts") + if isinstance(payload_max_contacts, int): + radio_manager.max_contacts = max(1, payload_max_contacts) + + payload_max_channels = payload.get("max_channels") + if isinstance(payload_max_channels, int): + radio_manager.max_channels = max(1, payload_max_channels) + + radio_manager.device_model = _clean_device_string(payload.get("model")) + radio_manager.firmware_build = _clean_device_string(payload.get("fw_build")) + radio_manager.firmware_version = _clean_device_string(payload.get("ver")) + + fw_ver = payload.get("fw ver") + payload_reports_device_info = isinstance(fw_ver, int) and fw_ver >= 3 + if payload_reports_device_info: + radio_manager.device_info_loaded = True + + if "path_hash_mode" in payload and isinstance(payload["path_hash_mode"], int): + radio_manager.path_hash_mode = payload["path_hash_mode"] radio_manager.path_hash_mode_supported = True - elif _captured_frame: - # Raw-frame fallback: - # byte 1 = fw_ver, byte 3 = max_channels, byte 81 = path_hash_mode + + if _captured_frame: + # Raw-frame fallback / completion: + # byte 1 = fw_ver, byte 2 = max_contacts/2, byte 3 = max_channels, + # bytes 8:20 = fw_build, 20:60 = model, 60:80 = ver, byte 81 = path_hash_mode raw = _captured_frame[-1] fw_ver = raw[1] if len(raw) > 1 else 0 - if fw_ver >= 3 and len(raw) >= 4: - radio_manager.max_channels = max(1, raw[3]) - if fw_ver >= 10 and len(raw) >= 82: + if fw_ver >= 3: + radio_manager.device_info_loaded = True + if radio_manager.max_contacts is None and len(raw) >= 3: + radio_manager.max_contacts = max(1, raw[2] * 2) + if len(raw) >= 4 and not isinstance(payload_max_channels, int): + radio_manager.max_channels = max(1, raw[3]) + if radio_manager.firmware_build is None: + radio_manager.firmware_build = _decode_fixed_string(raw, 8, 12) + if radio_manager.device_model is None: + radio_manager.device_model = _decode_fixed_string(raw, 20, 40) + if radio_manager.firmware_version is None: + radio_manager.firmware_version = _decode_fixed_string(raw, 60, 20) + if ( + not radio_manager.path_hash_mode_supported + and fw_ver >= 10 + and len(raw) >= 82 + ): radio_manager.path_hash_mode = raw[81] radio_manager.path_hash_mode_supported = True logger.warning( @@ -114,6 +169,17 @@ async def run_post_connect_setup(radio_manager) -> None: logger.info("Path hash mode: %d (supported)", radio_manager.path_hash_mode) else: logger.debug("Firmware does not report path_hash_mode") + if radio_manager.device_info_loaded: + logger.info( + "Radio device info: model=%s build=%s version=%s max_contacts=%s max_channels=%d", + radio_manager.device_model or "unknown", + radio_manager.firmware_build or "unknown", + radio_manager.firmware_version or "unknown", + radio_manager.max_contacts + if radio_manager.max_contacts is not None + else "unknown", + radio_manager.max_channels, + ) logger.info("Max channel slots: %d", radio_manager.max_channels) except Exception as exc: logger.debug("Failed to query device info capabilities: %s", exc) diff --git a/frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js b/frontend/prebuilt/assets/BotCodeEditor-E4psGF8Z.js similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js rename to frontend/prebuilt/assets/BotCodeEditor-E4psGF8Z.js index 817145d7..d953e995 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js and b/frontend/prebuilt/assets/BotCodeEditor-E4psGF8Z.js differ diff --git a/frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js.map b/frontend/prebuilt/assets/BotCodeEditor-E4psGF8Z.js.map similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js.map rename to frontend/prebuilt/assets/BotCodeEditor-E4psGF8Z.js.map index 808e44f1..f7ae4131 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js.map and b/frontend/prebuilt/assets/BotCodeEditor-E4psGF8Z.js.map differ diff --git a/frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js b/frontend/prebuilt/assets/CrackerPanel-BMhE9DO0.js similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js rename to frontend/prebuilt/assets/CrackerPanel-BMhE9DO0.js index 6e3a52ef..1a90c5d9 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js and b/frontend/prebuilt/assets/CrackerPanel-BMhE9DO0.js differ diff --git a/frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js.map b/frontend/prebuilt/assets/CrackerPanel-BMhE9DO0.js.map similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js.map rename to frontend/prebuilt/assets/CrackerPanel-BMhE9DO0.js.map index e8e997bd..83ca2333 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js.map and b/frontend/prebuilt/assets/CrackerPanel-BMhE9DO0.js.map differ diff --git a/frontend/prebuilt/assets/MapView-B_EeV5Ur.js b/frontend/prebuilt/assets/MapView-C3Y0Y3CX.js similarity index 92% rename from frontend/prebuilt/assets/MapView-B_EeV5Ur.js rename to frontend/prebuilt/assets/MapView-C3Y0Y3CX.js index 347043ff..c1b70bf9 100644 Binary files a/frontend/prebuilt/assets/MapView-B_EeV5Ur.js and b/frontend/prebuilt/assets/MapView-C3Y0Y3CX.js differ diff --git a/frontend/prebuilt/assets/MapView-B_EeV5Ur.js.map b/frontend/prebuilt/assets/MapView-C3Y0Y3CX.js.map similarity index 99% rename from frontend/prebuilt/assets/MapView-B_EeV5Ur.js.map rename to frontend/prebuilt/assets/MapView-C3Y0Y3CX.js.map index 71be677d..e90ec524 100644 Binary files a/frontend/prebuilt/assets/MapView-B_EeV5Ur.js.map and b/frontend/prebuilt/assets/MapView-C3Y0Y3CX.js.map differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js b/frontend/prebuilt/assets/NeighborsMiniMap-DJtDAXli.js similarity index 85% rename from frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js rename to frontend/prebuilt/assets/NeighborsMiniMap-DJtDAXli.js index 3cb59969..b81cf34a 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js and b/frontend/prebuilt/assets/NeighborsMiniMap-DJtDAXli.js differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js.map b/frontend/prebuilt/assets/NeighborsMiniMap-DJtDAXli.js.map similarity index 97% rename from frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js.map rename to frontend/prebuilt/assets/NeighborsMiniMap-DJtDAXli.js.map index d1562189..0663dc85 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js.map and b/frontend/prebuilt/assets/NeighborsMiniMap-DJtDAXli.js.map differ diff --git a/frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js b/frontend/prebuilt/assets/PathRouteMap-CJMPJrMX.js similarity index 95% rename from frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js rename to frontend/prebuilt/assets/PathRouteMap-CJMPJrMX.js index 3744db84..01a06822 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js and b/frontend/prebuilt/assets/PathRouteMap-CJMPJrMX.js differ diff --git a/frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js.map b/frontend/prebuilt/assets/PathRouteMap-CJMPJrMX.js.map similarity index 99% rename from frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js.map rename to frontend/prebuilt/assets/PathRouteMap-CJMPJrMX.js.map index 71173ea5..a1c51033 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js.map and b/frontend/prebuilt/assets/PathRouteMap-CJMPJrMX.js.map differ diff --git a/frontend/prebuilt/assets/Popup-BcLg2SsA.js b/frontend/prebuilt/assets/Popup-Sx9rYjUO.js similarity index 81% rename from frontend/prebuilt/assets/Popup-BcLg2SsA.js rename to frontend/prebuilt/assets/Popup-Sx9rYjUO.js index 19c5d90b..ed365fae 100644 Binary files a/frontend/prebuilt/assets/Popup-BcLg2SsA.js and b/frontend/prebuilt/assets/Popup-Sx9rYjUO.js differ diff --git a/frontend/prebuilt/assets/Popup-BcLg2SsA.js.map b/frontend/prebuilt/assets/Popup-Sx9rYjUO.js.map similarity index 97% rename from frontend/prebuilt/assets/Popup-BcLg2SsA.js.map rename to frontend/prebuilt/assets/Popup-Sx9rYjUO.js.map index 0ef254ef..71186547 100644 Binary files a/frontend/prebuilt/assets/Popup-BcLg2SsA.js.map and b/frontend/prebuilt/assets/Popup-Sx9rYjUO.js.map differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js b/frontend/prebuilt/assets/RepeaterDashboard-BVkivKK9.js similarity index 98% rename from frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js rename to frontend/prebuilt/assets/RepeaterDashboard-BVkivKK9.js index 4a2f919d..c1217e40 100644 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js and b/frontend/prebuilt/assets/RepeaterDashboard-BVkivKK9.js differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js.map b/frontend/prebuilt/assets/RepeaterDashboard-BVkivKK9.js.map similarity index 99% rename from frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js.map rename to frontend/prebuilt/assets/RepeaterDashboard-BVkivKK9.js.map index eaafc690..e389fcc8 100644 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js.map and b/frontend/prebuilt/assets/RepeaterDashboard-BVkivKK9.js.map differ diff --git a/frontend/prebuilt/assets/SearchView-RhP_4yQI.js b/frontend/prebuilt/assets/SearchView-B9Wzo6fY.js similarity index 97% rename from frontend/prebuilt/assets/SearchView-RhP_4yQI.js rename to frontend/prebuilt/assets/SearchView-B9Wzo6fY.js index 745500be..e03fffae 100644 Binary files a/frontend/prebuilt/assets/SearchView-RhP_4yQI.js and b/frontend/prebuilt/assets/SearchView-B9Wzo6fY.js differ diff --git a/frontend/prebuilt/assets/SearchView-RhP_4yQI.js.map b/frontend/prebuilt/assets/SearchView-B9Wzo6fY.js.map similarity index 99% rename from frontend/prebuilt/assets/SearchView-RhP_4yQI.js.map rename to frontend/prebuilt/assets/SearchView-B9Wzo6fY.js.map index 92affefe..a0dffb9c 100644 Binary files a/frontend/prebuilt/assets/SearchView-RhP_4yQI.js.map and b/frontend/prebuilt/assets/SearchView-B9Wzo6fY.js.map differ diff --git a/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js b/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js deleted file mode 100644 index 6869fb78..00000000 Binary files a/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js and /dev/null differ diff --git a/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js.map b/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js.map deleted file mode 100644 index 07281cb0..00000000 Binary files a/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js.map and /dev/null differ diff --git a/frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js b/frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js new file mode 100644 index 00000000..9b1f5454 Binary files /dev/null and b/frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js differ diff --git a/frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js.map b/frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js.map new file mode 100644 index 00000000..c57b875d Binary files /dev/null and b/frontend/prebuilt/assets/SettingsModal-BaQM9_V7.js.map differ diff --git a/frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js b/frontend/prebuilt/assets/VisualizerView-BwKcK8sl.js similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js rename to frontend/prebuilt/assets/VisualizerView-BwKcK8sl.js index af776e07..a2734f4d 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js and b/frontend/prebuilt/assets/VisualizerView-BwKcK8sl.js differ diff --git a/frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js.map b/frontend/prebuilt/assets/VisualizerView-BwKcK8sl.js.map similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js.map rename to frontend/prebuilt/assets/VisualizerView-BwKcK8sl.js.map index 136bb923..06571d02 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js.map and b/frontend/prebuilt/assets/VisualizerView-BwKcK8sl.js.map differ diff --git a/frontend/prebuilt/assets/hooks-BmAJqTPK.js b/frontend/prebuilt/assets/hooks-BmAJqTPK.js deleted file mode 100644 index 4058ac9a..00000000 Binary files a/frontend/prebuilt/assets/hooks-BmAJqTPK.js and /dev/null differ diff --git a/frontend/prebuilt/assets/hooks-DO7QgCnP.js b/frontend/prebuilt/assets/hooks-DO7QgCnP.js new file mode 100644 index 00000000..2cef1953 Binary files /dev/null and b/frontend/prebuilt/assets/hooks-DO7QgCnP.js differ diff --git a/frontend/prebuilt/assets/hooks-BmAJqTPK.js.map b/frontend/prebuilt/assets/hooks-DO7QgCnP.js.map similarity index 90% rename from frontend/prebuilt/assets/hooks-BmAJqTPK.js.map rename to frontend/prebuilt/assets/hooks-DO7QgCnP.js.map index b98d8243..3cdee39f 100644 Binary files a/frontend/prebuilt/assets/hooks-BmAJqTPK.js.map and b/frontend/prebuilt/assets/hooks-DO7QgCnP.js.map differ diff --git a/frontend/prebuilt/assets/index-C_33NHxn.js b/frontend/prebuilt/assets/index-D_aPHvl1.js similarity index 99% rename from frontend/prebuilt/assets/index-C_33NHxn.js rename to frontend/prebuilt/assets/index-D_aPHvl1.js index c7f74c27..8c93a4cb 100644 Binary files a/frontend/prebuilt/assets/index-C_33NHxn.js and b/frontend/prebuilt/assets/index-D_aPHvl1.js differ diff --git a/frontend/prebuilt/assets/index-C_33NHxn.js.map b/frontend/prebuilt/assets/index-D_aPHvl1.js.map similarity index 63% rename from frontend/prebuilt/assets/index-C_33NHxn.js.map rename to frontend/prebuilt/assets/index-D_aPHvl1.js.map index e70e0516..b6986016 100644 Binary files a/frontend/prebuilt/assets/index-C_33NHxn.js.map and b/frontend/prebuilt/assets/index-D_aPHvl1.js.map differ diff --git a/frontend/prebuilt/assets/leaflet-DNHXPpW7.js b/frontend/prebuilt/assets/leaflet-Kmmg5omM.js similarity index 99% rename from frontend/prebuilt/assets/leaflet-DNHXPpW7.js rename to frontend/prebuilt/assets/leaflet-Kmmg5omM.js index 569781ad..53f969d2 100644 Binary files a/frontend/prebuilt/assets/leaflet-DNHXPpW7.js and b/frontend/prebuilt/assets/leaflet-Kmmg5omM.js differ diff --git a/frontend/prebuilt/assets/leaflet-DNHXPpW7.js.map b/frontend/prebuilt/assets/leaflet-Kmmg5omM.js.map similarity index 99% rename from frontend/prebuilt/assets/leaflet-DNHXPpW7.js.map rename to frontend/prebuilt/assets/leaflet-Kmmg5omM.js.map index 2ecb582f..ef691c56 100644 Binary files a/frontend/prebuilt/assets/leaflet-DNHXPpW7.js.map and b/frontend/prebuilt/assets/leaflet-Kmmg5omM.js.map differ diff --git a/frontend/prebuilt/assets/separator-BtL4plOj.js b/frontend/prebuilt/assets/separator-DL1oR9lA.js similarity index 89% rename from frontend/prebuilt/assets/separator-BtL4plOj.js rename to frontend/prebuilt/assets/separator-DL1oR9lA.js index 86f4b38f..037115b4 100644 Binary files a/frontend/prebuilt/assets/separator-BtL4plOj.js and b/frontend/prebuilt/assets/separator-DL1oR9lA.js differ diff --git a/frontend/prebuilt/assets/separator-BtL4plOj.js.map b/frontend/prebuilt/assets/separator-DL1oR9lA.js.map similarity index 98% rename from frontend/prebuilt/assets/separator-BtL4plOj.js.map rename to frontend/prebuilt/assets/separator-DL1oR9lA.js.map index b5ea8ebe..31b61e45 100644 Binary files a/frontend/prebuilt/assets/separator-BtL4plOj.js.map and b/frontend/prebuilt/assets/separator-DL1oR9lA.js.map differ diff --git a/frontend/prebuilt/index.html b/frontend/prebuilt/index.html index dbb38492..fc096e33 100644 Binary files a/frontend/prebuilt/index.html and b/frontend/prebuilt/index.html differ diff --git a/frontend/src/components/settings/SettingsRadioSection.tsx b/frontend/src/components/settings/SettingsRadioSection.tsx index c201f47a..2869c5c9 100644 --- a/frontend/src/components/settings/SettingsRadioSection.tsx +++ b/frontend/src/components/settings/SettingsRadioSection.tsx @@ -333,6 +333,35 @@ export function SettingsRadioSection({ ? `Connection paused${health?.connection_info ? ` (${health.connection_info})` : ''}` : 'Not connected'; + const deviceInfoLabel = useMemo(() => { + const info = health?.radio_device_info; + if (!info) { + return null; + } + + const model = info.model?.trim() || null; + const firmwareParts = [info.firmware_build?.trim(), info.firmware_version?.trim()].filter( + (value): value is string => Boolean(value) + ); + const capacityParts = [ + typeof info.max_contacts === 'number' ? `${info.max_contacts} contacts` : null, + typeof info.max_channels === 'number' ? `${info.max_channels} channels` : null, + ].filter((value): value is string => value !== null); + + if (!model && firmwareParts.length === 0 && capacityParts.length === 0) { + return null; + } + + let label = model ?? 'Radio'; + if (firmwareParts.length > 0) { + label += ` running ${firmwareParts.join('/')}`; + } + if (capacityParts.length > 0) { + label += ` (max: ${capacityParts.join(', ')})`; + } + return label; + }, [health?.radio_device_info]); + const handleConnectionAction = async () => { setConnectionBusy(true); try { @@ -377,6 +406,7 @@ export function SettingsRadioSection({ {connectionStatusLabel} + {deviceInfoLabel &&
{deviceInfoLabel}
}