fix/release-artifacts-and-notes
354
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
214997144f |
feat(deps): upgrade Tauri to 2.11.5, and own the Android context it stopped setting
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 22m51s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Failing after 25s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 14s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m19s
The plugin versions could not be matched upward without this: both
tauri-plugin-log 2.9.0 and tauri-plugin-updater 2.10.1 require tauri
^2.10, and the tree was on 2.9.5. So the framework moves with them --
tauri 2.9.5 -> 2.11.5, tauri-build 2.5.3 -> 2.6.3, wry 0.53.5 -> 0.55.1
-- and every plugin's Rust crate and npm package is now pinned to the
same version on both sides.
That upgrade broke Android outright, and the breakage is the interesting
part.
Seven call sites in this crate reach JNI through
ndk_context::android_context(), which reads a process-global pair of
pointers. Nothing here ever set that global. `tao` did -- the windowing
layer under wry, three levels below anything this project names in
Cargo.toml. tao 0.34.5 called initialize_android_context() while starting
the activity and our code read what it left behind. tao 0.35.3 keeps the
same two pointers in a private struct and no longer publishes them.
The result, on every launch, was:
PANIC at ndk-context/src/lib.rs:72: android context was not initialized
8: ndk_context::android_context
9: jellytau_lib::run::{{closure}}
Not a crash in our code, and not a change to our code: an undocumented
side effect of a transitive dependency disappeared. Relying on someone
else to populate a global is a dependency that does not appear in
Cargo.toml and gives no warning when it goes.
src-tauri/src/android_context.rs now owns that invariant instead of
assuming it. JNI_OnLoad captures the JavaVM as the shared library loads
-- the earliest moment available, and nothing in tao, wry or tauri
defines one to collide with. The Context is resolved lazily via
ActivityThread.currentApplication() and pinned as a global reference for
the process lifetime, since ndk_context stores a bare pointer and does
not own it. It publishes the Application rather than the Activity:
SecureStorage.initialize() immediately reduces its argument to
applicationContext anyway, and an Application cannot outlive itself the
way a retained Activity would.
Restoring the global keeps all seven callers untouched. Threading a VM
and Context handle through five credential call sites would have been a
larger change with more risk, on the credential path.
Failure now degrades instead of aborting: it is logged and credentials
fall back to the encrypted-file path, which the app already supports.
Verified on a device, R8-minified, not merely compiled:
[INIT] Android JavaVM and Application published to ndk_context
Android SecureStorage initialized successfully
Android Keystore available via SecureStorage
[INIT] Using system keyring for credential storage
[CodecDetection] Detected 7 video codecs: av1,h263,h264,hevc,...
-- the real keystore path, not the fallback, and the app stays up. None
of this is reachable by CI: nothing there runs the app.
Also fixed here, both found the same way:
- `tauri android build --apk true` is now `--apk`. The CLI took a value
until 2.10; from 2.11 the stray `true` is a positional and the build
fails before starting. Three call sites in build-android.sh and one
in build-release.yml -- the latter builds the signed APK, by far the
most-downloaded artifact.
- scripts/build-android.sh ran `npm install` on its clean-build path in
a bun project, ignoring bun.lock and re-resolving the tree. That is
exactly how the plugin crate/package versions drift apart again.
scripts/check-tooling.sh now fails on any npm/yarn/pnpm invocation or
foreign lockfile, and runs in CI.
DR-222, DR-223.
|
||
|
|
9a19d30e6c |
fix(build): make the release actually buildable, and check it before tagging
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 18m41s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Successful in 31s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 9s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m5s
Preparing v0.10.0 meant building the release locally first. It did not build. Two separate defects were sitting on master, both invisible to every gate this project has, for the same reason: nothing in build-and-test.yml runs `tauri build`. Only a tag does. So the first time anyone would have discovered either was a failed release. **Tauri plugin versions had drifted apart.** Tauri refuses to build when a plugin's Rust crate and npm package are on different minor versions: tauri-plugin-log (v2.8.0) : @tauri-apps/plugin-log (v2.9.0) tauri-plugin-updater (v2.9.0) : @tauri-apps/plugin-updater (v2.10.1) Introduced by the updater and diagnostics work in this same branch -- `cargo add` took what the pinned toolchain allowed while `bun add` took latest, and the caret ranges let them separate. cargo check, clippy, cargo test and svelte-check all passed. Matching upward pulled wry 0.53.5 -> 0.54.2 along with wasm-bindgen, web-sys and webkit2gtk: the webview layer, which on Linux is the video playback path. That is not a change to make while cutting a release, so the npm packages are pinned down to the crates instead -- exactly, not by caret, since the caret is what allowed the drift. The upgrade is worth doing deliberately, with a playback check, and ci-operations.md says so. CI now runs `tauri info`, which performs the same comparison without building. Verified by reintroducing the mismatch and watching it fail. **The AppImage target had never been built.** It was added earlier in this branch because the release notes had advertised an AppImage for months while tauri.conf.json never produced one. It does not work out of the box: linuxdeploy carries its own `strip`, too old to parse the .relr.dyn section modern toolchains emit, and it fails on every bundled library -- strip: libzstd.so.1: unknown type [0x13] section `.relr.dyn' failed to bundle project `failed to run linuxdeploy` Ubuntu 23.10+ links with -z pack-relative-relocs by default, so the CI builder image fails exactly as a modern Arch host does. NO_STRIP=true is linuxdeploy's documented escape hatch. The resulting 153 MB AppImage was verified to be well-formed and to actually start. Without this the release would have failed at the Linux build step -- the artifact check added earlier refuses to publish when no AppImage is produced, which is the behaviour we want, but it would have refused a tagged build rather than a local one. Also: the traceability extractor now reads the tooling shell scripts that carry TRACES comments. DR-207, DR-213 and DR-220 all had them and were counted as uncovered because only .ts/.svelte/.rs were scanned. Listed individually rather than globbing scripts/*.sh -- most implement nothing, and adding one should be a decision. DR-221. |
||
|
|
5d02628689 |
fix(release): publish real notes, and stop shipping old releases' installers
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 15m1s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Successful in 42s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 10s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m6s
Two defects found while preparing v0.9.2, both of which had been shipping for months without anything to notice them by. **Every release note was the same 1,050 bytes.** All 35 releases from v0.0.1 to v0.9.1 published identical generic install instructions whose "What's New" section read "See CHANGELOG.md" -- a link that does not resolve from a release page. A reader learned nothing about what changed in any release the project has ever made. The body now comes from the `## <version>` section of CHANGELOG.md, and a missing section fails the release: notes that say nothing are worse than a build that waits for a maintainer to write two sentences. The 35 published bodies have been backfilled from the changelog via the tea CLI. This also corrects something introduced two commits ago. That change generated the body from `bun run release:notes`, which CLAUDE.md is explicit about -- its output is "a reviewed draft, not a final changelog". Publishing it unreviewed proved the point immediately: the v0.9.1..HEAD range contains a repo-wide prettier sweep, so every file in src/ counted as changed, their TRACES resolved to nearly the whole matrix, and the draft claimed the release had added the entire application. The script now skips cosmetic commits (chore(format), chore(deps), style) and reports how many rather than silently returning a smaller set, but it stays a local drafting tool. **Every release from v0.1.0 to v0.8.2 shipped every Windows installer ever built.** src-tauri/target/*/release/bundle/ is not versioned, cargo never cleans it, and the runner reuses the target directory -- so the copy step's bundle/**/*-setup.exe glob collected the lot. v0.8.2 carried sixteen installers, thirteen of them stale; v0.5.0 offered users a download list going back to 0.1.0. Eight months, and nothing to notice it by: the upload loop reported success, the files were real, and the page looked busy rather than wrong. It stopped only because an unrelated cargo cache change wiped the runner's target dir, so it was dormant, not fixed. Both desktop builds now remove the bundle directory before building, so a stale file cannot exist to be copied. Filtering the copy by version would have hidden it instead. The Linux job gets the same treatment: it was never hit only because Linux packaging is newer, and the glob is identical. scripts/check-release-artifacts.sh is the backstop for whatever reintroduces one by a route nobody predicted. It runs before the SBOM, the checksums and the upload -- all of which describe the file set, so a stale artifact has to be caught before it is hashed and published as part of the release. Verified against a reconstruction of the real v0.8.2 accumulation. DR-219, DR-220, UT-210. |
||
|
|
cac9afa6bd |
Merge pull request 'Chore/infra hardening' (#15) from chore/infra-hardening into master
Reviewed-on: #15 |
||
|
|
2e3a864ef0 |
Merge branch 'master' into chore/infra-hardening
Traceability Validation / Check Requirement Traces (pull_request) Successful in 11s
|
||
|
|
1d56517f07 |
docs(specs): add backend-owned stream selection
Traceability Validation / Check Requirement Traces (pull_request) Successful in 11s
Rust becomes the single owner of which stream to play — direct play or
transcode, at what ceiling, over what transport — and hands every player
backend a self-describing StreamSelection instead of a bare URL. mpv,
ExoPlayer and the HTML5/hls.js path all consume one decision rather than
three places re-deriving it.
The motivating leak is concrete. VideoPlayer.svelte determines transport with
`currentStreamUrl.includes(".m3u8")`, in two places, for a URL Rust
constructed and therefore already knows the shape of. That is the boundary
rule in miniature: not item-type taxonomy, but the same error of
reconstructing a domain fact in the presentation layer because the wire shape
did not carry it. A tagged Transport enum deletes it.
The design line, which ExoPlayer forces: Rust decides *what stream*, the
player decides *how to deliver it*. ExoPlayer has genuine adaptive track
selection; this spec must not reimplement or fight it. Rust only adapts where
the player cannot (mpv) and the server actually offers a ladder.
Six phases, and phase 1 stands alone as pure ownership movement with no
behaviour change. Phase 4 (direct-play negotiation) is what removes the
transcode and unblocks the Linux native-video work. Phase 5 (adaptation) is
gated on counting EXT-X-STREAM-INF entries in a real playlist — the
acceptance criteria require that count be recorded before it is either
started or dropped.
Takes DR-121 from read-through-media-cache.md, which specced Rust-owned
quality reporting but never built it; that spec keeps its capture half.
|
||
|
|
99d96163d8 |
docs(specs): correct the spike's crash and ABR findings
Three corrections to the Linux native-video spike, each of which reverses something recorded earlier in the same session: - The crash is unexplained. It was first blamed on hwdec=auto-safe's Vulkan failures, on a misreading of the logs — those are two per run at start-up, not per-frame, and every clean run has the same two. A 300s soak on auto-safe survived. So did 240s of automated fullscreen toggling (~120 transitions) and 240s of continuous resizing (~2000 reallocations). Three hypotheses, none reproduced. Recorded rather than dismissed: an intermittent fault nobody can reproduce is worse to inherit than a deterministic one. - G5 drops to amber. It looked and felt smooth, but the only SIGSEGV observed came from the only session in which fullscreen was exercised, and the spike has no lifecycle handling at all — it never frees the render context. An implementation must bind that to the GL context's lifetime regardless of what caused this crash, because Android already paid for that lesson as DR-184. - Finding 3's premise is in doubt. "The webview path already has real ABR via hls.js" was never checked against the URLs this app builds: get_video_stream_url requests a single rendition, the frontend has no level-handling code at all, and a quality switch is implemented by re-opening the stream. If the playlist is single-variant there is no adaptation to lose. The decisive test needs a live server and is recorded as unrun. Also records hardware decode working through the render API (nvdec-copy engaged), and that hwdec=vaapi silently fell back to software on this box. |
||
|
|
eda6e36d3d |
Merge pull request 'Infrastructure hardening: CI enforcement, supply chain, updater, diagnostics' (#14) from chore/infra-hardening into master
Publish Documentation / Build & publish docs to gitea-pages (push) Canceled after 0s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 15m0s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 38s
Traceability Validation / Check Requirement Traces (push) Successful in 13s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m5s
Reviewed-on: #14 |
||
|
|
f11f5eddd5 |
docs(specs): record the Linux native-video compositing spike
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 15m3s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Successful in 38s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 20s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m11s
Adds the spike write-up and re-opens finding 2 of playback-backend-unification.md, which concluded that video cannot unify on a native engine because the webview owns the surface. That finding's general form has since been falsified on Android, where native video composites behind a transparent WebView and ships on by default. The evidence behind it was also entirely about foreign-window embedding -- mpv's render API, drawing into a GL context we own inside Tauri's own GTK tree, was never tested. The spike tests that one claim and comes back green on Linux for both X11 and Wayland, bar the Tauri default_vbox() half of G1. Findings 3-6 are deliberately left standing. Finding 3 in particular -- mpv has no adaptive bitrate -- is an independent disqualifier that a green compositing result does not clear, and the spike says so rather than reading as a green light. The next-free-id line moves to UR-079 / IR-033 / DR-219: this branch allocated UR-077 and UR-078 for the updater and diagnostics work, and DR-215 through DR-218 with them, after that line was last written. Authored in a parallel session working in the same checkout; committed here so it travels with the rest of the branch. |
||
|
|
f3fa45f742 |
feat(diagnostics): persistent redacted logging and an exportable bundle
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 22m12s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Successful in 37s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 11s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m10s
The app forgot everything it did the moment it exited. The Rust half
logged through env_logger to stdout only -- invisible to anyone who
launched from a desktop icon, and on Android worse than that: stdout is
not logcat, so the backend produced no visible output at all on the
platform carrying this project's hardest bugs. The autoplay deadlock,
the truncated-stream restart and the background-audio stall were all
diagnosed by talking a user through `adb logcat`, because there was no
other way to see anything. A panic left nothing behind at all.
Logs now go to a size-capped rotating file, to logcat on Android, and to
the webview console in dev. A panic is recorded with its backtrace before
the process dies. The frontend's messages are forwarded into the same
file, so one timeline holds both halves of the app in order -- which is
what makes a race between them legible after the fact, and races between
them are the expensive bug class here.
Redaction runs in the log FORMATTER, not at export time. A credential
sitting in a file on the device is already a disclosure; stripping it on
the way out would be too late. The exporter redacts a second time to
cover files written by builds that predate this. api_key, X-Emby-Token,
Authorization, "AccessToken" and Token="..." all reduce to [REDACTED],
while host, item ids and filenames are deliberately kept -- a log scrubbed
of those is one nobody can debug anything from. Server URLs keep scheme
and host and drop any embedded user:pass@.
Two things the tests caught that review would not have:
- redact_headers recursed on its own output. The replacement keeps the
header NAME, so the next call matched the same header forever; the
test died with a stack overflow. It is a forward scan now.
- The frontend forwarder used `void plugin.error(...)`. `void` discards
a promise's value but not its rejection, so in any webview without
IPC -- a unit test, SSR, a browser preview -- every log line became an
unhandled rejection. 20 of them showed up the first time coverage
ran. Each call now attaches a catch.
Only info and above cross the IPC boundary: debug is per-tick player
state and forwarding it would be thousands of calls a minute for output
nobody reads. A failing forwarder never propagates and never prevents the
console write.
Nothing is transmitted anywhere. The export writes a zip and reports its
path; the user attaches it themselves, which is also what keeps this from
becoming telemetry. An Android share intent is explicitly out of scope --
it is Kotlin work that belongs with the other native code.
The panic hook chains to the previous hook rather than replacing it,
because utils/lock.rs installs a silencing hook around tests that provoke
poisoned locks on purpose.
Spec in docs/specs/diagnostics-and-logging.md; UR-078 / DR-218 / UT-209.
Verified: 1079 frontend tests and the coverage gate, 759 Rust tests,
clippy -D warnings, svelte-check 0 errors, and cargo check for
aarch64-linux-android.
|
||
|
|
fb72bf3005 |
docs(ci): drop the runner-health references the amend missed
The commit that removed .gitea/workflows/runner-health.yml was amended with a git add whose pathspec named the already-deleted file, so the add aborted and only the deletion was staged -- leaving ci-operations.md still describing a scheduled job that no longer exists. The runner section now says what to check by hand and why there is no job: on a single-slot runner a daily job takes the slot and pulls the builder image to run df, and df inside a container does not reliably describe the host disk. |
||
|
|
6897b290ed |
docs: add the governance and CI-operations files the project never had
The repo had no SECURITY.md, CONTRIBUTING.md, code of conduct, or issue and PR templates. For a client that handles Jellyfin credentials and ships signed binaries, the missing one that actually matters is SECURITY.md: there was no stated way to report a vulnerability privately, so the only available channel was the public tracker. CONTRIBUTING.md documents the gates as they now stand, including the three ratchets and which direction each is allowed to move, and the two rules that surprise people: bug fixes start with a failing test, and Jellyfin's taxonomy stays in Rust. The bug template asks the three playback questions -- streaming or downloaded, transcoding or direct, music or video -- because those answers decide which of several very different code paths a report is about, and reconstructing them over several round trips is most of the cost of a playback bug report. docs/build/ci-operations.md is the missing operations manual: how to change the builder image and in what order (image pushed before the workflow that names it, or CI breaks), why tags are dated rather than :latest or per-SHA, what each secret is for, and what losing the updater private key would mean -- installed desktop clients only accept payloads signed by the key matching the public key they shipped with, so losing it means everyone reinstalls by hand. Disk exhaustion on the runner is documented as a manual check rather than a scheduled job. A daily job would occupy the only slot on a single-slot runner and pull the whole builder image to run `df` -- and `df` inside a container does not reliably describe the host's disk, so it would spend real build capacity reporting a number that might be wrong. What the doc records instead is the part that is actually hard to rediscover: the symptoms (cargo dying mid-link, docker refusing to pull, actions/cache quietly not saving) and that `docker volume prune` needs `-a` to touch named volumes, which is how it filled up unnoticed. Two things in these docs are stated plainly because they are true and were not written down anywhere: without branch protection every gate in the pipeline is advisory, and the Gitea instance -- canonical remote, signing secrets, registry, runner -- is not backed up by anything in this repository. |
||
|
|
3211c96ecf |
feat(updater): in-app update on desktop, releases link on Android
Anyone who installed an AppImage or ran the Windows installer was frozen
on that version forever. Nothing in the app ever mentioned a new release
existed, and the release notes were the only announcement.
Desktop now checks a signed manifest, shows the version and its notes in
Settings, and installs and relaunches on request. The signature check is
the whole point: it is what stops a substituted download from being
installed by the app itself. Windows binaries stay unsigned for
SmartScreen purposes -- that is a code-signing certificate, a separate
problem -- but the update payload is verified against our own key.
Android is deliberately not wired to the updater. An app may not replace
its own APK; that is the package installer's job, and the plugin has no
Android implementation. It gets a link to the releases page instead of a
button that would throw.
The plugins are gated with a target-triple cfg rather than
cfg(desktop). Cargo only evaluates target cfgs in a [target.'cfg(..)']
table, so cfg(desktop) matches nothing, silently drops the dependency,
and fails much later with "Permission updater:default not found" -- which
is exactly what the first attempt here did.
Where the manifest lives took some finding. This Gitea serves
/releases/download/<tag>/<asset> but 404s on
/releases/latest/download/<asset> (verified against a real asset), so
there is no stable latest-release URL. The gitea-pages branch is
force-pushed wholesale by publish-docs.yml, so it cannot host the file
either. latest.json therefore gets its own orphan branch, read over the
raw-file URL, and is published from a scratch repo in RUNNER_TEMP rather
than by switching branches in the checkout -- doing that would have left
the following steps standing on a one-commit history, and the next step
but one runs release:notes against the real commit range.
Also fixed, all of it release-integrity:
- "appimage" is in bundle.targets. The release notes have advertised an
AppImage for months; tauri.conf.json never built one, the artifact
step globbed for *.AppImage, found nothing, and said nothing. The
step now fails instead.
- The .AppImage.tar.gz/.sig pair and the NSIS .sig are collected. A
manifest referencing a signature that was never uploaded fails only
on the user's machine, so the manifest step also refuses to write an
entry with an empty signature.
- Release notes are generated by release:notes from the traceability
graph, which is what CLAUDE.md has asked for all along, instead of a
fixed heredoc that said "see CHANGELOG.md for detailed changes" and
linked "GitHub Issues" on a Gitea-hosted project.
- The notes tell users how to verify a download with SHA256SUMS.
Requirements UR-077 / DR-217, tests UT-208 (12 cases over the version
comparison and the platform decision, including that a pre-release does
not offer itself as an upgrade to the matching release).
Verified: 1070 frontend tests, cargo check for both the host and
aarch64-linux-android (confirming the plugins are absent there), clippy
-D warnings, svelte-check 0 errors.
|
||
|
|
96abc3afef |
docs(specs): make "a spec becomes an architecture doc" the written rule
The sixteen specs folded in last commit were folded because someone noticed they had gone stale, not because anything said they should be. Without the rule written down the directory drifts straight back to a mix of promises and descriptions, and neither can be trusted: you cannot tell from a file whether it describes the build or proposes a change to it. So: docs/specs/ holds only unshipped work, there is no "Implemented" resting state, and the fold-in and the deletion happen in the same commit. The template now asks for the destination architecture doc **up front**, which is a design check rather than bookkeeping — a feature that fits no existing doc usually has an unclear layer assignment, and it is cheaper to find that out at spec time. It also tells the author which half of what they are writing is durable (invariants, rejected alternatives, the defect a decision prevents) and which half dies with the file (phases, migration steps, acceptance criteria). The review checklist gains a Lifecycle section, including the case that gets lost otherwise: out-of-scope work worth doing has to be written where it will still be found after the spec is gone. |
||
|
|
f6653e6a8b |
ci(security): add a supply-chain gate, checksums and an SBOM
The project shipped signed Android builds and unsigned desktop binaries
with no vulnerability scanning of any kind. Nothing checked the ~500
crate Rust graph or the JS packages against an advisory feed, and nothing
checked that what we redistribute inside an MIT bundle permits it.
The first cargo-deny run found eight vulnerabilities and one
unsoundness -- bytes, four in rustls-webpki, time, two in quick-xml and
rand -- every one of them closed by a `cargo update` nobody had a reason
to run. That update is in this commit; 740 Rust tests and clippy
-D warnings pass on the new lockfile.
Two structural fixes matter as much as the gate itself:
- deny.toml scopes the graph to the targets we actually ship. Without
it the Apple targets pull in plist -> quick-xml and report two DoS
advisories against a crate that is in no binary we release. Ignoring
those by ID would silence them everywhere, including where they
would matter; scoping makes them correctly absent.
- libmpv is pinned by rev instead of branch = "master". A branch means
the revision is whatever Cargo.lock happens to hold and any
`cargo update` silently substitutes new upstream code -- in the one
dependency that is not from crates.io and that links a C library
into the player. The rev is the commit already locked, so this pins
current behaviour rather than changing it.
Licence findings are recorded rather than waved through. libmpv and
libmpv-sys are LGPL-2.1, satisfied here by dynamic linking against the
system library; deny.toml carries the two obligations that follow (keep
the linkage dynamic, ship libmpv's licence text with any bundle carrying
the .so). MPL-2.0 crates are file-level copyleft and fine unmodified.
Releases now publish SHA256SUMS (verified in-job with `sha256sum -c`
before upload) and a CycloneDX SBOM for both halves, so "does this
release contain <vulnerable crate>?" has an answer that is not "rebuild
the tag and re-resolve it".
Workflows pin jellytau-builder:2026.08 instead of :latest. While every
job said :latest, rebuilding the image changed what every build compiled
against, including rebuilds of old release tags.
Also folded in, because both were the same class of problem:
- publish-docs.yml downloaded mdBook from GitHub releases into
/usr/local/bin at job time -- a toolchain install in CI, which
CLAUDE.md explicitly forbids, and a hard dependency on GitHub's CDN
at publish time. It is in the builder image now.
- extract-traces.ts only ever read .ts/.svelte/.rs, so every
requirement implemented by *configuration* was invisible to the
matrix that measures it. DR-205, DR-206, DR-207 and DR-215 all carry
TRACES comments nothing read, and each counted as uncovered while
being covered. Coverage was really 90%, not 88%; MIN_THRESHOLD moves
to 89 accordingly. CI workflows stay excluded and there is a test
saying why: traceability-check.yml quotes "a TRACES: comment" beside
deliberately-undefined example IDs, which the extractor would read
as real traces and then fail its own dangling-ID check.
Supply-chain requirement is DR-216.
🔴 The builder image must be rebuilt and pushed
(scripts/build-builder-image.sh 2026.08) before this reaches master --
the workflows now name a tag and tools that do not exist in the registry
yet.
|
||
|
|
32043a2152 |
docs: fold shipped specs into the architecture docs and delete them
A spec was a promise; sixteen of them had become descriptions of code that already shipped, sitting beside four that describe work still outstanding, with nothing in the file telling the two apart. Half the statuses were also wrong — audio-equalizer read "Accepted" with the EQ live on both platforms, the native video spec said the flag stays off after the default was flipped on. The shipped designs move into docs/architecture, which is the maintained description of the build, and the spec files go. Git history keeps the originals; what a future change still needs is carried across: - 01-rust-backend: favourites rewritten (the old section named a file that no longer exists and called shipped buttons "planned"), domain vocabulary owned by Rust (SearchScope, exclusions, the bitrate ladder), background workers - 02-svelte-frontend: app shell and chrome, library mosaic, series/episode navigation, downloaded browse, safe-area insets, native-video store, logging - 03-data-flow: locally-indexed search - 05-platform-backends: audio settings on ExoPlayer, the equalizer's band vocabulary, native video compositing, the background-audio handoff - 06-downloads-and-offline: one storage model, offline catalog visibility - 09-security: path confinement and input binding docs/specs/README.md now says what the directory is for and where each shipped design went. Deferred work the specs recorded is kept beside the code it concerns rather than lost: season-bounded autoplay, the two dead search commands, why indexing is a full crawl. requirements.md had fourteen stale statuses — Android audio parity still read "Linux only", DR-150 still said the native-video default was off, DR-190 was Proposed after DR-196 implemented it, and five tooling requirements were Proposed after landing. Three unbuilt specs suggested requirement ids that have since been allocated to other work; each now carries a warning. |
||
|
|
8f5c9023d0 |
ci: make the frontend gates real, and fix the coverage script
The repo configured four frontend gates and enforced one of them. eslint
and prettier ran in no workflow and no hook; `bun run check` ran only in
build-release.yml, so a type error could sit on master until somebody cut
a tag; and `bun run test:coverage` had been dead for months.
CI (build-and-test.yml) now runs format:check, lint, check and coverage
alongside the existing boundary and doc-link tripwires.
The coverage script failure was a version mismatch, not a config problem:
@vitest/coverage-v8 resolved to 4.1.10, whose peer range pins vitest
exactly, while package.json asked for ">=1.0.0 <5.0.0" and got 4.0.16 --
every run died on a missing BaseCoverageProvider export. The loose range
is what allowed the pair to drift, so it is now ^4.1.10.
Two ratchets, same policy as MIN_THRESHOLD in traceability-check.yml:
eslint --max-warnings=159 (0 errors; 159 is today's backlog, only
ever lower it)
vitest thresholds (statements 51 / branches 45 /
functions 46 / lines 52, measured at
54.6 / 48.7 / 49.6 / 55.1)
no-console is promoted from "off" to "error": the logger-facade
migration it was waiting on is finished -- 8 calls remained, 2 of them
real stragglers in the settings page, now on the facade the file already
imported. The sink itself, tests, and scripts/ are exempted; a CLI whose
stdout is the product is not a stray debug statement.
The threshold was verified to bite by raising it to 99 and watching the
run go red, not by assuming an unfailed gate works.
DR-205 moves to Done; the coverage gate is DR-215.
|
||
|
|
ad48d89dfe |
chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again. |
||
|
|
d095e1f410 |
fix(ci): drop the bash-only shopt from the Linux artifact step
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 13m55s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m22s
Traceability Validation / Check Requirement Traces (push) Successful in 16s
Build & Release / Run Tests (push) Successful in 14m0s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m9s
Build & Release / Build Linux (push) Successful in 16m39s
Build & Release / Build Windows (push) Successful in 20m42s
Build & Release / Build Android (push) Successful in 29m18s
Build & Release / Create Release (push) Successful in 13s
"Prepare Linux artifacts" ran `shopt -s nullglob`, but the runner executes
`run:` blocks with POSIX sh, where shopt does not exist. It exited 127 and
failed the step -- so build-linux never uploaded, create-release (which
needs all three build jobs) never ran, and v0.9.0 and v0.9.1 both compiled
successfully but published nothing. The last release with assets is v0.8.2.
Reproduced under busybox sh: the current block prints "shopt: not found",
passes the unmatched rpm glob through literally ("cp: can't stat
'.../bundle/rpm/*.rpm'"), and exits 127. Without nullglob an unmatched
pattern stays literal, so test each candidate with [ -e ] instead; the
same input then exits 0 with the AppImage and deb copied.
traceability-check.yml already carries this rule in two places (`case`
instead of `[[ == ]]`, a pipe instead of a here-string). Keeping the fix
POSIX rather than adding `shell: bash` follows that convention and drops
the dependency on bash being present in the builder image.
v0.9.1
|
||
|
|
a7365b9511 |
fix(ci): cache the cargo registry, not the 16 GB target dir
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 20m10s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m17s
Traceability Validation / Check Requirement Traces (push) Successful in 15s
Build & Release / Run Tests (push) Successful in 13m59s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m13s
Build & Release / Build Linux (push) Failing after 16m29s
Build & Release / Build Android (push) Canceled after 0s
Build & Release / Create Release (push) Canceled after 0s
Build & Release / Build Windows (push) Canceled after 11m6s
The runner's 74 GB disk kept filling. Measured on the box: 24 GB of
act cache, 23.18 GB of it created in 20 days -- ~1.15 GB/day against a
30-day-unused / 90-day-used GC, so it could never converge.
Cause: src-tauri/target (16 GB locally: 9.6G debug, 3.2G release, 2.4G
android) was cached under five separate keys, all keyed on
hashFiles('**/Cargo.lock'). The release script stamps the version into
Cargo.lock, so all five invalidated on every chore(release) -- 32
distinct lockfile revisions in three months.
- Cache only registry/index, registry/cache and git/db. registry/src is
omitted as well: cargo re-extracts it from the 155 MB of .crate
tarballs rather than storing 1.1 GB extracted.
- Collapse the five per-job keys into one shared cargo-registry key.
They existed to keep debug/release target artifacts from clobbering
each other; with target uncached, registry contents are
target-independent and every job wants the same crates.
- Split cargo-xwin into its own key. It tracks the xwin version in the
builder image, not our lockfile, so keying it on Cargo.lock was
re-downloading the whole Windows SDK on every release bump.
- CARGO_INCREMENTAL=0: never reused across runs, 3.5 GB of the debug dir.
- Installer artifact retention 30d -> 7d; tagged releases carry the
binaries anyway.
Inflow drops from ~5 GB to ~150 MB per lockfile change. Tradeoff: Rust
jobs now compile cold every run (~31min vs ~9min on a cache hit for the
Linux release build). Most runs already paid that, since a release bump
invalidated every key. sccache with a hard size cap is the way back if
it bites.
|
||
|
|
16658889a2 |
fix(home): restart the hero banner timer on a manual change
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 14m18s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m31s
Traceability Validation / Check Requirement Traces (push) Successful in 17s
Build & Release / Run Tests (push) Failing after 14m7s
Build & Release / Build Linux (push) Skipped
Build & Release / Build Windows (push) Skipped
Build & Release / Build Android (push) Skipped
Build & Release / Create Release (push) Skipped
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. |
||
|
|
98b2ede8bd |
fix(arch): build with custom-protocol so the package can load its own UI
Publish Documentation / Build & publish docs to gitea-pages (push) Canceled after 0s
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 14m7s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Traceability Validation / Check Requirement Traces (push) Successful in 21s
The PKGBUILD ran a bare `cargo build --release`. Without `--features tauri/custom-protocol` Tauri does not embed the frontend and serves it from devUrl instead, so the package compiled, linked, installed and passed every check — then launched into "Could not connect to localhost: Connection refused". `tauri build` passes that feature for you and the Android build passes it explicitly; this path never did, so the Arch package has never worked. Adds a check() that catches it at build time. It tests for the *assets*, not for the dev URL: devUrl is part of the config blob generate_context!() embeds either way, so grepping for it reports a failure on a correct build. A content-hashed filename from the vite output can only appear if the bundle was embedded — which is also why the fixed binary is ~400 KB larger. Found by installing the package and launching it, which is the only thing that would have found it. |
||
|
|
38d56e6c89 |
fix(scripts): check links in tracked files, not everything on disk
Publish Documentation / Build & publish docs to gitea-pages (push) Canceled after 0s
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 17m30s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Traceability Validation / Check Requirement Traces (push) Successful in 19s
The prune list was wrong three times running: it walked the scratch worktrees under .claude/, then makepkg's vendored cargo registry under packaging/arch/src/, reporting a dependency's broken README as if it were ours. Every one of those directories is already git-ignored, so asking git for the file list makes the exclusion rule the same one the repo already maintains — and it cannot drift the way a hand-kept prune list did. It also makes the script do what its header always said it did: check tracked markdown. Untracked-but-unignored files are included on purpose, so a new doc is checked before it is committed rather than after. The find(1) path stays as a fallback for a non-git checkout. CI was unaffected — a fresh checkout has none of those directories — but the local gate cried wolf, which is how a gate stops being read. |
||
|
|
4f4741cee5 |
fix(release): stamp the Arch package version, and ship the licence with it
Publish Documentation / Build & publish docs to gitea-pages (push) Canceled after 0s
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 17m49s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Traceability Validation / Check Requirement Traces (push) Successful in 24s
pkgver sat at 0.0.18 while the tree was on 0.8.x, because the Arch package is built by makepkg rather than the tauri bundler and set-version.sh never touched it. makepkg produced a package whose version bore no relation to the source it was built from — the exact failure that script exists to prevent, in the one file it had missed. Dev versions are converted to a pkgver Arch accepts: a hyphen separates pkgver from pkgrel, so 0.9.0-3-gabc1234 becomes 0.9.0.r3.gabc1234. pkgrel resets to 1, since a new upstream version restarts its packaging revisions. Also installs LICENSE into /usr/share/licenses — MIT is not in Arch's common licences, so a package under it has to carry the text. Arch is not part of the automated release (build-release.yml covers linux, windows and android), so v0.9.0 is unaffected; this applies to anyone building the package by hand. |
||
|
|
20e2331560 |
chore(release): 0.9.0
🏗️ Build and Test JellyTau / Run Tests (push) Skipped
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 6m43s
Traceability Validation / Check Requirement Traces (push) Successful in 13s
Build & Release / Run Tests (push) Failing after 17m59s
Build & Release / Build Linux (push) Skipped
Build & Release / Build Windows (push) Skipped
Build & Release / Build Android (push) Skipped
Build & Release / Create Release (push) Skipped
|
||
|
|
bb140a8734 |
docs(release): write the v0.9.0 changelog and refresh artifact names
release:notes is not usable for this batch: it maps changed files to their TRACES, and the logging sweep touched 63 files spanning most of the codebase, so it reports nearly every user requirement as changed — including ones explicitly not implemented. Written by hand instead. Also updates the checklist's artifact names for the rename and adds the rpm, which the checklist never listed because it was never published. |
||
|
|
28b600304f |
fix(scripts): check registry auth properly before pushing the builder image
`docker info | grep Username` only reports a Docker Hub session, so for a private registry the guard never matched: every push dropped into an interactive docker login, which hangs a non-interactive run. Checks the credential store for the specific registry instead, and refuses with instructions rather than prompting when there is no TTY. |
||
|
|
8fbf4d92cb |
ci: match release bundles by extension, and ship the rpm
Renaming the app to JellyTau renamed its bundles, and the release job globbed `bundle/deb/jellytau_*.deb`. The copy was wrapped in `if [ -f ... ]`, so the rename would have dropped the .deb from the release silently — a green build producing an incomplete release. Matching by extension removes the coupling between the product name and the pipeline, and an empty dist/linux now fails the job instead of passing quietly. That `if [ -f "dir/"*.ext ]` guard was also wrong on its own terms: with more than one match, test gets extra arguments and returns false. Found while verifying the rename: the rpm has been built by every release since deb+rpm became the bundle targets, and never copied, published or documented. It ships now. Also declares the package rename. Tauri kebab-cases productName into the Debian package name, so "JellyTau" produces `jelly-tau` — a different package from the `jellytau` earlier releases installed, which would have put a second copy alongside the old one. deb now declares Replaces/Conflicts/Provides and rpm Obsoletes/Provides, verified in the built control file. TRACES: | DR-214 |
||
|
|
d32ca13d00 |
chore: give the project its own identity instead of the scaffold's
Cargo.toml still carried `description = "A Tauri App"` and `authors = ["you"]`, package.json's description was empty with no author or repository, and there was no LICENSE file at all despite package.json declaring MIT. The user-visible half matters more. productName was the scaffold's lowercase "jellytau", which is what the Android *release* build shows under its icon and what the deb/rpm/NSIS bundles carry as their display name. It went unnoticed because build.gradle.kts overrides the label to "JellyTau Debug" for the debug build type — the install a developer sees every day was the only correctly-cased one. mainBinaryName pins the executable filename to "jellytau" so build-windows-cross.sh and the Arch PKGBUILD, which both resolve it by name, need no change. strings.xml moves into the canonical android tree rather than being edited in gen/, since sync-android-sources.sh already copies res/values/*.xml — so the fix survives the next regeneration. Bundle metadata (publisher, copyright, category, descriptions, licence) was absent entirely, so the packages shipped with no maintainer or description. The hand-written PKGBUILD and .desktop had all of it; only the generated packaging was wrong. Adds .env.example: three scripts require signing vars from a gitignored .env and .gitignore already whitelists the example, but none existed. TRACES: | DR-214 |
||
|
|
2a3f08f8a4 |
build: hand containerised build artifacts back to the host user
The compose services bind-mount the repo and build as root, so every artifact they leave in src-tauri/target belongs to root on the host. It accumulates: 11,124 such files had built up, enough that cargo clean and scripts/clean.sh failed with EACCES — and a plain cargo build died part-way through, because build scripts compile for the host and land in target/debug even when cross-compiling to Android. That is what blocked the device build in this batch. Restores ownership at the end of each containerised build, reading the intended owner from the checkout so no uid has to be plumbed through from the host. A no-op when not running as root, so the native build scripts call it unconditionally. Running the containers as the host uid is the tidier fix and stays open — it needs the cargo/bun cache volumes moved off /root first, which is why this is not a one-line user: directive. TRACES: | DR-213 |
||
|
|
68ca1d585d |
chore: regenerate bindings and the traceability matrix
bindings.ts picks up the library-exclusion commands and types from tauri-specta. The matrix regenerates because validation.ts and its test are gone — the doc link checker caught the stale references, which is the first time that gate has paid for itself on a generated artifact rather than a hand-written link. Also drops exclusions::is_excluded: a wrapper over is_excluded_by that only a test called, while the trait impls hoist the snapshot themselves. The test now calls the same path production does. |
||
|
|
0815445aa7 |
feat(library): exclude chosen folders from music browsing
Replaces a hardcoded filter that dropped anything named "Podcasts" from music results — one user's library layout compiled into the shipped product, keyed on an English literal, applied only at the six call sites someone had remembered. Exclusion is now a user setting stored in Rust and applied at the repository layer's convergence points, so scope is decided once and is the same on every screen. It matches on folder id rather than name: a title is not what an item is, which is why an album legitimately called "Podcasts" used to vanish. Deliberately not filtered: get_item (an id asked for by name was navigated to on purpose, and refusing it would break playback of anything inside a hidden folder), get_downloaded_items (hiding a download would leave the user unable to delete a file whose disk usage they can still see), and the offline cache (an exclusion is a view preference and must be reversible without a re-crawl). Also removes src/lib/utils/validation.ts — six exported validators with no caller outside their own test file, which made the module read as covered input validation while guarding nothing. TRACES: UR-076 | DR-209 | UT-203 |
||
|
|
048c99ebcc |
fix(downloads): allow the deliberate join_absolute_paths lint in a test
The assertion documents that PathBuf::join discards its base when handed an absolute path — which is why confinement has to happen after the join, not instead of it. clippy::join_absolute_paths flags that shape, correctly for production code, so the lint is allowed here rather than the test weakened. Worth recording: this lint would not have caught the original defect. The real join sites pass a variable, and it only fires on a literal. |
||
|
|
34026d22b4 |
fix(logging): keep debug logging in a packaged debug build
import.meta.env.DEV is true only under the vite dev server, but scripts/build-android.sh produces the debug APK with a plain `bun run build` — so the logger defaulted to warn there too and the debug package lost every frontend message from logcat. `bun run android:logs` is a documented workflow that depends on them. vite now defines __JT_DEBUG_BUILD__ from Tauri's TAURI_ENV_DEBUG, which the CLI sets while running beforeBuildCommand. The decision is split into a pure resolveDefaultLogLevel(isDevServer, isDebugBuild) because neither import.meta.env.DEV nor a vite define can be varied from inside a test. Also replaces the pinned requirement counts in extract-traces.test.ts with invariants. The pins guarded nothing the computeCoverage fixtures don't already cover, while forcing every branch that adds a requirement to edit the numbers — the comment above them had become a ledger of which branch contributed which row. TRACES: | DR-204 | UT-201 |
||
|
|
aeb29f916b | docs(requirements): add rows for the path-confinement and query-binding work | ||
|
|
f83c7ed1f0 |
fix(downloads): confine download paths to the download root
file_path and target_dir reached PathBuf::join unchecked from the frontend, and mark_download_completed stored a caller-supplied path that is later fed to remove_file. A correct sanitiser already existed — download_item_and_start used it — but download_item is itself a command taking file_path raw, so the guard was simply routed around. It now lives inside download_item, alongside a join-then-confine check modelled on media_server::resolve_path. Sanitising is per path component, not whole-string: the latter would silently turn downloads/x.mp3 into downloads_x.mp3 and relocate every existing download. TRACES: | DR-211 | UT-205 |
||
|
|
b313b61717 |
fix(repository): bind query parameters and encode URL values
Three consistency fixes, each one applying a pattern the same file already used a few lines away: the offline get_items type filter now binds placeholders like search at offline.rs:1786 does, build_get_items_endpoint percent-encodes its values like the Genres block below it does, and player_set_volume clamps NaN and out-of-range input at the command boundary rather than relying on each backend to do it. TRACES: | DR-212 | UT-206 |
||
|
|
fb6bd5cae1 |
fix(thumbnails): confine cache writes to the cache directory
item_id and image_type reached the cache filename unsanitised while tag was already being sanitised, and Path::join neither folds .. nor keeps the base when handed an absolute path. Applies the tag's existing rule to all three parts and adds a starts_with(cache_dir) check at the point of use, modelled on media_server::resolve_path. Not exploitable as shipped — server URLs must be HTTPS (auth/mod.rs) and Android blocks cleartext, so the id would have to come from a server the user chose to trust. This makes the write path consistent with how the rest of the codebase already handles caller-supplied paths. TRACES: | DR-210 | UT-204 |
||
|
|
da6b039b29 |
fix(downloads): confine download paths to the download root
Both halves of the path a download writes to arrived from the frontend
unchecked. `start_download` and the queue pump built their target as
`PathBuf::from(target_dir).join(file_path)`, and `mark_download_completed`
stored a frontend-supplied `file_path` on the row verbatim — the same
column that is later read back into `std::fs::remove_file` when a
download is deleted. A correct sanitiser already existed and
`download_item_and_start` used it, but `download_item` is a command in
its own right, so calling it directly routed the guard around.
The guard moves inside. `confine_to_root` folds `..` away lexically and
requires the result to sit inside the storage root, modelled on
`media_server::resolve_path` — the check comes after the join because
`Path::join` drops the base when the joined half is absolute, so an
absolute `file_path` is obeyed rather than folded. `confine_queued_path`
sanitises a queued path per component (so the already-safe name
`download_item_and_start` passes in is not sanitised into a second,
different one) and confines it. Applied in `download_item`, at both join
sites, and to what `mark_download_completed` writes.
Every path the app builds for itself is returned unchanged, including
the absolute ones `download_series`/`download_season` produce from
`${targetDir}/videos`, so no existing row or file on disk is orphaned.
The pump fails an offending row rather than skipping it, because the
pump re-queries and would otherwise not terminate.
Not a live vulnerability: reaching these commands with hostile input
needs script execution in a webview whose CSP is `script-src 'self'`.
This is hardening and consistency.
TRACES: DR-211 | UT-205
|
||
|
|
080cdbf383 |
fix(player): clamp volume at the command boundary
player_set_volume passed `volume` through untouched. Each backend clamps to 0.0..=1.0 for itself, so local playback was already safe, but the remote branch reaches no backend: it converts with `(volume * 100.0) as i32`, which turns infinity into i32::MAX. NaN is handled explicitly since f32::clamp returns NaN for a NaN input and it then survives every comparison downstream. TRACES: DR-212 | UT-206 |
||
|
|
6b7ce512ed |
fix(online): percent-encode query values and path ids
build_get_items_endpoint pasted ParentId, IncludeItemTypes, SortBy and SortOrder straight into the query string while the Genres parameter twenty lines below and the SearchTerm parameter both percent-encode theirs. Encode them the same way, per list element so the commas Jellyfin splits on survive. The per-call ids interpolated into request paths (item, person and playlist ids) get the same treatment; a Jellyfin GUID is unchanged by encoding, so this is consistency, not a behaviour change. self.user_id is left alone throughout, as it is at the endpoint builders already. TRACES: UR-007 | DR-212 | UT-206 |
||
|
|
55b37ba2f4 |
ci: make clippy a hard gate
The advisory step existed because the tree carried a warning backlog. Measured on 1.97.1 — the pinned toolchain CI actually uses — that backlog is three warnings, not the ~51 the comment claimed: two unnecessary_sort_by in smart_cache and one redundant into_iter in offline. Fixed, so clippy now runs with -D warnings and a warning means new breakage. Worth recording why this took a toolchain pin to do safely: the same tree measured 0 warnings on 1.92.0 and 3 on 1.97.1. Flipping the flag on a local measurement, without the pin, would have reddened CI on the next push. TRACES: | DR-206 |
||
|
|
d52470e0cd |
fix(offline): bind item-type filter as query parameters
get_items built its `AND i.item_type IN (…)` fragment by interpolating
each requested type into the SQL string, while `search`, `get_favorites`
and `prune_stale_catalog` in the same file bind the identical filter as
`?` placeholders. Follow the existing pattern so the listing query is
consistent with its neighbours.
The type values bind between the six parent-matching ids and the
favourites user id, matching where `{type_filter}` lands in the
statement.
TRACES: UR-065 | DR-212 | UT-206
|
||
|
|
e12f0065a6 |
fix(thumbnails): confine cache writes to the cache directory
The thumbnail cache built its filename from `item_id`, `image_type` and `tag`, but only sanitised the tag. `Path::join` neither folds `..` nor keeps its base when handed an absolute path, so a malformed id could place a cache write outside the cache directory. Sanitise all three parts through one helper using the rule the tag already used (non-alphanumerics become `_`), so ids and types that were already safe keep producing exactly the same filename, and resolve the result against the cache dir with a lexical `..` fold plus a `starts_with` check, modelled on `media_server::resolve_path`. The database still stores the raw key and the resolved path, so the lookup in `get_cached_path` keeps matching what the caller asks for. |
||
|
|
63d4df0cde |
chore(tooling): keep lint and format out of the scratch worktrees
.claude/worktrees holds full checkouts of this repo, generated .svelte-kit trees included, so 'eslint .' was linting every in-flight branch — 410 errors, none of them ours. Same root cause the doc-link checker hit. |
||
|
|
6b90582e3e |
chore(tooling): add lint/format gates, pin the toolchain, enforce commit checks
Adds the frontend's first linter and formatter — the Rust half has had cargo fmt --check and clippy in CI for a while, while 274 TS/Svelte files had only svelte-check. ESLint runs clean; 159 findings are recorded as warnings rather than suppressed, so the backlog is visible without painting CI red. Also: `bun run test` no longer drops into watch mode (the "Before Committing" list told people to run a command that never returns), the traceability ratchet moves 82% -> 88%, a pre-commit hook enforces the fast half of that list instead of relying on memory, the dead webdriverio e2e suite and its five devDeps are removed, and the Rust toolchain is pinned to 1.97.1 so the developer machine and the CI builder image stop being five releases apart. TRACES: | DR-205, DR-206, DR-207 |
||
|
|
ea3c765561 |
chore: remove unused frontend validation module
`src/lib/utils/validation.ts` exported six validators (validateItemId,
validateImageType, validateMediaSourceId, validateUrlPathSegment,
validateNumericParam, validateQueryParamValue). Nothing outside its own
213-line test suite ever called them, so the module read as covered,
guarded input validation while guarding nothing — a green test run over
code no input ever passes through.
Deleting it does not weaken any check that was running; it removes the
false assurance that one was.
Note: the layer this validation belongs in per CLAUDE.md ("Validate all
inputs in Rust command handlers") does not implement it either. That is
a separate concern and is left untouched here.
|
||
|
|
ac3cd67164 |
feat(library): exclude chosen folders from music browsing
Replaces `src/lib/utils/podcastFilter.ts` — a shipped personal workaround that dropped any item whose name, album, album artist or artist was literally "Podcasts" — with a real user setting applied in Rust. The old filter was wrong twice over: it hardcoded one user's folder layout keyed on an English literal, and it put a domain rule (what a query should return) in the presentation layer. It slipped past `check:boundary` only because it matched on names rather than on an item-type array. - `repository::exclusions` owns the rule and the process-wide id set, the same shape as `online::STREAMING_QUALITY` so it survives a repository being rebuilt on re-login. - `HybridRepository` applies it where the cache and server legs of every cache-first query converge (`parallel_race` / `race_with_refresh`), plus the bespoke `get_items` path and the server-only reads. Filtering before the "has content" check is what makes a cache page of nothing but hidden items fall through to the server. - Exclusion is by stable item id, never by name, and matches an item's own id or any container link it carries (parent, album, library, series, season, artist). - A direct `get_item` lookup and the Downloads surface are deliberately unfiltered: hiding those would break playback and file management of anything inside a hidden folder. - `LibrarySettings` persists to `app_settings` and is restored in the setup hook, alongside the streaming-quality cap. Default is an empty list — nobody inherits the old "Podcasts" behaviour. - New commands `library_get_settings`, `library_set_settings` and `library_get_exclusion_candidates`; the candidates read goes through `get_items_unfiltered` so an already-hidden folder still appears in the picker and the setting can be undone. - Settings page gains a "Hidden Folders" section that renders the backend's candidate list and sends back ticked ids; it decides nothing. TRACES: UR-076 | DR-209 | UT-203 |
||
|
|
f5bee069c0 |
fix(desktop): give the window a real title and a usable default size
tauri.conf.json still carried the scaffold defaults: a lowercase "jellytau" title in an 800x600 window. The title is what the OS shows in the task switcher and window list, and 800x600 is too small for a media library grid with a mini player docked at the bottom. Now "JellyTau" at 1280x800, with minWidth/minHeight held at the old 800x600 so the layout still has a defined floor when a user drags the window small. |
||
|
|
adcdadfcaf |
ci: run the documentation link checker
Wires scripts/check-doc-links.sh into build-and-test.yml next to the existing boundary tripwire, and exposes it as `bun run check:links`. The docs are the maintained source of truth for architecture and process and cross-reference each other heavily, so a rename that misses a link quietly turns a doc into a dead end. Pure shell — nothing is installed at job time. The script itself is landing separately; this job step is red until it does. |