feat(login): show the backend's server-version verdict, and drop a dead route builder
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 29m23s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 44s
📱 Test APK / Build test APK (push) Successful in 43m47s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 6m33s
Traceability Validation / Check Requirement Traces (push) Successful in 14s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 5m21s

Two frontend halves of the version-compatibility work.

The login flow now renders ServerCompatibility. A server below the floor blocks
with a message naming the minimum; a server newer than this build gets a
non-blocking note and proceeds; an unreadable version says nothing at all,
because refusing — or even warning — on a version string we could not parse would
punish the user for a limitation of ours.

The frontend never receives a version number to reason about, only the opaque
verdict, for the same reason it never receives an item-type list. Rust decides
whether the server is usable; the frontend decides only how that reads.

Separately, imageCache.getCachedImageUrl is deleted. It built
${serverUrl}/Items/${itemId}/Images/${imageType} in Svelte — a Jellyfin route in
the presentation layer, which is domain logic by this project's own litmus test
(would it change if Jellyfin changed its API?). check:boundary does not catch it:
the tripwire flags item-type array literals, not route strings.

It was also entirely unused. Nothing outside its own file and test ever called
it; the live path is CachedImage.svelte -> commands.imageGetUrl -> Rust, which
was already correct. So the leak was in dead code and the fix is a deletion
rather than a migration.

One consequence left deliberately unacted: that function was the last
convertFileSrc caller, so the asset-protocol grant narrowed to
$APPDATA/thumbnails/** under DR-198 now has no caller at all. Dropping a
capability grant is a security change that deserves its own commit and its own
testing on Android, not a side effect of deleting dead code. Noted in the file.

TRACES: UR-012, UR-085 | DR-285, DR-286

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 20:10:06 +02:00
co-authored by Claude Opus 5
parent 9e2278080d
commit 6c188a2b44
6 changed files with 170 additions and 102 deletions
+18 -63
View File
@@ -1,11 +1,23 @@
// Image cache service - Handles lazy caching of thumbnails with LRU eviction
// TRACES: UR-007 | DR-016
// Image cache service — cache statistics, limits and eviction.
//
// This module used to also export `getCachedImageUrl`, which built
// `${serverUrl}/Items/${itemId}/Images/${imageType}` in the frontend. That was a
// Jellyfin route in the presentation layer — domain logic by this project's own
// litmus test (would it change if Jellyfin changed its API?) — and it was
// **dead**: nothing outside this file and its test ever called it. The live path
// is CachedImage.svelte -> commands.imageGetUrl -> Rust, which was already
// correct. It was deleted rather than migrated (DR-285).
//
// Note for whoever touches the CSP next: that function was the last
// `convertFileSrc` caller, so the asset-protocol grant narrowed to
// `$APPDATA/thumbnails/**` under DR-198 now has no caller at all and is a
// candidate for removal. Left in place here deliberately — dropping a capability
// grant is a security change that deserves its own commit and its own testing on
// Android, not a side effect of deleting dead code.
//
// TRACES: UR-007, UR-085 | DR-016, DR-285
import { convertFileSrc } from "@tauri-apps/api/core";
import { commands } from "$lib/api/bindings";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("ImageCache");
/**
* Statistics about the thumbnail cache
@@ -16,63 +28,6 @@ export interface ImageCacheStats {
limitBytes: number;
}
/**
* Get an image URL, checking cache first then falling back to server.
* Triggers background caching if not cached.
*
* @param serverUrl - The Jellyfin server base URL
* @param itemId - The Jellyfin item ID
* @param imageType - The image type (Primary, Backdrop, etc.)
* @param options - Image options (maxWidth, maxHeight, quality, tag)
* @returns The image URL (local asset URL if cached, server URL otherwise)
*/
export async function getCachedImageUrl(
serverUrl: string,
itemId: string,
imageType: string = "Primary",
options: {
maxWidth?: number;
maxHeight?: number;
quality?: number;
tag?: string;
} = {},
): Promise<string> {
const tag = options.tag || "default";
// Try to get cached version
try {
const cachedPath = await commands.thumbnailGetCached(itemId, imageType, tag);
if (cachedPath) {
// Convert file path to asset URL for Tauri. This is the only remaining
// convertFileSrc caller, which is why the asset-protocol scope is narrowed
// to $APPDATA/thumbnails/** — a path outside it resolves to nothing.
// TRACES: UR-012 | DR-134, DR-198
return convertFileSrc(cachedPath);
}
} catch (e) {
log.debug("Failed to check thumbnail cache:", e);
}
// Build server URL
const params = new URLSearchParams();
if (options.maxWidth) params.set("maxWidth", options.maxWidth.toString());
if (options.maxHeight) params.set("maxHeight", options.maxHeight.toString());
if (options.quality) params.set("quality", options.quality.toString());
if (options.tag) params.set("tag", options.tag);
const serverImageUrl = `${serverUrl}/Items/${itemId}/Images/${imageType}?${params.toString()}`;
// Trigger background caching (fire and forget)
commands.thumbnailSave(itemId, imageType, tag, serverImageUrl).catch((e) => {
// Silently fail - caching is best-effort
log.debug("Background thumbnail cache failed:", e);
});
// Return server URL for immediate display
return serverImageUrl;
}
/**
* Get thumbnail cache statistics
*/