From 1e052373a41be81a21dc57499ec3d322fae6926d Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Thu, 14 May 2026 17:15:25 +0300 Subject: [PATCH] refactor(webui): route stats interop through shell bridge - move stats route legacy handoffs onto explicit SoulSyncWebShellBridge methods\n- stop relying on ad hoc window globals from React code for artist navigation and playback\n- update shell bridge tests and route test doubles to enforce the expanded bridge contract --- webui/src/app/router.test.tsx | 5 ++ webui/src/platform/shell/bridge.test.ts | 10 ++++ webui/src/platform/shell/globals.d.ts | 44 ++++++++--------- webui/src/routes/issues/-route.test.tsx | 5 ++ webui/src/routes/stats/-route.test.tsx | 10 ++-- webui/src/routes/stats/-ui/stats-page.tsx | 59 +++++++++++++++-------- webui/static/shell-bridge.js | 15 ++++++ 7 files changed, 101 insertions(+), 47 deletions(-) diff --git a/webui/src/app/router.test.tsx b/webui/src/app/router.test.tsx index 63295b4b..95f90e97 100644 --- a/webui/src/app/router.test.tsx +++ b/webui/src/app/router.test.tsx @@ -63,6 +63,11 @@ function createShellBridge(overrides: Partial = {}): ShellBridge { navigateToArtistDetail: vi.fn(), cancelSimilarArtistsLoad: vi.fn(), showReactHost: vi.fn(), + navigateToArtistDetail: vi.fn(), + playLibraryTrack: vi.fn(), + startStream: vi.fn(), + showLoadingOverlay: vi.fn(), + hideLoadingOverlay: vi.fn(), ...overrides, }; } diff --git a/webui/src/platform/shell/bridge.test.ts b/webui/src/platform/shell/bridge.test.ts index 9682bbe7..29546e29 100644 --- a/webui/src/platform/shell/bridge.test.ts +++ b/webui/src/platform/shell/bridge.test.ts @@ -19,6 +19,11 @@ describe('waitForShellContext', () => { setActivePageChrome: vi.fn(), cancelSimilarArtistsLoad: vi.fn(), showReactHost: vi.fn(), + navigateToArtistDetail: vi.fn(), + playLibraryTrack: vi.fn(), + startStream: vi.fn(), + showLoadingOverlay: vi.fn(), + hideLoadingOverlay: vi.fn(), } as NonNullable; await expect(waitForShellContext()).resolves.toEqual({ @@ -41,6 +46,11 @@ describe('waitForShellContext', () => { setActivePageChrome: vi.fn(), cancelSimilarArtistsLoad: vi.fn(), showReactHost: vi.fn(), + navigateToArtistDetail: vi.fn(), + playLibraryTrack: vi.fn(), + startStream: vi.fn(), + showLoadingOverlay: vi.fn(), + hideLoadingOverlay: vi.fn(), } as NonNullable; const contextPromise = waitForShellContext(); diff --git a/webui/src/platform/shell/globals.d.ts b/webui/src/platform/shell/globals.d.ts index 2e254c24..5ee321c2 100644 --- a/webui/src/platform/shell/globals.d.ts +++ b/webui/src/platform/shell/globals.d.ts @@ -45,29 +45,29 @@ declare global { ) => void; cancelSimilarArtistsLoad: () => void; showReactHost: (pageId: ShellPageId) => void; + navigateToArtistDetail: ( + artistId: string | number, + artistName: string, + sourceOverride?: string | null, + options?: Record, + ) => void; + playLibraryTrack: ( + track: { + id: string | number; + title: string; + file_path: string; + bitrate?: string | number | null; + artist_id?: string | number | null; + album_id?: string | number | null; + _stats_image?: string | null; + }, + albumTitle: string, + artistName: string, + ) => void | Promise; + startStream: (searchResult: Record) => void | Promise; + showLoadingOverlay: (message?: string) => void; + hideLoadingOverlay: () => void; }; - navigateToArtistDetail?: ( - artistId: string | number, - artistName: string, - sourceOverride?: string | null, - options?: Record, - ) => void; - playLibraryTrack?: ( - track: { - id: string | number; - title: string; - file_path: string; - bitrate?: string | number | null; - artist_id?: string | number | null; - album_id?: string | number | null; - _stats_image?: string | null; - }, - albumTitle: string, - artistName: string, - ) => void | Promise; - startStream?: (searchResult: Record) => void | Promise; - showLoadingOverlay?: (message?: string) => void; - hideLoadingOverlay?: () => void; } } diff --git a/webui/src/routes/issues/-route.test.tsx b/webui/src/routes/issues/-route.test.tsx index 7bbc0353..023e24b8 100644 --- a/webui/src/routes/issues/-route.test.tsx +++ b/webui/src/routes/issues/-route.test.tsx @@ -25,6 +25,11 @@ function createShellBridge(overrides: Partial = {}): ShellBridge { navigateToArtistDetail: vi.fn(), cancelSimilarArtistsLoad: vi.fn(), showReactHost: vi.fn(), + navigateToArtistDetail: vi.fn(), + playLibraryTrack: vi.fn(), + startStream: vi.fn(), + showLoadingOverlay: vi.fn(), + hideLoadingOverlay: vi.fn(), ...overrides, }; } diff --git a/webui/src/routes/stats/-route.test.tsx b/webui/src/routes/stats/-route.test.tsx index 1b447aa7..604e8493 100644 --- a/webui/src/routes/stats/-route.test.tsx +++ b/webui/src/routes/stats/-route.test.tsx @@ -23,6 +23,11 @@ function createShellBridge(overrides: Partial = {}): ShellBridge { setActivePageChrome: vi.fn(), activateLegacyPath: vi.fn(), showReactHost: vi.fn(), + navigateToArtistDetail: vi.fn(), + playLibraryTrack: vi.fn(), + startStream: vi.fn(), + showLoadingOverlay: vi.fn(), + hideLoadingOverlay: vi.fn(), ...overrides, }; } @@ -41,11 +46,6 @@ function renderStatsRoute(initialEntries = ['/stats']) { describe('stats route', () => { beforeEach(() => { window.SoulSyncWebShellBridge = createShellBridge(); - window.navigateToArtistDetail = vi.fn(); - window.playLibraryTrack = vi.fn(); - window.startStream = vi.fn(); - window.showLoadingOverlay = vi.fn(); - window.hideLoadingOverlay = vi.fn(); window.showToast = vi.fn(); vi.stubGlobal( 'fetch', diff --git a/webui/src/routes/stats/-ui/stats-page.tsx b/webui/src/routes/stats/-ui/stats-page.tsx index 1ab9654a..a16c48de 100644 --- a/webui/src/routes/stats/-ui/stats-page.tsx +++ b/webui/src/routes/stats/-ui/stats-page.tsx @@ -14,6 +14,8 @@ import { YAxis, } from 'recharts'; +import type { ShellBridge } from '@/platform/shell/bridge'; + import { useReactPageShell } from '@/platform/shell/route-controllers'; import type { @@ -72,7 +74,7 @@ const STATS_CHART_CURSOR = { } as const; export function StatsPage() { - useReactPageShell('stats'); + const bridge = useReactPageShell('stats'); const navigate = useNavigate({ from: Route.fullPath }); const queryClient = useQueryClient(); @@ -202,31 +204,42 @@ export function StatsPage() { - + playStatsTrack(bridge, track)} + />
+ void openArtistDetail(bridge, artistId, artistName) + } /> + void openArtistDetail(bridge, artistId, artistName) + } /> + void openArtistDetail(bridge, artistId, artistName) + } /> + void openArtistDetail(bridge, artistId, artistName) + } + onPlay={(track) => playStatsTrack(bridge, track)} />
@@ -606,7 +619,10 @@ function StatsRecentPlays({
{tracks.length === 0 ? : null} {tracks.map((track, index) => ( -
+
; } -async function openArtistDetail(artistId: string | number, artistName: string) { +async function openArtistDetail( + bridge: ShellBridge, + artistId: string | number, + artistName: string, +) { await window.SoulSyncWebRouter?.navigateToPage('library'); window.setTimeout(() => { - window.navigateToArtistDetail?.(artistId, artistName); + bridge.navigateToArtistDetail(artistId, artistName); }, 300); } -async function playStatsTrack(track: { title: string; artist: string; album: string }) { +async function playStatsTrack( + bridge: ShellBridge, + track: { title: string; artist: string; album: string }, +) { const resolvedTrack = await resolveStatsTrack(track.title, track.artist); if (resolvedTrack) { - void window.playLibraryTrack?.( + await bridge.playLibraryTrack( { id: resolvedTrack.id, title: resolvedTrack.title, @@ -887,23 +910,19 @@ async function playStatsTrack(track: { title: string; artist: string; album: str return; } - window.showLoadingOverlay?.(`Searching for ${track.title}...`); + bridge.showLoadingOverlay(`Searching for ${track.title}...`); try { const streamResult = await streamStatsTrack(track.title, track.artist, track.album); - window.hideLoadingOverlay?.(); + bridge.hideLoadingOverlay(); if (streamResult) { - if (typeof window.startStream === 'function') { - await window.startStream(streamResult); - } else { - window.showToast?.('Streaming not available', 'error'); - } + await bridge.startStream(streamResult); return; } window.showToast?.('Track not found in library or any source', 'error'); } catch (error) { - window.hideLoadingOverlay?.(); + bridge.hideLoadingOverlay(); window.showToast?.(getErrorMessage(error), 'error'); } } diff --git a/webui/static/shell-bridge.js b/webui/static/shell-bridge.js index 7db84a76..c1ac1d1b 100644 --- a/webui/static/shell-bridge.js +++ b/webui/static/shell-bridge.js @@ -177,6 +177,21 @@ window.SoulSyncWebShellBridge = { showReactHost(pageId) { showReactHost(pageId); }, + navigateToArtistDetail(artistId, artistName, sourceOverride, options) { + return navigateToArtistDetail(artistId, artistName, sourceOverride, options); + }, + playLibraryTrack(track, albumTitle, artistName) { + return playLibraryTrack(track, albumTitle, artistName); + }, + startStream(searchResult) { + return startStream(searchResult); + }, + showLoadingOverlay(message) { + return showLoadingOverlay(message); + }, + hideLoadingOverlay() { + return hideLoadingOverlay(); + }, }; function _handleShellLinkClick(event) {