- v3.2.0 renamed asset_face.personId to personGroupId and dropped person.id
in favour of a (ownerId, personGroupId) key, breaking every face/person
query.
Smart Search produced takenAfter/takenBefore as plain YYYY-MM-DD strings,
but the Immich search API validates them as ISO 8601 datetimes and rejects
a bare date with invalid_format / format: datetime. Every search containing
a date failed with 'Failed to fetch assets'.
Expand date-only values after the existing today-guardrail (which compares
YYYY-MM-DD strings and must keep doing so): takenAfter to the start of the
day, takenBefore to the end of the day so a single-day range such as
'videos yesterday' is not empty. Values that already carry a time component
are left untouched.
.dockerignore did not list .env, so `docker build` copied any local .env —
credentials included — into the image build context (and into the image
wherever the Dockerfile does a broad `COPY . .`). Anyone building the image
with a populated .env present would bake their secrets into the resulting
image layers.
Add `.env` and `.env.*.local` to .dockerignore so local environment files
never enter the build context. Runtime configuration is still supplied the
intended way — environment variables / an env_file at `docker run` /
compose time — so nothing about normal deployment changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The myRole select added for issue #286 wasn't included in the GROUP BY
clause, causing a 500 ("column album_user.role must appear in the
GROUP BY clause") whenever includeShared=true was requested.
Closes#286. The album picker for add_to_album/remove_from_album only
showed albums owned by the current user. Add an opt-in ?includeShared
param to /api/albums/list that also includes albums where the user
has 'editor' role (upload access), and group the picker into 'My
Albums' / 'Shared Albums' sections. Other callers of listAlbums()
don't pass includeShared, so they keep the existing owner-only
behavior unchanged.
Merges potential-trips + potential-trip-assets into
potential-albums-dates + potential-albums-assets respectively, keyed
by a new ?groupBy=day|trip query param. The actual grouping logic is
extracted into two new helper files (potentialAlbumsGrouping.helper.ts,
potentialAlbumsAssets.helper.ts), each holding a day-mode and
trip-mode function the unified handlers call into.
Client-facing functions (listPotentialTrips, listPotentialTripAssets)
keep their existing names/signatures — only their internal path and a
groupBy param changed — so no frontend component call sites need
updating. Also drops the two now-unused LIST_POTENTIAL_TRIP*_PATH
route constants.
Addresses the maintainer's open caching-strategy question by removing
the need for a second endpoint/cache entirely: one endpoint per
concern, two grouping strategies.
Immich v3 dropped asset.deviceId/deviceAssetId (af4cfce on main).
This branch predates that migration, so its own SELECT_ORPHAN_RANGE
query still referenced the column, which would 500 on any current
Immich v3 instance. Match the field list already used by the sibling
potential-albums-assets.ts endpoint, which was updated for v3.
clusterTrips's gap check compared array-index adjacency between
DaySignal entries. Since `days` only contains dates with >=1 photo
(GROUP BY DATE(...) with no zero-photo rows), two trips separated by
months with no photos in between were array-adjacent (gapLen=0) and
got merged into one bogus trip. Compare actual elapsed calendar days
between the last and next away day instead.
The local tag table can lag behind Immich's own tag state (e.g. tags
created directly in Immich, or before this app's sync ran), causing
remove_tag to silently no-op on a tag that actually exists. Mirror the
sibling "tag" action's approach and resolve against the Immich API,
matching on the tag's full-path "value" field. Drops the now-unused
db/sql local lookup.
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.
Removes a tag (by name, matching the tag's full path value) from the
matched assets via DELETE /tags/:id/assets. The tag is looked up
without creating it — removing a tag that doesn't exist is a no-op
rather than an error, so add/remove pairs are safe in the same
workflow.
Registered in the node palette, action node, run-detail view, and
action config (same tag-name field as Add Tag).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.