Files
frambe/.claude-context/sessions/2026-09-23-immich-v3-migration.md

6.0 KiB

Immich v3 API Migration & Deploy Debugging

Date: 2026-09-23 Project: frambe Status: ongoing Tags: immich-v3, api-migration, docker-compose, env-vars, deployment, search-metadata

Context

Immich released v3 (1 July 2026) with a large set of breaking API changes. Frambe called several endpoints that were removed, renamed, or reshaped, so albums/people/ random/favorites all needed updating. After shipping the code fixes (v1.6.0 on dev), the "Start slideshow" button flashed "no photos found" despite albums showing a nonzero count — which turned out to be a deploy/env problem, not a code problem.

Key Decisions

  • Bumped 1.5.2 → 1.6.0 (minor). Rationale: substantial multi-endpoint compatibility fix pack for a breaking upstream change, but no new user-facing features. Not a major (no feature pack), not a patch (more than a small fix).
  • Album assets now fetched via POST /search/metadata with albumIds, because v3 removed AlbumResponseDto.assets. Kept a v2 fallback (al.assets if present) so the same code works against both server versions during transition.
  • Added a searchMetadata() pagination helper (pages via assets.nextPage, integer page/size) rather than one-shot single-page fetches — album/person sets can exceed one page of 250.
  • Person assets moved to POST /search/metadata with personIds, keeping the legacy GET /people/:id/assets as a try/catch fallback. Considered leaving the old endpoint alone (not listed as removed in the migration doc) but chose the canonical v3 path with fallback for durability.
  • visibility handling is still open — see Open Questions. Initially forced visibility:'timeline' everywhere to keep archived/hidden assets off frames; this is the leading suspect for empty album results and may need to be dropped for album/person queries.
  • Deploy stays scp + ssh, not the Gitea API: server.js is ~27KB, over the Gitea update_file ~16KB silent-truncation limit. Uploader BAT verifies md5 + node --check on the server before it will commit/push.

Technical Details

  • Immich creds are set inline in docker-compose.yml under environment: (IMMICH_URL, IMMICH_API_KEY), NOT in .env. Compose ALSO references an env_file: (.env) that is gitignored and currently MISSING from ~/frambe on hal-blackpearl — this makes every docker compose command abort before it does anything (rebuild, exec, up all fail with "env file ... not found").
  • App reads process.env.IMMICH_API_KEY (server.js line 15) and process.env.IMMICH_URL (line 14). These match the compose environment: block. A debug test that printed $API_KEY showed empty — that was the WRONG var name in the test shell, a red herring, not an app bug.
  • Proven working in-container (with a valid key): POST $IMMICH_URL/api/search/metadata body {"albumIds":["<uuid>"],"size":5} returns assets.total:5 — so the v3 albumIds search logic is correct. The empty result in the UI is a runtime deploy/env issue, not the query.
  • Server on hal-blackpearl uses Alpine node image: has wget, not curl. Use wget -qO- for in-container HTTP tests.
  • Frambe app endpoint: http://localhost:3030/api/albums/:id (host port 3030).
  • v3 endpoint/shape changes applied:
    • GET /albums list: ?shared=true → ?isShared=true; added ?isOwned=true.
    • GET /albums/:id: AlbumResponseDto.assets removed → search/metadata.
    • GET /assets/random REMOVED → POST /search/random (body {size, visibility}, returns a bare array; code tolerates array or {assets:{items}}).
    • POST /search/metadata: page/size now integers; omitted visibility now defaults to "any" (was "timeline").
    • AssetResponseDto.duration: now a number in milliseconds (was "H:MM:SS.mmm" string) → normDuration() normalizes both to ms.
    • Error bodies moved to Zod shape + X-Correlation-ID header — NOT handled because Frambe only reads HTTP status codes, not error bodies. No change needed.

Code Changes

  • server.js — VERSION → 1.6.0; added normDuration(), searchMetadata(); mapAsset uses normDuration; /api/albums uses isShared/isOwned; /api/albums/:id uses search/metadata (v2 fallback); /api/people/:id uses personIds search (legacy fallback); /api/assets/random uses search/random; /api/assets/favorites routed through searchMetadata. Added logErr to more catch blocks.
  • package.json — version 1.4.1 → 1.6.0 (was stale).
  • public/js/app.js — no changes (consumes the server's normalized shape).

Open Questions / Next Steps

  • Immediate blocker: .env missing in ~/frambe on hal-blackpearl, so Compose aborts. Fix by either (a) cp .env.example .env and fill real values (TZ=Australia/Melbourne, admin password, Frambe API token), or (b) drop the env_file: line from docker-compose.yml since the Immich creds already live in the environment: block. Recommendation leaning toward (b) — removes dependence on a gitignored file that keeps disappearing.
  • Once Compose runs again: docker compose up -d --build --force-recreate, then re-test curl http://localhost:3030/api/albums/<uuid> — expected to return photos now that v1.6.0 code + valid key are both live.
  • Decide on visibility: if albums still come back empty after the container is confirmed current, drop visibility:'timeline' from the album/person searches (keep it only where wanted). Album assets aren't guaranteed timeline-visible, so forcing timeline can zero out results. Possibly make it a config toggle.
  • docker-compose.yml still has the obsolete top-level version: attribute → harmless warning; remove when convenient.
  • Not yet done: CHANGELOG entry / README version bump — deferred to the main merge.
  • Merge dev → main via Gitea UI once frames are confirmed working.

Artifacts