From e2c9d683113945050e74eceeb05fd227c3722b9f Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Fri, 24 Jul 2026 22:22:43 +0200 Subject: [PATCH] docs(downloads): mark UR-055/056 Done; fix colliding UT ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The browsable Downloaded library + Transfers split + on-disk usage (f25deba, plus today's grouping/perf fixes) fully implement UR-055 and UR-056, but the requirements doc still listed them and DR-081..085 as Planned. Flip to Done. Also fix UT-id collisions: the downloaded-browse and formatBytes tests reused UT-046..050 (already assigned to smart-cache/playlist tests in the matrix). Reassign to UT-071..078 and register them in §4, including the new music/TV container-rollup and orphan-leaf regression tests. --- docs/requirements.md | 22 +++-- docs/traceability.md | 122 +++++++++++++++++----------- src-tauri/src/repository/offline.rs | 14 ++-- src/lib/utils/formatBytes.test.ts | 2 +- 4 files changed, 99 insertions(+), 61 deletions(-) diff --git a/docs/requirements.md b/docs/requirements.md index a6c39cd7..0115c4b1 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -65,8 +65,8 @@ For a narrative overview of the system design, see | UR-052 | While offline, library pages show only media available on the device by default, with an opt-in toggle that additionally reveals the cached server catalog as greyed-out entries which can be queued for download on the next reconnect | High | Done | | UR-053 | Restrict media downloads to unmetered networks via a "WiFi Only" setting: when enabled, queued downloads are held while the device is on cellular or a metered connection (including metered WiFi hotspots) and resume automatically once an unmetered network is available | Medium | Done (pending device verification) | | UR-054 | Reach account actions (Settings, Downloads, Display preferences, Sign out) from every authenticated screen via a single account menu anchored to the user's name, identical on desktop and mobile (see [ux-flows.md §1.2](ux-flows.md)) | High | Done | -| UR-055 | Browse downloaded media as an offline-scoped library — reusing the same library grids, cards, and detail pages as online browsing, showing only libraries/containers with downloaded content — with the transfer-progress list demoted to a secondary "Transfers" view (see [ux-flows.md §7.2](ux-flows.md)) | High | Planned | -| UR-056 | See how much disk each downloaded item/album/series consumes, in familiar rounded units shown on the card and detail page, with a device total on the Downloaded surface and a reclaim amount stated at the point of removal (see [ux-flows.md §7.3.1](ux-flows.md)) | Medium | Planned | +| UR-055 | Browse downloaded media as an offline-scoped library — reusing the same library grids, cards, and detail pages as online browsing, showing only libraries/containers with downloaded content — with the transfer-progress list demoted to a secondary "Transfers" view (see [ux-flows.md §7.2](ux-flows.md)) | High | Done | +| UR-056 | See how much disk each downloaded item/album/series consumes, in familiar rounded units shown on the card and detail page, with a device total on the Downloaded surface and a reclaim amount stated at the point of removal (see [ux-flows.md §7.3.1](ux-flows.md)) | Medium | Done | | UR-057 | Settings apply the instant a control is changed — no "Save" button and no save/dirty state — so leaving the page never loses a change; sliders show a live readout while dragging but persist on release (see [ux-flows.md §8.1](ux-flows.md)) | Medium | Done | --- @@ -231,11 +231,11 @@ Internal architecture, components, and application logic. | DR-079 | `isConnected` derives from backend-reported server reachability alone; `navigator.onLine` is advisory and may only trigger a recheck, never force or clear the offline state (a reachable LAN server while the browser reports offline, and an unreachable server on a live link, must both resolve correctly) | Connectivity | UR-052, UR-043 | Done | | DR-080 | With the catalog-browse gate off, an empty offline `get_items` result is authoritative "no downloads here" and must be returned as-is; the hybrid repository must not treat it as a cache miss and fall through to the server | Storage | UR-052, UR-013 | Done | | DR-074 | WiFi-only download gate: `NetworkState`/`NetworkType` transport model reported from the platform via `set_network_state`, checked in `pump_download_queue` before starting any pending row (cellular/metered/unknown fail closed, WiFi and Ethernet require `NOT_METERED`); blocked rows stay `pending` and re-pump on network change, with a `waitingForNetwork` event driving the "Waiting for WiFi" notice. Also wires the previously inert Smart Caching / Queue Pre-caching / WiFi Only settings toggles to `CacheConfig` | Downloads | UR-053 | Done (pending device verification) | -| DR-081 | `/downloads` split into a default **Downloaded** browse view and a secondary **Transfers** activity view, with a view switch and a Transfers badge shown only while transfers are active | UI | UR-055 | Planned | -| DR-082 | Offline-scoped browse entry point in the repository client: browse downloaded content only (offline repository `get_items`/`get_libraries` — downloaded items plus their containers) independent of server reachability, without merging server catalog | Storage | UR-055 | Planned | -| DR-083 | Downloaded browse reuses library grids, cards, and detail pages via the offline-scoped source; omits libraries/containers with no downloaded content; badges partially- vs fully-downloaded containers; play uses the local file; remove available at item/album/season/series level | UI | UR-055 | Planned | -| DR-084 | Transfers view renders only in-flight rows (downloading/queued/paused/failed/waiting-for-WiFi) with Pause/Resume/Cancel/Retry; completed transfers leave the view and appear in Downloaded | UI | UR-055 | Planned | -| DR-085 | Per-item on-disk size: stat downloaded files, aggregate to album/season/series subtotals and a device total, format in consistent rounded human units; surface size on cards and detail pages, the device total on the Downloaded surface, and a reclaim figure in the remove confirmation | Downloads | UR-056 | Planned | +| DR-081 | `/downloads` split into a default **Downloaded** browse view and a secondary **Transfers** activity view, with a view switch and a Transfers badge shown only while transfers are active | UI | UR-055 | Done | +| DR-082 | Offline-scoped browse entry point in the repository client: browse downloaded content only (offline repository `get_items`/`get_libraries` — downloaded items plus their containers) independent of server reachability, without merging server catalog | Storage | UR-055 | Done | +| DR-083 | Downloaded browse reuses library grids, cards, and detail pages via the offline-scoped source; omits libraries/containers with no downloaded content; badges partially- vs fully-downloaded containers; play uses the local file; remove available at item/album/season/series level | UI | UR-055 | Done | +| DR-084 | Transfers view renders only in-flight rows (downloading/queued/paused/failed/waiting-for-WiFi) with Pause/Resume/Cancel/Retry; completed transfers leave the view and appear in Downloaded | UI | UR-055 | Done | +| DR-085 | Per-item on-disk size: stat downloaded files, aggregate to album/season/series subtotals and a device total, format in consistent rounded human units; surface size on cards and detail pages, the device total on the Downloaded surface, and a reclaim figure in the remove confirmation | Downloads | UR-056 | Done | | DR-086 | Settings page persists each control on change via per-group writers (`playerSetAudioSettings` / `playerSetVideoSettings` / `updateCacheConfig`) rather than a batch Save action; slider controls persist on `change` (pointer release) not each `input` tick; no Save button, `saving`, or `saveMessage` state | Settings | UR-057 | Done | --- @@ -378,6 +378,14 @@ Internal architecture, components, and application logic. | UT-069 | `isConnected` follows backend reachability alone: false when the server is unreachable on a live link, true for a reachable server while `navigator.onLine` is false | DR-079 | Done | | UT-070 | Hybrid `get_items` returns an empty offline result as-is when the catalog-browse gate is off, without querying the server | DR-080 | Done | | UT-066 | WiFi-only download gate: cellular and metered WiFi blocked, unmetered WiFi/Ethernet allowed, unknown/none fail closed, desktop default ungated; plus the frontend network reporter (transport reporting, change subscription, teardown, fail-open queries) | DR-074 | Done | +| UT-071 | Byte-size formatter: zero/negative/non-finite → "0 B"; decimal unit thresholds; 2–3 significant-figure banding; trailing-zero trimming; largest-unit cap | DR-085 | Done | +| UT-072 | Downloaded-only browse returns a downloaded leaf and its container, filtered to the requested album parent; a non-downloaded sibling is omitted | DR-082, DR-083 | Done | +| UT-073 | An empty downloaded-only browse is authoritative — no rows, no error — regardless of the catalog-browse flag | DR-082 | Done | +| UT-074 | Only libraries with downloaded content are listed; an empty one is omitted | DR-082 | Done | +| UT-075 | Disk usage reports a leaf's own size, a container's summed descendants, and reconciles the device total with the sum of leaves | DR-085 | Done | +| UT-076 | Downloaded library browse lists album containers, not their individual tracks; drilling into the album returns the tracks | DR-082, DR-083 | Done | +| UT-077 | Downloaded TV library browse lists the series, not seasons/episodes; drilling returns the season then the episode | DR-082, DR-083 | Done | +| UT-078 | A downloaded leaf with no cached container (e.g. a movie) still surfaces at the library level | DR-082, DR-083 | Done | ### Integration Tests diff --git a/docs/traceability.md b/docs/traceability.md index 36f52959..8de7a18b 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -1,11 +1,11 @@ # Code Traceability Matrix -**Generated:** 7/24/2026, 9:05:27 PM +**Generated:** 7/24/2026, 10:21:18 PM ## Summary - **Total Files Scanned:** 284 -- **Total TRACES Found:** 268 +- **Total TRACES Found:** 269 - **Requirements Covered:** - User Requirements (UR): 53 - Integration Requirements (IR): 14 @@ -908,7 +908,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-039 -**Locations:** 8 file(s) +**Locations:** 9 file(s) - **File:** [`src/routes/library/music/+page.svelte`](src/routes/library/music/+page.svelte#L1) - **Line:** 1 @@ -919,6 +919,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/routes/library/tv/+page.svelte`](src/routes/library/tv/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L37) + - **Line:** 37 + - **Context:** `Unknown` - **File:** [`src/lib/components/home/Carousel.svelte`](src/lib/components/home/Carousel.svelte#L1) - **Line:** 1 - **Context:** `Unknown` @@ -2866,7 +2869,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-034 -**Locations:** 10 file(s) +**Locations:** 11 file(s) - **File:** [`src/routes/library/music/+page.svelte`](src/routes/library/music/+page.svelte#L1) - **Line:** 1 @@ -2877,12 +2880,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/routes/library/tv/+page.svelte`](src/routes/library/tv/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/lib/components/home/Carousel.svelte`](src/lib/components/home/Carousel.svelte#L1) - - **Line:** 1 +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L37) + - **Line:** 37 - **Context:** `Unknown` - **File:** [`src/lib/components/home/HeroBanner.svelte`](src/lib/components/home/HeroBanner.svelte#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src/lib/components/home/Carousel.svelte`](src/lib/components/home/Carousel.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` - **File:** [`src/lib/stores/home.ts`](src/lib/stores/home.ts#L2) - **Line:** 2 - **Context:** `Unknown` @@ -3419,14 +3425,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 2 - **Context:** `Unknown` -### UT-050 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/utils/formatBytes.test.ts`](src/lib/utils/formatBytes.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - ### UT-052 **Locations:** 1 file(s) @@ -3486,6 +3484,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 4 - **Context:** `Unknown` +### UT-071 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/utils/formatBytes.test.ts`](src/lib/utils/formatBytes.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` + ### UR-016 **Locations:** 2 file(s) @@ -3838,38 +3844,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 136 - **Context:** `Unknown` -### UT-046 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2686) - - **Line:** 2686 - - **Context:** `Unknown` - -### UT-047 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2809) - - **Line:** 2809 - - **Context:** `Unknown` - -### UT-048 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2828) - - **Line:** 2828 - - **Context:** `Unknown` - -### UT-049 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2850) - - **Line:** 2850 - - **Context:** `Unknown` - ### UT-051 **Locations:** 1 file(s) @@ -3900,4 +3874,60 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 2385 - **Context:** `Unknown` +### UT-072 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2686) + - **Line:** 2686 + - **Context:** `Unknown` + +### UT-073 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2809) + - **Line:** 2809 + - **Context:** `Unknown` + +### UT-074 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2828) + - **Line:** 2828 + - **Context:** `Unknown` + +### UT-075 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2850) + - **Line:** 2850 + - **Context:** `Unknown` + +### UT-076 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2708) + - **Line:** 2708 + - **Context:** `Unknown` + +### UT-077 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2742) + - **Line:** 2742 + - **Context:** `Unknown` + +### UT-078 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2788) + - **Line:** 2788 + - **Context:** `Unknown` + diff --git a/src-tauri/src/repository/offline.rs b/src-tauri/src/repository/offline.rs index 208d4a0c..469f0ec4 100644 --- a/src-tauri/src/repository/offline.rs +++ b/src-tauri/src/repository/offline.rs @@ -2683,7 +2683,7 @@ mod tests { /// UT: downloaded-only browse returns a downloaded leaf AND its container, /// filtered to the requested album parent. A non-downloaded sibling is omitted. /// - /// TRACES: UR-055 | DR-082, DR-083 | UT-046 + /// TRACES: UR-055 | DR-082, DR-083 | UT-072 #[tokio::test] async fn test_get_downloaded_items_returns_leaf_and_container() { let db = create_test_db(); @@ -2705,7 +2705,7 @@ mod tests { /// not their leaves — a music library shows the album, not the individual /// downloaded songs. The leaf is still reachable by drilling into the album. /// - /// TRACES: UR-055 | DR-082, DR-083 + /// TRACES: UR-055 | DR-082, DR-083 | UT-076 #[tokio::test] async fn test_get_downloaded_items_library_lists_albums_not_tracks() { let db = create_test_db(); @@ -2739,7 +2739,7 @@ mod tests { /// Episodes — the same "individual songs" bug seen for music, for TV. The /// season and episode are still reachable by drilling into the series. /// - /// TRACES: UR-055 | DR-082, DR-083 + /// TRACES: UR-055 | DR-082, DR-083 | UT-077 #[tokio::test] async fn test_get_downloaded_items_library_lists_series_not_episodes() { let db = create_test_db(); @@ -2785,7 +2785,7 @@ mod tests { /// A downloaded leaf with no cached container (e.g. a Movie, or a track whose /// album isn't in the cache) still surfaces at the library level. /// - /// TRACES: UR-055 | DR-082, DR-083 + /// TRACES: UR-055 | DR-082, DR-083 | UT-078 #[tokio::test] async fn test_get_downloaded_items_library_keeps_orphan_leaves() { let db = create_test_db(); @@ -2806,7 +2806,7 @@ mod tests { /// UT: an empty downloaded-only browse is authoritative — no rows, no error, /// regardless of the catalog-browse flag (which the DR-080 fallthrough uses). /// - /// TRACES: UR-055 | DR-082 | UT-047 + /// TRACES: UR-055 | DR-082 | UT-073 #[tokio::test] async fn test_get_downloaded_items_empty_is_authoritative() { let db = create_test_db(); @@ -2825,7 +2825,7 @@ mod tests { /// UT: only libraries with downloaded content are listed; an empty one is omitted. /// - /// TRACES: UR-055 | DR-082 | UT-048 + /// TRACES: UR-055 | DR-082 | UT-074 #[tokio::test] async fn test_get_downloaded_libraries_omits_empty() { let db = create_test_db(); @@ -2847,7 +2847,7 @@ mod tests { /// UT: disk usage reports a leaf's own size, a container's summed descendants, /// and reconciles the device total with the sum of leaves. /// - /// TRACES: UR-056 | DR-085 | UT-049 + /// TRACES: UR-056 | DR-085 | UT-075 #[tokio::test] async fn test_download_disk_usage_aggregates_containers() { let db = create_test_db(); diff --git a/src/lib/utils/formatBytes.test.ts b/src/lib/utils/formatBytes.test.ts index 6f21aa74..74039776 100644 --- a/src/lib/utils/formatBytes.test.ts +++ b/src/lib/utils/formatBytes.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { formatBytes } from "./formatBytes"; -// TRACES: UR-056 | DR-085 | UT-050 +// TRACES: UR-056 | DR-085 | UT-071 describe("formatBytes", () => { it("renders zero and non-positive as '0 B'", () => { expect(formatBytes(0)).toBe("0 B");