Commit Graph

227 Commits

Author SHA1 Message Date
ddidderr ff35f0d95f feat(tauri): make unpack logs viewer usable for debugging
The original unpack logs window was a flat, monolithic scroll of every
unrar invocation glued together as one continuous textfield. That is
fine for a sanity check but hostile to actually finding a failed
extraction in a session with 30+ games: empty lines from unrar bloated
the view, there was no way to focus on a single game, no filtering, and
no way to narrow in on the entries that actually failed.

This rewrites the viewer to be a proper debugging surface while keeping
the backend untouched -- it still consumes the existing
`get_unpack_logs` command and `unpack-logs-updated` event.

User-visible changes:

* Empty / whitespace-only lines are stripped from stdout and stderr
  before rendering, so unrar's padding no longer drowns out real output.
* Two-pane layout: a sidebar lists every captured invocation (badge,
  archive basename, finish time); the right pane shows the selected
  entry's metadata, stdout and stderr.
* "Errors only" checkbox filters the sidebar to entries whose `success`
  flag is false (sidecar exit != 0 or one of the pre-spawn failure
  paths). This is the primary affordance for "find the unpack that
  broke".
* Regex input filters lines (not entries) -- both per-log when viewing
  one, and across the list: entries that contribute zero matching lines
  are hidden, and the remaining ones display a per-entry match counter
  next to the badge. Regex is case-insensitive; a bad pattern reddens
  the input and renders the parser error inline rather than silently
  dropping all matches.
* Prev / Next buttons plus arrow keys (and j/k) step through the
  filtered list one entry at a time, with the active row auto-scrolled
  into view. Selection is tracked by the entry's index in the full log
  ring so it survives filter toggles and live appends.

Code organization:

The component, its types, helpers (`basename`, `nonEmptyLines`,
`formatLogTime`, `isUnpackLogsView`) and its CSS are moved out of
`App.tsx` / `App.css` into a dedicated `UnpackLogsWindow.tsx` +
`UnpackLogsWindow.css` pair. The viewer has no shared state with the
main window and lives behind its own `?view=unpack-logs` route, so
keeping ~200 lines of debug-UI plumbing inside `App.tsx` was just
noise. `App.tsx` now imports `UnpackLogsWindow` and `isUnpackLogsView`
and otherwise looks the same as before.

Intentionally out of scope:

* No backend changes. The Rust side already records everything needed;
  this is purely a presentation improvement.
* No "view all logs concatenated" mode. The flat view was what we just
  replaced -- if it is ever wanted back, it can be added as a third
  pane mode.
* Regex is applied to displayed lines only, not to archive paths or
  meta. Filtering by archive name is easy enough via the basename in
  the sidebar; adding a second filter for it now would be premature.
* Logs are still process-local and capped at `MAX_UNPACK_LOGS` (100)
  in the Rust state -- unchanged from b35755f.

Test plan:

* `tsc --noEmit` and `vite build` are clean.
* Manual: trigger several successful and failed unpacks (rename one
  archive between handshake and extraction to force a canonicalize
  failure), open Unpack Logs, and verify:
    - empty lines are gone from stdout/stderr,
    - sidebar lists every entry with the right OK/FAIL badge,
    - "Errors only" hides the OK rows,
    - typing a regex narrows lines in the open entry, hides entries
      with no matches, and shows the per-entry match counts,
    - an invalid regex (e.g. `[`) reddens the field and shows the
      parser error rather than crashing,
    - arrow keys / j / k step through the filtered list and the
      active row scrolls into view,
    - new entries arriving via `unpack-logs-updated` while the window
      is open keep the current selection rather than jumping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:54:50 +02:00
ddidderr b35755f4e6 feat(tauri): add unpack logs viewer for unrar attempts
Captures stdout, stderr, exit status and start/finish timestamps for every
unrar sidecar invocation and exposes them through a dedicated "Unpack Logs"
window. Triggered by the need to debug why a particular game's archive
failed to extract -- previously the only artifact of a failed unpack was a
log line in the Tauri process stdout, which is awkward to inspect on an
end-user machine.

Implementation:

* `LanSpreadState` gains an in-memory ring buffer (`unpack_logs`) capped at
  `MAX_UNPACK_LOGS` (100). The previous monolithic `do_unrar` is split into
  `prepare_unrar_paths` and `run_unrar_sidecar` so every failure path (mkdir
  failure, canonicalize failure, non-UTF-8 destination, sidecar spawn error,
  non-zero exit) records an `UnpackLogEntry` before bailing.
* A `get_unpack_logs` Tauri command returns the current snapshot; an
  `unpack-logs-updated` event is emitted after every write so the viewer can
  refresh without polling.
* The React `App` component now routes on `?view=unpack-logs` and renders a
  dedicated `UnpackLogsWindow`. The main window opens the viewer via
  `WebviewWindow` with label `unpack-logs`; an existing window is focused
  instead of being recreated.

Capability scoping: the new window is given its own capability file
(`capabilities/unpack-logs.json`) granting only `core:default`. The main
capability is unchanged in window scope and only gains the two permissions
the main window itself needs (`core:window:allow-set-focus` to focus an
existing log window, `core:webview:allow-create-webview-window` to spawn
it). Splitting the capability keeps the log window from inheriting
`shell:allow-open`, `dialog:default` and `store:default`, which it has no
reason to use.

Known limitations (intentionally out of scope here):

* Logs are process-local; they vanish on app restart. Persistence can be
  added later if it turns out users want to inspect failures across runs.
* Entries are presented as a flat chronological list identified by archive
  path. No per-game grouping or filtering yet -- the archive filename is
  usually enough to identify the game in practice.
* The `unpack-logs-updated` event carries no payload; the viewer re-fetches
  the full snapshot on every notification. Acceptable given the 100-entry
  cap, but a payload-bearing event would be cheaper if the cap grows.

Test plan:

* `just clippy` and `just build` are clean.
* Manual: start the GUI, point it at a games directory containing at least
  one peer-hosted game, trigger an install, then click "Unpack Logs". The
  window should show one entry per unrar invocation with stdout, stderr,
  status code and timestamps; stderr/error lines render in the warning
  color. Triggering further unpacks should update the open window live via
  the `unpack-logs-updated` event without manual refresh.
* Negative path: rename or remove the archive between handshake and
  extraction to force a canonicalize failure; confirm a failed entry with
  the corresponding stderr appears in the viewer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:27:59 +02:00
ddidderr a8edcd7450 test(peer-cli): cover full docker scenario matrix
Merge the S18-S36 scenario ideas into the official peer-cli scenario matrix
and add a Docker-backed runner that now exercises S1-S36 with concrete file
proofs. The runner creates temporary fixtures under .lanspread-peer-cli, drives
JSONL peer containers, checks transferred roots with diff and SHA-256 manifests,
and covers startup, discovery, transfer, failure, mutation, concurrency, mesh,
lifecycle, and catalog edge cases.

The scenarios exposed a few harness/runtime boundary gaps that would otherwise
make the contract ambiguous. The peer CLI now rejects self-connects, rejects
commands for game IDs outside the receiver catalog, filters unknown remote games
from its command/event surface, and reports duplicate active same-game commands
as operation-in-progress errors. The peer core also refuses non-catalog download
commands before transfer, and PeerGameDB has a unit check that address changes
preserve identity and library state.

S12 and S28 remain unit-level invariants because the CLI cannot stably race raw
serve-gate requests or rebind a live listener without restart. The runner treats
those scenarios as covered by just test and checks the expected unit test names
appear in the output.

Test Plan:
- just fmt
- python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py
- RUSTC_WRAPPER= just test
- RUSTC_WRAPPER= just clippy
- RUSTC_WRAPPER= just peer-cli-build
- just peer-cli-image
- python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py
- git diff --check

Refs: PEER_CLI_SCENARIOS.md S1-S36
2026-05-19 06:28:16 +02:00
ddidderr 8b3aefd2db test(peer-cli): document full manual scenario pass
Record the May 18 manual Docker pass over PEER_CLI_SCENARIOS.md so the
scenario matrix has current evidence for every row. The run log now covers the
clean direct-connect and aggregation rerun, exact diff checks for downloaded
files, custom renamed-RAR fixtures for conflict and version-skew cases, and the
latest-only transfer behavior for S15-S17.

S12 remains verified by unit tests instead of a CLI race because the raw serving
gates are below the peer-cli command surface. The run log names the exact tests
that cover the non-catalog, missing-sentinel, active-operation, and local-path
serving gates.

Test Plan:
- just peer-cli-image
- RUSTC_WRAPPER= just peer-cli-build
- manual Docker peer-cli runs for S1-S17 using JSONL stdin commands
- diff -r transferred game directories against source fixture directories
- just fmt
- RUSTC_WRAPPER= just test
- RUSTC_WRAPPER= just clippy
- git diff --check

Refs: PEER_CLI_SCENARIOS.md
2026-05-18 22:43:15 +02:00
ddidderr 41e9a0efc1 refactor(peer): split local library and operation UI events
Replace the `a9f9845` local-update dedup cache with explicit peer event
semantics. Local scans now emit `LocalLibraryChanged` when the library changes,
while operation mutations emit `ActiveOperationsChanged` from the mutation
path. Tauri keeps joining those facts into the existing `games-list-updated`
payload, so the frontend contract stays stable.

This removes the cache/invalidation coupling between scan emission and
operation state. The remaining forced local snapshot is explicit: accepted game
directory changes can refresh the UI for an equivalent new path without sending
a peer library delta.

Operation guard cleanup and liveness cancellation now publish the same active
operation snapshot as normal command-handler transitions. The peer CLI JSONL
events follow the same split with `local-library-changed` and
`active-operations-changed`.

Test Plan:
- `just fmt`
- `CARGO_BUILD_RUSTC_WRAPPER= just test`
- `CARGO_BUILD_RUSTC_WRAPPER= just clippy`
- `git diff --check`

Refs: CLEAN_CODE_PLAN_1.md
2026-05-18 21:25:20 +02:00
ddidderr be00a7a298 fix(peer): exchange full library snapshots during handshake
Peer A failed to learn Peer B's games. The handshake only carried
library_rev/library_digest metadata, and the post-handshake sync path
compared those revisions against per-peer revision numbers that were
never advanced via this code path, so the games map for the remote
peer stayed empty and the UI never showed them.

The fix is to put the authoritative library data into the handshake
itself. Hello and HelloAck now carry a LibrarySnapshot directly, and
both perform_handshake_with_peer (outbound) and accept_inbound_hello
(inbound) apply that snapshot to the peer DB before emitting the UI
events. The initial peer-game-list event is now driven by the
handshake rather than by a follow-up LibrarySummary/LibrarySnapshot
roundtrip.

Bumps PROTOCOL_VERSION to 4 because the wire layout of Hello/HelloAck
changed. Per CLAUDE.md's protocol policy there is no compatibility
shim; older peers will fail the version check and be ignored.

Cleanups that fall out of the new design:

  - The Hello / HelloAck library_rev and library_digest fields were
    duplicated by the embedded LibrarySnapshot (which carries its own
    library_rev, and whose digest is recomputed on apply). Collapsed
    both messages to just `library: LibrarySnapshot` to remove the
    foot-gun where the two could diverge.
  - Request::LibrarySummary and Request::LibrarySnapshot are now dead
    on the sender side and were removed along with their stream.rs
    handlers and the LibrarySummary struct. LibraryDelta stays — it
    is still sent from handlers.rs when the local library changes.
  - record_remote_library previously called update_peer_library and
    then apply_library_snapshot, which immediately overwrote the
    rev/digest just written. Added update_peer_features and rewired
    the call site so each peer-DB field is written exactly once.
    update_peer_library is retained because discovery.rs still uses
    it for the mDNS TXT-record path, where no snapshot is available.
  - Removed the now-unused LibraryUpdate enum, select_library_update,
    send_local_library_summary, send_local_library_update_if_needed,
    LocalLibraryState::delta_since, build_library_summary,
    send_library_summary, and send_library_snapshot.

Behavior change visible to users: when two peers come up on the LAN
they now see each other's full game lists immediately after the
handshake instead of waiting for a follow-up sync that, in the broken
case, never made the games visible at all.

Test Plan

  - just clippy (clean for the touched crates)
  - just test (workspace: all suites pass, including the two new
    handshake tests: outbound_hello_carries_local_library_snapshot
    and inbound_hello_applies_remote_library_snapshot, the latter
    asserting PeerDiscovered + PeerCountUpdated + ListGames events
    fire with the remote game visible)
  - Manual: start `just peer-cli-alpha` and `just peer-cli-bravo` in
    separate terminals; confirm each peer's game list shows the
    other's library entries after discovery completes, without
    requiring any additional command.

Refs

  - FINDINGS.md: triage note that Claude's review surfaced only
    in-scope cleanups (dead variants, duplicated header fields,
    redundant DB writes, stale test fixture), all addressed here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:06:39 +02:00
ddidderr ce51d92df0 refactor(peer): tighten listener-addr handshake invariant
Follow-up hardening for 348a02c, where `listen_addr` was added to Hello and
HelloAck as `Option<SocketAddr>`. Code review surfaced three concrete problems
that the previous commit left open:

1. Cold-start asymmetry. Discovery and the QUIC/mDNS advertiser are spawned
   concurrently. If discovery saw a cached peer advertisement before our own
   advertiser had written `ctx.local_peer_addr`, our outbound Hello carried
   `listen_addr: None`. The receiver's `peer_record_addr` then returned `None`
   and silently dropped the Hello while we still recorded their HelloAck, so
   peer A learned about peer B but B never learned about A until a later
   handshake happened to win the race.

2. Duplicate game-list pipeline. The previous commit added
   `refresh_peer_games`, which post-handshake issued a `ListGames` to fetch
   `peer.games`. The library-sync path (`LibrarySnapshot`) already populates
   the same field. Both could race on first contact and overwrite each other.
   Worse, `refresh_peer_games` was misnamed: a `peer_game_count > 0` guard
   turned it into a fetch-once-then-no-op helper, while
   `handle_library_summary` independently re-triggered a full handshake when
   `previous_count == 0` was observed, producing a redundant ping-pong on
   every first contact.

3. Argument explosion. `perform_handshake_with_peer`, `spawn_library_resync`,
   and `after_peer_library_recorded` had grown to 6-8 individual parameters
   and acquired `#[allow(clippy::too_many_arguments)]` opt-outs. Every caller
   was destructuring the same fields out of `Ctx`/`PeerCtx`.

Changes (all in one commit because they jointly enforce the same invariant:
"a peer is only ever recorded by its listener address, and the local
listener address must exist before we participate in the protocol"):

- `Hello.listen_addr` and `HelloAck.listen_addr` are now `SocketAddr`, not
  `Option<SocketAddr>`. Wire-incompatible, but PROTOCOL_VERSION already moved
  to 3 in 348a02c so no additional version bump is needed.
- `required_listen_addr` reads `ctx.local_peer_addr` and returns an
  `eyre::Result`; `build_hello_from_state` and `build_hello_ack` both call
  it, so an outbound or inbound Hello can no longer be constructed before
  the local QUIC listener is bound. The inbound path maps this into a
  `Response::InternalPeerError` so the remote peer fails cleanly instead of
  seeing a malformed HelloAck.
- `run_peer_discovery` blocks on `wait_for_local_peer_addr` (25 ms poll,
  shutdown-aware) before subscribing to the mDNS browser. This closes the
  cold-start race for outbound handshakes at the source.
- `refresh_peer_games`, `request_game_list_from_peer`, and the
  `previous_count == 0` re-handshake trigger are removed. The post-handshake
  flow now relies solely on `LibrarySummary`/`LibrarySnapshot`/`LibraryDelta`
  for peer-library state; `ListGames` survives only for the
  `request_game_details_*` paths that fetch per-game file descriptions on
  demand.
- New `HandshakeCtx` (with `from_ctx` and `from_peer_ctx` constructors)
  replaces the long argument lists. All `too_many_arguments` allow-attrs in
  `handshake.rs` are gone, and call sites in `handlers.rs`, `discovery.rs`,
  and `stream.rs` collapse to a single clone.
- `handle_library_delta` no longer acquires a read lock on the apply path:
  the `peer_addr` lookup moved into the `else` resync branch where it is
  actually needed.
- `accept_inbound_hello`'s `remote_addr` parameter is renamed to
  `transport_addr`. It is now used only for warn-log formatting, and the
  new name signals that this is the ephemeral QUIC source port, never the
  authoritative listener address that gets recorded.

User-visible effect: on cold start, peers can no longer end up with an
asymmetric view of each other ("A sees B but B never sees A"). First-contact
library sync now does one handshake plus one snapshot/delta exchange instead
of the previous handshake + ListGames + redundant follow-up handshake. The
direct-connect CLI path (`handle_connect_peer_command`) now fails fast with
"local peer listener address is not ready" if invoked before the QUIC server
has bound; this is intentional - the previous behaviour would have sent a
Hello that the receiver had to silently discard.

Test Plan:
- just fmt
- just clippy
- just test (80 peer + 3 cli + 5 tauri tests pass)
- just build
- Manual: bring up `just peer-cli-alpha`/`bravo`/`charlie`, confirm symmetric
  peer discovery and that games show up on every side after one library
  digest cycle, with no duplicated ListGames traffic in trace logs.

Refs: Review feedback on commit 348a02c (listener-address handshake fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:21:19 +02:00
ddidderr 348a02c35f fix(peer): record listener addresses during handshakes
Peers discovered over mDNS could still attribute later library sync traffic to
temporary QUIC source ports. In a real GUI LAN run this made Host B try to
push its library to Host A's outbound port instead of Host A's advertised
listener, so Host A discovered the peer but never saw its games.

Carry the stable listener address in Hello and HelloAck, and key library sync
messages by peer_id instead of inferring identity from the transport source
address. The handshake path now explicitly refreshes an empty peer library from
the known listener address, matching the reliability of the direct-connect CLI
path without overwriting richer snapshot state when it already arrived.

This changes the current wire protocol, so PROTOCOL_VERSION is bumped to 3 and
all peers must be rebuilt together. The architecture note now documents that
listener addresses come from mDNS or Hello/HelloAck, never from ephemeral QUIC
source ports.

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

Refs: Local Linux/Win11 GUI LAN test logs from 2026-05-18.
2026-05-18 17:27:15 +02:00
ddidderr 642463d7eb certs fixed 2026-05-18 16:19:27 +02:00
ddidderr c795e9de02 deps: deno and cargo update 2026-05-17 23:16:42 +02:00
ddidderr 48b1d2821f build: production profile, justfile adjustments 2026-05-17 23:10:58 +02:00
ddidderr d891a14a46 deps: cargo update 2026-05-17 14:53:23 +02:00
ddidderr adaf0028af Cargo.toml: new resolver version 2026-05-17 14:53:10 +02:00
ddidderr 274b9d2fd4 test(peer-cli): add large exact-transfer coverage
Add deeper peer CLI coverage for file-transfer integrity and multi-peer
chunking. The alpha fixture now carries a real renamed RAR archive larger
than 100 MB for alienswarm, which gives the chunk planner enough work to
split a single game archive across multiple peers.

Expose completed chunk source details as a peer event and have the CLI print
that event as JSONL. This keeps transfer behavior in lanspread-peer while the
CLI remains a harness that reports what the peer runtime did. The Tauri shell
logs the event at debug level so the shared PeerEvent enum stays exhaustive.

Document the new S13/S14 scenarios and record the manual run evidence,
including SHA-256 manifests and the per-peer byte split for the large archive.

Test Plan:
- just fmt
- just test
- just peer-cli-build
- just clippy
- just peer-cli-image
- unrar t -idq crates/lanspread-peer-cli/fixtures/fixture-alpha/alienswarm/alienswarm.eti
- Manual peer CLI: bravo -> deep-small-client bfbc2 download with matching SHA-256 manifests
- Manual peer CLI: alpha -> deep-stage-b alienswarm download with matching SHA-256 manifests
- Manual peer CLI: alpha + deep-stage-b -> deep-stage-c alienswarm download with chunk events from both peers and matching SHA-256 manifests

Refs: PEER_CLI_SCENARIOS.md S13 S14
2026-05-17 10:25:26 +02:00
ddidderr 5d58791192 fix(peer-cli): fail missing downloads from peer event
The peer core already emits NoPeersHaveGame when a requested game cannot be
served by any known peer. The JSONL harness still waited for the generic file
detail timeout before returning the download command error, which made the
manual scenario slower and less precise.

Correlate the existing no-peers event with the pending CLI download command so
the harness returns a deterministic error immediately. This is harness
bookkeeping only; game availability and peer behavior remain owned by
lanspread-peer.

Test Plan:
- just fmt
- just test
- just clippy
- just peer-cli-build
- just peer-cli-image
- just peer-cli-alpha, just peer-cli-bravo, just peer-cli-charlie
- In charlie, send {"cmd":"download","game_id":"not-a-game"}; observe
  no-peers-have-game followed by error "no peers have game not-a-game"

Refs: PEER_CLI_SCENARIOS.md
2026-05-17 09:34:20 +02:00
ddidderr 10a1f57183 fix(peer): preserve advertised addresses for QUIC peers
After renewing the dev certificate, peers could complete handshakes but then
lost each other during liveness checks. Inbound QUIC streams report the client's
ephemeral source port, while the peer database is supposed to track the peer's
advertised listening address. Recording the ephemeral address created unstable
peer entries that could not be pinged later.

Resolve transport source addresses back to the unique known peer on the same IP,
and keep an existing advertised address when an inbound Hello arrives from that
peer. Goodbye events now report the stored peer address as well.

This keeps the core peer behavior in lanspread-peer; the CLI only observes the
resulting peer snapshots.

Test Plan:
- just fmt
- just test
- just clippy
- just peer-cli-build
- just peer-cli-image
- just peer-cli-alpha, just peer-cli-bravo, just peer-cli-charlie
- list-peers after the ping idle window shows advertised peer addresses with
  populated game lists instead of ephemeral-port peers disappearing

Refs: PEER_CLI_SCENARIOS.md
2026-05-17 09:34:10 +02:00
ddidderr 84f533aeee fix(peer): renew expired QUIC dev certificate
Manual peer CLI runs could discover peer advertisements, but QUIC handshakes did
not complete. The checked-in self-signed certificate expired on March 2, 2026,
so peers running on May 17, 2026 could start listening but could not establish
valid TLS sessions with each other.

Regenerate cert.pem from the existing key.pem, preserving the existing subject
and DNS:localhost SAN. The renewed development certificate is valid until April
23, 2126 and still matches key.pem.

Test Plan:
- openssl x509 -in cert.pem -noout -subject -issuer -dates -ext subjectAltName
- openssl x509 -noout -modulus -in cert.pem | openssl sha256
- openssl rsa -noout -modulus -in key.pem | openssl sha256
- just peer-cli-image
- just peer-cli-alpha, just peer-cli-bravo, just peer-cli-charlie

Refs: PEER_CLI_SCENARIOS.md
2026-05-17 09:34:00 +02:00
ddidderr 31e1a7eb3b fix(peer-cli): use writable Docker config for image builds
The peer CLI Docker recipes failed on this host because Docker Buildx tried to
write activity metadata under /home/pfs/.docker, which is read-only here. Use a
repo-local ignored Docker config by default, while still respecting an explicit
DOCKER_CONFIG from the caller.

This keeps the peer-cli image and network recipes runnable without requiring
host-global Docker client state to be writable.

Test Plan:
- just peer-cli-image

Refs: PEER_CLI_SCENARIOS.md
2026-05-17 09:33:48 +02:00
ddidderr 9a677947ed testing: peer CLI pre-defined peers 2026-05-17 07:54:12 +02:00
ddidderr 3380d137fc fix: ignore local watcher access events
The peer CLI could flood LocalGamesUpdated events when run from the Docker
harness. The local monitor rescans game roots, and some bind-mounted filesystems
report those read/close operations back as notify access events. Treating those
non-mutating events as real library changes queued another rescan, making the
headless CLI unusable for manual peer-to-peer testing.

Ignore access events before mapping paths to game IDs. Create, modify, remove,
and rename events still flow through the existing per-game rescan gate, while
fallback scans continue to reconcile missed writes.

Test Plan:
- just fmt
- just test
- just clippy

Refs: manual peer-cli P2P testing
2026-05-16 19:50:10 +02:00
ddidderr ed007f7844 test: add peer CLI game directory fixtures
Add three reusable peer CLI game directory fixtures for local smoke tests.
Each fixture is a complete games root that can be passed directly to
--games-dir, with catalog-backed game IDs, version.ini sentinels, and real RAR
archives renamed to .eti.

The fixtures intentionally overlap in two places so multi-peer tests can cover
shared availability. Alpha and bravo both contain ggoo, while bravo and charlie
both contain cnc4. The archives contain generated random payload files rather
than meaningful game data; this keeps the fixtures fake while still exercising
RAR-backed ETI handling.

The tradeoff is committing roughly 34 MiB of binary fixture data. That is
intentional here because the fixtures need real archives for CLI tests instead
of synthetic text placeholders.

Test Plan:
- Ran git diff --check.
- Ran unrar t -idq over every .eti file in the fixture tree.

Refs: none
2026-05-16 19:30:01 +02:00
ddidderr 754afd5621 refactor(peer): drop --no-mdns toggle, mDNS is always on
The peer runtime previously accepted an `enable_mdns: bool` flag, plumbed
through `PeerStartOptions`, `spawn_peer_runtime`, `run_peer`, `Ctx`, and
`PeerCtx`. The lanspread-peer-cli harness exposed the toggle as
`--no-mdns` so test scenarios could fall back to explicit `connect`
commands when mDNS could not be relied on, in particular when multiple
peers ran inside `--network host` containers and could not advertise
independently.

That host-networking workaround no longer exists: the previous commit
moves harness containers onto a macvlan network, where each peer is a
real LAN device and mDNS just works between them. There is no scenario
left in the codebase where disabling mDNS is desirable. Per the project's
protocol policy in CLAUDE.md ("there is only one wire version, no
compatibility shims, no fallback paths"), an opt-out path with no current
caller is exactly the kind of dead code we should not carry.

Remove the flag and every plumbing point that exists only to support it:

- `PeerStartOptions::enable_mdns` and the custom `Default` impl that set
  it to `true`; the struct now derives `Default` and just carries
  `state_dir`.
- The `enable_mdns` parameter on `start_peer_with_options`,
  `spawn_peer_runtime`, `run_peer`, and `Ctx::new`.
- The `enable_mdns` fields on `Ctx` and `PeerCtx` and the propagation
  through `to_peer_ctx`.
- The `if ctx.enable_mdns` guard in `spawn_startup_services`;
  `spawn_peer_discovery_service` is now always spawned.
- The `if ctx.enable_mdns { ... } else { ... }` branch in
  `run_server_component`: the mDNS advertiser and event monitor are now
  unconditionally started, and the no-mDNS-fallback log line that read
  "mDNS disabled; direct peer address is ..." is gone. The
  `direct_connect_addr` helper is kept because the mDNS-on branch still
  uses it as a fallback when `local_peer_addr` has not yet been
  populated.
- The internal test helpers in `handlers.rs`, `services/local_monitor.rs`,
  and `services/stream.rs` that passed `true` as the trailing
  `enable_mdns` arg to `Ctx::new`.
- In `lanspread-peer-cli`: the `--no-mdns` arg parsing, the
  `Args::enable_mdns` field, the `mdns` key on the `cli-started` event
  payload, and the `--no-mdns` mention in the help text and the crate
  README.

The `Args::name` field is wired to the harness identity but is otherwise
untouched. The macvlan network created by `just peer-cli-net` is the
runtime prerequisite for this change to be observable across containers;
on a single workstation, two harness binaries on `127.0.0.1` discover
each other through mDNS on the loopback interface as before.

Test Plan:
- `just fmt`
- `just clippy`
- `just test`
- `just peer-cli-build`
- Two peers on macvlan: `just peer-cli-run alpha` and
  `just peer-cli-run beta`; check that each emits `peer-discovered` and
  `peer-connected` events without an explicit `connect` JSONL command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:51:54 +02:00
ddidderr 93c6133ea9 feat(peer-cli): run harness containers on a macvlan network
The previous `peer-cli-run` recipe attached containers with `--network host`.
That makes every peer share the host network namespace, which means two
harness containers on the same machine cannot independently advertise mDNS
or look like distinct LAN devices: they all reuse the host's interface, the
mDNS daemon is single-instance per namespace, and any port the peer binds
is a host-wide port. That defeats the whole point of running multiple
peers side-by-side for end-to-end testing.

Switch the recipe to a Docker macvlan network. Each container gets its own
MAC and IP carved out of the real LAN subnet, sends and receives multicast
on the parent NIC, and appears to the rest of the home network as a fresh
device. mDNS discovery then works between peers exactly as it would for
two laptops on the same LAN, with no relay, reflector, or special routing.

Add a `peer-cli-net` recipe that creates the network idempotently (the
`docker network inspect` short-circuits when it already exists), make
`peer-cli-run` depend on it, and parameterise the parent interface,
subnet, and gateway as justfile variables so they can be overridden from
the command line for machines whose LAN does not match the defaults:

    just LANSPREAD_PARENT_IFACE=enp4s0 \
         LANSPREAD_SUBNET=10.0.0.0/24 \
         LANSPREAD_GATEWAY=10.0.0.1 \
         peer-cli-run alpha

The well-known macvlan limitation that the host cannot reach its own
macvlan children over the network is intentionally not worked around:
agents drive each peer through `docker run -i` stdin/stdout, which is the
docker control socket, not the LAN. Host-to-peer connectivity is not part
of the mental model and is not needed for any current test scenario.

Test Plan:
- `just peer-cli-image`
- `docker network create -d macvlan ... lanspread` succeeds on a host with
  the default `eth0` interface (or with overridden variables on others).
- `just peer-cli-run alpha` and `just peer-cli-run beta` in two terminals;
  both containers come up on the LAN with distinct IPs and discover each
  other via mDNS without any `connect` command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:51:28 +02:00
ddidderr dc9e13e6a1 feat(peer-cli): add JSONL peer test harness
Agents need a way to exercise multiple peers without launching the Tauri GUI.
Add `lanspread-peer-cli` as a workspace crate that starts the core peer runtime,
reads JSON commands from stdin, and writes result, event, and error records as
JSONL on stdout.

The harness supports status, peer listing, game listing, direct connect,
set-game-dir, download, install, uninstall, wait-peers, and shutdown commands.
It can seed tiny fixture archives that use a fixture unpacker, or delegate real
archives to an external `unrar` program when one is supplied.

Add a Dockerfile, `.dockerignore`, and `just` recipes for building the binary,
building the image, and running named harness containers with state and games
mounted under `target/peer-cli/`. The documentation now lists the crate and the
new test harness commands in the project map, with a crate-local README for the
JSONL protocol.

This commit depends on the non-GUI peer hooks introduced in the previous commit:
startup options, local-ready events, direct connects, snapshots, and explicit
post-download install policy. It does not add old-peer compatibility paths.

Test Plan:
- `git diff --check`
- `just fmt`
- `just clippy`
- `just test`
- `just peer-cli-build`
- Not run: `just peer-cli-image` requires a Docker daemon and base image access.

Depends-on: e711cf3454
Refs: crates/lanspread-peer-cli/README.md
2026-05-16 18:33:18 +02:00
ddidderr e711cf3454 fix(peer): settle current-protocol local state cleanup
The follow-up backlog had drifted into three settled peer/runtime issues: the
legacy game-list fallback contradicted the one-wire-version policy, the Tauri
shell still re-derived local install state from disk after peer snapshots, and
`Availability::Downloading` existed even though active operations are already
reported through a separate operation table.

Remove the legacy `AnnounceGames` request and fallback service. Discovery now
ignores peers that do not advertise the current protocol and a peer id, and
library changes are sent through the current delta path only. This keeps the
runtime aligned with the documented current-build-only interoperability model.

Make peer `LocalGamesUpdated` snapshots authoritative for local fields in the
Tauri database. The GUI-side catalog still owns static metadata such as names,
sizes, and descriptions, but downloaded, installed, local version, and
availability now come from the peer runtime instead of a second whole-library
filesystem scan. Snapshot reconciliation also pins the missing-begin and
missing-finish lifecycle cases in tests.

Collapse availability back to the settled `Ready` and `LocalOnly` states.
Aggregation now counts only `Ready` peers as download sources, and the frontend
no longer carries a dead `Downloading` enum value.

The core peer also exposes the small non-GUI hooks needed by scripted callers:
startup options for state and mDNS, a local-ready event, direct connection, peer
snapshots, and an explicit post-download install policy. Those hooks reuse the
same current protocol path and do not add compatibility shims.

Test Plan:
- `git diff --check`
- `just fmt`
- `just clippy`
- `just test`

Refs: BACKLOG.md, FINDINGS.md, IMPL_DECISIONS.md
2026-05-16 18:32:24 +02:00
ddidderr 6242d64583 fix(peer): repair update lifecycle regressions
FINDINGS.md identified three merge blockers in the post-plan install/update
flow.

Updates now use FetchLatestFromPeers so the Tauri update command bypasses
local manifest serving and asks peers that advertise the latest version for
fresh file metadata. PeerGameDB now aggregates and validates file descriptions
from latest-version peers, keeping stale cached metadata for older versions
from poisoning chunk planning when filenames stay the same but sizes change.

Download-to-install handoff now performs explicit async state transitions.
The download task mutates Downloading to Installing or Updating under the
active-operation write lock, clears the cancellation token, and then runs the
install transaction. OperationGuard remains armed only as crash or abort
cleanup and is disarmed after normal explicit cleanup, so final refreshes no
longer race a deferred Drop cleanup.

Local library index writers now serialize the load/mutate/save window with one
async mutex. The index fingerprint also includes the root version.ini contents
so a same-length version rewrite in the same mtime second still updates the
reported local version.

The tradeoff is that local index mutations are serialized in-process instead
of moved into a dedicated actor. That keeps the fix small and scoped to the
merge blockers while preserving the existing scanner API.

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

Refs:
- FINDINGS.md
2026-05-16 14:19:10 +02:00
ddidderr 8890d78642 crazy 2026-05-16 13:15:34 +02:00
ddidderr a251233653 refactor(peer): split download pipeline into modules
The download pipeline had grown into one large file that mixed sentinel
transaction handling, peer planning, transport, retry, and top-level
orchestration. Split it into a download/ module tree with one file per
concern so future lifecycle changes can be reviewed at the right boundary.

The public crate surface remains download::download_game_files. Helper types
and functions are kept pub(super) or private so the refactor does not widen
the API or encourage new callers to depend on internals. The version.ini
transaction helpers stay local to version_ini.rs; the proposed fs_util
extraction is intentionally left for the later atomic-index work, where a
second caller exists.

There is no intended runtime behavior change.

Test Plan:
- just fmt
- just test
- just clippy
- just build

Refs: none
2026-05-16 12:16:08 +02:00
ddidderr 504ee1bc02 CLAUDE.md: add policy about legacy versions not being supported 2026-05-16 11:50:02 +02:00
ddidderr be196f9e4b refactor: type game availability state
Game::availability used string labels that were carried through persisted
library data, protocol summaries, and the Tauri-facing game payload. That
allowed invalid states to exist and required legacy summary conversion code to
defensively map strings back into protocol availability values.

Move Availability to lanspread-db and re-export it from lanspread-proto so the
persisted Game type and wire GameSummary type share one serde enum. The JSON
spelling stays Ready, Downloading, or LocalOnly, so the serialized shape does
not change for current library indexes or peer payloads.

Add typed helpers for sentinel-derived download state. Game::set_downloaded
keeps downloaded and Ready/LocalOnly in lockstep and intentionally collapses
non-ready local state, including Downloading, back to LocalOnly. That matches
the current local-summary contract where active operations are suppressed
instead of advertised as Downloading. Game::normalized_availability keeps the
legacy Game-to-summary path from publishing an inconsistent Ready value when
downloaded is false.

Update the follow-up status note so typed availability is no longer listed as
open work.

Test Plan:
- just fmt
- just test
- just clippy
- just build

Refs: none
2026-05-16 11:49:01 +02:00
ddidderr fdad162240 fix(peer): write local library index atomically
The local library index used tokio::fs::write directly on the canonical
library_index.json path. That truncates the existing index before writing the
new bytes, so a crash or power loss could leave a zero-length or partial cache.

Write the index through a sibling temp file, sync it, rename it over the
canonical path, and sync the parent directory on Unix. Loading the index also
sweeps a stale temp file before parsing the canonical file. That keeps the
existing cache valid after an interrupted write while still letting a normal
scan rebuild from disk if the canonical index is missing or corrupt.

This follows the existing temp-plus-rename pattern used for version.ini and
install intents. It intentionally does not add locking; local library writes
are already serialized by the peer operation flow.

Test Plan:
just fmt
just test
just clippy

Refs: none
2026-05-16 10:01:34 +02:00
ddidderr cc805777d8 docs(peer): sync follow-up status
Update FOLLOW_UP_2.md after the follow-up stack landed. The document now marks
the active-operation reconciliation, recovery matrix, rollback tests,
installed-only rescan, scanner gating, serve gating, and TempDir consolidation
as completed.

The remaining open items are intentionally limited to the deferred hygiene
called out by the plan: typed availability, atomic library-index writes, and a
future download.rs split.

Test Plan:
- git diff --check

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:23:02 +02:00
ddidderr 894eb5af6a test(peer): consolidate temp dir helper
Move the repeated test TempDir implementations into a single peer
test_support module. The shared helper keeps the existing automatic cleanup
behavior and uses an atomic suffix plus timestamp so parallel tests do not
collide on the same path.

This is intentionally limited to test hygiene. It does not change the
availability model, split download.rs, or touch production scan/install
behavior beyond importing the shared helper from test modules.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:21:43 +02:00
ddidderr 7731a9daa0 test(peer): cover serve gating dispatch
Add focused serve-side tests for the gates around peer requests. GetGame now has
coverage for the non-catalog, active-operation, and missing-sentinel cases that
should return GameNotFound instead of exposing local files.

The full-file and chunk handlers both depend on the same transfer gate before
touching the QUIC send stream. Extract that gate into a small helper and test
the same cases there, plus the existing local-path exclusion, so both dispatch
paths stay aligned without adding fake QUIC stream plumbing.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:16:37 +02:00
ddidderr 2a94445391 test(peer): cover local monitor rescan gating
Add dispatch-level tests for the local game monitor paths called out in
FOLLOW_UP_2.md. The new coverage verifies watcher events are dropped while a
game has an active operation, burst events for one game collapse through the
pending set to at most one extra rescan, fallback scans pick up sideloaded
catalog games, and non-catalog roots stay invisible to the library state.

The non-catalog test exposed that an empty local library initialized with
digest zero, while the computed digest for an empty map is nonzero. That made
the first empty scan produce a meaningless empty LibraryDelta. Initialize the
empty state with the computed empty digest so a non-catalog-only scan leaves no
delta behind.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:13:38 +02:00
ddidderr c7b7ab7576 test(peer): cover installed-only rescan readiness
Add a focused local-library test for the case where a game starts with only a
committed `local/` install and later gains a root `version.ini` sentinel. The
single-game rescan now has coverage showing it promotes the summary from
LocalOnly to Ready while preserving the installed flag and reading the local
version.

This pins the cached-index transition called out in FOLLOW_UP_2.md without
touching scanner dispatch or broader monitor behavior.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:09:50 +02:00
ddidderr 3abb2e051b test(peer): cover uninstall rollback restore
Add coverage for the uninstall branch where `local` has already been moved to
`.local.backup`, but deleting that backup fails. The Unix-gated test makes a
child directory non-writable before uninstall starts, so recursive deletion of
the renamed backup fails without adding production hooks.

The test verifies rollback restores the previous local install, removes the
backup path, and clears the intent. It is gated to Unix because deletion
permission behavior is platform-specific; Windows coverage would need a
different failure mechanism rather than pretending this setup is portable.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:08:28 +02:00
ddidderr bb483f01f6 test(peer): cover update commit rollback
Add a focused transaction test for the branch where update extraction succeeds
but promoting `.local.installing` to `local` fails. The fake unpacker creates a
non-empty `local/` conflict after extraction, so the commit rename fails without
adding production hooks or brittle platform-specific permission tricks.

The assertion verifies the old install is restored from `.local.backup`, the
conflict and staging directories are removed, the backup is consumed, and the
intent is cleared back to None.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:06:32 +02:00
ddidderr 47733713ca test(peer): cover install recovery matrix
FOLLOW_UP_2.md called out that recovery only covered one intent-driven update
row. Replace that single-case assertion with a table over the ten recovery
rows documented in PLAN.md, spanning Installing, Updating, and Uninstalling
intents across local, staging, and backup directory states.

The cases intentionally use markerless reserved directories while an intent is
present. That pins the contract that the intent log proves Lanspread ownership
during crash recovery, including the crash windows before ownership markers are
dropped. The test still keeps the existing None-intent markerless case separate
so user-owned reserved names remain protected.

Running the larger table in parallel exposed that this module's TempDir helper
could collide on pid plus timestamp paths. Add a local atomic suffix so these
tests stop deleting each other's directories without doing the broader helper
consolidation reserved for the later hygiene phase.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:04:53 +02:00
ddidderr 95e70ef520 fix(ui): reconcile active operations from local scans
Local operation spinners were driven by begin, finish, and failure event
history. If one of those lifecycle events was missed, the Tauri bridge could
keep a stale active operation and the React state would keep showing an
in-progress spinner until restart.

Peer local scan updates now carry an authoritative active-operation snapshot.
The peer still suppresses active game roots from peer-facing library deltas,
but it emits LocalGamesUpdated to the UI even when no library delta changed so
the snapshot can clear stale state after rollback or completion. The Tauri
bridge replaces its active-operation map from that snapshot, emits it with the
games-list payload, and the React merge uses it to restore download, install,
update, and uninstall spinners from current peer state rather than event
history alone.

This also enables the Tauri lib unit-test target so the reconciliation helper
can stay covered by the workspace test recipe.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
2026-05-16 09:01:17 +02:00
ddidderr b5d20c1e72 fix(peer): refresh settled install state after operations
The follow-up review found a few stale lifecycle edges around local game
transactions. Recovery could sweep active roots, post-operation refreshes
still re-ran full startup recovery, and the UI kept inferring local-only state
from downloaded and installed flags instead of the backend availability.

This updates the peer lifecycle so startup recovery skips active operations,
install/update/uninstall refresh only the affected game after the operation
guard is dropped, and path-changing game-directory updates are rejected while
operations are active. It also removes the dead UpdateGame command, drops the
unused manifest_hash write field while preserving old JSON reads, renames the
internal install-finished event, and carries availability through the DB,
peer summaries, Tauri refreshes, and the React model.

The included follow-up documents record the review source, implementation
decisions, and the remaining FOLLOW_UP_2.md work so later commits can stay
small instead of reopening the completed plan items.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_PLAN.md
2026-05-16 08:50:51 +02:00
ddidderr fce34c7bd2 docs(peer): document transactional install model
Update the peer README and architecture notes to match the landed runtime:
version.ini is the download sentinel, local/ is the install predicate, install
state is recovered through .lanspread.json intents, and watcher rescans are
operation-gated rather than time-debounced.

Add IMPL_DECISIONS.md with the implementation-time choices that were not
already prescribed by PLAN.md, including the just test recipe, the UI event
compatibility bridge, reuse of the existing library index for per-ID rescans,
and the split between active operation state and download cancellation tokens.

Test Plan:
- just fmt
- just clippy
- just test
- just build

Refs: PLAN.md
2026-05-15 18:21:09 +02:00
ddidderr c5dfbf99a0 feat(ui): delegate install lifecycle to the peer
Remove the Tauri-side whole-game backup and unpack flow. The Tauri shell now
provides an injected unrar sidecar implementation and lets the peer own
install, update, uninstall, rollback, and recovery decisions.

Route install commands by local state: missing version.ini fetches from peers,
downloaded archives without local/ send InstallGame directly, and already
installed games are left to the Play action. Updates request a fresh download
and uninstalls forward UninstallGame. The UI mirrors peer operation events for
downloading, installing, updating, and uninstalling.

Render installed-but-not-downloaded games as LocalOnly and surface the local
version for downloaded-but-not-installed games. Add a secondary uninstall
affordance that does not change the main Install/Open action.

Test Plan:
- just fmt
- just clippy
- just test
- just build

Refs: PLAN.md
2026-05-15 18:20:45 +02:00
ddidderr 6c8a2bb9f0 feat(peer): add transactional local game operations
Implement the peer-owned state model from PLAN.md. A root-level version.ini
is now the download completion sentinel, local/ as a directory is the install
predicate, and exact root-level version.ini detection prevents nested files
from becoming sentinels by accident.

Add the peer operation table that gates downloads, installs, updates, and
uninstalls by game ID. Serving paths now reject non-catalog games, active
operations, missing sentinels, and any request that points under local/.
Remote aggregation treats LocalOnly peers as non-downloadable so they do not
contribute peer counts, candidate source selection, or latest-version checks.

Move install-side filesystem mutation into lanspread-peer::install. The new
module writes atomic .lanspread.json intents, uses .local.installing and
.local.backup with .lanspread_owned markers, and performs startup recovery
from recorded intent plus filesystem state. Downloads now buffer version.ini
chunks in memory and commit the sentinel last through .version.ini.tmp.

Replace the fixed 15-second monitor with notify-backed non-recursive watches,
per-ID rescan gating, and a 300-second fallback scan. The optimized rescan
path updates one cached library-index entry and active operation IDs preserve
their previous summary during scans.

Test Plan:
- just fmt
- just clippy
- just test
- just build

Refs: PLAN.md
2026-05-15 18:18:55 +02:00
ddidderr bff58c6013 cleanup: remove server.sh 2026-05-15 17:41:27 +02:00
ddidderr 627d8c4533 plan.md 2026-05-15 16:07:17 +02:00
ddidderr 0d2520fd16 fix(ui): stop showing manually deleted games as installed
A game that the user deletes from disk while the launcher is running stayed
visible as "Installed" in the UI indefinitely, both as a status label and as
a member of the Installed tab. After a restart the Install button reappeared
but the game still wrongly showed up under Installed. The backend rescan
(`set_all_uninstalled` + `update_game_installation_state` in
src-tauri/src/lib.rs) was already producing the correct `installed: false`
on each refresh; the React store was just refusing to honour it.

Two independent UI bugs were in play:

1. The `games-list-updated` listener merged each update with
   `previous?.install_status ?? ...`, which preserved a prior `Installed`
   value regardless of what the backend now reported. The fix introduces
   `mergeGameUpdate`: the backend `installed` flag wins for settled state
   (Installed vs NotInstalled), while genuine in-progress states
   (CheckingPeers / Downloading / Unpacking) are preserved across refreshes
   so concurrent backend ticks cannot blow away an active download UI.
   `status_message` and `status_level` are cleared only when the local
   `installed` / `downloaded` flags actually flip, so a transient error
   ("No peers currently have this game.") survives a cosmetic refresh but
   is wiped once the underlying state changes.

2. The Installed tab filter was `installed || downloaded`, which leaked
   downloaded-but-not-yet-installed games into a tab whose label promises
   only ready-to-play titles. It now filters on `installed` alone, matching
   `getActionLabel`'s own definition of when "Install" appears.

While the install-state semantics were being sorted out, the filter
taxonomy was clarified to match what users actually mean:

  | Button     | Filter                                            |
  |------------|---------------------------------------------------|
  | All Games  | installed || downloaded || peer_count > 0         |
  | Local      | installed || downloaded                           |
  | Installed  | installed                                         |

The "Available" button was renamed "Local" because users do not think of
themselves as a peer; Local means "on my system, whether the archive is
still packed or already installed". "All Games" previously surfaced every
row in the bundled game.db, including catalogue entries that no peer on
the LAN holds — confusing, since those games cannot be acted on. It now
scopes to LAN-reachable games. The `isUnavailable` helper and its
`Unavailable` action label are left in place: with this filter no
displayed game can hit that state today, but the helper is cheap to keep
as a safety net for transient peer-count flips and for a possible future
"also show catalogue-only entries" toggle.

Tooltips were rewritten to a consistent `Show games … on your system` /
`Show all games available on the LAN` pattern, all phrased from the user's
point of view (no "peer" jargon in user-facing strings; doc/code comments
still use "peer" where it reflects the actual protocol).

Two stale comments were dropped along the way: a note on
`getInitialGameDir` that claimed it only sets the directory if not already
set (the function unconditionally calls `setGameDir` when a value is
persisted), and a leftover `// Rest of your component remains the same`
marker from an earlier scaffold.

Test plan:

- `npm --prefix crates/lanspread-tauri-deno-ts exec tsc -- --noEmit`
  passes (run as part of this change).
- `just run`, point the launcher at a game directory holding two installed
  games, then manually `rm -rf` each game's local folder. Within one
  refresh cycle the Installed tab should empty and each game's action
  button should flip to Install / Download as appropriate, without
  needing a restart.
- Start a download and verify the UI does not regress to NotInstalled
  when the next `games-list-updated` arrives mid-flight.
- Cycle through All Games / Local / Installed and confirm membership
  matches the table above; in particular, a game whose archive is
  downloaded but not installed appears under Local and All Games but not
  Installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:22:02 +02:00
ddidderr e7bb14dc7c deps: deno update --latest 2026-05-15 11:14:41 +02:00
ddidderr 5ec47d6c47 deps: upgrade Rust dependencies 2026-05-15 11:10:53 +02:00
ddidderr 2e3d6a9abb update CLAUDE.md, README.md and justfile 2026-05-15 11:07:26 +02:00