release: v0.12.0 (#520)

Forty-one commits since v0.11.0. See CHANGELOG.md for the full list and the upgrade notes.
This commit is contained in:
Ralksta
2026-08-23 23:18:14 +02:00
committed by GitHub
parent 2f546f440d
commit 10ec68f493
119 changed files with 6888 additions and 758 deletions

View File

@@ -124,6 +124,7 @@ export interface SubpageSummary {
class ImmichClient {
private hasLoggedAlbums = false;
private hasWarnedNoCredentials = false;
private pendingAlbumsPromise: Promise<ImmichAlbum[]> | null = null;
private pendingAlbumPromises = new Map<string, Promise<ImmichAlbum | null>>();
private pendingAssetPromises = new Map<string, Promise<ImmichAsset | null>>();
@@ -132,6 +133,30 @@ class ImmichClient {
return getConfig();
}
/**
* Whether a request to Immich is possible at all.
*
* Deliberately *not* `needsSetup`: a deployment with credentials but no
* gallery.yaml can talk to Immich perfectly well, and the admin panel depends
* on that to browse albums before the file exists. Gating on `needsSetup`
* made every call return null before the fetch, so `ping()` reported the
* server as disconnected without a single line in the log (#507).
*/
private get hasCredentials(): boolean {
const { apiUrl, apiKey } = this.config.immich;
return !!apiUrl && !!apiKey;
}
/** Says once why nothing is being requested, rather than failing in silence. */
private warnNoCredentials(what: string): void {
if (this.hasWarnedNoCredentials) return;
this.hasWarnedNoCredentials = true;
console.warn(
`[Immich] No API URL or API key configured — skipping ${what} and every ` +
'later request. Set IMMICH_API_URL and IMMICH_API_KEY, or run the setup wizard at /install.',
);
}
/** Cache write that carries the configured stale window. */
private cacheSet<T>(key: string, data: T): void {
cache.set(key, data, this.config.cacheTtl, this.config.staleMaxAge);
@@ -155,7 +180,10 @@ class ImmichClient {
}
private async request<T>(endpoint: string, body?: unknown): Promise<T | null> {
if (this.config.needsSetup) return null;
if (!this.hasCredentials) {
this.warnNoCredentials(endpoint);
return null;
}
const url = `${this.config.immich.apiUrl}${endpoint}`;
@@ -234,7 +262,7 @@ class ImmichClient {
contentRange: string | null;
status: 200 | 206;
} | null> {
if (this.config.needsSetup || !this.config.immich.apiUrl) return null;
if (!this.hasCredentials) return null;
const endpoint = `/assets/${encodeURIComponent(assetId)}/video/playback`;
const url = `${this.config.immich.apiUrl}${endpoint}`;
@@ -303,7 +331,7 @@ class ImmichClient {
assetId: string,
size: ImageSize = 'preview',
): Promise<{ stream: ReadableStream; contentType: string; contentLength: string | null } | null> {
if (this.config.needsSetup || !this.config.immich.apiUrl) return null;
if (!this.hasCredentials) return null;
const endpoint =
size === 'original'
@@ -433,7 +461,15 @@ class ImmichClient {
console.log('─'.repeat(80) + '\n');
}
this.cacheSet(cacheKey, filtered);
// Not while the install is unfinished. The dummy setup config carries an
// empty allowlist, so `filtered` is [] no matter what Immich returned —
// and caching that outlives the wizard that fixes it, because
// invalidateAll() runs in the install route's own module instance
// (Next bundles each route separately; see lib/install.ts). The gallery
// then looks empty until the server restarts.
if (!this.config.needsSetup) {
this.cacheSet(cacheKey, filtered);
}
return filtered;
} catch (error) {
return this.staleOrThrow<ImmichAlbum[]>(cacheKey, error, 'album list');
@@ -776,9 +812,12 @@ class ImmichClient {
try {
const res = await this.request<{ res: string }>('/server/ping');
return !!res;
} catch {
} catch (error) {
// "Is Immich reachable?" — unreachable is the answer, not an exception.
// Both callers (/api/health, /api/admin/status) render this as a status.
// It is still logged: a red badge in the admin panel with nothing in the
// log leaves the operator guessing (#507).
console.warn('[Immich] Ping failed:', error);
return false;
}
}