feat: PhotoPrism M0 bring-up — compose stack, web client, sidecar, migrate

Replace the legacy mule-image backend with PhotoPrism plus a thin
SvelteKit client and a Node sidecar for endpoints PhotoPrism doesn't
expose (file rename), and add a two-phase migrator (metadata via PUT,
heaps → albums) for the existing Postgres library.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 16:06:58 +02:00
parent 423a73a8a6
commit 8c2526d982
69 changed files with 12048 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
import { browser } from '$app/environment';
import type { PpClientConfig, PpSessionResponse, PpUser } from '$lib/types/photoprism';
const STORAGE_KEY = 'pp_session';
interface PersistedSession {
id: string;
accessToken: string;
previewToken: string;
downloadToken: string;
user: PpUser;
}
function loadInitial(): PersistedSession | null {
if (!browser) return null;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw) as PersistedSession;
} catch {
return null;
}
}
/**
* Single-source session state for the Svelte client. Components import the
* `session` object and read its reactive fields; mutations go through the
* helpers below. State is mirrored to localStorage so a hard refresh keeps
* the user signed in.
*/
const initial = loadInitial();
export const session = $state<{
id: string | null;
accessToken: string | null;
previewToken: string | null;
downloadToken: string | null;
user: PpUser | null;
}>({
id: initial?.id ?? null,
accessToken: initial?.accessToken ?? null,
previewToken: initial?.previewToken ?? null,
downloadToken: initial?.downloadToken ?? null,
user: initial?.user ?? null
});
export function isAuthenticated(): boolean {
return Boolean(session.accessToken);
}
export function adoptSession(resp: PpSessionResponse, cfg?: PpClientConfig): void {
session.id = resp.id;
session.accessToken = resp.access_token;
session.previewToken = (cfg ?? resp.config)?.previewToken ?? '';
session.downloadToken = (cfg ?? resp.config)?.downloadToken ?? '';
session.user = resp.user;
persist();
}
export function clearSession(): void {
session.id = null;
session.accessToken = null;
session.previewToken = null;
session.downloadToken = null;
session.user = null;
if (browser) localStorage.removeItem(STORAGE_KEY);
}
function persist(): void {
if (!browser || !session.accessToken) return;
const payload: PersistedSession = {
id: session.id ?? '',
accessToken: session.accessToken,
previewToken: session.previewToken ?? '',
downloadToken: session.downloadToken ?? '',
user: session.user!
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
}
/**
* Build a thumbnail URL for a photo. PhotoPrism's thumb endpoint is
* /api/v1/t/:hash/:token/:size — the token is the per-session
* previewToken, which the session response provides on login.
*/
export function thumbUrl(hash: string, size = 'tile_500'): string {
if (!session.previewToken) return '';
return `/api/v1/t/${hash}/${session.previewToken}/${size}`;
}