Preserve local playback when it is authoritative, make renderer conflicts explicit and persist the selected policy. Filter unsupported queue rows, restore portable Qobuz identities after disconnect, and fence takeover commands to avoid stale queue and playback loops.
cx
Add `is_set_volume_action` to PendingQueueAction (set for CtrlSrvrSetVolume) and
complete it on the SrvrCtrlVolumeChanged echo, so rapid volume changes can
supersede the in-flight one instead of colliding on the single pending slot (the
"pending queue action already active" spam). The frontend adapter clears the
superseded pending before sending the next SetVolume (lands with the qbz-slint
controller changes).
cc
Move the frontend-agnostic pure functions and enums out of the Tauri
qconnect adapter into the shared crates, with zero behavior change. The
Tauri side keeps thin re-exports so all existing call sites compile
unchanged.
Into qconnect-app (new `session` module):
- should_arm_renderer_watchdog
- compute_connection_state + ServerActiveState + ConnectionDecision
- deferred_join_reason, should_reask_queue_state
- RendererStatus + from_wire
- quality_from_max_audio_quality
- QCONNECT_RENDERER_LOST_TIMEOUT_MS + JoinSession reason consts
Into qconnect-core (next to evaluate_remote_queue_admission):
- validate_track_origins_for_admission (pure over TrackOrigin); the
Tauri command wrapper stays thin and only maps wire origins to core
origins.
The 6 pure-fn unit tests move into the destination crates' test
modules. R5 honored: compute_connection_state matrix cells are relocated
verbatim, not changed.
cc
Decoder now reads field #100 (previously decoded-then-dropped) on QueueState
and carries it through to QConnectQueueState.last_server_queue_hash via the
QueueStateReplaced reducer path (full-state replace already clones it).
cc
Cross-check test now pins admission to is_castable_to_qconnect (incl. OfflineCache)
instead of the streamable predicate. Dedupe the queuePlaybackService test builder.
Document is_castable_to_qconnect/from_source_str_strict/TrackOriginTag as the shared
QConnect-admission gate consumed by the Slint port.
cc
Three sites disagreed: model blocked OfflineCache, live gate allowed it, TS
sync filter blocked qobuz_download. Decision: ALLOW — an offline copy's queue
id is the real Qobuz track id. Flip the TS sync filter and fix the stale model
doc comment to match the live gate.
Also add Rust lock-in tests (offline_cache_is_castable / offline_cache_is_admitted)
and restore the missing $app/environment vitest mock the config already aliases,
so queuePlaybackService.test.ts can load.
cc
is_qobuz_streamable was dead code. Add is_castable_to_qconnect as the
single source of truth and depend qconnect-core on qbz-models so the
admission gate and the cast predicate derive from one enum.
cc
Root cause: server pause/resume commands send None for position and
tracks, which the renderer state blindly overwrote. Then renderer
reports sent null position to the server, making controllers show the
track as finished.
Three-part fix:
1. renderer.rs: only overwrite position/tracks when server provides
a non-None value (retain last known values through pause/resume)
2. app.rs + qconnect_service.rs: update renderer state position from
frontend periodic reports so the internal state always has the
real playback position
3. +page.svelte: increase position report frequency from 5s to 2s
for smoother seekbar updates on controllers