* fix(#425): log screen doesn't auto-scroll on initial load Replace animateScrollToItem(filteredLogs.size) with scrollToItem(Int.MAX_VALUE). animateScrollToItem requires the LazyColumn to have completed layout before it can calculate animation targets — on initial load the list hasn't laid out yet, so the scroll silently fails. scrollToItem is instant and clamps Int.MAX_VALUE to the last item, working reliably even on first composition. * feat(#425): add auto-refresh polling for live log updates LogsViewModel: add @Volatile loadInProgress guard to prevent concurrent loads, and startAutoRefresh()/stopAutoRefresh() that polls GET /api/logs every 5 seconds via viewModelScope coroutine. LogsScreen: wire up DisposableEffect(Unit) to start polling when the screen is composed and stop when disposed — so polling only runs while the user is viewing the log screen. Combined with scrollToItem(Int.MAX_VALUE) from previous commit, logs now both auto-refresh AND auto-scroll to the latest entry. * fix(#425): add missing viewModelScope import for CI build LogsViewModel used viewModelScope directly in startAutoRefresh() but never imported it — the original code only used it indirectly via safeLaunchLoad which has its own import in ViewModelExt.kt. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
970f4bfe7c
commit
d4320a9289
2 files changed
+40
-1
No files matched your search
@@ -32,6 +32,7 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -125,12 +126,18 @@ fun LogsScreen(
|
||||
viewModel.loadLogs()
|
||||
}
|
||||
|
||||
// Auto-refresh logs via polling while the screen is visible
|
||||
DisposableEffect(Unit) {
|
||||
viewModel.startAutoRefresh()
|
||||
onDispose { viewModel.stopAutoRefresh() }
|
||||
}
|
||||
|
||||
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||
|
||||
// Auto-scroll to bottom when new logs arrive (unless paused)
|
||||
LaunchedEffect(filteredLogs.size, pauseScroll) {
|
||||
if (!pauseScroll && filteredLogs.isNotEmpty()) {
|
||||
listState.animateScrollToItem(filteredLogs.size)
|
||||
listState.scrollToItem(Int.MAX_VALUE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package com.m57.hermescontrol.ui.logs
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class LogsUiState(
|
||||
val isLoading: Boolean = false,
|
||||
@@ -21,7 +26,14 @@ class LogsViewModel : ViewModel(), ToastHost {
|
||||
private val _uiState = MutableStateFlow(LogsUiState())
|
||||
val uiState: StateFlow<LogsUiState> = _uiState.asStateFlow()
|
||||
|
||||
@Volatile
|
||||
private var loadInProgress = false
|
||||
|
||||
private var autoRefreshJob: Job? = null
|
||||
|
||||
fun loadLogs() {
|
||||
if (loadInProgress) return
|
||||
loadInProgress = true
|
||||
safeLaunchLoad(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.getLogs(lines = 1000) } },
|
||||
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||
@@ -29,6 +41,7 @@ class LogsViewModel : ViewModel(), ToastHost {
|
||||
val body = data
|
||||
val logsList = body.lines ?: body.logs ?: emptyList()
|
||||
_uiState.update { it.copy(isLoading = false, logs = logsList) }
|
||||
loadInProgress = false
|
||||
},
|
||||
onError = { errorMsg ->
|
||||
_uiState.update {
|
||||
@@ -37,10 +50,29 @@ class LogsViewModel : ViewModel(), ToastHost {
|
||||
errorMessage = "Failed to load logs: $errorMsg",
|
||||
)
|
||||
}
|
||||
loadInProgress = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Start auto-refreshing logs every 5 seconds. */
|
||||
fun startAutoRefresh() {
|
||||
if (autoRefreshJob?.isActive == true) return
|
||||
autoRefreshJob =
|
||||
viewModelScope.launch {
|
||||
while (isActive) {
|
||||
delay(5_000)
|
||||
loadLogs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop auto-refreshing logs. */
|
||||
fun stopAutoRefresh() {
|
||||
autoRefreshJob?.cancel()
|
||||
autoRefreshJob = null
|
||||
}
|
||||
|
||||
override fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user