From 99eddfc2ef78a345b8db2012308f3a40053dd0ce Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sat, 7 Mar 2026 23:40:18 -0800 Subject: [PATCH] Update status bar and boot up more quickly with actual radio status --- app/main.py | 33 +++++++++--- app/routers/health.py | 14 +++++- frontend/src/App.tsx | 15 ++++-- frontend/src/components/StatusBar.tsx | 26 +++++----- frontend/src/hooks/useRadioControl.ts | 4 +- frontend/src/test/api.test.ts | 1 + frontend/src/test/fanoutSection.test.tsx | 1 + frontend/src/test/settingsModal.test.tsx | 1 + frontend/src/test/statusBar.test.tsx | 50 +++++++++++++++++++ .../src/test/useWebSocket.dispatch.test.ts | 1 + frontend/src/types.ts | 1 + tests/test_health_mqtt_status.py | 28 ++++++++++- tests/test_main_startup.py | 48 ++++++++++++++++++ 13 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 frontend/src/test/statusBar.test.tsx create mode 100644 tests/test_main_startup.py diff --git a/app/main.py b/app/main.py index b1f1e6f..3a188af 100644 --- a/app/main.py +++ b/app/main.py @@ -1,3 +1,4 @@ +import asyncio import logging from contextlib import asynccontextmanager from pathlib import Path @@ -34,6 +35,22 @@ setup_logging() logger = logging.getLogger(__name__) +async def _startup_radio_connect_and_setup() -> None: + """Connect/setup the radio in the background so HTTP serving can start immediately.""" + try: + connected = await radio_manager.reconnect(broadcast_on_success=False) + if connected: + await radio_manager.post_connect_setup() + from app.websocket import broadcast_health + + broadcast_health(True, radio_manager.connection_info) + logger.info("Connected to radio") + else: + logger.warning("Failed to connect to radio on startup") + except Exception as e: + logger.warning("Failed to connect to radio on startup: %s", e) + + @asynccontextmanager async def lifespan(app: FastAPI): """Manage database and radio connection lifecycle.""" @@ -47,13 +64,6 @@ async def lifespan(app: FastAPI): await ensure_default_channels() - try: - await radio_manager.connect() - logger.info("Connected to radio") - await radio_manager.post_connect_setup() - except Exception as e: - logger.warning("Failed to connect to radio on startup: %s", e) - # Always start connection monitor (even if initial connection failed) await radio_manager.start_connection_monitor() @@ -65,9 +75,18 @@ async def lifespan(app: FastAPI): except Exception as e: logger.warning("Failed to start fanout modules: %s", e) + startup_radio_task = asyncio.create_task(_startup_radio_connect_and_setup()) + app.state.startup_radio_task = startup_radio_task + yield logger.info("Shutting down") + if startup_radio_task and not startup_radio_task.done(): + startup_radio_task.cancel() + try: + await startup_radio_task + except asyncio.CancelledError: + pass await fanout_manager.stop_all() await radio_manager.stop_connection_monitor() await stop_message_polling() diff --git a/app/routers/health.py b/app/routers/health.py index 39a53c9..30901a7 100644 --- a/app/routers/health.py +++ b/app/routers/health.py @@ -14,6 +14,7 @@ router = APIRouter(tags=["health"]) class HealthResponse(BaseModel): status: str radio_connected: bool + radio_initializing: bool = False connection_info: str | None database_size_mb: float oldest_undecrypted_timestamp: int | None @@ -45,9 +46,20 @@ async def build_health_data(radio_connected: bool, connection_info: str | None) except Exception: 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 + + radio_initializing = bool(radio_connected and (setup_in_progress or not setup_complete)) + return { - "status": "ok" if radio_connected else "degraded", + "status": "ok" if radio_connected and not radio_initializing else "degraded", "radio_connected": radio_connected, + "radio_initializing": radio_initializing, "connection_info": connection_info, "database_size_mb": db_size_mb, "oldest_undecrypted_timestamp": oldest_ts, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5337841..b6e3df0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -107,7 +107,6 @@ export function App() { health, setHealth, config, - setConfig, prevHealthRef, fetchConfig, handleSaveConfig, @@ -233,6 +232,12 @@ export function App() { const prev = prevHealthRef.current; prevHealthRef.current = data; setHealth(data); + const initializationCompleted = + prev !== null && + prev.radio_connected && + prev.radio_initializing && + data.radio_connected && + !data.radio_initializing; // Show toast on connection status change if (prev !== null && prev.radio_connected !== data.radio_connected) { @@ -243,13 +248,17 @@ export function App() { : undefined, }); // Refresh config after reconnection (may have changed after reboot) - api.getRadioConfig().then(setConfig).catch(console.error); + fetchConfig(); } else { toast.error('Radio disconnected', { description: 'Check radio connection and power', }); } } + + if (initializationCompleted) { + fetchConfig(); + } }, onError: (error: { message: string; details?: string }) => { toast.error(error.message, { @@ -376,9 +385,9 @@ export function App() { incrementUnread, updateMessageAck, checkMention, + fetchConfig, prevHealthRef, setHealth, - setConfig, activeConversationRef, hasNewerMessagesRef, setActiveConversation, diff --git a/frontend/src/components/StatusBar.tsx b/frontend/src/components/StatusBar.tsx index 5dc4d03..037fabf 100644 --- a/frontend/src/components/StatusBar.tsx +++ b/frontend/src/components/StatusBar.tsx @@ -22,6 +22,12 @@ export function StatusBar({ onMenuClick, }: StatusBarProps) { const connected = health?.radio_connected ?? false; + const initializing = health?.radio_initializing ?? false; + const statusLabel = initializing + ? 'Radio Initializing' + : connected + ? 'Radio OK' + : 'Radio Disconnected'; const [reconnecting, setReconnecting] = useState(false); const handleReconnect = async () => { @@ -67,23 +73,19 @@ export function StatusBar({ RemoteTerm -
+
{config && ( @@ -106,7 +108,7 @@ export function StatusBar({
)} - {!connected && ( + {!connected && !initializing && (