M9 diagnostics follow-ups: playback relabel, sort, connected fix, per-skip + track-identity #105

Merged
bvandeusen merged 4 commits from dev into main 2026-06-30 19:19:15 -04:00
Showing only changes of commit cdfc79e6ab - Show all commits
@@ -102,6 +102,7 @@ class DiagnosticsReporter @Inject constructor(
launch { collectServerHealth() }
launch { collectUpnpDrops() }
launch { collectPlayerState() }
launch { collectTrackChanges() }
launch { collectRoutes() }
launch { heartbeatLoop() }
}
@@ -162,6 +163,30 @@ class DiagnosticsReporter @Inject constructor(
}
}
// Emit a snapshot at each track/queue-index change — captures a
// skip-induced local↔Sonos desync at the INSTANT it happens, which the
// 45s heartbeat misses. Note: uiState is a conflated StateFlow, so a
// very rapid skip burst may coalesce intermediate indices (we still get
// the boundaries + the local-vs-Sonos snapshot).
private suspend fun collectTrackChanges() {
playerController.uiState
.map { it.queueIndex to it.currentTrack?.id }
.distinctUntilChanged()
.collect { (index, _) ->
val ui = playerController.uiState.value
record("playback", buildJsonObject {
put("event", "track_change")
put("local_index", index)
put("local_pos_ms", ui.positionMs)
put("sonos_track", remoteState.trackNumber)
put("sonos_pos_ms", remoteState.positionMs)
put("upnp_loading", ui.isUpnpLoading)
put("server_health", networkStatus.state.value.name)
put("route", outputPicker.routesState.value.current.name)
})
}
}
private suspend fun collectRoutes() {
// 'playback' — route changes happen for all outputs. This only ever
// logs the ACTIVE route (routesState.current), so no "connected" flag.