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 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;