5 Commits
Author SHA1 Message Date
dtourolle 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.
2026-08-21 18:15:58 +02:00
dtourolle 88e15e3e12 merge: Android runtime security (B1, B3)
Correct the POST_NOTIFICATIONS mechanism: the lockscreen notification is exempt
because of the MediaSession token, not because it belongs to a foreground
service — FGS notifications are explicitly NOT exempt. So no permission prompt
and no checkSelfPermission gate; instead both notification builders bind the
token once and log loudly if it is ever null, turning a silent failure into a
logcat line. Stop the webview undoing the network security config:
mixedContentMode COMPATIBILITY, allowFileAccess/allowContentAccess false.

Conflict resolution: this branch's DR-198 collided with the Tauri branch's, so
it was renumbered DR-200 (3 TRACES in JellyTauPlaybackService.kt and the UR-006
matrix row updated). DR-199 was uncontested. Pinned counts summed to DR 191 /
total 334; UR-071 takes both DR-198 and DR-199.
2026-08-16 23:03:29 +02:00
dtourolle 2d21f092d5 fix(android): stop the webview undoing the network security config
MainActivity set mixedContentMode = MIXED_CONTENT_ALWAYS_ALLOW together with
allowFileAccess/allowContentAccess = true, which is a blanket cleartext opt-in
reached by hand — the exact thing network_security_config.xml exists to prevent
and its own comment warns against. Nothing needed any of the three:

- file:// is never loaded. Cached thumbnails go through convertFileSrc, which
  on Android resolves to http://asset.localhost/... and is answered by wry's
  request interceptor rather than the filesystem; downloaded media goes over the
  loopback HTTP server (DR-137), which exists precisely because the asset/file
  route cannot stream a large file.
- content:// is never loaded. The manifest's FileProvider is for outbound share
  intents, not webview navigation.
- Mixed content never arises. Tauri serves the UI from http://tauri.localhost
  (use_https_scheme defaults false and is not set), and both 127.0.0.1 and
  asset.localhost are loopback/.localhost origins Chromium treats as potentially
  trustworthy. A plain-HTTP remote server would be mixed content, but the network
  security config already rejects it first — so ALWAYS_ALLOW bought nothing.

COMPATIBILITY_MODE rather than NEVER_ALLOW is a deliberate hedge: the platform
default at targetSdk 21+ is NEVER_ALLOW, so this is still one step looser, and it
keeps passive content working if the analysis missed a path. The two files now
cross-reference each other so the pair cannot drift apart again.

Also records why POST_NOTIFICATIONS is declared but never requested. An audit
read the missing runtime request as a threat to the lockscreen controls; it is
not. A foreground-service notification is explicitly NOT exempt, but a
media-session one is, and the platform predicate (Notification.isMediaNotification)
requires MediaStyle AND a non-null session token. Confirmed on device: appops
POST_NOTIFICATION: ignore with the transport notification live. So no permission
prompt is added and startForeground stays ungated — a guard there would trade a
cosmetic problem for the "did not then call Service.startForeground()" kill.
What is added is the guard matching the real precondition: both builders bind the
token once and log an error if it is ever null, since SystemUI's media carousel
is gated on the same predicate and a token-less notification loses the lockscreen
controls entirely, silently.

TRACES: UR-006, UR-071 | DR-198, DR-199
2026-08-16 22:59:47 +02:00
dtourolle 38dd1129e5 feat(security): set a restrictive CSP and scope the asset protocol to thumbnails
`app.security.csp` was `null`, so the webview ran with no Content-Security-Policy
at all: any script that reached the web layer would have inherited the whole IPC
surface. There is no known injection path today (one app-owned `{@html}`, no
`innerHTML`/`eval`), so this is defence in depth rather than a fix for an open
hole.

`script-src 'self'` is the restrictive half — Tauri nonces SvelteKit's inline
bootstrap script at build time, so no `'unsafe-inline'` is needed — together with
`object-src`/`frame-src 'none'` and `base-uri 'self'`. `img-src`/`media-src`/
`connect-src` cannot be restrictive: the Jellyfin origin is typed in by the user
at run time and is routinely plain http on a LAN, so they allow `http:`/`https:`.
That is a wide grant for data, but it still bars `file:`/`filesystem:` and does
not touch script execution. A run-time policy naming the server exactly was
rejected: Tauri derives the header from immutable config when it serves the HTML,
so it would mean rebuilding config and reloading the webview on every server
change. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"`
attributes into markup; `worker-src`/`media-src` keep `blob:` for hls.js's
demuxer worker and its MSE object URL; `ipc:`/`http://ipc.localhost` keeps
`invoke` working. `devCsp` mirrors it with the eval/inline/websocket allowances
Vite's dev server needs.

The asset-protocol scope narrows from `$APPDATA/**` — the storage root holding
the SQLite database and the encrypted-token fallback file — to
`$APPDATA/thumbnails/**`. Since DR-137 moved downloaded media to the loopback
media server, `imageCache` is the only `convertFileSrc` caller left.

Needs manual verification on both platforms: thumbnails, online HLS video and
offline downloaded video cannot be exercised headlessly.
2026-08-16 22:58:53 +02:00
dtourolle 09780103a7 Split software arch desc for easier manintenance. Many fixes related to next video playing and remote playback
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 12s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 1s
2026-03-01 19:47:46 +01:00