fix(android): refresh the phone's own row when it reports

The fleet card reads last-seen from the claw, and only a landed report
moves it. Periodic work is deferred while the device idles — Doze
stretches the fifteen-minute floor to hours — so a phone sitting in the
human's hand could show itself as "last seen 3h ago" with nothing wrong
on the claw.

Kick a status run on resume, the same way the commands worker already
is, and reload the fleet whenever a run lands. The landed-run signal is
a new clock in StatusRepository, written in the same edit that saves the
snapshot (which only happens once every post of the run succeeded), so
the card reloads on a report that actually reached the claw.
This commit is contained in:
vhaudiquet committed 2026-10-20 07:40:43 +02:00
1 parent 4a91635730
commit 385788c935
4 files changed
+39 -5

No files matched your search

@@ -32,6 +32,7 @@ import rs.rhema.phone.ui.RhemaTheme
import rs.rhema.phone.ui.SessionsScreen
import rs.rhema.phone.ui.SettingsScreen
import rs.rhema.phone.work.enqueuePhoneCommandsNow
import rs.rhema.phone.work.enqueuePhoneStatusNow
class MainActivity : ComponentActivity() {
@@ -77,11 +78,16 @@ class MainActivity : ComponentActivity() {
}
}
// The control queue (phone commands) is freshest when the human is
// holding the phone; KEEP coalesces rapid resume bursts.
// Both queues are freshest when the human is holding the phone; KEEP
// coalesces rapid resume bursts. The status run is the one that matters
// to the fleet: periodic work is deferred while the device idles (Doze
// stretches the 15-minute floor to hours), so without this kick the
// phone's own row can say it was last seen hours ago while it sits in
// the human's hand.
override fun onResume() {
super.onResume()
enqueuePhoneCommandsNow(applicationContext)
enqueuePhoneStatusNow(applicationContext)
}
}
@@ -3,6 +3,7 @@ package rs.rhema.phone.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@@ -38,6 +39,7 @@ class StatusRepository(private val store: DataStore<Preferences>, private val js
private object Keys {
val snapshot = stringPreferencesKey("last_phone_snapshot")
val reportedAt = longPreferencesKey("last_phone_report_at")
val notificationCursor = stringPreferencesKey("last_notification_id")
}
@@ -46,10 +48,20 @@ class StatusRepository(private val store: DataStore<Preferences>, private val js
runCatching { json.decodeFromString<PhoneSnapshot>(raw) }.getOrNull()
}
// When the last run landed every post, epoch millis; 0 before the
// first one. The fleet card watches this so it reloads after a report
// lands: the last-seen it draws is the claw's clock, which only moves
// when a run has actually reached it.
val lastReportAt: Flow<Long> =
store.data.map { it[Keys.reportedAt] ?: 0L }
// Only called after every ingest post of a run succeeded, so a failed run
// retries the whole transition set next time instead of swallowing it.
suspend fun setSnapshot(snapshot: PhoneSnapshot) {
store.edit { it[Keys.snapshot] = json.encodeToString(snapshot) }
store.edit {
it[Keys.snapshot] = json.encodeToString(snapshot)
it[Keys.reportedAt] = System.currentTimeMillis()
}
}
// Cursor into the claw's notification list: the last id this device showed.
@@ -58,6 +58,11 @@ fun PhonesCard(container: AppContainer) {
var forgetCandidate by remember { mutableStateOf<PhoneInfo?>(null) }
var forgotName by remember { mutableStateOf<String?>(null) }
val deviceId by container.settings.deviceIdFlow.collectAsState(initial = "")
// The last run that landed every post. The fleet is read from the claw,
// so this device's last-seen only moves when a report has actually
// reached it; reloading on this flow keeps the phone from calling
// itself hours stale while it is in hand.
val lastReportAt by container.status.lastReportAt.collectAsState(initial = 0L)
fun load() {
scope.launch {
@@ -73,7 +78,9 @@ fun PhonesCard(container: AppContainer) {
}
}
LaunchedEffect(Unit) { load() }
// First paint loads the fleet as-is; every landed status run reloads it,
// so the row this phone reads about itself carries the new last-seen.
LaunchedEffect(lastReportAt) { load() }
RhemaCard(title = stringResource(R.string.card_phones), meta = stringResource(R.string.phones_hint)) {
Column(