--- name: directus-image-previews description: Work on the directus-extension-generate-image-previews bundle (Directus 11 image transform pre-generation). Use when changing the warmer, backfill, scheduler, admin module, endpoints, presets, deployment, or when debugging preview generation in this repo. --- # directus-image-previews — working rules Read this before changing the extension. It encodes hard-won invariants; breaking them silently produces cache misses or production incidents. For general Directus-extension patterns, see the global `directus-extensions` skill. ## Scope A Directus **bundle** (tested on **11.17.4**, manifest `host: ^11.0.0`) that pre-generates image transform cache files: - `src/hook/` — `files.upload` warmer + in-process nightly `schedule('*/15 * * * *')`. - `src/endpoint/` — `/image-previews/*` admin API. - `src/module/` — admin page (progress, storage, config, diagnostics, API reference). - `src/lib/` — settings, state, presets, warmer, backfill, diagnostics, storage, window, format. - `dev/` — isolated Directus 11.17.4 stack + deploy/rollback scripts. ## Invariants — do not regress 1. **Cache filename** = `__.`, beside the original in the storage root. 2. **Hash = `object-hash@3` defaults** (sha1/hex, `unorderedArrays:false` → array order matters, `unorderedObjects:true`), over the **resolved** transform array. 3. **`resolvePreset` order**: `transforms[]` → `toFormat` (if `format`/`quality`) → `resize` (+`extract`). 4. **Never pass `{ key }` alone** — `resolvePreset` ignores `key`. Always pass the full preset object. 5. **`format:'auto'` needs an explicit `acceptFormat`** (`avif`/`webp`); otherwise server-side resolves to jpg/png and the hash won't match the browser. 6. **Preset registry mirrors the controller**: `[...SYSTEM_PRESETS, ...storage_asset_presets]`, first match wins. `GET /settings` returns only custom presets. 7. **`AssetsService.getAsset` awaits the write**; destroy/unpipe the returned stream to free the FD. 8. **Local storage writes are not atomic** — never generate the same file from two workers concurrently. 9. **State dir** is `/.image-previews` (writable). Do not use `/directus/extensions/...` (root-owned → EACCES). 10. **Env values from the Directus context are coerced** (booleans/numbers). Stringify before parsing. 11. **All authoritative state is file-backed** (PM2 cluster). Never in-memory. 12. **Hooks never throw into Directus core**; all behavior is gated by `enabled`. ## Presets / formats semantics - System presets are all `format:'auto'` → one file per configured `formats` (default `avif,webp`). - Custom preset with a fixed `format` → exactly that format, `formats` is ignored. - Custom preset with `auto` → follows `formats`. - Custom preset with no `format`/`quality` → resize-only (original extension, still hashed). - A custom key equal to a system key is **shadowed** (system wins) and flagged by `/presets`. ## Backfill / scheduler semantics - `Run now` → `POST /run` with `ignoreWindow:true`; runs until cancelled. - Nightly → `schedule('*/15 * * * *')`: only inside the maintenance window, no active run, and not already completed today. It starts/resumes the backfill. - The backfill **pauses** when the window closes (`run.paused=true`) and resumes next night. - Pre-scans storage filenames and counts existing preview files as `already cached` (skips generation). - `total = image files × tasksPerFile`; `done` counts cached + generated + failed. - One run at a time (in-process guard + `run.lock`). ## State files (on the Directus host/container) ``` /.image-previews/ settings.json effective config run.json progress (running/done/total/generated/hits/failed/rate/eta/paused/cancelled/error) state.jsonl completed {file_id,preset_key,format} → resume failures.jsonl append-only failure events; replay = latest per task; compacted under run.lock run.lock cluster-wide run lock cancel.flag cancellation request ``` ## Debugging - `GET /image-previews/status` — progress, window, paused, last error. - `GET /image-previews/presets` — effective formats, shadowed keys. - `GET /image-previews/resolve-plan?fileId=&key=` — predicted cache filename, no write. **Use this first.** - `POST /image-previews/generate` `{fileId,keys?,formats?,dryRun?}` — one file, per-task results. - `GET /image-previews/ops?limit=` — recent operations with errors (e.g. unsupported preset format). - Directus log (file transport): `Added extensions`, `Extensions reloaded`, `image-previews: …`. - Common failure: custom preset format not supported by sharp (e.g. `bmp`) → error in `/ops`; fix the preset. ## Verification gate Before shipping a change that touches generation, prove: 1. `resolve-plan` filename for `?key=system-medium-cover` is byte-identical to the file produced by a real browser request with `Accept: image/avif`. 2. `npm test` green, `npm run typecheck` clean, `npm run build` emits both bundles. 3. Isolated `dev/` stack: upload → 12 files (6 presets × 2 formats), backfill idempotent, pause/cancel works. ## Build / deploy - **Never build on the production host** (no Node; npm is unreliable there). - Build locally: `npm ci && npm run build && npm test`. - Package `dist/` + `package.json` (+ README/LICENSE) into a tarball; SCP it. - Install: copy `dist` **first**, `package.json` **last**; back up the previous `dist`. - `EXTENSIONS_AUTO_RELOAD=true` in prod — never copy a half-built extension into the watched dir. If a reload looks ambiguous, restart the container. - Rollback: restore the backed-up `dist` or remove the extension directory. - Public repo: `git@code-at-locus.im:Rawman/directus-extension-generate-image-previews.git`, branch `master`. `.opencode/` internal notes are untracked; only `.opencode/skills/` is tracked. ## Checklist for any change - [ ] Invariants above untouched (or explicitly justified). - [ ] Tests added/updated; 43+ green; typecheck clean. - [ ] `resolve-plan` hash-match still proven. - [ ] No new in-memory state; state dir still writable. - [ ] Endpoint stays admin-guarded; no secrets/file contents logged. - [ ] Deploy payload is `dist` + `package.json`; backup taken; reload verified. - [ ] README/AGENTS updated if behavior or settings changed. ## Links - Global skill: `directus-extensions` (bundles, context APIs, storage, transforms, deployment). - Docs: https://directus.com/docs/guides/extensions/overview - Transforms: https://directus.com/docs/guides/files/transform - Source anchors: `api/src/services/assets.ts`, `api/src/utils/transformations.ts`, `api/src/controllers/assets.ts`, `api/src/constants.ts`.