From b0c8c06b2bd31197baa276cbdb645735cb186918 Mon Sep 17 00:00:00 2001 From: Claudio Date: Mon, 18 May 2026 19:59:35 +0000 Subject: [PATCH] Sidebar: suppress global count badges for non-admin users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PhotoPrism's /api/v1/config.count returns library-wide aggregates to any authenticated session, with no per-user scoping. The timeline itself IS scoped (a guest sees zero photos), but the sidebar was rendering admin-side totals next to Review / Hidden / Archive / Tags / Map / root for non-admins — including a freshly-registered "test" user with role=guest and BasePath="". Until PhotoPrism gains per-user counters, the SPA now derives an `isAdminUser` flag and gates every count that's drawn from configQuery on it. Non-admin users see the labels without badges; counts re-appear automatically when promoted. Per-folder counts from the sidecar (which DO scope to BasePath) are unaffected. --- .../lib/components/layout/LeftSidebar.svelte | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/layout/LeftSidebar.svelte b/web/src/lib/components/layout/LeftSidebar.svelte index 4579c87..41b6399 100644 --- a/web/src/lib/components/layout/LeftSidebar.svelte +++ b/web/src/lib/components/layout/LeftSidebar.svelte @@ -92,6 +92,16 @@ enabled: isAuthenticated() })); + // PhotoPrism's /api/v1/config.count returns library-wide aggregates + // to any authenticated session regardless of role — the timeline + // itself IS scoped per-user, but the precomputed counters aren't. + // Showing those numbers in a non-admin's sidebar is misleading + // (e.g. the `test` user with role=guest saw the admin's library + // totals next to Review / Hidden / Archive). Until PhotoPrism gains + // per-user count scoping, just suppress the global-derived badges + // for anyone who isn't the admin. + const isAdminUser = $derived(session.user?.Role === 'admin'); + const marksQuery = createQuery(() => ({ queryKey: ['marks'], queryFn: getAllMarks, @@ -213,7 +223,9 @@ })); const rootCount = $derived( userBasePath() === '' - ? (configQuery.data?.count?.all ?? 0) + ? isAdminUser + ? (configQuery.data?.count?.all ?? 0) + : 0 : (scopedRootCountQuery.data?.[''] ?? 0) ); @@ -425,16 +437,19 @@ // separate "everything regardless of folder" destination would just // duplicate it for users whose photos live under the root. const views: ViewItem[] = [ - { kind: 'route', href: '/map', label: 'Map', getCount: () => geoQuery.data?.features?.length }, + { kind: 'route', href: '/map', label: 'Map', getCount: () => (isAdminUser ? geoQuery.data?.features?.length : undefined) }, // Tags hosts four tabs (Labels (auto) / Keywords / Ratings / Colors); // the badge sums each tab's badge so the sidebar number is the // total of what the inner tabs show. Keywords is lazy — it only // contributes after /tags?tab=keywords has been visited once. + // Suppressed for non-admins because every contributing query is + // library-wide rather than per-user. { kind: 'route', href: '/tags', label: 'Tags', getCount: () => { + if (!isAdminUser) return undefined; const labels = configQuery.data?.count?.labels; if (labels === undefined) return undefined; const keywords = keywordsQuery.data?.length ?? 0; @@ -449,13 +464,14 @@ href: '/review', label: 'Review', getCount: () => { + if (!isAdminUser) return undefined; const review = configQuery.data?.count?.review; if (review === undefined) return undefined; return review + (stacksQuery.data?.length ?? 0) + (crossFolderQuery.data?.groups.length ?? 0); } }, - { kind: 'section', id: 'hidden', label: 'Hidden', getCount: () => configQuery.data?.count?.hidden }, - { kind: 'section', id: 'archive', label: 'Archive', getCount: () => configQuery.data?.count?.archived } + { kind: 'section', id: 'hidden', label: 'Hidden', getCount: () => (isAdminUser ? configQuery.data?.count?.hidden : undefined) }, + { kind: 'section', id: 'archive', label: 'Archive', getCount: () => (isAdminUser ? configQuery.data?.count?.archived : undefined) } ]; function isRouteActive(href: string): boolean {