diff --git a/src/renderer/style.css b/src/renderer/style.css index 4be1724b..68e70ba6 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -148,7 +148,7 @@ body:focus-visible, .mining-image-toast { position: absolute; - top: 16px; + top: 80px; right: 16px; padding: 6px; border-radius: 10px; diff --git a/stats/src/components/sessions/SessionsTab.test.tsx b/stats/src/components/sessions/SessionsTab.test.tsx index 2d92fc5a..b1d8baa2 100644 --- a/stats/src/components/sessions/SessionsTab.test.tsx +++ b/stats/src/components/sessions/SessionsTab.test.tsx @@ -71,8 +71,9 @@ test('buildBucketDeleteHandler deletes every session in the bucket when confirm assert.equal(onErrorCalled, false); }); -test('buildBucketDeleteHandler signals onStart after confirm, before deleting', async () => { +test('buildBucketDeleteHandler signals deleted session IDs after confirm, before deleting', async () => { const events: string[] = []; + let startedIds: number[] | null = null; const bucket = makeBucket([ makeSession({ sessionId: 11 }), @@ -91,8 +92,9 @@ test('buildBucketDeleteHandler signals onStart after confirm, before deleting', events.push('confirm'); return true; }, - onStart: (count) => { - events.push(`start:${count}`); + onStart: (ids) => { + startedIds = ids; + events.push('start'); }, onSuccess: () => { events.push('success'); @@ -104,7 +106,8 @@ test('buildBucketDeleteHandler signals onStart after confirm, before deleting', await handler(); - assert.deepEqual(events, ['confirm', 'start:3', 'delete', 'success']); + assert.deepEqual(events, ['confirm', 'start', 'delete', 'success']); + assert.deepEqual(startedIds, [11, 22, 33]); }); test('buildBucketDeleteHandler does not call onStart when confirm returns false', async () => { diff --git a/stats/src/components/sessions/SessionsTab.tsx b/stats/src/components/sessions/SessionsTab.tsx index 74fadcae..3ade75ae 100644 --- a/stats/src/components/sessions/SessionsTab.tsx +++ b/stats/src/components/sessions/SessionsTab.tsx @@ -30,7 +30,7 @@ export interface BucketDeleteDeps { apiClient: { deleteSessions: (ids: number[]) => Promise }; confirm: (title: string, count: number) => boolean | Promise; /** Called once confirmation passes, just before the delete request begins. */ - onStart?: (count: number) => void; + onStart?: (sessionIds: number[]) => void; onSuccess: (deletedIds: number[]) => void; onError: (message: string) => void; } @@ -48,7 +48,7 @@ export function buildBucketDeleteHandler(deps: BucketDeleteDeps): () => Promise< const ids = bucket.sessions.map((s) => s.sessionId); try { if (!(await confirm(title, ids.length))) return; - onStart?.(ids.length); + onStart?.(ids); await client.deleteSessions(ids); onSuccess(ids); } catch (err) { @@ -74,9 +74,8 @@ export function SessionsTab({ const [search, setSearch] = useState(''); const [visibleSessions, setVisibleSessions] = useState([]); const [deleteError, setDeleteError] = useState(null); - const [deletingSessionId, setDeletingSessionId] = useState(null); + const [deletingSessionIds, setDeletingSessionIds] = useState>(() => new Set()); const [deletingBucketKey, setDeletingBucketKey] = useState(null); - const [deletingCount, setDeletingCount] = useState(0); useEffect(() => { setVisibleSessions(sessions); @@ -135,8 +134,11 @@ export function SessionsTab({ if (!confirmed) return; setDeleteError(null); - setDeletingSessionId(session.sessionId); - setDeletingCount(1); + setDeletingSessionIds((prev) => { + const next = new Set(prev); + next.add(session.sessionId); + return next; + }); try { await apiClient.deleteSession(session.sessionId); setVisibleSessions((prev) => prev.filter((item) => item.sessionId !== session.sessionId)); @@ -144,8 +146,11 @@ export function SessionsTab({ } catch (err) { setDeleteError(err instanceof Error ? err.message : 'Failed to delete session.'); } finally { - setDeletingSessionId(null); - setDeletingCount(0); + setDeletingSessionIds((prev) => { + const next = new Set(prev); + next.delete(session.sessionId); + return next; + }); } }; @@ -156,7 +161,12 @@ export function SessionsTab({ bucket, apiClient, confirm: confirmBucketDelete, - onStart: (count) => setDeletingCount(count), + onStart: (ids) => + setDeletingSessionIds((prev) => { + const next = new Set(prev); + for (const id of ids) next.add(id); + return next; + }), onSuccess: (ids) => { const deleted = new Set(ids); setVisibleSessions((prev) => prev.filter((s) => !deleted.has(s.sessionId))); @@ -174,7 +184,11 @@ export function SessionsTab({ await handler(); } finally { setDeletingBucketKey(null); - setDeletingCount(0); + setDeletingSessionIds((prev) => { + const next = new Set(prev); + for (const session of bucket.sessions) next.delete(session.sessionId); + return next; + }); } }; @@ -219,7 +233,7 @@ export function SessionsTab({ setExpandedId(expandedId === s.sessionId ? null : s.sessionId) } onDelete={() => void handleDeleteSession(s)} - deleteDisabled={deletingSessionId === s.sessionId} + deleteDisabled={deletingSessionIds.has(s.sessionId)} onNavigateToMediaDetail={onNavigateToMediaDetail} /> {expandedId === s.sessionId && ( @@ -288,7 +302,7 @@ export function SessionsTab({ setExpandedId(expandedId === s.sessionId ? null : s.sessionId) } onDelete={() => void handleDeleteSession(s)} - deleteDisabled={deletingSessionId === s.sessionId} + deleteDisabled={deletingSessionIds.has(s.sessionId)} onNavigateToMediaDetail={onNavigateToMediaDetail} /> {expandedId === s.sessionId && ( @@ -315,7 +329,7 @@ export function SessionsTab({ )} - + ); } diff --git a/stats/src/hooks/useCoverImages.ts b/stats/src/hooks/useCoverImages.ts index b0ff4a08..a0051d10 100644 --- a/stats/src/hooks/useCoverImages.ts +++ b/stats/src/hooks/useCoverImages.ts @@ -52,7 +52,13 @@ export function useCoverImages(sessions: SessionSummary[]): CoverImageMap { }, getCoverRetryDelayMs(attempt)); } - setImages({}); + if (requests.animeIds.length === 0 && requests.videoIds.length === 0) { + setImages({}); + return () => { + cancelled = true; + }; + } + void load(requests.animeIds, requests.videoIds, 0); return () => { diff --git a/stats/src/lib/cover-retry.ts b/stats/src/lib/cover-retry.ts index 61c17719..757e7fff 100644 --- a/stats/src/lib/cover-retry.ts +++ b/stats/src/lib/cover-retry.ts @@ -5,6 +5,7 @@ export function appendCoverRetryToken(src: string, retryToken = 0): string { const normalizedToken = String(Math.trunc(retryToken)); try { + // Dummy base lets URL parse relative API paths; it is never returned as a real host. const url = new URL(src, 'http://subminer.local'); url.searchParams.set(COVER_RETRY_PARAM, normalizedToken); if (src.startsWith('/')) {