web: admin surfaces so the PhotoPrism UI is never needed

- Account tab in General settings — self-service password change.
- UsersDialog (admin-only footer entry) — full /api/v1/users CRUD with
  admin-issued password reset.
- People as a fifth tag category alongside Labels/Keywords/Colors/Ratings,
  backed by /api/v1/subjects and the `person:` DSL clause.
- About tab in Library settings — version, library counts, feature chips,
  and a collapsible env-config help panel for the bits PP has no runtime
  API for (OIDC, TF, WebDAV).
- Library tab expanded with Indexer-advanced, extra Downloads checksums,
  and a Features grid that only renders keys PhotoPrism actually returns.
- Fix the SettingsDialog null-draft race the same way GeneralSettingsDialog
  already had: normalize on open, never null on close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 08:25:53 +02:00
parent 24dfa996b3
commit a7b8a60473
8 changed files with 1196 additions and 37 deletions

View File

@@ -6,8 +6,10 @@
getPhoto,
listLabels,
listPhotos,
listSubjects,
type PhotoMarksMap,
type PpLabel
type PpLabel,
type PpSubject
} from '$lib/services/photoprism';
import {
filtersToQ,
@@ -65,7 +67,9 @@
// filter on top would make drill counts disagree with the badges (a
// label badge of 157 could otherwise drill into 0 photos because the
// session is scoped to a folder that has none of them).
const useServer = $derived(category === 'labels' || category === 'keywords');
const useServer = $derived(
category === 'labels' || category === 'keywords' || category === 'people'
);
const drillQ = $derived(
useServer && selectedValue
? filtersToQ({
@@ -128,6 +132,11 @@
queryFn: listLabels,
enabled: isAuthenticated() && category === 'labels'
}));
const subjectsQuery = createQuery<PpSubject[]>(() => ({
queryKey: ['subjects'],
queryFn: listSubjects,
enabled: isAuthenticated() && category === 'people'
}));
const drillTitle = $derived.by(() => {
if (!selectedValue) return '';
if (category === 'labels') {
@@ -137,6 +146,10 @@
return hit?.Name ?? selectedValue;
}
if (category === 'keywords') return selectedValue;
if (category === 'people') {
const hit = (subjectsQuery.data ?? []).find((s) => s.Slug === selectedValue);
return hit?.Name ?? selectedValue;
}
if (category === 'ratings') return starLabel(parseInt(selectedValue, 10));
if (category === 'colors') {
return (