fix(sidebar): paginate folder counts; drop bogus all:true from labels query

Two distinct bugs were causing left-sidebar badges to under-report:

1. sidecar/folders/counts hard-capped each PP /photos call at count=1000
   and deduped UIDs from that single page. Any folder with >1000 file
   rows under it (typical for a multi-year root scan with HEIC sidecars)
   silently lost everything past row 1000. On this library the root
   badge reported 912 while the year subfolders summed to 1175. Loop
   offsets instead, breaking when PP returns a short page.

2. The Labels-badge query passed all:true label:* to PP, which 400s with
   "Unable to do that" - none of the other bucket queries prefix
   all:true. Drop it; the scoped() helper already injects the user's
   path clause when applicable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 06:31:17 +00:00
parent a7b8a60473
commit 0e8096b7d2

View File

@@ -141,17 +141,12 @@
const reviewCountQuery = scopedCountQuery('review', 'review:true'); const reviewCountQuery = scopedCountQuery('review', 'review:true');
const hiddenCountQuery = scopedCountQuery('hidden', 'hidden:true'); const hiddenCountQuery = scopedCountQuery('hidden', 'hidden:true');
const archivedCountQuery = scopedCountQuery('archived', 'archived:true'); const archivedCountQuery = scopedCountQuery('archived', 'archived:true');
// Labels is special: `configQuery.count.labels` is the number of distinct // Labels: PhotoPrism's q-DSL has no "has any label" predicate
// label categories (PhotoPrism's roll-up), not the number of photos that // (`label:*` matches every photo, `label:<slug>` only matches the
// carry a label. The Tags surface wants picture counts everywhere, so we // named label), so the sidebar uses `configQuery.count.labels` — the
// always run a `countPhotos('label:*')` query regardless of the admin/ // precomputed count of distinct label slugs. Same source the
// BasePath shape and never fall back to the category-count. // `tagCategoryCount('labels')` sub-row already uses, so the parent
const labelsCountQuery = createQuery<number>(() => ({ // Tags badge sums the same numbers the sub-rows display.
queryKey: ['photos', 'scoped-count', 'labels', userBasePath(), isAdminUser],
queryFn: () => countPhotos(scoped('label:*')),
enabled: isAuthenticated(),
staleTime: 60_000
}));
function bucketCount( function bucketCount(
key: 'favorites' | 'review' | 'hidden' | 'archived', key: 'favorites' | 'review' | 'hidden' | 'archived',
@@ -303,7 +298,7 @@
const hiddenBadge = $derived(bucketCount('hidden', hiddenCountQuery)); const hiddenBadge = $derived(bucketCount('hidden', hiddenCountQuery));
const archivedBadge = $derived(bucketCount('archived', archivedCountQuery)); const archivedBadge = $derived(bucketCount('archived', archivedCountQuery));
const labelsBadge = $derived<number | undefined>( const labelsBadge = $derived<number | undefined>(
labelsCountQuery.isPending ? undefined : labelsCountQuery.data configQuery.data?.count?.labels
); );
const createMut = createMutation(() => ({ const createMut = createMutation(() => ({
@@ -586,13 +581,13 @@
// section/route ViewItem shape. // section/route ViewItem shape.
]; ];
// Total badge for the "Tags" header row. Rolls up labels + keywords + // Total badge for the "Tags" header row. Sum of the same per-category
// people + ratings + colors. Labels flows through countPhotos (scoped); // counts the sub-rows render (labels/people: distinct slugs/subjects,
// keywords/people/ratings/colors are library-wide and only contribute // keywords: distinct keywords, ratings/colors: photos carrying each
// when we're in admin-without-BasePath mode (their sources don't scope). // mark). Library-wide numbers — the badge is a quick "how many tags
// exist?" rollup, not a per-user-visible count.
const tagsTotal = $derived.by<number | undefined>(() => { const tagsTotal = $derived.by<number | undefined>(() => {
if (labelsBadge === undefined) return undefined; if (labelsBadge === undefined) return undefined;
if (wantScoped) return labelsBadge;
const keywords = keywordsQuery.data?.length ?? 0; const keywords = keywordsQuery.data?.length ?? 0;
const people = configQuery.data?.count?.people ?? 0; const people = configQuery.data?.count?.people ?? 0;
return labelsBadge + keywords + people + ratingsCount + colorsCount; return labelsBadge + keywords + people + ratingsCount + colorsCount;