makeDeviceAssetId(item.assetId) was passed as an extra 5th argument
before tagId, which doesn't match uploadAssetBuffer's signature
(asset, payload, uploadHeaders, jsonHeaders, tagId?) and broke the
TS build. Match the call pattern used by the sibling nextcloud/immich
processors.
The id list was built via raw string interpolation into sql.raw().
Switch to sql.join with proper parameter binding, matching the
pattern used by the other match branches in this function.
Adds condition types common in smart-album systems of other photo
managers (Apple Photos, Lightroom, Google Photos):
- File Size: min/max in MB (fractional) over asset_exif.fileSizeInByte
- File Name / Path: contains / not contains / starts with / ends with
against originalFileName or originalPath (ILIKE, wildcards escaped so
user text matches literally)
- File Extension: is any of / is none of a comma-separated list,
case-insensitive, matched after the last dot
- Face Count: min/max visible, non-deleted detected faces — 'how many
people', complementing the person condition's 'who'
- Time of Day: from/to hour over localDateTime (capture wall-clock
time, no timezone math); From > To wraps past midnight (22 to 5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a "Resolution" condition type filtering assets by image
dimensions, useful for isolating low-resolution images for cleanup or
selecting high-resolution ones.
Three metrics with optional min/max bounds:
- Megapixels (width x height / 1e6, fractional values allowed)
- Short edge in pixels (LEAST of width/height)
- Long edge in pixels (GREATEST of width/height)
Short/long edge are orientation-agnostic, so a 4000x3000 landscape and
a 3000x4000 portrait are treated identically. Uses the exifImageWidth/
exifImageHeight columns on asset_exif, which the condition queries
already join; assets without recorded dimensions never match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a "Tag" condition type that filters assets by Immich tags, with
the same match modes as the person condition (has any of / has all of /
has none of). Selected tags also match their child tags via Immich's
tag_closure hierarchy table, mirroring how tag browsing behaves in
Immich itself.
- conditionBuilder: tag case with EXISTS subqueries on tag_asset,
descendant-inclusive via tag_closure (plus a direct id check so a
missing self-closure row can't drop matches)
- ConditionEditor: Tag entry + TagPicker multi-select (PersonPicker
pattern) with tag color chips
- conditionSummary: node label ("Tag any of: ...")
- new GET /api/tags route + client handler + drizzle schema for
Immich's tag table
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All workflow trigger queries were capped at .limit(10000) with no
ORDER BY and no pagination, so on libraries larger than 10k assets an
all_assets ("full library scan") trigger silently evaluated an
arbitrary 10k subset. The IF/SWITCH condition queries had the same cap,
and bulk actions sent every asset id in a single Immich API request.
- resolveAssetTrigger: keyset pagination (ORDER BY id, batches of 5000)
for all three trigger types
- IF/SWITCH: evaluate conditions against the incoming asset set in
chunks instead of one capped inArray query
- actionExecutor: call Immich bulk endpoints in batches of 1000 ids
Verified on a 26,063-asset library (Immich v3.0.1): all_assets now
resolves 26,063 (previously 10,000), a person-filter IF matched 813
assets vs 142 under the cap, and a 25,250-asset remove_from_album
branch completed via batched requests.
Fixes#296.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Immich v3 split album-membership rights out of album.update into
albumAsset.create / albumAsset.delete, and assigning tags to assets
requires tag.asset. The hardcoded WORKFLOW_PERMISSIONS list predates
this, so auto-generated workflow keys 403 at runtime on Add to Album
(and Add Tag) actions:
Immich API error 403: {"message":"Missing required permission: albumAsset.create"}
Also, validate-api-key.ts literal-matched required permissions against
the key's permission list, so a key created with the "All" checkbox
(stored as ["all"]) failed validation for every permission — and the
settings page refuses to save a key that fails validation, making it
impossible to work around with a full-permission key.
- Add albumAsset.create, albumAsset.delete, tag.asset to
WORKFLOW_PERMISSIONS (matching the endpoints actionExecutor.ts
actually calls: PUT/DELETE /albums/:id/assets, PUT /tags/:id/assets)
- Add albumAsset.create to IMPORT_PERMISSIONS for the same reason
- Treat a granted "all" permission as satisfying all requirements in
validate-api-key.ts
Follow-up to #294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Updated albumColumns and AlbumThumbnail components to display placeholder when no thumbnail is available.
- added album_user table (album ownership moved here, role='owner')
- drop asset.deviceId/deviceAssetId and albums.ownerId
- asset.duration varchar -> integer (milliseconds)
- removed isArchived instead use visibility
The /api/assets/albums-by-assets route accepts a POST body shaped
{ assetIds: string[] } and is invoked from the bulk-duplicate-finder
page with every asset across every duplicate group. With a large
duplicate set the body easily exceeds Next.js's default 1 MB
bodyParser limit and the route returns 413 before the handler runs.
Concrete case: a library with ~14,500 duplicate groups produces a
request body around 1.4-1.8 MB, which is rejected. The page renders
but never finishes loading (413 in browser console).
Fix: add the per-route Next.js bodyParser config to bump the limit
to 10 MB, leaving plenty of headroom for very large libraries while
still bounding worst-case request size.
Upstream already shows the selected ring + clickable check badge on each
thumbnail. This adds the missing piece for dense grids: a slight opacity
reduction on non-selected items while a selection is in progress, so the
selected set stays visible at a glance.
Implemented by passing selectionMode={selectable && selectedIds.length > 0}
from AssetGrid to LazyGridImage; LazyGridImage applies opacity-60 to its
container when selectionMode && !photo.isSelected.
- Updated importJobs schema to include 'ente' as a valid platform.
- Enhanced import-shared API to handle Ente shared links, including detection and fetching of albums.
- Introduced new ente.ts module for Ente-specific decryption and API interactions.
- Implemented thumbnail and video handling for Ente assets in import-shared API.
- Created EnteSharedLinkProcessor for processing Ente shared links during import jobs.
- Updated asset interfaces to accommodate Ente-specific encryption artifacts.
- Registered EnteSharedLinkProcessor in the import processors registry.
- Added necessary dependencies (bs58, libsodium-wrappers-sumo) to package.json and bun.lock.
- Updated FindInput component to use rc-mentions for better performance and flexibility.
- Removed unused styles related to react-mentions and added new styles for rc-mentions in globals.scss.
- Enhanced search functionality to handle mentions and clear input more effectively.
- Updated FindPage to manage searched queries and loading state.
Today the Analytics heatmap hard-codes "last 12 months" both in the SQL
(`WHERE fileCreatedAt >= CURRENT_DATE - INTERVAL '1 YEAR'`) and in the
frontend's month labels loop. Users with multi-year libraries can't look
back at 2018 or 2020 from this view.
Changes:
- API `/api/analytics/statistics/heatmap` now accepts an optional `year`
query param. With it, the query spans the full calendar year (Jan 1 to
Dec 31) and `fillMissingDates` walks that range. Without it, the legacy
rolling 12-month behaviour is preserved (including the "skip the
repeating current month from last year" seam guard).
- Response shape changed from `[]` to `{ data, year, availableYears }`.
`availableYears` comes from a DISTINCT EXTRACT(YEAR ...) over the user's
assets so the UI can render a correct dropdown without guessing.
- Component shows a Select next to the heatmap: "Last 12 months" (default)
plus one entry per year that actually has assets. Month labels and the
`weeksPerMonth` computation switch accordingly.
- No migrations needed; drizzle query only.
Callers of `getHeatMapData()` now receive an object — there was only one
caller (the heatmap component) so the change is localised.
Two small fixes in PeopleFilters:
1. `handleChange` preserved the current page when any other filter changed.
Symptom: the user is on page 2, types a name in the search box, and the
list shows zero results until they click back to page 1 manually. The new
query returns fewer total people, so page=2 is past the end.
Fix: force page=1 whenever the change doesn't explicitly set `page`.
2. The DropdownMenuTrigger for the sort menu had a leftover
`onClick={() => handleChange({ page: nextPage })}` — looks copy-pasted
from the "next page" button right above it. Opening the sort menu would
silently advance the pagination. Dropped; Radix's DropdownMenuTrigger
opens the popover by itself.
Small-to-medium LLMs (qwen2.5:7b locally, Gemini 2.0 flash in #218) frequently echo the prompt's "Today's date is …" context back as a `takenAfter: <today>` filter even when the user query has no date reference. This silently filters out all results (photos cannot be taken in the future), surfacing as "No results found / Taken After 2026-04-19" in the UI.
Two changes:
1. Tighten the prompt to explicitly say today's date should only be used to resolve relative expressions that actually appear in the query.
2. Guardrail: if the model still returns `takenAfter >= today` (or `takenBefore > today`), drop it. This is narrow and defensible — a future date literally cannot match any asset, so removing it never loses a valid match. No locale-specific regex, no guessing intent; just reject provably invalid filters.
Repro with qwen2.5:7b via Ollama OpenAI-compat endpoint, exact prompt that parseFindQuery produces today:
Input: "beach"
Output: {"query":"beach","takenAfter":"2026-04-19"}
With this patch the stale `takenAfter` is dropped before hitting Immich.
Ref: #218 (same symptom reported with Gemini)
The /api/find/search handler was hard-coding `Authorization: Bearer ${currentUser?.accessToken}`. When Power Tools authenticates via `IMMICH_API_KEY` (no cookie session), `accessToken` is undefined and Immich returns 401 — which surfaces in the UI as "No results found", making the feature silently unusable in API-key deployments.
This swaps the inline header construction for the existing `getUserHeaders()` helper in `src/helpers/user.helper.ts`, which already handles both modes: `x-api-key` when `isUsingAPIKey`, `Authorization: Bearer …` otherwise.
Also adds an explicit 403 when there's no authenticated user, instead of letting the request flow forward with `Bearer undefined`.
Repro (before):
curl -X POST http://localhost:8001/api/find/search -d '{"query":"beach"}' \
→ {"assets":[],"filters":{…},"error":"Failed to fetch assets"}
After:
same request → 40 assets with embeddings matching "beach"
This updates the container image to the official immich-power-tools repository; no other configuration changes were made.
Updated from previous (now deprecated) image references. Some files were still referencing the old image instance ´ghcr.io/varun-raj/immich-power-tools:latest´, updated them to ´ghcr.io/immich-power-tools/immich-power-tools:latest´ across the project (README.md, docker-compose.yml, and docker-compose-all-immich.yml).