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