The rotation interval was installed once when the banner mounted and never touched again, so a swipe, arrow or dot tap inherited whatever was left of the running countdown — swiping 5.5s into a 6s interval moved the banner on half a second later. The timer moves into heroRotation.ts as a small restartable object so it can be unit-tested, and every manual navigation path restarts it from that moment. Verified red-first: with restart() reverted to leave a running timer alone, the regression test fails. Release 0.9.1.
10 KiB
Defect windows — which bugs were present when
For each fixed defect, the releases it was actually present in. Companion to CHANGELOG.md, which says what changed; this says how long each fault had been shipping before it did.
"Present since" is the first release containing the defective code, not the first release where a user could hit it — those differ, sometimes by months, and the gap is called out where it matters. "How dated" records the evidence, so a row can be re-checked or disputed:
| Method | Meaning |
|---|---|
pickaxe |
git log -S<token> on the defective token — the commit that introduced the exact string, then the earliest tag containing it. Strongest evidence. |
feature |
The defect is inseparable from a feature that landed whole (bad rung in a new algorithm, missing caller in new plumbing), dated to that feature's release. |
absence |
The fix adds something that was never there. Dated to when the surrounding code was built, since there is no introducing commit to find. Weakest — treat as "no later than". |
Present since the first release
Nine defects date to the initial proof of concept (v0.0.1, 2026-06-23) and shipped for between two weeks and seven weeks short of two months before anyone hit them. That is the dominant pattern here: not regressions, but original assumptions that went unexercised until a later feature leaned on them.
| Defect | Present since | Fixed in | Shipped broken for | How dated |
|---|---|---|---|---|
AudioStreamIndex=0 pinned the video stream as the audio track (DR-140) |
v0.0.1 | v0.4.6 | ~7 weeks | pickaxe |
Download URL spelled videoBitrate, which Jellyfin does not bind (DR-123) |
v0.0.1 | v0.5.1 | ~7 weeks | pickaxe |
pause_download / resume_download were no-ops (DR-168) |
v0.0.1 | v0.5.3 | ~7.5 weeks | pickaxe |
.part sidecar named by with_extension, so no cleanup path matched it (DR-169) |
v0.0.1 | v0.5.3 | ~7.5 weeks | pickaxe |
Range sent on every retry regardless of the response (DR-170) |
v0.0.1 | v0.5.3 | ~7.5 weeks | pickaxe |
/Items/Latest requested with the default GroupItems=false |
v0.0.1 | v0.5.1 | ~7 weeks | pickaxe |
SubtitleStreamIndex omitted from PlaybackInfo, letting the server burn in (DR-176) |
v0.0.1 | v0.5.5 | ~8 weeks | pickaxe |
No PlaySessionId, and one hardcoded DeviceId, on every stream URL (DR-177) |
v0.0.1 | v0.5.5 | ~8 weeks | pickaxe |
download_item never recorded media_type; NULL read as 'audio' (DR-135) |
v0.0.1 | v0.4.6 | ~7 weeks | pickaxe |
download_album read its track list from the local cache (DR-173) |
v0.0.1 | v0.5.5 | ~8 weeks | pickaxe |
Device profile carried no MaxAudioChannels (DR-141) |
v0.0.1 | v0.4.6 | ~7 weeks | absence |
| Streaming ceiling fixed at 20 Mbps with no way to lower it (UR-074) | v0.0.1 | v0.5.3 (as a feature) | ~7.5 weeks | pickaxe |
| Hero banner auto-rotation never restarted after a manual swipe (DR-038) | v0.0.1 | v0.9.1 | ~8.5 weeks | pickaxe |
Why they took so long to surface
Four of these were latent until a later feature exercised them, which is why the fix lands so far from the cause:
- The
videoBitratecasing was harmless while every download wasoriginal. It became visible only once a quality picker existed to select against — and then produced no error, just a full-size file, because Jellyfin discards an unbound query key silently. - The unconditional
Rangeheader was inert for the same reason:originalis the one rung served with aContent-Lengthand real byte-range support. It started corrupting files in v0.5.1, the moment the casing fix made transcoded downloads actually transcode. So the code dates to v0.0.1 and the corruption to v0.5.1 — a one-release window for the visible symptom. - The missing
PlaySessionIdonly bites when a stream is re-opened for the same item. Nothing re-opened one until quality switching, transcoded seek and audio-track switching existed. - The omitted
SubtitleStreamIndexonly bites on sources whose own default subtitle track is image-based, since that is what forces the server from sidecar to burn-in.
Two were masked by soft failure: the asset protocol being disabled (DR-134)
was hidden by the thumbnail cache falling back to the server copy whenever the
server was reachable, and AudioStreamIndex=0 was hidden by servers that
silently correct an out-of-range index — which is exactly why it was reported as
"some videos have no audio" rather than as a bug in the client.
Introduced by a feature, fixed later
| Defect | Present since | Fixed in | How dated |
|---|---|---|---|
| Native-path resume position never applied (both layers assumed the other seeked) | v0.0.9/v0.0.10 | v0.5.1 | feature (PlayerAdapter contract) |
get_downloaded_items matched "this library exists" rather than constraining the item to it (DR-167) |
v0.0.17 | v0.5.3 | feature (browsable downloaded library) |
SCOPE_ITEM_TYPES — the frontend/backend boundary leak (DR-063) |
v0.0.17 | v0.2.1 | pickaxe |
check:boundary anchored to the query site, blind to a named const (DR-094) |
v0.0.17 | v0.2.1 | feature (tripwire landed with the leak it missed) |
| Coverage gate divided by hardcoded denominators, reporting 158% (DR-093) | v0.0.1 | v0.2.1 | pickaxe |
| Tap deferral raced the WebView's synthesized click (DR-092 → DR-098) | v0.1.5 | v0.2.7 | feature (the deferral itself) |
Transport for webview media decided from el.paused in the DOM (DR-097) |
v0.0.9/v0.0.10 | v0.2.7 | feature (Html5PlayerAdapter) |
pick_current_episode rung 3 returned the first gap, not the furthest watched |
v0.3.0 | v0.5.1 | feature |
mirror_user_data mirrored is_favorite alone and returned early (DR-155) |
v0.4.0 | v0.5.1 | pickaxe |
| Stop-report path never fed the sync queue that existed for it (DR-154) | v0.4.6 | v0.5.1 | feature (queue + drain landed with no producer) |
| Background-audio base applied in two display-only places (DR-159) | v0.2.9 | v0.5.3 | pickaxe |
| Positions reported as 0 before the first tick, and always 0 for webview media (DR-178/179/180) | v0.5.3 | v0.5.5 | feature (DR-159's tick boundary) |
| Length-less handoff transcode left to the player's own load-error retry, which can only restart it (DR-203) | v0.0.16 | v0.8.2 | feature (the handoff's progressive-mp3 choice) |
Three of these are worth separating out, because the defect is not a mistake in the code so much as plumbing that was built and never connected:
repository_get_next_up_episodesaccepted aseries_idfrom the day it was written, and no caller passed one until v0.3.0.- The sync queue and its drain were built, tested and running in v0.4.6 with neither of its two would-be producers ever called.
- Both halves of the watched-state backend existed with no caller before v0.5.3.
An automated check cannot see any of these — the code is present, tested and reachable in principle. Only tracing a requirement to a call site catches it.
Short windows (one release or less)
| Defect | Present since | Fixed in | Note |
|---|---|---|---|
experimentalNativeVideo defaulted on, shipping audio with a blank screen (DR-161 → DR-172) |
v0.5.3 | v0.5.4 | One release. The decode path was fine; the compositing step never ran. |
| Webview-shaped audio profile insufficient — server ignores a profile's audio codec (DR-149) | v0.4.7 | v0.4.8 | The v0.4.7 fix for DR-148 was necessary and not sufficient. |
Android versionCode floor went stale (minor*100 yielding less than the 5002 already in the field) |
v0.5.0 | v0.5.3 | Caught before a broken APK shipped; no released build was un-installable. |
| Subtitle sidecar work reverted by a commit assembled from a stale tree | v0.5.5 | v0.5.5 | Never released broken — both commits are in v0.5.5. |
Fixed twice / never actually broken
-
Autoplay time reset (v0.0.2). Two commit objects carry this identical change:
dcf08f30(merged via Gitea PR #3, tagged v0.0.2) andfa7cb6e9(the local original). Both have the same parent674c8e5cand the same diff. A merge chain pulledfa7cb6e9and its follow-up1e599627into master's history during v0.5.5, sogit log v0.5.4..v0.5.5lists an autoplay fix that changed no file in that release —nextEpisodeService.tsis byte-identical across the tag boundary. The fix shipped in v0.0.2 and has not regressed.This is the one case where reading the changelog off
git logsubjects would have produced a false entry, and it is a good argument for the project's practice of deriving release notes from TRACES rather than commit subjects.
Recurring shapes
Four causes account for most of the table:
- An omitted parameter is not a neutral default.
SubtitleStreamIndex,AudioStreamIndex,GroupItemsandMaxAudioChannelsall had a server-side default that was actively wrong, and in three of the four the server's choice was more expensive than the one intended — burn-in forcing a full re-encode being the extreme case. - Silent binding failures.
videoBitRateproduced no error, no warning and a plausible-looking file. So did an unboundRange, and so did the coverage gate dividing by a stale denominator. - Two layers each assuming the other acts. Native resume (adapter recorded the position, backend never seeked), end-of-playback dispatch (two paths, one unreachable), and the surface/attach split in v0.5.0's native video.
- A guard keyed on state that moves. The tap deferral keyed suppression on a
timer handle the callback had already cleared; the HTML5 toggle keyed
play-vs-pause on
el.paused, which flips while buffering.
Reproducing this
The pickaxe rows can be re-derived directly:
git log --oneline --reverse -S'<defective token>' -- src-tauri/src # introducing commit
git tag --contains <sha> | sort -V | head -1 # first release with it
Blaming the lines a fix removed (git blame at the fix's parent) is faster to run
across many commits but was not used for the rows above: it reliably lands on
whichever commit last touched the adjacent lines, which is usually not the commit
that introduced the defect. It was used only to shortlist candidates.