feat(web): header pill showing PhotoPrism indexer status
Subscribes to PhotoPrism's /api/v1/ws channel on login and surfaces index.indexing / index.updating / index.completed events as a small status pill in the header (next to the AnimatedMule wordmark). - Shows "Indexing" + the current filename (basename, monospace) during the scan pass, "Finalizing — <step>" during faces/counts/folders/ purge/moments, and "Indexed in Ns" for ~4s after completion before fading. - Per-file events arrive many per second on large libraries — throttled to 150 ms with a trailing-edge update so the pill stays calm and always lands on the most recent filename. Step and completion events bypass the throttle. - Filename slot is fixed at 24ch so the pill width stays constant through a run (no horizontal jitter as filenames change length); the full path is exposed via the parent's `title` for hover. - WS reconnect uses exponential backoff capped at 30 s, and the store tears down cleanly on logout so we don't leak sockets across identities. Defensive parsing throughout: PhotoPrism's WS protocol isn't a stable contract, so unknown event shapes are ignored rather than thrown — worst-case the pill stays idle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
49
web/src/lib/components/layout/IndexerStatusPill.svelte
Normal file
49
web/src/lib/components/layout/IndexerStatusPill.svelte
Normal file
@@ -0,0 +1,49 @@
|
||||
<!--
|
||||
Compact status pill that appears in the header while PhotoPrism's
|
||||
indexer is doing work. Driven by the indexer store, which subscribes
|
||||
to PhotoPrism's WS channel. Renders nothing when idle so it never
|
||||
steals header real estate from the user.
|
||||
|
||||
The `detail` (current path/file) is exposed via `title` rather than
|
||||
rendered inline — the pill stays narrow even on slow flashes through
|
||||
a deep library, and hover surfaces the detail for users who care.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { indexer } from '$lib/stores/indexer.svelte';
|
||||
import { Loader2 } from 'lucide-svelte';
|
||||
|
||||
// PhotoPrism's `fileName` arrives as the full relative path
|
||||
// (`subdir/IMG_0554.HEIC.jpg`). The basename is enough for inline
|
||||
// recognition; the full path stays in the `title` for users who hover.
|
||||
const basename = $derived.by(() => {
|
||||
const d = indexer.detail;
|
||||
if (!d) return '';
|
||||
const i = d.lastIndexOf('/');
|
||||
return i >= 0 ? d.slice(i + 1) : d;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if indexer.active || indexer.label}
|
||||
<div
|
||||
class="flex items-center gap-1.5 rounded-full border border-border bg-background/80 px-2.5 py-1 text-xs text-foreground shadow-sm backdrop-blur"
|
||||
title={indexer.detail ?? indexer.label}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{#if indexer.active}
|
||||
<Loader2 class="h-3 w-3 animate-spin text-primary" />
|
||||
{/if}
|
||||
<span class="whitespace-nowrap">{indexer.label}</span>
|
||||
{#if basename}
|
||||
<!-- Fixed-width slot so the pill stops shrinking/growing as
|
||||
PhotoPrism rattles through files of different name lengths.
|
||||
`w-[24ch]` locks the column; `truncate` ellipsises anything
|
||||
longer. The full path remains in the parent's `title`. -->
|
||||
<span
|
||||
class="w-[24ch] truncate text-left font-mono text-[10px] text-muted-foreground"
|
||||
>
|
||||
{basename}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user