Add specs for the account menu, downloads-as-offline-library, offline downloaded-only filter, and scoped search (+ boundary revision). Add the new UR/DR entries to requirements.md, update ux-flows, and regenerate the traceability matrix. TRACES: UR-049, UR-050, UR-052, UR-053, UR-054, UR-055, UR-056
11 KiB
Spec: Offline "downloaded only" filtering (issue #10)
Status: Implemented Scope: Frontend (connectivity store) + Rust (hybrid repository). No new commands, no schema changes, no UI additions. Requirements: UR-052 → DR-078, DR-079, DR-080 (see requirements.md). Tracking: issue #10 — "when offline the filter to show only downloaded media does not work."
Summary
Offline, a library page is supposed to show only media on the device, with a "Show all server media" toggle that additionally reveals the cached server catalog greyed out (queueable for download on reconnect). In practice the toggle does not gate the listing — every server item still appears. This spec fixes that with two independent changes; either one alone leaves the bug visible.
Background: what already exists
Verified in code. The feature is built and mostly correct — this is a two-point repair, not new infrastructure. Do not rebuild the toggle, the command, or the SQL gate.
-
The SQL gate works and is unit-tested. offline.rs —
get_itemsappends the synced-catalogUNIONbranch only wheninclude_catalog_browse()is true; with it false, only downloaded/local rows return. Guarded bytest_get_items_toggle_gates_synced_catalog(UT-067). Do not touch the query. -
The toggle → backend path is wired. The
showServerCatalogstore and theset_show_server_catalogcommand (catalog.rs) drive the process-wideINCLUDE_CATALOG_BROWSEflag.pushCatalogVisibilityin offlineCatalog.ts computesinclude = connected || showCatalogand pushes it on every change. -
Home-screen queries are already downloads-only.
get_latest_items,get_resume_items,get_recently_played_audio,get_resume_moviesallINNER JOIN downloads ... status = 'completed'. They are unaffected — leave them. -
MediaCardalready greys and queues. MediaCard.svelte —isServerOnlyrenders the greyed, inert card with a queue button; the queued row heals itsstream_urlon reconnect via the offlineCatalog service. Leave it.
The two defects
Defect A — offline is never actually entered (DR-079)
pushCatalogVisibility keys off isConnected, but
connectivity.ts derives:
isConnected = isOnline && isServerReachable // isOnline = navigator.onLine
navigator.onLine is documented in that same file as advisory only — the
Rust ConnectivityMonitor is the source of truth (principle: reachability from
real traffic, DR-055). When the server is unreachable but the device link is
up (server down, wrong LAN, VPN dropped), isOnline stays true, so isConnected
stays true, so include stays true, so the backend keeps returning the full
catalog. The user is "offline" in every meaningful sense but the toggle never
gets a chance to gate anything.
This is the primary cause: it explains why the filter looks dead rather than merely inverted — the gate never closes.
Defect B — an intentionally empty result falls through to the server (DR-080)
With the gate off and nothing downloaded in a library, offline get_items
correctly returns few or zero rows. But
hybrid.rs treats a cache result as a
hit only if data.has_content(). An empty offline result is indistinguishable
from a cache miss, so HybridRepository::get_items (and parallel_race, used by
~10 other reads) falls through to the server and returns the full server list —
re-defeating the filter even after Defect A is fixed.
Design
Fix A: isConnected follows backend reachability alone (DR-079)
In connectivity.ts, redefine the derived store:
export const isConnected = derived(
connectivity,
($c) => $c.isServerReachable
);
navigator.onLine stays wired to what it is good for — a trigger for an
immediate recheck (online/offline listeners already call
checkServerReachable()); it must no longer be a term in the offline decision.
Leave isOnline on the state object and the listeners intact.
Consider whether the optimistic isServerReachable: true startup default
(connectivity.ts) should hold until the
first real check resolves. Keep it — flipping the app to "offline" on launch is a
worse regression than a brief full-catalog flash before the first probe. Note the
choice in a comment.
Blast radius — this is the reason this is a spec, not a patch. isConnected
is consumed beyond this feature (banners, MediaCard, mini-player gating,
anything importing it). Enumerate consumers first:
grep -rn "isConnected" src/ | grep -v node_modules
For each, confirm "server unreachable" (not "device link down") is the correct trigger. It almost always is — that is the whole point of the reachability model — but verify rather than assume, and call out anything that genuinely wanted the device link in the PR description.
Fix B: an empty offline result is authoritative when the gate is off (DR-080)
The backend must distinguish "cache is cold, go ask the server" from "user asked for downloads only and there are none here." The gate flag already encodes intent — reuse it.
Add a getter beside the existing setter in offline.rs:
pub fn include_catalog_browse() -> bool { /* pub, already exists privately */ }
In hybrid.rs get_items: when
!include_catalog_browse(), treat the offline result as authoritative and return
it as-is even when empty — do not spawn/await the server fallback for this
call. When the flag is on (online fast-path, or offline with the toggle on),
behaviour is unchanged: empty cache still falls through to the server.
Keep it surgical:
- Scope the change to
get_items. The gate is aget_itemsconcept; do not thread it intoparallel_raceor the other readers, which have no catalog gate and legitimately want the server on an empty cache. - Preserve the online path exactly: with the flag on (its default, and always so while reachable) the method behaves as it does today, including the background cache refresh on a hit.
- The flag is process-global
Relaxed; it is set from the frontend before the query. That ordering already holds for the SQL gate — no new synchronization.
Why both
Fix A closes the gate; Fix B stops the hybrid from re-opening it. A alone: with
downloads present the list still gets padded by the server fallback whenever a
library's cache is thin. B alone: the gate never closes because isConnected
never goes false on a live link. Ship them together.
Out of scope
- The SQL gate, the toggle, the command,
INCLUDE_CATALOG_BROWSE— all correct. MediaCardgreying / queue-on-reconnect — correct.- Home-screen and resume queries — already downloads-only.
- The Rust
ConnectivityMonitorreachability logic itself — unchanged; this spec only stops the frontend from diluting its verdict withnavigator.onLine. - Any new IPC command, DB column, or settings entry.
- Making the "Show all server media" toggle reachable from Settings (that is a UX-placement question, tracked separately under UR-051's toggle note).
Acceptance criteria
- [~] With the server unreachable on a live device link, a library page lists only downloaded media when the toggle is off (IT-016 — pending e2e; unit coverage via UT-069 + gate tests).
- Turning the toggle on reveals the greyed-out cached catalog; turning it off hides it again — without leaving/re-entering the page (SQL gate + toggle wiring unchanged; UT-068 confirms the flag is pushed on toggle change).
- A library with downloads and a thin cache does not get padded with non-downloaded server items when offline with the toggle off (Defect B — UT-070: gate off + empty offline result returned as-is, server not queried).
isConnectedis false whenever the server is unreachable, regardless ofnavigator.onLine; true for a reachable server even if the browser reports offline (UT-069).- Every existing
isConnectedconsumer still behaves correctly (banner in+layout.svelte,MediaCard,favorites.tsserver-write skip — all want "server unreachable", which is the new semantics;CastButton's localisConnectedis unrelated). Full frontend suite (616 tests) green. - Online behaviour is unchanged: with the flag on (its default, always so
while reachable)
get_itemskeeps the offline fast-path and background refresh (UT-067 + gate-on fall-through test). - [~] A download queued from a greyed offline card resolves and starts on reconnect (IT-017 — regression check, no code change; offlineCatalog resume path untouched).
bun run check,bun run test, andbun run test:rustpass;cd src-tauri && cargo fmt && cargo clippyclean (no new warnings in the touched files).
Testing
Rust (offline.rs / hybrid.rs test modules):
- UT-070 — hybrid
get_itemswith the gate off returns an empty offline result as-is and does not query the server. Assert via a mock online repo whoseget_itemsbumps a call counter that must stay at zero. - Gate on + empty cache still falls through to the server (guard the online path).
- UT-067 (
test_get_items_toggle_gates_synced_catalog) must still pass untouched.
Frontend (vitest, src/lib/**/*.test.ts):
- UT-069 —
isConnectedfollowsisServerReachablealone: false when unreachable withnavigator.onLine === true; true when reachable withnavigator.onLine === false. - UT-068 —
pushCatalogVisibilityresolvesserverReachable || showCatalogand pushes to the backend on a change of either input (extend the existing offlineCatalog tests).
Integration (IT-016, IT-017) are documented as pending in requirements.md; wire them if the e2e harness can simulate an unreachable-server-on-live-link state, otherwise leave them pending with a note.
New/changed requirement code keeps its TRACES: comments — see
CLAUDE.md. The affected files already carry tags:
connectivity.ts (… | DR-079), hybrid.rs (… | DR-080), offline.rs
(… | DR-078). Update the getter's tag when you expose it.
Notes for the implementer
- Read docs/architecture/07-connectivity.md before Fix A — it is the canonical statement of the reachability model this fix restores fidelity to.
- Fix B relies on the frontend having pushed the flag before the query runs; that ordering already holds for the SQL gate today. No new locking.
- Another session is active in this repo (WiFi-only downloads, account menu
landed alongside this work). Check
git diffbefore "repairing" unexpected changes, and expect requirement IDs around UR-052 / DR-078 to be adjacent to other new rows.