fix(sidebar): use count.labels for Tags badge, drop bogus label:* query

PhotoPrism's q-DSL has no "has any label" predicate: `label:*` matches
every photo regardless of label, `label:<slug>` only matches that one
slug, and `keywords:*` behaves the same way. The prior `all:true label:*`
returned a 400 (and the earlier "drop all:true" follow-up made it return
the unfiltered library size, which then fed into tagsTotal and inflated
the parent Tags badge to ~library_size on admin sessions).

Switch labelsBadge to the precomputed `configQuery.count.labels` — the
same source the Tags sub-row's `tagCategoryCount('labels')` already
uses. The parent Tags badge now sums the exact same numbers the sub-rows
display: labels, keywords, people (distinct slugs/keywords/subjects)
plus ratings/colors (photos carrying each mark). Drop the wantScoped
short-circuit since the values are all library-wide now anyway.

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 fc5f30fad1

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;