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
-
+
-
- {connected ? 'Connected' : 'Disconnected'}
-
+
{statusLabel}
{config && (
@@ -106,7 +108,7 @@ export function StatusBar({
)}
- {!connected && (
+ {!connected && !initializing && (