fix(ui): derive operation status from snapshots

The launcher was mixing lifecycle event handlers with the games-list snapshot
when deciding the card status. That left multiple writers for the same
install_status field and made event ordering visible in React.

Make games-list-updated active_operations the authoritative source for busy
status. Lifecycle events no longer mutate the card status; they only keep their
non-status side effects such as rescans and error messages. The only remaining
optimistic status is CheckingPeers before the backend emits its next snapshot.

Add a frontend reducer test that proves an install stays in Installing while an
active install snapshot exists, then settles to Installed only after the active
operation clears with installed local state.

Test Plan:
- git diff --check
- just fmt
- just frontend-test
- just build

Refs: local install/download status snapshot cleanup
This commit is contained in:
2026-05-19 23:48:34 +02:00
parent db03533bd4
commit 5df82aa4f3
6 changed files with 105 additions and 131 deletions
@@ -13,9 +13,9 @@ export interface GameActions {
/**
* Thin wrappers over the backend `run_game` / `install_game` / `update_game`
* / `uninstall_game` / `remove_downloaded_game` commands. We mark peer-backed
* downloads as "checking peers" and already-downloaded installs as "installing"
* up-front so the UI doesn't have to wait for the first backend event.
* / `uninstall_game` / `remove_downloaded_game` commands. Peer-backed downloads
* are marked as "checking peers" until the backend emits an authoritative
* operation snapshot.
*/
export const useGameActions = (games: UseGamesResult): GameActions => {
const play = useCallback(async (id: string) => {
@@ -32,9 +32,7 @@ export const useGameActions = (games: UseGamesResult): GameActions => {
if (!success) return;
const game = games.games.find(item => item.id === id);
if (game?.downloaded && !game.installed) {
games.markInstalling(id);
} else {
if (!game?.downloaded) {
games.markChecking(id);
}
} catch (err) {