Compare commits

..
2 Commits
Author SHA1 Message Date
dtourolle fd1277746d chore(release): v0.13.3
🏗️ Build and Test JellyTau / Run Tests (push) Skipped
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 3m57s
📱 Test APK / Build test APK (push) Successful in 20m27s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 3m45s
Traceability Validation / Check Requirement Traces (push) Successful in 11s
Build & Release / Run Tests (push) Successful in 10m38s
Build & Release / Build Linux (push) Successful in 14m21s
Build & Release / Build Windows (push) Successful in 10m47s
Build & Release / Build Android (push) Successful in 20m26s
Build & Release / Create Release (push) Successful in 43s
Resuming the app after time in the background no longer replaces the page
on screen with "Failed to load item".
2026-09-24 07:40:35 -04:00
dtourolle d7136aef48 fix(library): resuming the app no longer blanks the page on screen
Coming back to the app after a few minutes in the background replaced the
Frasier series page with "Failed to load item". Android cuts a backgrounded
app's network, the app declares the server offline, and on resume the
reconnect and offline-filter reloads refresh the page. Every backend call in
that refresh answered from the cache, yet something in it threw, and:

- any throw replaced the whole page with an error, although it was a
  refresh of content already on screen;
- no later successful reload of the same item cleared that error, so it
  stayed until the viewer navigated away;
- the catch logged nothing and turned every non-Error value (backend
  errors arrive as plain strings) into the generic text, so neither logcat
  nor the screen said what failed.

A failed refresh now keeps the page and logs the value actually thrown;
every successful load clears the error; failing to open an item still shows
one, with the backend's own message. The decision lives in
detailLoadError.ts so it is unit-tested.

The throw itself is not yet identified - the next occurrence names itself
in the log.

DR-297, UT-267.
2026-09-24 07:40:14 -04:00
10 changed files with 179 additions and 51 deletions
+12
View File
@@ -9,6 +9,18 @@ generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
For how long each fixed defect had been shipping before it was found, see
[docs/defect-windows.md](docs/defect-windows.md).
## v0.13.3
### 🐛 Fixes
- **Coming back to the app no longer replaces the page with "Failed to load
item".** After a few minutes in the background, resuming the app on a series
page could swap the whole page for that error, and it stayed until you
navigated away. The page refreshes itself on resume; a refresh that fails now
leaves what you were looking at on screen, and the error clears as soon as a
load succeeds. When a page genuinely cannot open, it now says why instead of
the generic message. (DR-297)
## v0.13.2
A series opens with its episodes in under a second. v0.13.1 fixed the season
+3 -1
View File
@@ -498,6 +498,7 @@ Internal architecture, components, and application logic.
| DR-294 | A download plays with no network. Playing a downloaded item asked the server for its `PlaybackInfo` — only to read the media-source id that subtitle URLs are keyed by — and `HybridRepository::get_playback_info` went to the server alone, so offline the call retried for seven seconds, failed, and the file on disk was never opened. A completed download for the current user now answers playback info from its download row, first and regardless of reachability: the local path, direct play, and the item id as media-source id (a download names no source, so the server served its default, which carries the item's id). Next Up had the same shape — server-only — and the TV landing page loads it in one `Promise.all` with its other rows, so offline that single failure blanked the whole page with Continue Watching and Latest sitting in the cache; it now falls back to the cache when the server cannot answer. And a slow cache read is waited for, never discarded: the cache is one SQLite connection behind one mutex, so any write in progress (the catalog sync at every launch, a download finishing) pushes a read past the 100 ms fast path, and `get_items`, the library list, genres and playlist items discarded such a read, waited on the server, and offline returned its error over data on disk — "More info" on a downloaded show failed exactly so. They keep the read running (`cache_try`) and wait for it when the server fails (`settle`); the cache-only reads (search, favourites) simply await the cache | Repository | UR-002, UR-071 | Done |
| DR-295 | A series page lists its episodes with one concurrent season fan-out. "More info" on Frasier took ~10 s: the page asked Rust for the episodes and for the current episode as two commands, each of which walked every season, and each walk fetched the eleven seasons one after another — so the wait was the sum of twenty-two listings, each a cache read slowed by whatever the database was writing (the catalog sync at launch measured it at ~4 s per walk). The seasons are now fetched together (`gather_season_episodes`, so the wait is the slowest season), and `repository_get_series_view` returns the episodes and the current episode from one walk, with Next Up and resume fetched alongside it | Repository | UR-062 | Done |
| DR-296 | Returning from background audio resumes the item the native player is actually on, not the one the video page was mounted with. An episode that ends while backgrounded advances in the backend (`advance_to_next_episode_audio_only`), but `player_exit_background_audio` returned only a position, so the webview reloaded the *previous* episode at the new episode's timestamp. The command now returns `BackgroundAudioResume { itemId, positionSeconds }` (`PlayerController::background_audio_resume`); `planHandoffReturn` yields `other-item` when the id differs from the mounted one, and the player page navigates to that episode with `resumeAt=<seconds>`, recording the outgoing episode as watched and suppressing the stale unmount stop report | Playback | UR-040, UR-023 | Done (pending device verification) |
| DR-297 | A background refresh never blanks a library detail page that is on screen, and every load that succeeds clears the page's error. Resuming the app after a few minutes in the background replaced the Frasier series page with "Failed to load item": the resume reload (reconnect / offline-filter change) is a refresh of content the cache had already answered, but any throw in it replaced the page with an error that no later successful reload of the same item cleared — and the catch logged nothing and turned backend errors (plain strings) into the generic text. A failed refresh now keeps the page and logs the thrown value; only failing to open an item shows an error, with the backend's own message | UI | UR-062 | Done |
---
@@ -567,7 +568,7 @@ Internal architecture, components, and application logic.
| UR-058 | - | DR-087, DR-142 |
| UR-060 | - | DR-090, DR-091, DR-111 |
| UR-061 | - | DR-092 |
| UR-062 | - | DR-101, DR-102, DR-103, DR-104, DR-107, DR-295 |
| UR-062 | - | DR-101, DR-102, DR-103, DR-104, DR-107, DR-295, DR-297 |
| UR-063 | - | DR-105 |
| UR-064 | - | DR-106 |
| UR-065 | IR-030 | DR-108, DR-109, DR-110, DR-111 |
@@ -859,6 +860,7 @@ Internal architecture, components, and application logic.
| UT-264 | Ten seasons whose listings each take 100 ms are gathered in well under the 1 s a sequential walk takes, and a season that fails to load leaves the other nine seasons' episodes in the result | DR-295 | Done |
| UT-265 | `planHandoffReturn` switches to the item the backend advanced to while backgrounded, and reloads in place when the backend is still on the mounted item or reports none | DR-296 | Done |
| UT-266 | After a background-audio episode advance, the controller's resume point names the new episode and carries no base from the previous one | DR-296 | Done |
| UT-267 | A failed refresh of a detail page already on screen shows no error, a successful load clears any error, a failure to open an item shows its message, and a backend error's plain-string text is shown rather than a generic fallback | DR-297 | Done |
### Integration Tests
| Test ID | Test Description | Traces To | Status |
+75 -44
View File
@@ -1,15 +1,15 @@
# Code Traceability Matrix
**Generated:** 9/24/2026, 4:46:58 AM
**Generated:** 9/24/2026, 7:40:34 AM
## Summary
- **Total Files Scanned:** 444
- **Total TRACES Found:** 1458
- **Total Files Scanned:** 446
- **Total TRACES Found:** 1460
- **Requirements Covered:**
- User Requirements (UR): 84
- Integration Requirements (IR): 27
- Development Requirements (DR): 252
- Development Requirements (DR): 253
- Jellyfin API Requirements (JA): 35
## Requirements by Type
@@ -26,7 +26,7 @@ IR-003, IR-004, IR-006, IR-008, IR-009, IR-010, IR-011, IR-012, IR-013, IR-014,
### Development Requirements (DR)
```
DR-001, DR-002, DR-003, DR-004, DR-005, DR-006, DR-007, DR-009, DR-010, DR-011, DR-012, DR-013, DR-014, DR-015, DR-016, DR-017, DR-018, DR-020, DR-021, DR-022, DR-023, DR-024, DR-025, DR-026, DR-027, DR-028, DR-029, DR-030, DR-032, DR-033, DR-034, DR-035, DR-036, DR-037, DR-038, DR-039, DR-040, DR-041, DR-042, DR-043, DR-044, DR-045, DR-047, DR-048, DR-049, DR-050, DR-051, DR-052, DR-053, DR-054, DR-055, DR-056, DR-057, DR-058, DR-059, DR-060, DR-061, DR-062, DR-063, DR-064, DR-065, DR-066, DR-067, DR-068, DR-069, DR-070, DR-074, DR-075, DR-076, DR-077, DR-078, DR-079, DR-080, DR-081, DR-082, DR-083, DR-084, DR-085, DR-086, DR-087, DR-088, DR-089, DR-090, DR-091, DR-092, DR-093, DR-095, DR-096, DR-097, DR-098, DR-099, DR-100, DR-101, DR-102, DR-103, DR-104, DR-105, DR-106, DR-107, DR-108, DR-109, DR-110, DR-111, DR-112, DR-113, DR-114, DR-115, DR-116, DR-117, DR-118, DR-119, DR-120, DR-121, DR-123, DR-126, DR-127, DR-128, DR-129, DR-130, DR-131, DR-132, DR-133, DR-134, DR-135, DR-136, DR-137, DR-140, DR-141, DR-142, DR-143, DR-147, DR-148, DR-149, DR-150, DR-151, DR-152, DR-153, DR-154, DR-155, DR-156, DR-157, DR-158, DR-159, DR-160, DR-162, DR-167, DR-168, DR-169, DR-170, DR-171, DR-173, DR-174, DR-175, DR-176, DR-177, DR-178, DR-179, DR-180, DR-181, DR-182, DR-183, DR-185, DR-186, DR-188, DR-189, DR-193, DR-195, DR-196, DR-197, DR-198, DR-201, DR-203, DR-204, DR-205, DR-206, DR-207, DR-209, DR-210, DR-211, DR-212, DR-213, DR-215, DR-216, DR-217, DR-218, DR-219, DR-220, DR-221, DR-223, DR-224, DR-225, DR-226, DR-227, DR-228, DR-230, DR-231, DR-232, DR-233, DR-234, DR-235, DR-238, DR-239, DR-240, DR-241, DR-242, DR-243, DR-244, DR-245, DR-246, DR-250, DR-251, DR-252, DR-253, DR-254, DR-255, DR-256, DR-257, DR-258, DR-259, DR-262, DR-263, DR-264, DR-265, DR-266, DR-267, DR-268, DR-269, DR-270, DR-271, DR-272, DR-274, DR-276, DR-277, DR-278, DR-279, DR-280, DR-281, DR-282, DR-283, DR-284, DR-285, DR-286, DR-287, DR-288, DR-289, DR-290, DR-291, DR-292, DR-293, DR-294, DR-295, DR-296
DR-001, DR-002, DR-003, DR-004, DR-005, DR-006, DR-007, DR-009, DR-010, DR-011, DR-012, DR-013, DR-014, DR-015, DR-016, DR-017, DR-018, DR-020, DR-021, DR-022, DR-023, DR-024, DR-025, DR-026, DR-027, DR-028, DR-029, DR-030, DR-032, DR-033, DR-034, DR-035, DR-036, DR-037, DR-038, DR-039, DR-040, DR-041, DR-042, DR-043, DR-044, DR-045, DR-047, DR-048, DR-049, DR-050, DR-051, DR-052, DR-053, DR-054, DR-055, DR-056, DR-057, DR-058, DR-059, DR-060, DR-061, DR-062, DR-063, DR-064, DR-065, DR-066, DR-067, DR-068, DR-069, DR-070, DR-074, DR-075, DR-076, DR-077, DR-078, DR-079, DR-080, DR-081, DR-082, DR-083, DR-084, DR-085, DR-086, DR-087, DR-088, DR-089, DR-090, DR-091, DR-092, DR-093, DR-095, DR-096, DR-097, DR-098, DR-099, DR-100, DR-101, DR-102, DR-103, DR-104, DR-105, DR-106, DR-107, DR-108, DR-109, DR-110, DR-111, DR-112, DR-113, DR-114, DR-115, DR-116, DR-117, DR-118, DR-119, DR-120, DR-121, DR-123, DR-126, DR-127, DR-128, DR-129, DR-130, DR-131, DR-132, DR-133, DR-134, DR-135, DR-136, DR-137, DR-140, DR-141, DR-142, DR-143, DR-147, DR-148, DR-149, DR-150, DR-151, DR-152, DR-153, DR-154, DR-155, DR-156, DR-157, DR-158, DR-159, DR-160, DR-162, DR-167, DR-168, DR-169, DR-170, DR-171, DR-173, DR-174, DR-175, DR-176, DR-177, DR-178, DR-179, DR-180, DR-181, DR-182, DR-183, DR-185, DR-186, DR-188, DR-189, DR-193, DR-195, DR-196, DR-197, DR-198, DR-201, DR-203, DR-204, DR-205, DR-206, DR-207, DR-209, DR-210, DR-211, DR-212, DR-213, DR-215, DR-216, DR-217, DR-218, DR-219, DR-220, DR-221, DR-223, DR-224, DR-225, DR-226, DR-227, DR-228, DR-230, DR-231, DR-232, DR-233, DR-234, DR-235, DR-238, DR-239, DR-240, DR-241, DR-242, DR-243, DR-244, DR-245, DR-246, DR-250, DR-251, DR-252, DR-253, DR-254, DR-255, DR-256, DR-257, DR-258, DR-259, DR-262, DR-263, DR-264, DR-265, DR-266, DR-267, DR-268, DR-269, DR-270, DR-271, DR-272, DR-274, DR-276, DR-277, DR-278, DR-279, DR-280, DR-281, DR-282, DR-283, DR-284, DR-285, DR-286, DR-287, DR-288, DR-289, DR-290, DR-291, DR-292, DR-293, DR-294, DR-295, DR-296, DR-297
```
### Jellyfin API Requirements (JA)
@@ -2717,8 +2717,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
**Locations:** 17 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L219)
- **Line:** 219
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L220)
- **Line:** 220
- **Context:** `Unknown`
- **File:** [`src/lib/api/repository-client.ts`](../src/lib/api/repository-client.ts#L157)
- **Line:** 157
@@ -2776,11 +2776,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1)
- **Line:** 1
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L219)
- **Line:** 219
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L220)
- **Line:** 220
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L324)
- **Line:** 324
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L329)
- **Line:** 329
- **Context:** `async function handlePlayAll() {`
- **File:** [`src/lib/components/library/EpisodeRow.svelte`](../src/lib/components/library/EpisodeRow.svelte#L17)
- **Line:** 17
@@ -2802,8 +2802,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1)
- **Line:** 1
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L135)
- **Line:** 135
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L136)
- **Line:** 136
- **Context:** `Unknown`
- **File:** [`src/routes/library/tv/+page.svelte`](../src/routes/library/tv/+page.svelte#L8)
- **Line:** 8
@@ -3191,8 +3191,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
**Locations:** 15 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L617)
- **Line:** 617
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L622)
- **Line:** 622
- **Context:** `Unknown`
- **File:** [`src/lib/components/library/ArtistDetailView.svelte`](../src/lib/components/library/ArtistDetailView.svelte#L138)
- **Line:** 138
@@ -3695,14 +3695,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1)
- **Line:** 1
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L150)
- **Line:** 150
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L151)
- **Line:** 151
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L242)
- **Line:** 242
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L243)
- **Line:** 243
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L465)
- **Line:** 465
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L470)
- **Line:** 470
- **Context:** `Unknown`
- **File:** [`src/routes/search/+page.svelte`](../src/routes/search/+page.svelte#L97)
- **Line:** 97
@@ -3730,8 +3730,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
**Locations:** 11 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L111)
- **Line:** 111
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L112)
- **Line:** 112
- **Context:** `Unknown`
- **File:** [`src/routes/library/music/+page.svelte`](../src/routes/library/music/+page.svelte#L71)
- **Line:** 71
@@ -5971,8 +5971,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
**Locations:** 11 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L186)
- **Line:** 186
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L187)
- **Line:** 187
- **Context:** `Unknown`
- **File:** [`src/lib/api/bindings.ts`](../src/lib/api/bindings.ts#L2477)
- **Line:** 2477
@@ -6990,6 +6990,20 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
- **Line:** 947
- **Context:** `Unknown`
### DR-297
**Locations:** 3 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1)
- **Line:** 1
- **Context:** `Unknown`
- **File:** [`src/lib/components/library/detailLoadError.test.ts`](../src/lib/components/library/detailLoadError.test.ts#L5)
- **Line:** 5
- **Context:** `Unknown`
- **File:** [`src/lib/components/library/detailLoadError.ts`](../src/lib/components/library/detailLoadError.ts#L27)
- **Line:** 27
- **Context:** `Unknown`
### JA-001
**Locations:** 1 file(s)
@@ -8608,8 +8622,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
**Locations:** 54 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L186)
- **Line:** 186
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L187)
- **Line:** 187
- **Context:** `Unknown`
- **File:** [`src/routes/library/music/+page.svelte`](../src/routes/library/music/+page.svelte#L1)
- **Line:** 1
@@ -10556,8 +10570,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
**Locations:** 30 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L111)
- **Line:** 111
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L112)
- **Line:** 112
- **Context:** `Unknown`
- **File:** [`src/routes/library/music/+page.svelte`](../src/routes/library/music/+page.svelte#L71)
- **Line:** 71
@@ -10950,14 +10964,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1)
- **Line:** 1
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L150)
- **Line:** 150
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L151)
- **Line:** 151
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L242)
- **Line:** 242
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L243)
- **Line:** 243
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L465)
- **Line:** 465
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L470)
- **Line:** 470
- **Context:** `Unknown`
- **File:** [`src/routes/search/+page.svelte`](../src/routes/search/+page.svelte#L97)
- **Line:** 97
@@ -10983,19 +10997,19 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
### UR-062
**Locations:** 40 file(s)
**Locations:** 42 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1)
- **Line:** 1
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L135)
- **Line:** 135
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L136)
- **Line:** 136
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L219)
- **Line:** 219
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L220)
- **Line:** 220
- **Context:** `Unknown`
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L324)
- **Line:** 324
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L329)
- **Line:** 329
- **Context:** `async function handlePlayAll() {`
- **File:** [`src/lib/api/repository-client.ts`](../src/lib/api/repository-client.ts#L157)
- **Line:** 157
@@ -11036,6 +11050,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
- **File:** [`src/lib/components/library/seriesNavigation.ts`](../src/lib/components/library/seriesNavigation.ts#L13)
- **Line:** 13
- **Context:** `Unknown`
- **File:** [`src/lib/components/library/detailLoadError.test.ts`](../src/lib/components/library/detailLoadError.test.ts#L5)
- **Line:** 5
- **Context:** `Unknown`
- **File:** [`src/lib/components/library/detailLoadError.ts`](../src/lib/components/library/detailLoadError.ts#L27)
- **Line:** 27
- **Context:** `Unknown`
- **File:** [`src/lib/utils/coalescedLoader.ts`](../src/lib/utils/coalescedLoader.ts#L15)
- **Line:** 15
- **Context:** `Unknown`
@@ -11364,8 +11384,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
**Locations:** 14 file(s)
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L617)
- **Line:** 617
- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L622)
- **Line:** 622
- **Context:** `Unknown`
- **File:** [`src/lib/components/library/ArtistDetailView.svelte`](../src/lib/components/library/ArtistDetailView.svelte#L138)
- **Line:** 138
@@ -15052,6 +15072,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
- **Line:** 7
- **Context:** `Unknown`
### UT-267
**Locations:** 2 file(s)
- **File:** [`src/lib/components/library/detailLoadError.test.ts`](../src/lib/components/library/detailLoadError.test.ts#L5)
- **Line:** 5
- **Context:** `Unknown`
- **File:** [`src/lib/components/library/detailLoadError.ts`](../src/lib/components/library/detailLoadError.ts#L27)
- **Line:** 27
- **Context:** `Unknown`
### UR-054
**Locations:** 7 file(s)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "jellytau",
"version": "0.13.2",
"version": "0.13.3",
"description": "A cross-platform Jellyfin client built with Tauri, SvelteKit and Rust.",
"author": "Duncan Tourolle <duncan@tourolle.paris>",
"license": "MIT",
+1 -1
View File
@@ -2275,7 +2275,7 @@ dependencies = [
[[package]]
name = "jellytau"
version = "0.13.2"
version = "0.13.3"
dependencies = [
"aes-gcm",
"argon2",
+1 -1
View File
@@ -4,7 +4,7 @@ name = "jellytau"
# `player-conformance`, and a second binary makes a bare `cargo run` —
# which `tauri dev` issues — ambiguous.
default-run = "jellytau"
version = "0.13.2"
version = "0.13.3"
description = "A cross-platform Jellyfin client"
authors = ["Duncan Tourolle <duncan@tourolle.paris>"]
license = "MIT"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "JellyTau",
"version": "0.13.2",
"version": "0.13.3",
"identifier": "com.dtourolle.jellytau",
"build": {
"beforeDevCommand": "bun run dev",
@@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest";
import { errorAfterLoad, loadErrorMessage } from "./detailLoadError";
/**
* TRACES: UR-062 | DR-297 | UT-267
*
* Resuming the app after a few minutes in the background replaced a series page
* that was on screen with "Failed to load item". The resume reload is a refresh
* of content the cache had already answered, yet any throw in it blanked the
* page — and no later successful reload of the same item ever cleared it.
*/
describe("errorAfterLoad", () => {
it("clears the error when a load succeeds", () => {
expect(errorAfterLoad({ ok: true }, { refreshing: true })).toBeNull();
expect(errorAfterLoad({ ok: true }, { refreshing: false })).toBeNull();
});
it("keeps a page on screen when a refresh of it fails", () => {
expect(errorAfterLoad({ ok: false, error: "boom" }, { refreshing: true })).toBeNull();
});
it("reports a failure to open an item that is not on screen yet", () => {
expect(errorAfterLoad({ ok: false, error: new Error("Offline") }, { refreshing: false })).toBe(
"Offline",
);
});
});
describe("loadErrorMessage", () => {
it("shows the text of a backend error, which arrives as a plain string", () => {
expect(loadErrorMessage("Repository not found")).toBe("Repository not found");
});
it("shows an Error's message", () => {
expect(loadErrorMessage(new TypeError("x is undefined"))).toBe("x is undefined");
});
it("falls back to a generic message for anything else", () => {
expect(loadErrorMessage(undefined)).toBe("Failed to load item");
expect(loadErrorMessage({})).toBe("Failed to load item");
expect(loadErrorMessage("")).toBe("Failed to load item");
});
});
@@ -0,0 +1,35 @@
// What the library detail page's error slot shows after a load.
//
// Extracted from `/library/[id]/+page.svelte` so it can be unit-tested.
export type LoadOutcome = { ok: true } | { ok: false; error: unknown };
/**
* The text of whatever a load threw. Backend commands reject with a plain
* string, not an `Error`, so reading only `Error.message` replaced every
* backend error with the generic fallback and hid what actually failed.
*/
export function loadErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) return error.message;
if (typeof error === "string" && error) return error;
return "Failed to load item";
}
/**
* The error to show once a load has ended.
*
* A success always clears it — a same-item reload used to leave a stale error
* up for good. A failed *refresh* of a page already on screen shows none: the
* page is still showing what the cache answered, and a background refresh
* (reconnect, resume, filter change) must not blank it. Only failing to open an
* item leaves the viewer with nothing to see.
*
* TRACES: UR-062 | DR-297 | UT-267
*/
export function errorAfterLoad(
outcome: LoadOutcome,
options: { refreshing: boolean },
): string | null {
if (outcome.ok || options.refreshing) return null;
return loadErrorMessage(outcome.error);
}
+7 -2
View File
@@ -1,4 +1,4 @@
<!-- TRACES: UR-035, UR-038, UR-048, UR-058, UR-062 | DR-043, DR-062, DR-102, DR-103, DR-142 -->
<!-- TRACES: UR-035, UR-038, UR-048, UR-058, UR-062 | DR-043, DR-062, DR-102, DR-103, DR-142, DR-297 -->
<script lang="ts">
import { untrack } from "svelte";
import { formatDuration } from "$lib/utils/duration";
@@ -52,6 +52,7 @@
} from "$lib/components/library/seriesNavigation";
import { createLogger } from "$lib/utils/logger";
import { createCoalescedLoader } from "$lib/utils/coalescedLoader";
import { errorAfterLoad } from "$lib/components/library/detailLoadError";
const log = createLogger("LibraryDetail");
@@ -250,8 +251,12 @@
}
}
}
error = errorAfterLoad({ ok: true }, { refreshing: !isNewItem });
} catch (e) {
error = e instanceof Error ? e.message : "Failed to load item";
// A failed refresh keeps the page up (DR-297), so this log is the only
// trace it leaves — it must carry the value actually thrown.
log.error(`Failed to ${isNewItem ? "load" : "refresh"} item ${itemId}:`, e);
error = errorAfterLoad({ ok: false, error: e }, { refreshing: !isNewItem });
} finally {
loading = false;
}