From 561c8cf9c0e16ff3753dd8ce66f53423f664985d Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Tue, 24 Feb 2026 19:59:46 -0800 Subject: [PATCH] More code cleanup and optimization --- app/radio.py | 10 +--- app/repository.py | 12 ++-- app/routers/radio.py | 80 +++++++++---------------- frontend/src/components/Sidebar.tsx | 4 +- frontend/src/styles.css | 13 ---- frontend/src/utils/contactAvatar.ts | 4 +- frontend/src/utils/conversationState.ts | 2 +- frontend/src/utils/visualizerUtils.ts | 12 +--- 8 files changed, 42 insertions(+), 95 deletions(-) diff --git a/app/radio.py b/app/radio.py index 14eba3a..cf00b35 100644 --- a/app/radio.py +++ b/app/radio.py @@ -2,7 +2,7 @@ import asyncio import glob import logging import platform -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, nullcontext from pathlib import Path from meshcore import MeshCore @@ -24,12 +24,6 @@ class RadioDisconnectedError(RadioOperationError): """Raised when the radio disconnects between pre-check and lock acquisition.""" -@asynccontextmanager -async def _noop_context(): - """No-op async context manager for optional nesting.""" - yield - - def detect_serial_devices() -> list[str]: """Detect available serial devices based on platform.""" devices: list[str] = [] @@ -196,7 +190,7 @@ class RadioManager: self._release_operation_lock(name) raise RadioDisconnectedError("Radio disconnected") - poll_context = _noop_context() + poll_context = nullcontext() if pause_polling: from app.radio_sync import pause_polling as pause_polling_context diff --git a/app/repository.py b/app/repository.py index f4eaa1f..46722fa 100644 --- a/app/repository.py +++ b/app/repository.py @@ -59,16 +59,14 @@ class ContactRepository: """, ( contact.get("public_key", "").lower(), - contact.get("name") or contact.get("adv_name"), + contact.get("name"), contact.get("type", 0), contact.get("flags", 0), - contact.get("last_path") or contact.get("out_path"), - contact.get("last_path_len") - if "last_path_len" in contact - else contact.get("out_path_len", -1), + contact.get("last_path"), + contact.get("last_path_len", -1), contact.get("last_advert"), - contact.get("lat") if contact.get("lat") is not None else contact.get("adv_lat"), - contact.get("lon") if contact.get("lon") is not None else contact.get("adv_lon"), + contact.get("lat"), + contact.get("lon"), contact.get("last_seen", int(time.time())), contact.get("on_radio", False), contact.get("last_contacted"), diff --git a/app/routers/radio.py b/app/routers/radio.py index cc5174b..9c53e77 100644 --- a/app/routers/radio.py +++ b/app/routers/radio.py @@ -177,6 +177,33 @@ async def send_advertisement() -> dict: return {"status": "ok"} +async def _attempt_reconnect() -> dict: + """Shared reconnection logic for reboot and reconnect endpoints.""" + if radio_manager.is_reconnecting: + return { + "status": "pending", + "message": "Reconnection already in progress", + "connected": False, + } + + success = await radio_manager.reconnect() + if not success: + raise HTTPException( + status_code=503, detail="Failed to reconnect. Check radio connection and power." + ) + + try: + await radio_manager.post_connect_setup() + except Exception as e: + logger.exception("Post-connect setup failed after reconnect") + raise HTTPException( + status_code=503, + detail=f"Radio connected but setup failed: {e}", + ) from e + + return {"status": "ok", "message": "Reconnected successfully", "connected": True} + + @router.post("/reboot") async def reboot_radio() -> dict: """Reboot the radio, or reconnect if not currently connected. @@ -184,7 +211,6 @@ async def reboot_radio() -> dict: If connected: sends reboot command, connection will temporarily drop and auto-reconnect. If not connected: attempts to reconnect (same as /reconnect endpoint). """ - # If connected, send reboot command if radio_manager.is_connected: logger.info("Rebooting radio") async with radio_manager.radio_operation("reboot_radio") as mc: @@ -194,32 +220,8 @@ async def reboot_radio() -> dict: "message": "Reboot command sent. Radio will reconnect automatically.", } - # Not connected - attempt to reconnect - if radio_manager.is_reconnecting: - return { - "status": "pending", - "message": "Reconnection already in progress", - "connected": False, - } - logger.info("Radio not connected, attempting reconnect") - success = await radio_manager.reconnect() - - if success: - try: - await radio_manager.post_connect_setup() - except Exception as e: - logger.exception("Post-connect setup failed after reconnect") - raise HTTPException( - status_code=503, - detail=f"Radio connected but setup failed: {e}", - ) from e - - return {"status": "ok", "message": "Reconnected successfully", "connected": True} - else: - raise HTTPException( - status_code=503, detail="Failed to reconnect. Check radio connection and power." - ) + return await _attempt_reconnect() @router.post("/reconnect") @@ -234,7 +236,6 @@ async def reconnect_radio() -> dict: if radio_manager.is_setup_complete: return {"status": "ok", "message": "Already connected", "connected": True} - # Connected but setup incomplete — retry setup logger.info("Radio connected but setup incomplete, retrying setup") try: await radio_manager.post_connect_setup() @@ -246,28 +247,5 @@ async def reconnect_radio() -> dict: detail=f"Radio connected but setup failed: {e}", ) from e - if radio_manager.is_reconnecting: - return { - "status": "pending", - "message": "Reconnection already in progress", - "connected": False, - } - logger.info("Manual reconnect requested") - success = await radio_manager.reconnect() - - if success: - try: - await radio_manager.post_connect_setup() - except Exception as e: - logger.exception("Post-connect setup failed after reconnect") - raise HTTPException( - status_code=503, - detail=f"Radio connected but setup failed: {e}", - ) from e - - return {"status": "ok", "message": "Reconnected successfully", "connected": True} - else: - raise HTTPException( - status_code=503, detail="Failed to reconnect. Check radio connection and power." - ) + return await _attempt_reconnect() diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 4180078..44450bc 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -6,7 +6,7 @@ import { type Conversation, type Favorite, } from '../types'; -import { getStateKey, type ConversationTimes } from '../utils/conversationState'; +import { getStateKey, type ConversationTimes, type SortOrder } from '../utils/conversationState'; import { getContactDisplayName } from '../utils/pubkey'; import { ContactAvatar } from './ContactAvatar'; import { isFavorite } from '../utils/favorites'; @@ -14,8 +14,6 @@ import { Input } from './ui/input'; import { Button } from './ui/button'; import { cn } from '@/lib/utils'; -type SortOrder = 'alpha' | 'recent'; - type FavoriteItem = { type: 'channel'; channel: Channel } | { type: 'contact'; contact: Contact }; type ConversationRow = { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d2dd546..44fa82d 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -32,12 +32,6 @@ body, } body { - font-family: - system-ui, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - sans-serif; /* Prevent overscroll/bounce on mobile */ overscroll-behavior: none; padding: var(--safe-area-top) var(--safe-area-right) var(--safe-area-bottom-capped) @@ -50,13 +44,6 @@ body { box-sizing: border-box; } -/* Fallback for browsers without dvh support */ -@supports not (height: 1dvh) { - .h-dvh { - height: 100vh; - } -} - /* Mobile sidebar override - ensures sidebar fills Sheet container */ [data-state] .sidebar { width: 100%; diff --git a/frontend/src/utils/contactAvatar.ts b/frontend/src/utils/contactAvatar.ts index fa927cf..440fbf9 100644 --- a/frontend/src/utils/contactAvatar.ts +++ b/frontend/src/utils/contactAvatar.ts @@ -15,8 +15,8 @@ const REPEATER_AVATAR = { textColor: '#ffffff', }; -// Simple hash function for strings -function hashString(str: string): number { +// DJB2 hash function for strings +export function hashString(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); diff --git a/frontend/src/utils/conversationState.ts b/frontend/src/utils/conversationState.ts index 67957ea..b4ce2c9 100644 --- a/frontend/src/utils/conversationState.ts +++ b/frontend/src/utils/conversationState.ts @@ -13,7 +13,7 @@ const LAST_MESSAGE_KEY = 'remoteterm-lastMessageTime'; const SORT_ORDER_KEY = 'remoteterm-sortOrder'; export type ConversationTimes = Record; -type SortOrder = 'recent' | 'alpha'; +export type SortOrder = 'recent' | 'alpha'; // In-memory cache of last message times (loaded from server on init) let lastMessageTimesCache: ConversationTimes = {}; diff --git a/frontend/src/utils/visualizerUtils.ts b/frontend/src/utils/visualizerUtils.ts index a4434d3..8e60060 100644 --- a/frontend/src/utils/visualizerUtils.ts +++ b/frontend/src/utils/visualizerUtils.ts @@ -1,5 +1,6 @@ import { MeshCoreDecoder, PayloadType } from '@michaelhart/meshcore-decoder'; import { CONTACT_TYPE_REPEATER, type Contact, type RawPacket } from '../types'; +import { hashString } from './contactAvatar'; // ============================================================================= // TYPES @@ -114,15 +115,6 @@ export const PACKET_LEGEND_ITEMS = [ // UTILITY FUNCTIONS (Data Layer) // ============================================================================= -function simpleHash(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = (hash << 5) - hash + str.charCodeAt(i); - hash = hash & hash; - } - return Math.abs(hash).toString(16).padStart(8, '0'); -} - export function parsePacket(hexData: string): ParsedPacket | null { try { const decoded = MeshCoreDecoder.decode(hexData); @@ -182,7 +174,7 @@ export function getPacketLabel(payloadType: number): PacketLabel { } export function generatePacketKey(parsed: ParsedPacket, rawPacket: RawPacket): string { - const contentHash = (parsed.messageHash || simpleHash(rawPacket.data)).slice(0, 8); + const contentHash = (parsed.messageHash || hashString(rawPacket.data).toString(16).padStart(8, '0')).slice(0, 8); if (parsed.payloadType === PayloadType.Advert && parsed.advertPubkey) { return `ad:${parsed.advertPubkey.slice(0, 12)}`;