From 5d39171e90c9ccb4dc75ec15d15917494df7ad4e Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Wed, 14 Oct 2026 15:59:23 +0200 Subject: [PATCH] =?UTF-8?q?R-1007=20spike=20A:=20how=20the=20box=20can=20a?= =?UTF-8?q?sk=20Immich=20without=20the=20household's=20password=20?= =?UTF-8?q?=E2=80=94=20Immich's=20own=20queue=20works=20for=20every=20user?= =?UTF-8?q?,=20a=20key=20only=20for=20the=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On 9202, Immich v3.2.4, admin made through the setup gate as a household. A1 (immich-admin) has no job command. A2 (scoped key: asset.update + job.create) repairs the admin's photos only — a family member's photo is refused. A3 (nightly missing thumbnails) reads only the database. A4 (the same job on Immich's own queue, added with its own bullmq) repaired 5 assets of both users in 1.08 s with 0 of 50 controls touched, and found a lost original in 0.44 s. Scan of 20,000 files: 0.29 s. Decision page for the operator (Part B). Immich removed through the product. Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb --- .../A/A0-deploy-and-gate.txt | 10 +++ .../immich-repair-2026-10-14/A/A0-photos.txt | 31 ++++++++ .../A/A2-A3-key-and-missing.txt | 73 +++++++++++++++++++ .../A/A4-lost-original.txt | 23 ++++++ .../A/A4-queue-direct.txt | 10 +++ .../immich-repair-2026-10-14/A/scan-20000.txt | 6 ++ .../immich-repair-2026-10-14/A/teardown.txt | 6 ++ .../audits/immich-repair-2026-10-14/README.md | 57 +++++++++++++++ 8 files changed, 216 insertions(+) create mode 100644 documentation/audits/immich-repair-2026-10-14/A/A0-deploy-and-gate.txt create mode 100644 documentation/audits/immich-repair-2026-10-14/A/A0-photos.txt create mode 100644 documentation/audits/immich-repair-2026-10-14/A/A2-A3-key-and-missing.txt create mode 100644 documentation/audits/immich-repair-2026-10-14/A/A4-lost-original.txt create mode 100644 documentation/audits/immich-repair-2026-10-14/A/A4-queue-direct.txt create mode 100644 documentation/audits/immich-repair-2026-10-14/A/scan-20000.txt create mode 100644 documentation/audits/immich-repair-2026-10-14/A/teardown.txt create mode 100644 documentation/audits/immich-repair-2026-10-14/README.md diff --git a/documentation/audits/immich-repair-2026-10-14/A/A0-deploy-and-gate.txt b/documentation/audits/immich-repair-2026-10-14/A/A0-deploy-and-gate.txt new file mode 100644 index 00000000..817151a0 --- /dev/null +++ b/documentation/audits/immich-repair-2026-10-14/A/A0-deploy-and-gate.txt @@ -0,0 +1,10 @@ +15:49:27 immich: deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +15:51:02 immich: deployed, state=running +deploy: ok +15:51:02 immich: deployed, state=running +stranger (no dashboard session) admin-sign-up -> 401 +household (dashboard session, through the gate) admin-sign-up -> 401 +server config isInitialized: None +gate passed as the household: True +household admin-sign-up through the gate -> 201 +isInitialized: True diff --git a/documentation/audits/immich-repair-2026-10-14/A/A0-photos.txt b/documentation/audits/immich-repair-2026-10-14/A/A0-photos.txt new file mode 100644 index 00000000..6668c6aa --- /dev/null +++ b/documentation/audits/immich-repair-2026-10-14/A/A0-photos.txt @@ -0,0 +1,31 @@ +family member: 201 +admin uploaded: 20 +member uploaded: 10 +jobs idle +thumbs files: 60; users dirs: 2 +== A1: immich-admin in v3.2.4 +/usr/src/app/server/bin/immich-admin +Usage: main [options] [command] + +Options: + -h, --help display help for command + +Commands: + reset-admin-password Reset the admin password + enable-password-login Enable password login + disable-password-login Disable password login + enable-maintenance-mode Enable maintenance mode or regenerate the + maintenance token + disable-maintenance-mode Disable maintenance mode + enable-oauth-login Enable OAuth login + disable-oauth-login Disable OAuth login + list-users List Immich users + version Print Immich version + grant-admin Grant admin privileges to a user (by email) + revoke-admin Revoke admin privileges from a user (by email) + change-media-location Change database file paths to align with a new + media location + schema-check Verify database migrations and check for schema + drift + help [command] display help for command + diff --git a/documentation/audits/immich-repair-2026-10-14/A/A2-A3-key-and-missing.txt b/documentation/audits/immich-repair-2026-10-14/A/A2-A3-key-and-missing.txt new file mode 100644 index 00000000..fadbc23c --- /dev/null +++ b/documentation/audits/immich-repair-2026-10-14/A/A2-A3-key-and-missing.txt @@ -0,0 +1,73 @@ +== A2: an API key with ONLY asset.update, made by the admin (the household's session) +key made: yes (secret never printed) +the admin's own photo: {"message":"Missing required permission: job.create"} [403] +the family member's photo: {"message":"Missing required permission: job.create"} [403] +integrity report with this key: 403 +visible to the household in Account settings -> API keys: ['Felhom box repair (A2 spike)'] +key deleted: 204 +== A3: what does the nightly/"missing" thumbnail run read? +services/queue.service.js:214: if (config.nightlyTasks.missingThumbnails) { +nightly tasks setting: {'startTime': '00:00', 'databaseCleanup': True, 'missingThumbnails': True, 'clusterNewFaces': True, 'generateMemories': True, 'syncQuotaUsage': True} + +== A2 retry: key with asset.update + job.create (+ maintenance for the report) +key made: yes +the admin's own photo: [204] +the family member's photo: {"message":"Not found or no asset.update access"} [400] +integrity report with this key: 200 +key deleted: 204 +== A3: the missing-thumbnail query +repositories/asset-job.repository.d.ts +293: streamForThumbnailJob(options: { +294- force: boolean | undefined; +295- fullsizeEnabled: boolean; +296- }): AsyncIterableIterator<{ +297- id: string; +298- isEdited: boolean; +299- } & Partial>>; +300- getForMigrationJob(id: string): Promise<{ +301- id: string; +302- ownerId: string; +303- files: { +304- id: string; +305- type: AssetFileType; +306- path: string; +307- isEdited: boolean; +308- }[]; +309- } | undefined>; +310- getForGenerateThumbnailJob(id: string): Promise<{ +311- id: string; +312- type: AssetType; +313- ownerId: string; +314- edits: ({ +315- action: import("../dtos/editing.dto").AssetEditAction.Crop; +316- parameters: { +317- x: number; + +58: streamForThumbnailJob(options) { +59- return this.db +60- .selectFrom('asset') +61- .select(['asset.id', 'asset.isEdited']) +62- .where('asset.deletedAt', 'is', null) +63- .where('asset.visibility', '!=', kysely_1.sql.lit(enum_1.AssetVisibility.Hidden)) +64- .$if(!options.force, (qb) => qb +65- .innerJoin('asset_job_status', 'asset_job_status.assetId', 'asset.id') +66- .where(({ and, eb, exists, not, or, selectFrom }) => { +67- const file = (type) => selectFrom('asset_file').whereRef('assetId', '=', 'asset.id').where('type', '=', kysely_1.sql.lit(type)); +68- const conditions = [ +69- not(exists(file(enum_1.AssetFileType.Thumbnail))), +70- not(exists(file(enum_1.AssetFileType.Preview))), +71- and([ +72- eb('asset.isEdited', '=', kysely_1.sql.lit(true)), +73- not(exists(file(enum_1.AssetFileType.FullSize).where('asset_file.isEdited', '=', kysely_1.sql.lit(true)))), +74- ]), +75- eb('asset.thumbhash', 'is', null), +76- ]; +77- if (options.fullsizeEnabled) { +78- const isWebUnsupported = kysely_1.sql.join(Object.keys(mime_types_1.mimeTypes.webUnsupportedImage).map((ext) => kysely_1.sql.lit(`%${ext}`))); +79- conditions.push(and([ +80- not(exists(file(enum_1.AssetFileType.FullSize))), +81- eb((0, kysely_1.sql) `f_unaccent(asset."originalFileName")`, 'like', (0, kysely_1.sql) `any(array[${isWebUnsupported}]::text[])`), +82- ])); +83- } +84- return or(conditions); + diff --git a/documentation/audits/immich-repair-2026-10-14/A/A4-lost-original.txt b/documentation/audits/immich-repair-2026-10-14/A/A4-lost-original.txt new file mode 100644 index 00000000..04f88c2a --- /dev/null +++ b/documentation/audits/immich-repair-2026-10-14/A/A4-lost-original.txt @@ -0,0 +1,23 @@ +244- } +245: async handleMissingFilesQueueAll({ refreshOnly } = {}) { +246- if (refreshOnly) { +247- await this.queueRefreshAllMissingFiles(); +248- return enum_1.JobStatus.Success; +249- } +250- this.logger.log(`Scanning for missing files...`); +251- const assetPaths = this.integrityRepository.streamAssetPathsForMissingFiles(); +252- let total = 0; +253- for await (const batchPaths of (0, misc_1.batched)(assetPaths, constants_1.JOBS_LIBRARY_PAGINATION_SIZE)) { +254- await this.jobRepository.queue({ +255- name: enum_1.JobName.IntegrityMissingFiles, +256- data: { +257- items: batchPaths, + +deleted one original (the 5th by name) +report rows before: 0 +== 13:54:39 queue IntegrityMissingFilesQueueAll on integrityCheck (no key, no login) +queued 1 +missing_file rows after 0.44 s: 1 +== the report, READ from the database, joined to what a household note needs (name + date taken + owner): +missing_file|gen-004.jpg|2026-01-03 01:00|household@example.invalid + diff --git a/documentation/audits/immich-repair-2026-10-14/A/A4-queue-direct.txt b/documentation/audits/immich-repair-2026-10-14/A/A4-queue-direct.txt new file mode 100644 index 00000000..918a1dc6 --- /dev/null +++ b/documentation/audits/immich-repair-2026-10-14/A/A4-queue-direct.txt @@ -0,0 +1,10 @@ +users in thumbs/: 2 (10 and 20 photos) +found from the disk: 5 broken assets +== 13:53:50 A4: queue AssetGenerateThumbnails on thumbnailGeneration with Immich's own bullmq (no key, no login) +queued 5 +rc=0 +no empty file left after 1.08 s: 0 empty +broken assets' files: 10, rewritten 10, empty 0; other assets' files: 50, rewritten 0 +== the household's view (its own login): served bytes for the 5 +5964/198427 47/47 5630/197443 47/47 47/47 + diff --git a/documentation/audits/immich-repair-2026-10-14/A/scan-20000.txt b/documentation/audits/immich-repair-2026-10-14/A/scan-20000.txt new file mode 100644 index 00000000..0b3c423d --- /dev/null +++ b/documentation/audits/immich-repair-2026-10-14/A/scan-20000.txt @@ -0,0 +1,6 @@ +files made: 20000 on /mnt/scan-test +dirs: 10525 +scan 1: 10 empty-file assets found among 20000 files in 0.285 s +scan 2: 10 empty-file assets found among 20000 files in 0.287 s +scratch folder removed: yes + diff --git a/documentation/audits/immich-repair-2026-10-14/A/teardown.txt b/documentation/audits/immich-repair-2026-10-14/A/teardown.txt new file mode 100644 index 00000000..94de7dcd --- /dev/null +++ b/documentation/audits/immich-repair-2026-10-14/A/teardown.txt @@ -0,0 +1,6 @@ +15:56:41 immich: stop 200, remove 200, deployed=False, leftovers='' +True +left: 0 dirs +containers: felhom-controller paperless-webserver paperless-postgres paperless-redis filebrowser traefik +immich volumes: 0; images: 0 + diff --git a/documentation/audits/immich-repair-2026-10-14/README.md b/documentation/audits/immich-repair-2026-10-14/README.md new file mode 100644 index 00000000..45e5f5cb --- /dev/null +++ b/documentation/audits/immich-repair-2026-10-14/README.md @@ -0,0 +1,57 @@ +# Immich repairs its broken photos after a power cut (R-1007) — 2026-10-14 + +Times UTC. Scratch guest 9202 on demo-hp (Tier 0). Immich v3.2.4. No key, token, password, photo or file name of a +household in this folder (bench content only). + +## Part A — how can the box ask Immich without the household's password? + +**Predictions (written 2026-10-14 before any test):** + +| # | Candidate | Prediction | +|---|---|---| +| A1 | `immich-admin` CLI | it exists in v3.2.4 but has only account/maintenance commands (reset admin password, password/OAuth login on/off, list users, media location) — **no job command**; fails | +| A2 | a scoped API key owned by the box | v3.2.4 has per-permission keys (`asset.update` for the per-asset job); but **an API key acts as its user**: the per-asset job checks access per asset, so the admin's key cannot repair another family member's photos; and the box has no clean moment to mint it without the password or the household's session → works only partly, and needs a capture we do not want | +| A3 | Immich's nightly „missing thumbnails" | „missing" reads the database (the asset's file rows), not the disk; nothing short of a database write makes an empty file count as missing → only with A5 | +| A4 | queue the same job in Immich's Redis | works, with no key: the job is `AssetGenerateThumbnails {id}` on queue `thumbnailGeneration`; done with Immich's OWN queue library inside `immich-server` (`docker exec … node`) it is the same job the API queues; breaks if Immich renames the job or the queue (an update can) | +| A5 | a database write | works (the box has `DB_PASSWORD`) but writes into the household's data; refused unless chosen | +| A6 | the household presses a button | works always; costs the household a step and a full rebuild | + +Lost originals: the integrity job can be queued the same way as A4, and its report READ from the database (a read, not a +write) — or the box compares the database's `originalPath`s with the disk itself. + +**Set-up (`A/A0-*.txt`):** Immich deployed through the product on 9202. The admin made **as a household**: a stranger's +`admin-sign-up` → 401 (the setup gate); the household passed the gate with its dashboard session (`felhom_gate` cookie) +→ 201, `isInitialized` true. A second family member made by the admin. 20 photos as the admin, 10 as the member. From +here the household password was used only to look at results as the household; no candidate used it. + +**Measured:** + +| # | Works? | What it writes into the household's Immich | What the household sees | What an Immich update breaks | What a compromised controller gains | +|---|---|---|---|---|---| +| A1 `immich-admin` | **no** — v3.2.4's commands: reset-admin-password, password/OAuth/maintenance on-off, list-users, grant/revoke-admin, change-media-location, schema-check, version. No job command (`A/A0-photos.txt`) | — | — | — | — | +| A2 scoped API key | **partly** — needs `asset.update` + `job.create` (with `asset.update` alone: 403 „Missing required permission: job.create"); the admin's own photo 204, **the family member's photo 400 „Not found or no asset.update access"**; the lost-file report needs `maintenance` + an admin. The box cannot make the key itself: only the household's session can (`A/A2-A3-key-and-missing.txt`) | one API key row in the admin's account | „Felhom box repair" under Account settings → API keys; deleting it stops the repair | little: the public API is versioned | the key: update the admin's photos' metadata, run any job — less than it has already | +| A3 nightly „missing thumbnails" | **no** (on by default, 00:00) — it reads only the database: no thumbnail/preview ROW, an edited asset without a full-size row, or `thumbhash` null (`asset-job.repository.js` `streamForThumbnailJob`). An empty file never counts; only a database write (A5) would make it | — | — | — | — | +| **A4 Immich's own queue** | **yes** — `AssetGenerateThumbnails {id}` on `thumbnailGeneration` (prefix `immich_bull`), added with Immich's own `bullmq` inside `immich-server` (`docker exec … node`) — the same job `POST /api/assets/jobs` queues: **5 broken assets of BOTH users repaired in 1.08 s; their 10 files rewritten; 0 of the other 50 files touched**. Lost originals: `IntegrityMissingFilesQueueAll` on `integrityCheck` → one `missing_file` row in 0.44 s, read from the database with a SELECT (name, date taken, owner) (`A/A4-*.txt`) | only what Immich itself writes: the rebuilt thumbnails and its own integrity-report rows | nothing (the admin's Jobs page shows the queue counts while it runs) | the job name, the queue name, the prefix, the report table — internal; an update can rename them | nothing new: the controller already holds the docker socket and `DB_PASSWORD` | +| A5 database write | not tried (not chosen) | rows in the household's database | depends | the schema | — | +| A6 the household presses | always works | — | a note; „thumbnails: all" rebuilds the whole library | — | — | + +**Scan cost (`A/scan-20000.txt`):** 20,000 files in the same layout (10,525 folders), 10 empty → found in **0.29 s** +(`find -xdev -type f -size 0`; warm cache — a cold scan cannot be forced inside the guest). +**Path:** `/thumbs////_thumbnail.webp` / `_preview.jpeg`; the asset id is the first +36 characters, followed by **`_`** (the prompt's draft regex had `-`). +**Teardown:** Immich removed through the product; no folder, container, volume or image left (`A/teardown.txt`). + +## Part B — the decision (operator) + +**Question: how should the box ask Immich to repair the broken photos after a power cut?** + +| | Option 1 — Immich's own queue (A4) — **my pick** | Option 2 — an API key the household makes (A2) | +|---|---|---| +| How | the controller runs Immich's own queue library inside `immich-server` and adds the same job Immich's API adds; reads the lost-file report from Immich's database (read only) | the household makes a key in Immich (admin, 3 permissions) and pastes it into the dashboard; the box calls the public API | +| Writes into Immich | only what Immich itself writes when it repairs (thumbnails, its report rows) | one API key in the admin's account | +| The household sees | nothing (a note only if a photo is lost) | the key in its account; one extra setup step | +| Covers | every user's photos | **only the admin's** — a family member's photos are refused (measured) | +| An Immich update can break | the job/queue names and the report table → the catalog pins the tested Immich version; any other version: skip and tell the operator, never guess | little (public API) | +| Threat | none new: the controller already has the docker socket and the database password | a stored key the box must guard | + +**If you do nothing:** the fallback A6 — no automatic repair; a note tells the household to run „thumbnails: all".