feat: connection status indicator in drawer (Issue #28)

feat: connection status indicator in drawer (Issue #28)
This commit is contained in:
M57 authored and GitHub committed 2026-06-19 12:40:32 +03:00
commit 66da56ca24
2 files changed
+65 -12

No files matched your search

@@ -10,8 +10,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -22,6 +25,16 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
/**
* Connection status for the WebSocket client.
*/
enum class ConnectionStatus {
DISCONNECTED,
CONNECTING,
CONNECTED,
RECONNECTING,
}
/**
* WebSocket client for the Hermes Dashboard JSON-RPC 2.0 interface.
*
@@ -74,6 +87,12 @@ object HermesWsClient {
/** Collect this from ViewModels to receive all parsed [WsEvent]s. */
val events: SharedFlow<WsEvent> = _events.asSharedFlow()
// ── Connection status flow ──────────────────────────────────────────
private val _connectionStatus = MutableStateFlow(ConnectionStatus.DISCONNECTED)
/** Observable connection status: DISCONNECTED / CONNECTING / CONNECTED / RECONNECTING */
val connectionStatus: StateFlow<ConnectionStatus> = _connectionStatus.asStateFlow()
// ── Callbacks (legacy / convenience) ─────────────────────────────────
var onMessage: ((WsEvent) -> Unit)? = null
@@ -93,20 +112,19 @@ object HermesWsClient {
}
intentionalClose.set(false)
currentBackoff = INITIAL_BACKOFF_MS
_connectionStatus.value = ConnectionStatus.CONNECTING
openSocket()
}
/** Cleanly close the WebSocket and stop auto-reconnect. */
fun disconnect() {
intentionalClose.set(true)
// B4 (Jun 18 2026, kanban t_2b834d90): also cancel any pending
// reconnect coroutine so a scheduled openSocket() can't fire after
// the user explicitly disconnected.
reconnectJob?.cancel()
reconnectJob = null
webSocket?.close(1000, "Client closed")
webSocket = null
connected.set(false)
_connectionStatus.value = ConnectionStatus.DISCONNECTED
}
// ── Send helpers ─────────────────────────────────────────────────────
@@ -156,6 +174,7 @@ object HermesWsClient {
if (intentionalClose.get()) return
if (!AuthManager.isAutoReconnect()) {
if (BuildConfig.DEBUG) Log.d(TAG, "Auto-reconnect disabled")
_connectionStatus.value = ConnectionStatus.DISCONNECTED
return
}
val delay = currentBackoff
@@ -205,6 +224,7 @@ object HermesWsClient {
) {
Log.i(TAG, "WebSocket opened")
connected.set(true)
_connectionStatus.value = ConnectionStatus.CONNECTED
currentBackoff = INITIAL_BACKOFF_MS
onConnected?.invoke()
}
@@ -244,6 +264,7 @@ object HermesWsClient {
Log.i(TAG, "WebSocket closed: $code $reason")
connected.set(false)
onDisconnected?.invoke(reason)
_connectionStatus.value = ConnectionStatus.RECONNECTING
scheduleReconnect()
}
@@ -256,6 +277,7 @@ object HermesWsClient {
connected.set(false)
onError?.invoke(t)
onDisconnected?.invoke(t.message)
_connectionStatus.value = ConnectionStatus.RECONNECTING
scheduleReconnect()
}
}