refactor(logging): route frontend console calls through the logger

TRACES: | DR-204

484 ungated `console.*` calls across 63 non-test frontend files shipped to
end users with no way to turn them off. Mechanical substitution, no control
flow, error handling or message semantics changed:

  console.log / console.debug -> log.debug
  console.info                -> log.info
  console.warn                -> log.warn
  console.error               -> log.error

Hand-written `"[Scope] …"` prefixes are dropped where the logger's scope
now carries them; scope names that already existed are preserved verbatim
(`[Auth]`, `[VideoPlayer]`, `[PiP]`, …) and inferred from the filename
where a file had none. `src/routes/player/[id]/+page.svelte` keeps its
`NextEpisode` and `AutoPlay` sub-scopes as separate loggers rather than
flattening them into the page scope.

`grep -rn 'console\.' src/` now matches nothing outside the tests and the
facade itself.
This commit is contained in:
2026-08-20 19:29:59 +02:00
parent 4c82a0a025
commit d54d8cc7c4
63 changed files with 686 additions and 490 deletions
+4 -1
View File
@@ -9,6 +9,9 @@
*/
import { commands } from "$lib/api/bindings";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("deviceId");
let cachedDeviceId: string | null = null;
@@ -34,7 +37,7 @@ export async function getDeviceId(): Promise<string> {
cachedDeviceId = deviceId;
return deviceId;
} catch (e) {
console.error("[deviceId] Failed to get device ID from backend:", e);
log.error("Failed to get device ID from backend:", e);
throw new Error("Failed to initialize device ID: " + String(e));
}
}
+4 -1
View File
@@ -6,6 +6,9 @@ import { commands } from "$lib/api/bindings";
import { auth } from "$lib/stores/auth";
import { isConnected } from "$lib/stores/connectivity";
import { setFavorite } from "$lib/stores/favorites";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("Favorites");
/**
* Toggle the favorite status of an item.
@@ -59,7 +62,7 @@ export async function toggleFavorite(
// 3. Mark as synced
await commands.storageMarkSynced(userId, itemId);
} catch (error) {
console.error("Failed to sync favorite to server:", error);
log.error("Failed to sync favorite to server:", error);
// Favorite is stored locally and will be synced later
// via sync queue (when implemented)
}
+5 -2
View File
@@ -3,6 +3,9 @@
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
@@ -48,7 +51,7 @@ export async function getCachedImageUrl(
return convertFileSrc(cachedPath);
}
} catch (e) {
console.debug("Failed to check thumbnail cache:", e);
log.debug("Failed to check thumbnail cache:", e);
}
// Build server URL
@@ -63,7 +66,7 @@ export async function getCachedImageUrl(
// Trigger background caching (fire and forget)
commands.thumbnailSave(itemId, imageType, tag, serverImageUrl).catch((e) => {
// Silently fail - caching is best-effort
console.debug("Background thumbnail cache failed:", e);
log.debug("Background thumbnail cache failed:", e);
});
// Return server URL for immediate display
+5 -2
View File
@@ -12,6 +12,9 @@
import { commands } from '$lib/api/bindings';
import type { NetworkType } from '$lib/api/bindings';
import { createLogger } from '$lib/utils/logger';
const log = createLogger('NetworkType');
/** The Android bridge, present only in the Android WebView. */
interface AndroidNetworkTypeBridge {
@@ -62,7 +65,7 @@ export async function reportNetworkState(): Promise<void> {
} catch (error) {
// Never let network reporting break the UI — the gate fails closed on
// the Rust side, so a missed report at worst delays a queued download.
console.warn('[NetworkType] Failed to report network state:', error);
log.warn('Failed to report network state:', error);
}
}
@@ -103,7 +106,7 @@ export async function areDownloadsAllowed(): Promise<boolean> {
try {
return await commands.getDownloadsAllowed();
} catch (error) {
console.warn('[NetworkType] Failed to query download gate:', error);
log.warn('Failed to query download gate:', error);
return true;
}
}
+5 -2
View File
@@ -15,6 +15,9 @@ import { goto } from "$app/navigation";
import { cancelAutoplayCountdown } from "$lib/api/autoplay";
import { nextEpisode } from "$lib/stores/nextEpisode";
import type { MediaItem } from "$lib/api/types";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("NextEpisode");
/** Guard against double-navigation */
let isNavigating = false;
@@ -46,11 +49,11 @@ export async function cancelAutoPlay() {
*/
function navigateToEpisode(episode: MediaItem) {
if (isNavigating) {
console.warn("[NextEpisode] Already navigating, skipping duplicate navigation to", episode.id);
log.warn("Already navigating, skipping duplicate navigation to", episode.id);
return;
}
isNavigating = true;
console.log("[NextEpisode] Navigating to next episode:", episode.id, episode.name);
log.debug("Navigating to next episode:", episode.id, episode.name);
nextEpisode.hidePopup();
goto(`/player/${episode.id}?restart=true`, { replaceState: true }).finally(() => {
isNavigating = false;
+11 -8
View File
@@ -17,6 +17,9 @@ import { writable, type Writable } from "svelte/store";
import { commands } from "$lib/api/bindings";
import { auth } from "$lib/stores/auth";
import { isConnected } from "$lib/stores/connectivity";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("OfflineCatalog");
/**
* When true (and offline), library grids reveal greyed-out versions of media
@@ -58,7 +61,7 @@ async function pushCatalogVisibility(connected: boolean, showCatalog: boolean):
try {
await commands.setShowServerCatalog(include);
} catch (err) {
console.warn("[OfflineCatalog] Failed to set catalog visibility:", err);
log.warn("Failed to set catalog visibility:", err);
// The backend is still on the old gate, so forget that we sent this —
// otherwise the next identical transition is skipped as a no-op and the
// frontend and backend disagree about the filter for the rest of the
@@ -115,12 +118,12 @@ export async function syncCatalog(): Promise<void> {
syncInProgress = true;
try {
const result = await commands.syncFullCatalog(handle);
console.info(
`[OfflineCatalog] Synced ${result.itemsCached} items (${result.librariesFailed} libraries failed)`
log.info(
`Synced ${result.itemsCached} items (${result.librariesFailed} libraries failed)`
);
await refreshSyncStatus();
} catch (err) {
console.warn("[OfflineCatalog] Full catalog sync failed:", err);
log.warn("Full catalog sync failed:", err);
} finally {
syncInProgress = false;
}
@@ -136,12 +139,12 @@ export async function resumeQueued(): Promise<void> {
try {
const result = await commands.resumeQueuedDownloads(handle);
if (result.resolved > 0 || result.failed > 0) {
console.info(
`[OfflineCatalog] Resumed queued downloads: ${result.resolved} resolved, ${result.failed} failed`
log.info(
`Resumed queued downloads: ${result.resolved} resolved, ${result.failed} failed`
);
}
} catch (err) {
console.warn("[OfflineCatalog] Failed to resume queued downloads:", err);
log.warn("Failed to resume queued downloads:", err);
}
}
@@ -151,7 +154,7 @@ export async function refreshSyncStatus(): Promise<void> {
const status = await commands.catalogSyncStatus();
lastCatalogSync.set(status.lastSyncedAt ?? null);
} catch (err) {
console.debug("[OfflineCatalog] Failed to fetch sync status:", err);
log.debug("Failed to fetch sync status:", err);
}
}
+4 -1
View File
@@ -13,6 +13,9 @@
*/
import { commands } from "$lib/api/bindings";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("capabilities");
export interface PlaybackCapabilities {
/** Audio renders through a webview `<audio>` element, not a native backend. */
@@ -52,7 +55,7 @@ export async function getPlaybackCapabilities(): Promise<PlaybackCapabilities> {
};
return cached;
} catch (err) {
console.warn("[capabilities] player_get_capabilities failed:", err);
log.warn("player_get_capabilities failed:", err);
// Do NOT cache the fallback — a later call should get the real answer.
return FALLBACK;
} finally {
+14 -11
View File
@@ -11,6 +11,9 @@
import { commands } from "$lib/api/bindings";
import { auth } from "$lib/stores/auth";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("PlaybackReporting");
/**
* Record the start of playback **locally**, with the context it started from.
@@ -32,8 +35,8 @@ export async function reportPlaybackStart(
const positionMs = Math.floor(positionSeconds * 1000);
const userId = auth.getUserId();
console.log(
"[PlaybackReporting] reportPlaybackStart - itemId:",
log.debug(
"reportPlaybackStart - itemId:",
itemId,
"positionSeconds:",
positionSeconds,
@@ -47,7 +50,7 @@ export async function reportPlaybackStart(
try {
await commands.storageUpdatePlaybackContext(userId, itemId, positionMs, contextType, contextId);
} catch (e) {
console.error("[PlaybackReporting] Failed to update playback context:", e);
log.error("Failed to update playback context:", e);
}
}
}
@@ -76,7 +79,7 @@ export async function reportPlaybackProgress(
// Reduce logging for frequent progress updates
if (Math.floor(positionSeconds) % 30 === 0) {
console.log("[PlaybackReporting] reportPlaybackProgress - itemId:", itemId, "position:", positionSeconds);
log.debug("reportPlaybackProgress - itemId:", itemId, "position:", positionSeconds);
}
// Update local DB only (progress updates are frequent, don't report to server)
@@ -84,7 +87,7 @@ export async function reportPlaybackProgress(
try {
await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs);
} catch (e) {
console.error("[PlaybackReporting] Failed to update local progress:", e);
log.error("Failed to update local progress:", e);
}
}
}
@@ -101,14 +104,14 @@ export async function reportPlaybackStopped(itemId: string, positionSeconds: num
const positionMs = Math.floor(positionSeconds * 1000);
const userId = auth.getUserId();
console.log("[PlaybackReporting] reportPlaybackStopped - itemId:", itemId, "positionSeconds:", positionSeconds);
log.debug("reportPlaybackStopped - itemId:", itemId, "positionSeconds:", positionSeconds);
// Update local DB first (always works, even offline)
if (userId) {
try {
await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs);
} catch (e) {
console.error("[PlaybackReporting] Failed to update local progress:", e);
log.error("Failed to update local progress:", e);
}
}
@@ -120,7 +123,7 @@ export async function reportPlaybackStopped(itemId: string, positionSeconds: num
const repo = auth.getRepository();
await repo.reportPlaybackStopped(itemId, positionMs);
} catch (e) {
console.warn("[PlaybackReporting] Stop-report did not reach the server; queued for sync:", e);
log.warn("Stop-report did not reach the server; queued for sync:", e);
}
}
}
@@ -133,14 +136,14 @@ export async function reportPlaybackStopped(itemId: string, positionSeconds: num
export async function markAsPlayed(itemId: string): Promise<void> {
const userId = auth.getUserId();
console.log("[PlaybackReporting] markAsPlayed - itemId:", itemId);
log.debug("markAsPlayed - itemId:", itemId);
// Update local DB first
if (userId) {
try {
await commands.storageMarkPlayed(userId, itemId);
} catch (e) {
console.error("[PlaybackReporting] Failed to mark as played in local DB:", e);
log.error("Failed to mark as played in local DB:", e);
}
}
@@ -153,6 +156,6 @@ export async function markAsPlayed(itemId: string): Promise<void> {
await repo.reportPlaybackStopped(itemId, item.durationMs);
}
} catch (e) {
console.error("[PlaybackReporting] Failed to report as played:", e);
log.error("Failed to report as played:", e);
}
}
+18 -15
View File
@@ -20,6 +20,9 @@ import { preloadUpcomingTracks } from "$lib/services/preload";
import { playerController } from "$lib/player";
import type { MediaItem } from "$lib/api/types";
import { get } from "svelte/store";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("playerEvents");
// PlayerStatusEvent and SleepTimerMode are generated by tauri-specta and
// imported from $lib/api/bindings — they are the authoritative shapes emitted
@@ -34,7 +37,7 @@ let isInitialized = false;
*/
export async function initPlayerEvents(): Promise<void> {
if (isInitialized) {
console.warn("Player events already initialized");
log.warn("Player events already initialized");
return;
}
@@ -43,9 +46,9 @@ export async function initPlayerEvents(): Promise<void> {
handlePlayerEvent(event.payload);
});
isInitialized = true;
console.log("Player event listener initialized");
log.debug("Player event listener initialized");
} catch (e) {
console.error("Failed to initialize player events:", e);
log.error("Failed to initialize player events:", e);
}
}
@@ -98,7 +101,7 @@ function handlePlayerEvent(event: PlayerStatusEvent): void {
case "buffering":
// Could show buffering indicator in UI
console.debug(`Buffering: ${event.percent}%`);
log.debug(`Buffering: ${event.percent}%`);
break;
case "error":
@@ -196,7 +199,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
// When local playback starts, ensure mode is set to local
const mode = get(playbackMode);
if (mode.mode !== "local") {
console.log("Setting playback mode to local");
log.debug("Setting playback mode to local");
playbackMode.setMode("local");
}
@@ -215,7 +218,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
// Trigger preloading of upcoming tracks in the background
preloadUpcomingTracks().catch((e) => {
// Preload failures are non-critical, already logged in the service
console.debug("[playerEvents] Preload failed (non-critical):", e);
log.debug("Preload failed (non-critical):", e);
});
} else if (state === "paused" && currentItem) {
// Keep current position and duration from store. The same track is
@@ -240,7 +243,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
// When local playback stops, revert to idle mode
const currentMode = get(playbackMode);
if (currentMode.mode === "local") {
console.log("Setting playback mode to idle");
log.debug("Setting playback mode to idle");
playbackMode.setMode("idle");
}
@@ -254,7 +257,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
function handleMediaLoaded(duration: number): void {
// Media is now loaded and ready
// The state_changed event will handle setting the playing state
console.debug(`Media loaded, duration: ${duration}s`);
log.debug(`Media loaded, duration: ${duration}s`);
}
/**
@@ -268,7 +271,7 @@ async function handlePlaybackEnded(): Promise<void> {
try {
await commands.playerOnPlaybackEnded(null, null);
} catch (e) {
console.error("[playerEvents] Failed to handle playback ended:", e);
log.error("Failed to handle playback ended:", e);
// Fallback: set idle state on error
player.setIdle();
}
@@ -287,18 +290,18 @@ async function handlePlaybackEnded(): Promise<void> {
* TRACES: UR-004, UR-040 | DR-130
*/
async function handleError(message: string, recoverable: boolean): Promise<void> {
console.error(`Playback error (recoverable: ${recoverable}): ${message}`);
log.error(`Playback error (recoverable: ${recoverable}): ${message}`);
if (recoverable) {
try {
if (await commands.playerRecoverStream()) {
console.log("Stream re-opened after a recoverable error - not stopping");
log.debug("Stream re-opened after a recoverable error - not stopping");
return;
}
} catch (e) {
// Fall through to the normal stop: a failed recovery attempt is still an
// error, and leaving the player running would strand it mid-failure.
console.error("Stream recovery attempt failed:", e);
log.error("Stream recovery attempt failed:", e);
}
}
@@ -308,9 +311,9 @@ async function handleError(message: string, recoverable: boolean): Promise<void>
// This also reports playback stopped to Jellyfin server
try {
await commands.playerStop();
console.log("Backend player stopped after error");
log.debug("Backend player stopped after error");
} catch (e) {
console.error("Failed to stop player after error:", e);
log.error("Failed to stop player after error:", e);
// Continue with state cleanup even if stop fails
}
@@ -359,7 +362,7 @@ function handleControlCommand(action: string, position: number | null): void {
void adapter.pause();
break;
default:
console.warn("[playerEvents] Unknown control command:", action);
log.warn("Unknown control command:", action);
}
}
+8 -5
View File
@@ -8,6 +8,9 @@
import { commands } from '$lib/api/bindings';
import type { CacheConfig } from '$lib/api/bindings';
import { auth } from '$lib/stores/auth';
import { createLogger } from '$lib/utils/logger';
const log = createLogger('Preload');
interface PreloadOptions {
/** Enable debug logging */
@@ -28,17 +31,17 @@ export async function preloadUpcomingTracks(options: PreloadOptions = {}): Promi
const userId = overrideUserId || auth.getUserId();
if (!userId) {
if (debug) console.log('[Preload] No active user session, skipping preload');
if (debug) log.debug('No active user session, skipping preload');
return;
}
if (debug) console.log('[Preload] Triggering preload for user:', userId);
if (debug) log.debug('Triggering preload for user:', userId);
// downloadBasePath is currently unused in the backend
const result = await commands.playerPreloadUpcoming(userId, '/downloads');
if (debug) {
console.log('[Preload] Result:', {
log.debug('Result:', {
queued: result.queuedCount,
alreadyDownloaded: result.alreadyDownloaded,
skipped: result.skipped
@@ -47,12 +50,12 @@ export async function preloadUpcomingTracks(options: PreloadOptions = {}): Promi
// Log meaningful results
if (result.queuedCount > 0) {
console.log(`[Preload] Queued ${result.queuedCount} track(s) for background download`);
log.debug(`Queued ${result.queuedCount} track(s) for background download`);
}
} catch (error) {
// Fail silently - preloading is a background optimization
// Don't interrupt the user's playback experience
console.warn('[Preload] Failed to preload upcoming tracks:', error);
log.warn('Failed to preload upcoming tracks:', error);
}
}
+8 -5
View File
@@ -13,6 +13,9 @@ import { auth } from "$lib/stores/auth";
// hand-written mirror — the mirror had already drifted (it predates `itemName`),
// and a drifted duplicate is how a field silently stops reaching the UI.
import type { SyncQueueItem } from "$lib/api/bindings";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("SyncService");
export type { SyncQueueItem };
export type SyncOperation =
@@ -42,14 +45,14 @@ class SyncService {
* Start the sync service (lifecycle managed by Rust backend)
*/
start(): void {
console.log("[SyncService] Started");
log.debug("Started");
}
/**
* Stop the sync service (lifecycle managed by Rust backend)
*/
stop(): void {
console.log("[SyncService] Stopped");
log.debug("Stopped");
}
/**
@@ -74,7 +77,7 @@ class SyncService {
payload ? JSON.stringify(payload) : null
);
console.log(`[SyncService] Queued ${operation} for item ${itemId}, id: ${id}`);
log.debug(`Queued ${operation} for item ${itemId}, id: ${id}`);
return id;
}
@@ -155,7 +158,7 @@ class SyncService {
*/
async cleanup(daysOld: number = 7): Promise<number> {
const deleted = await commands.syncCleanupCompleted(daysOld);
console.log(`[SyncService] Cleaned up ${deleted} old completed operations`);
log.debug(`Cleaned up ${deleted} old completed operations`);
return deleted;
}
@@ -194,7 +197,7 @@ class SyncService {
const userId = auth.getUserId();
if (userId) {
await commands.syncClearUser(userId);
console.log("[SyncService] Cleared sync queue for user");
log.debug("Cleared sync queue for user");
}
}
}