> For the complete documentation index, see [llms.txt](https://docs.fylr.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fylr.io/releases/2026/v6.34.0.md).

# Release v6.34.0 (2026-07-15)

Published 2026-07-15 13:25:41Z

* [fylr\_checksums.txt](https://s3.eu-central-1.wasabisys.com/fylr-releases/v6.34.0/fylr_checksums.txt)
* [fylr\_v6.34.0\_darwin\_amd64.tar.gz](https://s3.eu-central-1.wasabisys.com/fylr-releases/v6.34.0/fylr_v6.34.0_darwin_amd64.tar.gz)
* [fylr\_v6.34.0\_darwin\_arm64.tar.gz](https://s3.eu-central-1.wasabisys.com/fylr-releases/v6.34.0/fylr_v6.34.0_darwin_arm64.tar.gz)
* [fylr\_v6.34.0\_linux\_amd64.tar.gz](https://s3.eu-central-1.wasabisys.com/fylr-releases/v6.34.0/fylr_v6.34.0_linux_amd64.tar.gz)
* [fylr\_v6.34.0\_windows\_amd64.zip](https://s3.eu-central-1.wasabisys.com/fylr-releases/v6.34.0/fylr_v6.34.0_windows_amd64.zip)

## Server

### Important

* **This release forces a blocking re-index.** Two indexing changes — fulltext-only fields becoming sortable (see *Sorting a list by a fulltext-only field* under **Fixed**) and file content now following the file field's expert search (see *File content indexing follows the file field's expert search* under **Improved**) — only take effect for existing records once they are re-indexed, so the upgrade triggers a full re-index automatically. While it runs the instance is not available for normal use; only `/inspect/` is reachable. On a large instance this can take a while.
* **All web sessions are invalidated by the upgrade.** Web-app access and refresh tokens are now bound to the browser (see *Sessions bound to the browser* under **Improved**), so every existing session is dropped once on upgrade and users sign in again. API-client and password-grant tokens are unaffected.
* **Two-factor authentication is a licensed capability.** Enabling it requires a license that carries the `two_factor_auth` capability (see *Two-factor authentication* under **New**).
* **IIIF is now version 3.** The Image and Presentation APIs speak IIIF 3.0 and the image `info.json` uses the v3 field names; clients that still read the v2 `info.json` must be updated (see *IIIF Image and Presentation API upgraded to version 3* under **Improved**).

### New

* **`await_index` for `/api/v1/db`**: The object save (`POST`) and delete (`DELETE`) endpoints accept an `await_index=<seconds>` query parameter. The request then only responds once the search-index jobs it queued — including the dependent objects that are re-indexed with it — have been processed and the affected indices refreshed, so the change is already visible in `/api/search` when the response arrives. Without the parameter indexing stays asynchronous as before, so a search directly after a write may not yet find the change. If the jobs are not processed within the given number of seconds the request still succeeds (the change is stored) and reports the unfinished wait in the `X-Fylr-Error` / `X-Fylr-Error-Code` response headers rather than failing. The parameter is documented in the [`/api/v1/db` API reference](https://docs.fylr.io/for-developers/api/endpoints/api-db). \[ce1071d7e]
* **Custom rendition presets**: A new *Custom Rendition Presets* block in the base config (file worker tab) holds named presets of on-demand custom-version options — format, size, quality, watermark, color profile and so on. A download or export requests a preset by name (`type: "custom"` with `custom_version_preset: "<name>"`); the server produces the rendition on the fly and names the downloaded file after the preset instead of `custom`. Presets are shared by all users and validated on save: a unique name, a display name, and at least one effective setting are required. See [Custom Version Presets](https://docs.fylr.io/for-administrators/readme/file-worker/custom-version-presets#configuration). \[3005c140d] \[9ec3bd54c]
* **Consolidate duplicate linked objects**: A new background task module `consolidate_objects` merges duplicates of linked objects such as keywords, actors or places. The task re-links every reference to the chosen source objects — in any link field, top level or nested — to one target object of the same objecttype, then deletes the sources (recoverable from the trash). Links that reference a source through a reverse-edit column are re-linked like any others. A nested row whose link would duplicate a sibling row already linking the target is dropped, unless the row carries other data such as a remark. A dry run mode logs the planned work without changing anything, and the task log and version comments name sources, target and de-duplications with their display names. Bidirectional links are kept consistent: re-pointing one updates the mirrored entry on the target and removes the stale mirror from a kept source, and the auto-added mirror version on the target carries the consolidation comment as well. The task runs with the rights of the user who created it: creating a task is refused when the user may not delete the source records, and references the user may not modify are left untouched — the affected source record is then kept instead of being deleted with dangling references — and an unconfirmed save answers with a confirmation dialog (`202`, repeat with `confirm=yes`) previewing how many records reference the sources, broken down by objecttype with the affected fields, and what confirming does to the target and the sources. The task-manager picker and the editor's *Replace by another record…* shortcut are described under *Consolidate duplicate linked objects* in the **Frontend** section. See [`consolidate_objects`](https://docs.fylr.io/for-users/additional-features/background-tasks#consolidate_objects). \[4c27dc665] \[20bd549ba] \[39743944d] \[e1f495f53] \[4734b1978]
* **Two-factor authentication (local accounts)**: Logins can now require a second factor. It is enabled in the base config (*Access* → *Two-factor authentication*), where one or more methods are selected, and enforced per group via a new "2FA required" flag. Phase 1 ships the email one-time code: an enforced user receives a 6-digit code by email after the password step and enters it to finish signing in. `system:root` and users without an email address are exempt. The feature requires a license carrying the `two_factor_auth` capability: without it the base-config section is marked unavailable and cannot be enabled (unlicensed local test instances have it available), and the settings API reports the effective capability values. The email method is only offered once an email server is configured. The non-interactive password grant (OAuth2 password) cannot present a second factor, so for enforced users it is refused — unless the admin deliberately opts in via the new *Allow password grant* switch (to keep API clients with password credentials working). Wrong codes are limited (3 attempts by default) before a new login is required, and the `USER_LOGIN` event records which second factor was used (`two_factor`: the method, `<none>`, or absent when 2FA is off). Setup is described under [Enabling Two-Factor Authentication](https://docs.fylr.io/help/tutorials/for-administrators/enabling-two-factor-authentication). \[1eadff193] \[170ad6760] \[a325ab6f1] \[b28ee4bf8] \[79e922e59] \[0dd2e4bf5]
* **Two-factor authentication — authenticator app (TOTP) and passkey**: In addition to the email code, the second factor can now be a time-based one-time password from an authenticator app (Google / Microsoft Authenticator, …) or a passkey (WebAuthn — fingerprint, face recognition, security key). Enforced users who haven't enrolled yet are walked through setup at login (QR code for TOTP, the browser's passkey ceremony for passkeys); with more than one option a picker lets the user choose, and later logins go straight to the last-used method with a "use a different method" link. The TOTP secret is encrypted at rest (`fylr.encryptionKey`). Both enrollments can be reset by admins in the user editor and by the users themselves in their settings; the `USER_LOGIN` event records the method in its `two_factor` field. See [Enabling Two-Factor Authentication](https://docs.fylr.io/help/tutorials/for-administrators/enabling-two-factor-authentication). \[581f45f75]
* **Passwordless sign-in with a passkey**: When the passkey method is enabled, the login form offers "Sign in with passkey" — no username or password: the browser suggests the stored passkey right in the login field (conditional UI) or via the explicit link (which also covers signing in with a passkey from a phone). The device confirms with fingerprint / face recognition / PIN; since that is possession plus biometrics, a passkey sign-in counts as a full two-factor login. Passkeys are now enrolled as discoverable credentials to make this possible. See [Enabling Two-Factor Authentication](https://docs.fylr.io/help/tutorials/for-administrators/enabling-two-factor-authentication). \[581f45f75]
* **Search `boost` parameter**: Every element of the `search` array in `POST /api/v1/search` accepts an optional `boost` (number, default 1), as known from easydb 5: a higher boost gives objects matching that element a higher `_score`, so `should` elements combined with sorting by `_score` rank preferred matches first. It works with every element type (`match`, `in`, `range`, `complex`, `nested`, geo); negative values are rejected. See the [`/api/v1/search` reference](https://docs.fylr.io/for-developers/api/endpoints/api-search). \[b2643cc11]
* **"Stay logged in" on the login form**: An admin can offer a *Stay logged in* checkbox on the login form via the base config (*Login* → *Stay logged in*: off by default, or 7 / 30 days). With the box checked, a completed login — including a passed two-factor challenge, the SAML round-trip or a passkey sign-in — keeps the user signed in browser-wide for the configured duration: a fylr link opened in a new tab, window or after a browser restart no longer asks for the login. The label names the duration ("Stay logged in (30 days)"), the period extends on use, and logging out (or switching the option off) ends it everywhere. Technically this is a server-side session bound to a persistent, HttpOnly cookie — tokens, their lifetimes and rotation are unchanged, and nothing token-like is parked in the browser's web storage. Enabled under [*Login* → *Stay Logged In*](https://docs.fylr.io/for-administrators/readme/access) in the base config. \[bcb002a1a]
* **Term recalculation (`/inspect`)**: A new maintenance operation rebuilds the `term` / `object_term` suggestion tables for every current object, so changes that affect how terms are extracted (for example reworked custom-data-type field mappings) can take effect on existing records without re-saving them. Start it from the new *Term Recalculation* page under the backend's `/inspect` (also linked from `/inspect/terms`); it shows the status and the last runs. The recalculation runs in the background below normal saves and below a re-index, so the instance stays usable while it works (it can take hours on a large instance), and it never writes the objects — no new versions and no changelog entries. It also removes terms no longer referenced by any object and re-indexes all objects and terms in place afterwards. On a fylr update, releases that change term extraction can request the recalculation automatically. See [Suggestions and terms](https://docs.fylr.io/for-developers/concepts/search-and-events#suggestions-and-terms). \[f9812bb10]
* **Collection share links with an external base URL**: A collection share preset (*Rights management* → *Presets* → *Collections*) can carry an optional *External URL for sharing links* — an absolute `http(s)` base URL, for example a portal on `https://media.example.com`. Share links of entries using the preset are then built from that base instead of the server's own URL: the copy/open link buttons in the share editor and the link in the share notification email. Everything after the base is unchanged (`…/collection/<uuid>?…`), so any webapp that serves fylr's collection deep-link structure can be targeted, and one preset per target offers several portals side by side. The mailed link is recorded in the `EMAIL_SENT` event info as `CollectionURL`. Without a configured URL share links behave as before. In the API the preset gains an optional `share_url` (context `collection` only). See [Presets](https://docs.fylr.io/for-administrators/permissions/presets). \[897f5643b]

### Improved

* **Idle database connections are released**: fylr kept every pooled database connection open forever, so even a completely idle server pinned up to `maxIdleConns` PostgreSQL backends per pool. A pooled connection that has been idle for 30 seconds is now closed and its backend released; a busy server keeps reusing its connections and is unaffected. This lowers the connection pressure when many fylr instances share one PostgreSQL server. Configurable via the new `fylr.yml` settings `db.connMaxIdleTimeSec` (default 30, an explicit `0` restores the previous keep-forever behavior) and `db.connMaxLifetimeSec` (maximum total connection age, default `0` = unlimited). Both keys are documented in [`fylr.example.yml`](https://docs.fylr.io/for-system-administrators/configuration/fylr.example.yml). \[a8a14bcaa]
* **Metadata import assembles multi-column linked objects**: An import mapping can address several columns of the same linked object — for example a nested keyword's name and its localization. The columns zip positionally back into one linked object per row: the first mapped column is the lookup key. A key match links the existing object (the file's further columns are discarded — a file import never edits a shared vocabulary record), an unmatched key creates the linked object with all imported columns, and when several existing objects share the key name, the further columns pick the one the file agrees with. Localized values compare only the languages both sides have, since exported files fill missing translations with the fallback language. Both columns must be part of the linked objecttype's preferred mask. See [Metadata Mapping](https://docs.fylr.io/for-administrators/metadata-mapping#multi-column-linked-objects-import). \[2f28bfbd7] \[f166814e9]
* **Date ranges round-trip through metadata mappings**: A daterange field can now be imported from file metadata, completing the export that already existed. The import reads the "from - to" form fylr's own export writes (including a single date and open-ended ranges), so a daterange survives an export/re-import cycle; arbitrary external metadata is not parsed, as no standard metadata representation for date ranges exists. See [Metadata Mapping](https://docs.fylr.io/for-administrators/metadata-mapping#field-types). \[624e4ef67]
* **Object loading, rendering, search and indexing allocate far less memory**: A profiling pass over the hot paths cuts the server's memory allocation churn roughly in half. Object JSON now streams directly into the response instead of being buffered per object, per-object rights are computed once per batch of objects sharing the same pool, tags and owner, the version history of never-edited objects is derived from the already loaded row instead of re-queried, index documents are encoded into reused buffers, event batches are written with a single bulk INSERT, and large sub-search results are drained with `search_after` instead of increasingly expensive offsets. Large `/db` list pages load about a third faster; API responses and index documents are unchanged, so no re-index is needed. \[66ca71125]
* **Password management is limited to password-login user types**: Users of type email, ldap and sso authenticate outside fylr (or only through a share link), so fylr no longer accepts a password, a "require password change at next login" flag or a login-validity window (`login_valid_from` / `login_valid_to`) for them. `/api/user` rejects those fields, `/api/user/change_password` refuses the request, and the password-forgot page reports that the user type cannot set a password. Only easydb and easydb\_self\_register accounts keep fylr-managed password login. See [User types](https://docs.fylr.io/for-administrators/permissions/user#user-types). \[c205310f9]
* **Cross-platform release binaries are statically linked (cgo disabled)**: The downloadable macOS, Windows and Linux binaries are now compiled without cgo, so they cross-compile for every platform without a C toolchain; they are statically linked and use the pure-Go modernc SQLite driver. The official Docker image is unchanged — it keeps cgo and the C-based SQLite driver — and PostgreSQL deployments are unaffected. See [Download binaries](https://docs.fylr.io/for-system-administrators/installation#download-binaries). \[0ea378245] \[459d4e702]
* **Archived, deleted or disabled users are signed out immediately**: When an administrator (or the inactivity janitor) archives or deletes a user, or disables their login, fylr now revokes that user's access tokens and broadcasts a logout event, so any open sessions drop to the login page right away instead of continuing until the access token would have expired. The logout event carries the reason (archived, deleted, login disabled, or a login validity window that now excludes the user). See the [`USER_LOGOUT` event](https://docs.fylr.io/for-administrators/events/event-type-reference#user_logout). \[61033fffb]
* **Backup of polyhierarchical objects**: `fylr backup` now writes polyhierarchical objecttypes — records with more than one parent — which the backup previously skipped. Each record is placed into a backup payload by its depth (the length of its longest parent path), so that every parent of every branch lands in an earlier payload and the multi-parent graph restores intact. In an all-versions backup, a parent that only a historic version still links but that has since been purged is left out rather than written as an unresolvable reference, which would otherwise break the restore. See [fylr backup](https://docs.fylr.io/for-system-administrators/migration/backup). \[890c8f513]
* **IIIF Image and Presentation API upgraded to version 3**: The IIIF endpoints now speak the IIIF Image API 3.0 and Presentation API 3.0. The image `info.json` uses the v3 structure — `@context` `image/3`, `type` `ImageService3`, `id` (no longer `@id`) and a single `profile` compliance level — and now advertises the service's capabilities (`sizes`, a `tiles` pyramid, `extraFeatures` including `sizeUpscaling`, `extraQualities`, `extraFormats`, `preferredFormats`). The image request URL gains the v3 `^` upscaling size prefix (`^max`, `^w,h`, `^pct:n`, …); without it a size larger than the requested region is clamped to the region. Size and mirror handling were corrected along the way: `w,h` returns the exact size, percentage sizes scale both dimensions, and the `!` mirror reflects horizontally. Clients that read the v2 `info.json` must update for the new field names. See [IIIF](https://docs.fylr.io/for-administrators/iiif#key-features-in-fylr). \[2d8d9f7fe]
* **Import / object loading**: Importing and loading large numbers of objects is significantly faster. The object-deduplication step during a load no longer scales quadratically with the number of linked and parent objects, and reading database rows into objects — which happens on every `/api/db` object load — is around 20% faster server-side, with further speedups in the database layer. No data or behaviour changes. \[5bc8b0028] \[f24d5f440]
* **`/db` save and linked objects**: Saving an object no longer requires the `read` right on linked objects it leaves unchanged. Read is required only when a save introduces a link target at a new location — a field, or a nested row (identified by its `_uuid`) and sub-field — where the object did not already link it. Editors can now save records that carry links to objects they may not see (for example records in a restricted pool, linked by another user), as long as they do not move a link to a new place or replace it with a different, not-previously-linked target. \[319005b47]
* **Big geo shapes simplified in `_standard`**: A `geo_json` field can hold very large geometries — a country border at metre resolution is millions of vertices. The copy of such a value rendered into a record's `_standard` (used for searching, filtering and display) is now simplified with the Ramer-Douglas-Peucker algorithm down to at most `fylr.geo.standardMaxVertices` vertices per shape (default 100; set `0` to disable), instead of being copied verbatim. This keeps `_standard` — and the `geo_shape` derived for the search index — compact for records that carry or link big areas. The field value, downloads, exports and the field's own index keep full precision; only the `_standard` copy is bounded. See [`fylr.geo.standardMaxVertices`](https://docs.fylr.io/for-system-administrators/configuration/fylr.example.yml). \[a0f606760]
* **License expiration & version reminders**: The administrator warning mails around a license's end now distinguish the full lifecycle: a *paid period ending soon* warning before the end date, a *grace period ending* warning while the instance is in its two-month post-expiry grace period, and a new *license expired* notice once that grace period is over. For **buy** licenses — unlimited in time, but only valid for binaries released up to the license's end date — deploying a binary released *after* that date now sends a *binary too new* warning (such a binary will not run). The former "binary older than one year" rule no longer applies to subscriptions, which are purely time-based (end date plus the two-month grace). Mail dates use the unambiguous DD.MM.YYYY notation, and the license validation now reports the grace-period end date (`grace_to` = end date plus two months), which the license manager shows next to the end date. See [License management](https://docs.fylr.io/for-administrators/readme/license-management#expiration-warnings-and-notifications). \[cec36c34f] \[a8987dc28]
* **Custom tags in export metadata mappings**: Export mappings can now write custom XMP tags. A field's tag can be any `XMP-<group>:<tag>` path typed into the mapping editor — previously only import mappings accepted custom paths — and the server defines unknown tags on the fly via a generated ExifTool config: known groups (such as `dc`) extend the built-in table so the tag lands in the real namespace, while unknown groups get an ExifTool-convention namespace named after the group. A tag can be written as a single value, a list, or localized per language (the editor's *Typ* selector), which the server maps to the matching XMP string, Bag or lang-alt structure. The *Typ* options follow the selected tag's real capability: a built-in tag shows its fixed shape as pre-set checkboxes that stay visible but locked where the tag leaves no choice — only the list opt-out (write a single joined value instead of a list) remains selectable where the tag supports lists — while a custom tag offers both options freely. Only XMP is extensible; custom EXIF or IPTC tags are not offered. See [Metadata mappings](https://docs.fylr.io/help/tutorials/for-administrators/default-export-metadata-mappings). \[45640e705] \[6ea464acb] \[51beb4b55]
* **Web login hardening**: The OAuth2 web login callback is hardened — its response is served non-cacheable and its cross-origin access is scoped tightly. No configuration change and no change to the login experience. See the [OAuth2 login callback](https://docs.fylr.io/for-developers/api/oauth2#step-2-callback-from-fylr-to-the-local-http-server). \[4a771a952]
* **Sessions bound to the browser**: An access or refresh token issued to the web app is now tied to the browser it was issued in, via a signed, HttpOnly `fylr-browser-id` cookie that must accompany the token. Tokens issued to API clients and to the OAuth2 password grant are unbound and unaffected, as are logins delivered to a cross-origin (web-only) frontend. Tokens handed to server-side plugin callbacks (db\_pre\_save, transitions, extensions, collection uploads and exports) are likewise issued unbound and short-lived, so a plugin can still call back into the API; export callbacks now also honour a configured `plugin_user`. All existing sessions are invalidated once on upgrade, so users sign in again. \[c6e2d52df] \[387cf4f87]
* **Logout ends the whole browser session**: Logging out of the web app, and SAML Single Logout (both SP- and IdP-initiated), now revoke every token of the browser that triggered it — all of its tabs — instead of only the tab that was used, while sessions on other devices stay signed in. An explicit "sign out everywhere" still revokes the user's sessions on all devices. The logout is also published as a `USER_LOGOUT` event (carrying the scope and browser) on the event stream. See the [`USER_LOGOUT` event](https://docs.fylr.io/for-administrators/events/event-type-reference#user_logout). \[fc441ef1d]
* **Plugin zip must be stored locally**: Installing a plugin from a zip file (`type: "zip"`) through the plugin management API now requires the uploaded zip to be stored in fylr rather than left on a remote — the plugin manager has to read and unpack it. A `zip_file` that references a file with `leave_on_remote` set is now rejected with a clear error instead of being accepted and later failing to load. See [Plugin release](https://docs.fylr.io/for-developers/plugin/release). \[c27f7a724]
* **Metadata mappings updated after datamodel changes**: When a datamodel change is committed, metadata mappings (import and export) are now cleaned up in the same transaction: an Easydb field reference pointing at a datamodel field that the commit removed is dropped from the mapping, so mappings no longer carry references to fields that no longer exist. The mapping field itself is kept even when all of its references are removed this way, so it stays in place to be re-targeted. \[f45bb064d]
* **Clearer migration restore logging for files without a filename**: Restoring a backup with `--upload-versions` no longer floods the debug log with meaningless `Filename of file 0 is empty` lines. File versions take their name from the parent original, and the remaining fallback warning now names the file by id and the entity it belongs to — `<objecttype>:<system object id>` for object files, and `pool` / `user` / `group` / `objecttype` / `plugin` / `base config` for base-config and custom-data files. See [Migration restore](https://docs.fylr.io/for-system-administrators/migration/restore). \[5dda0fbab]
* **Migration restore prefills the easydb 5 video rename rule**: When a backup whose source is an easydb 5 instance is selected for restore in `/inspect/migration`, the *rename versions* table is now prefilled with `video.1920p:1080p` — easydb 5 names that video rendition `1920p` where fylr calls it `1080p`. The suggestion only appears for a freshly selected easydb 5 backup with no previous restore run; a restore resumed from a prior run keeps its saved rename list, including an empty one cleared on purpose. See [Rename Versions](https://docs.fylr.io/for-system-administrators/migration/renaming-renditions-during-migration#via-inspect-migration). \[fbd1088ca]
* **`/inspect` collection browser**: The Collections page of the `/inspect` console is now a searchable, paged tree browser. It pages through the collections, finds them by name or id, builds the ancestor chain up from every hit and expands branches on demand, and can filter by owner and user type, so it stays responsive on instances with very many collections. See [Collections](https://docs.fylr.io/for-system-administrators/inspect/collections). \[ce02ff875]
* **Workflow save reports invalid actions with clear errors**: Saving a workflow whose webhook action has no webhook selected — the default state while no Workflow Webhook is configured in the base config — used to fail with an internal error chain. The save is still rejected, but each case now returns a dedicated, translated message with its own error code, so the frontend can tell the user what to fix: no webhook selected, webhook name not found in the base config, unknown webhook callback, and change-owner action without an owner. See [Workflow webhooks](https://docs.fylr.io/for-administrators/readme/workflow-webhooks#callback-modes). \[caa7ebf01]
* **Logins may be up to 500 characters**: The API limit on `user.login` was raised from 100 to 500 characters. SSO-provisioned logins — for example Shibboleth persistent ids, which combine the IdP and SP entity URLs with an opaque handle — can exceed 100 characters; logging in with such an id worked, but `fylr backup` refused to read the user back and failed. The password limit stays at 100. \[9fcf54fe0]
* **Backups keep files lossless**: `fylr backup --include-files` packs file bytes into the backup itself (under `files/`), so the restore uploads them from the local backup instead of fetching them from the source — the restore no longer depends on the source instance staying reachable. It takes a mode: `--include-files=original` packs each non-leave file's original but not its renditions (preview versions), which the restore regenerates on the target, so this backup cannot be restored with `--upload-versions`; `--include-files=with-versions` also packs the renditions, so a restore with `--upload-versions` uploads them from the backup byte-identical instead of regenerating them (without it they are regenerated, exactly as for a URL-only backup). Renditions pack next to their original as `<id>-<original>-<version>.<ext>` under the original's id, and each packed rendition's `path` is part of the payload. Manual renditions — their original has *produce versions* off — cannot be regenerated, so the restore always uploads them, with or without `--upload-versions`, and keeps the original non-producing on the target. A manual rendition can itself be *leave on remote*; it is then never packed and returns as a reference to its upstream. Files stored *leave on remote* keep their real upstream URL: the backup requests it via the new `files_real_url=1` query parameter on the `/db` list endpoint (requires the *root* system right, since the upstream URL can be a pre-signed link that bypasses per-file permissions), and the restore re-creates them as references to that upstream — for a *with-versions* backup a leave original's packed renditions are uploaded from the backup too. The `/inspect/migration` backup form gets an *Include Files* selector, and its viewer browses nested backup directories. See [`--include-files`](https://docs.fylr.io/for-system-administrators/migration/backup#include-files). \[032b58353] \[f0e84d5231] \[2ce1477238] \[d24e3e1a3f]
* **File content indexing follows the file field's expert search**: The text extracted from a file (OCR and embedded file metadata) is written to a record's `metadata_fulltext` — and thus found by the expert / fulltext search — only when the file field has its *expert* search enabled. Before, file content was always indexed there regardless of the field's search settings, so it could not be kept out of the fulltext index. Existing instances need a re-index for the change to apply to records saved before the update; this release forces it (see **Important**). See [Search in text of files](https://docs.fylr.io/help/tutorials/for-administrators/search-text-in-images-or-office-files). \[89ccfb86f]
* **API responses opt out of caching by default**: Every API response now carries `Cache-Control: no-store, no-cache`, so browsers, shared proxies and CDNs never store responses — most of them carry data specific to the authenticated user. Caching stays available as an explicit opt-in: a `GET` with `?cache_seconds=<n>` answers with `Cache-Control: max-age=<n>, public` as before, and endpoints with their own caching policy (file downloads, IIIF tiles) keep setting their own header. \[5c3da1af6]
* **A base-config save that changes nothing is skipped**: Saving the base configuration with data identical to what is stored no longer performs an update. Previously every such save rewrote the configuration rows and made every server process reload the entire base configuration, and wrote a *Base Config Update* event — plugins or scripts that push their state into the config on a timer kept the whole installation busy with no-op reloads and flooded the event list. Now nothing is written, no reload happens and no event is recorded; a save that does change something behaves as before. \[01ec56cfa]

### Fixed

* **Faster first frontend load after a restart**: The plugin static files the web frontend loads were assembled on demand on the first request after a server start or update, reading each plugin's package as it went. The first user could therefore wait several seconds — long enough, on occasion, to see a blank page — while that ran. Those files are now prepared ahead of time: in the background at startup, and for a single plugin when it is installed or updated, so the first load is served from a ready cache. \[2128a6e95]
* **Legacy empty-string event data cleaned up on upgrade**: Older versions could leave stray `""` values in the `event` table's JSON `info` and `group_ids` columns; the upgrade rewrites them to NULL. \[48bb0fd7b]
* **Lists mapped into the same nested table align row-by-row**: When a metadata import mapping wrote two plain (non-localized) lists into the same nested table — for example a remark list and a linked keyword list — the second list's rows started after the first list's instead of pairing up with them. Each value now fills the first row that does not yet hold its field, the way localized lists already behaved, so all lists of one nested table land in the same rows. See [Metadata Mapping](https://docs.fylr.io/for-administrators/metadata-mapping). \[e6d2c4b7e]
* **List-valued metadata tags no longer carry a whitespace `val`**: In the file metadata JSON, every list (Bag) tag had a junk `val` consisting only of newlines and indentation, picked up from the XML around the list values. The `val` guess now only applies to scalar values; the list content stays in `prt` as before. \[5df103079]
* **LDAP connections**: With multiple configured LDAP connections, a dead first connection is now skipped and the next one is tried, instead of failing the login. A user search that matches more than one directory entry is always refused — previously three or more matches silently logged the user in as the first entry. And a connection's *CA certificate* setting is now actually used to verify the LDAP server's certificate; before, it was ignored and verification always ran against the system's trusted roots, so servers with a private CA could only connect with TLS verification disabled. See [LDAP login](https://docs.fylr.io/tutorials/ldap#user-login). \[61bf72940]
* **Nested sub-searches no longer truncate at 10,000 hits**: A sub-search (`"==?"`) matching more than 10,000 objects contributed only its first 10,000 hits to the outer search when it was placed inside a nested query. Sub-search drains now page with `search_after` over a point in time and re-drain from the start on every compile, so all hits are counted; this also lifts the `max_result_window` ceiling for very large drains. See [sub-searches](https://docs.fylr.io/for-users/search-and-filter/query-language#sub-search). \[66ca71125]
* **Custom metadata tags typed in lower case now import back**: A custom XMP tag whose name was entered starting with a lower-case letter (for example `XMP-custom:list`) exported correctly but was never read back, so a round-trip mapping imported nothing from those tags. ExifTool reads every tag with its name's first letter capitalized, so the import search generated from the name as typed no longer matched what was written to the file. The import now matches custom tags regardless of the letter case they were written in, which also repairs existing mappings without re-saving them; the mapping editor stores newly entered custom tags in ExifTool's canonical casing. See [Metadata mappings](https://docs.fylr.io/help/tutorials/for-administrators/default-export-metadata-mappings). \[4a07ab16d] \[be03768e42]
* **Image conversion fails on newer ImageMagick (HEIC on Windows)**: Images that take the ImageMagick path for conversion could fail to produce a preview, ending with *NoDecodeDelegateForThisImageFormat `convert`* in the logs and events. This affected HEIC files on installations whose `vips` was built without HEIC support — typically Windows — and formats such as ICO or PPM everywhere. fylr invoked ImageMagick through the deprecated `magick convert` sub-command, which newer ImageMagick 7 releases no longer accept, reading `convert` as an input filename instead. fylr now calls `magick` directly, with the input file ahead of the image operators as ImageMagick 7 expects; the IIIF tile renderer uses the same call. See [fylr on Windows](https://docs.fylr.io/for-system-administrators/installation/windows#magick.exe-imagemagick). \[e4820955e]
* **PDF and office thumbnails in webp**: A thumbnail or preview version of a PDF or office document configured to output `webp` failed to produce, ending with *Unsupported param value 'webp'*. The office cookbook's `browserthumbs` and `preview` recipes only allowed `png` and `jpg`, while the image cookbook's `browserthumbs` already allowed `webp`; a produce-config version shared by both — for example one named `browserthumbs` set to `webp` — therefore worked for images but failed for PDF and office files. Both office recipes now accept `webp` as well, and the rendition is produced as expected. \[94722488b]
* **Editing a collection share now re-checks the owner's rights**: When the rights preset of an existing collection share was changed, fylr saved the change without re-running the grantability check that creating a share goes through. Such edits are now validated just like new shares on every save endpoint, and warn when the owner is missing a required right. See [Setting up collection sharing](https://docs.fylr.io/help/tutorials/for-administrators/setting-up-collection-sharing). \[488fda280] \[933685092]
* **SAML login loop for users in many groups**: A SAML/SSO user whose identity provider sends many group memberships could no longer log in. fylr stored the whole login assertion inside the `fylr-saml-token` cookie, which then exceeded the browser's per-cookie size limit; the browser silently dropped the cookie and the login bounced back to the start. The SAML session is now kept server-side and the cookie carries only a short reference, so login works no matter how many groups a user has. Single sign-out keeps working for the whole session, and active SAML sessions are listed under `/inspect/saml-sessions/`. See [SAML setup](https://docs.fylr.io/tutorials/auth/saml#base-config). \[bf6f3ff65]
* **Collection shared twice with the same user**: Saving or restoring a collection whose access list references the same on-the-fly collection or email user more than once — for example a workfolder that was shared twice with the same recipient — no longer aborts the whole transaction with a database unique-key error. The user is created once and both access-list entries resolve to it; the access list is unchanged. \[4908aa514]
* **Migration backup lost user and group permissions and owners**: A backup written by `fylr backup` did not include the object permissions (*Permissions* tab) set directly on user and group records, and restoring a backup reset every user's and group's owner to `root`. Backups now carry both, so permissions and ownership of users and groups survive a migration. See [fylr backup](https://docs.fylr.io/for-system-administrators/migration/backup). \[cf3a87505]
* **File-production cache**: Produced renditions are now written to the cache atomically — under a temporary name that is moved into place only once the producing program has finished — instead of straight to their final cache path. An interrupted production (for example a worker killed by the OS while building the large intermediate image for an IIIF zoom of a very large asset) no longer leaves a partial file behind for later requests to pick up and serve, which could otherwise make IIIF zoom and tile requests for that asset fail. A periodic cleanup removes any leftover temporaries. \[80dbff4c2]
* **Slow plugin assets, intermittent white screen**: Every request for a file inside a plugin zip re-opened the whole zip from storage to read that one file, and the response forbade caching without offering a validator, so browsers re-fetched every plugin asset on every page load. With large plugin zips this cost seconds per tiny file and could leave the web frontend on a white screen while it loaded its plugin bundles. Requested files are now extracted once into a disk cache and served with an `ETag`, so repeat loads revalidate to an instant `304 Not Modified`. Cache entries unused for three days are removed automatically, and a plugin update switches to fresh entries by itself. \[d302c2186]
* **Indexing on large instances**: Bulk index requests no longer exceed the search cluster's `http.max_content_length`. The indexer counts every request-body byte and flushes a batch before the document that would push it over `fylr.elastic.maxMem`, which at startup is adjusted to stay 5% below the cluster limit; previously a batch could overshoot the limit and be rejected with *413 Request Entity Too Large*. This limit now always applies, including when `fylr.elastic.maxMem` is left unset — which previously left it unbounded, so a whole batch went out as a single request and could still be rejected. Such a rejection — or any other bulk error the cluster answers without a response body — was silently ignored, leaving objects, users, groups or pools missing from the index with no log line and no event; it now produces an *Index error* event per affected document and a single log line summarising the failed batch. Empty bulk requests, which the cluster answered with a logged *request body is required* error, are no longer sent. The `maxMem` setting is documented in [`fylr.example.yml`](https://docs.fylr.io/for-system-administrators/configuration/fylr.example.yml). \[e62eb9817] \[d82a0803d]
* **Logout on web-only instances**: On an instance running without a database (web-only mode), every logout caused a recovered panic in the server log and the token-revocation request was answered with an error. The `/api/oauth2/revoke` endpoint now answers *200 OK* on such an instance — it has issued no tokens, so any token is unknown, for which RFC 7009 prescribes a success response. Requests carrying an access token to a database-less instance no longer cause the same recovered panic either; they are answered as unauthenticated. \[015d5348d]
* **Login-flow page polish**: The server-rendered login-flow pages fix several small blemishes: the password show/hide toggle sits inside the input field — matching the in-app password fields — instead of beside it, the set-password page no longer shows a duplicate reveal-password button, input fields can no longer be resized vertically in Firefox, and over-long placeholder texts are truncated. The *go to research* link is only offered when guest access is enabled — matching the login page — and the error and not-found pages always offer a *go to login* link, so no page of the flow is left without an action. \[f242e9630] \[f09b8c4a0]
* **`/inspect` JSON tree view**: Empty arrays in the JSON tree views of the `/inspect` pages — for example `eas.rput.blockedHosts` on `/inspect/config/` — are now shown as `[]` instead of `{}`. See [the `/inspect` console](https://docs.fylr.io/for-system-administrators/inspect#html-dashboards-or-json). \[aa39bb454]
* **Sorting a list by a fulltext-only field**: A field whose mask enables only the fulltext search (the *fulltext* checkbox, not *expert*) can now be sorted on. The frontend already offered such fields in the sort manager, but the server indexed no sort value for them, so sorting a list by such a field had no effect on the order. These fields are now indexed like any other searchable field. Existing instances must be re-indexed for sorting by a fulltext-only field to take effect on records saved before the update. While that re-index is still running, sorting a list by such a field shows a hint that it takes effect once re-indexing completes. \[19afc7d0f] \[b4a0a9e132]
* **IIIF viewer in `/inspect` not shown in Firefox**: The IIIF viewer linked from a file's `/inspect` page (*Open in IIIF Viewer*) showed no image in Firefox — the zoomable map collapsed to zero height — while it worked in Chrome. The `/inspect` pages now declare the HTML5 doctype and the viewer uses a standard full-height layout, so the image fills the window in both browsers. See [Files](https://docs.fylr.io/for-system-administrators/inspect/files). \[28e2d4c32]
* **`/inspect` Technical Info hidden by collapsible sections**: On an `/inspect` page whose last collapsible section runs to the end of the page — a file's metadata groups, an object's file and CSV sections — the *Technical Info* footer was folded into that section and hidden along with it, even on page load. The footer is now kept out of the collapsible sections. See [the `/inspect` console](https://docs.fylr.io/for-system-administrators/inspect). \[1258d4842]
* **Linked-object suggestions that lead to no results**: The search input's suggestion list could offer linked objects that no record of the searched objecttype links to. Picking such a suggestion filters the list by that link and returns nothing, so the suggestion was a dead end. `/api/v1/suggest` now accepts `linked_objects_referenced_by_objecttypes`: when set, linked-object suggestions are restricted to objects at least one record of these objecttypes links to — through the direct, nested or reverse link the frontend filters on click — so every suggestion yields results; the list is filled by paging through the text matches in relevance order. Without the parameter every text match is returned as before. The web frontend passes the searched objecttypes, and clicking a suggestion that is a hierarchy ancestor of a linked object now filters the whole subtree, matching how such objects are suggested. See [`/api/v1/suggest`](https://docs.fylr.io/for-developers/api/endpoints/api-suggest). \[175128a09] \[a888157e8]
* **SAML/LDAP login for users archived by the inactivity janitor**: A user signing in through SAML or LDAP was locked out if the inactivity janitor (`system.janitor.users.archive_nonactive_after_days`) had archived the account for being idle. The federated authentication itself succeeded, but the archived account was then refused and the login looped back to the login page, while the login event was still recorded as a success — so the cause was invisible in the event log. A successful SAML/LDAP login now reactivates the account: it clears the archived flag and refreshes `last_seen_at` (which also keeps the janitor from immediately re-archiving it), and logs the reactivation. Disabled and not-yet-valid accounts are still refused. See [Services → Janitor](https://docs.fylr.io/for-administrators/readme/services#archive-inactive-users-after-n-days). \[a742f4353]
* **SAML/LDAP login collision hidden behind a raw database error**: When a federated login could not find its user by the configured lookup column and created a new one, but the account already existed under a different unique key — for example an SSO account whose stored reference no longer matched its email — the insert collided on the unique email (or login) and the login was refused with only a `duplicate key value violates unique constraint` line in the event log. The event now names the offending key and the mapped reference and email, so an administrator can find and reconcile the duplicate account. The login still fails and the user still sees the generic login message. \[b9e5c7d1a]
* **Wrong permissions after switching an objecttype from polyhierarchy to hierarchy**: Switching an objecttype from polyhierarchy to single hierarchy did not remove the extra parents objects had acquired while polyhierarchical — a reindex only queues search jobs, and an object's stored parent links are rewritten only when it is re-saved. Objects therefore kept all their old parents: they stayed listed under categories they no longer belonged to, showed phantom expand carets, and had their permissions computed from an arbitrary wrong parent's subtree, producing *missing permission* errors even for the root user. Only each object's first (main) parent is now honoured once the objecttype is no longer polyhierarchical, without touching the stored rows. The change is therefore reversible: switching the objecttype back to polyhierarchy re-exposes the additional parents. See [Hierarchies and polyhierarchies](https://docs.fylr.io/for-developers/concepts/hierarchies-and-polyhierarchies). \[593cae1f2]
* **`/inspect` object page showed no parents under a polyhierarchy datamodel**: The `/inspect` object page can render an object against any datamodel version through its datamodel selector. Its parent sections, however, keyed off the current objecttype — objecttypes are not versioned per datamodel — so viewing an object against a datamodel in which its objecttype is polyhierarchical (for example an object as it looked before a polyhierarchy-to-hierarchy switch) showed no parents at all. The page now reads the hierarchy mode from the datamodel it renders against, and the generic *Parents* list — which had always been empty for polyhierarchical objects, even in the current datamodel — now lists their parents. The datamodel, mask and linked-version selection also carries across the object and render views. See [Objects](https://docs.fylr.io/for-system-administrators/inspect/objects). \[cb4d51270]
* **Saving without `_pool` on a pool-managed objecttype no longer panics**: A `POST /api/v1/db/<objecttype>` that omitted `_pool` for an objecttype with pool management crashed the save in a recovered panic, which reached the client as an unspecific *ServerGeneric* error and left a stacktrace in the server log. The save is now rejected up front with the proper *ObjecttypeHasPoolManagement* validation error; a `dry_run` of such a save, which used to panic as well, returns the same error. See [Pools](https://docs.fylr.io/for-administrators/permissions/pools). \[10201f371]
* **Preset "Created" date fixed for older presets**: In the preset manager, the *Created* field of presets created before fylr began recording it showed a zero date ("AD 1-1-1 …"). The upgrade backfills the missing timestamp — from the preset's last-modified time where available, otherwise the upgrade time — so the field shows a real date. \[48bb0fd7b]
* **No more repeated "ldap: initialized config" log lines and LDAP re-dials**: On systems where something saved the base config periodically — a plugin writing state into its config block, for example — fylr logged `ldap: initialized config` at INFO every few seconds although the LDAP/SAML configuration never changed, and, with LDAP configured, re-dialed the LDAP server on every save and cache reload. The change detection now hashes the compiled configuration instead of its transport representation and keeps a cache entry per distinct configuration, including failed dial attempts (a broken LDAP server is reported from cache instead of being dialed again until the configuration changes). An unconfigured LDAP/SAML module now logs at DEBUG only, so systems without LDAP see no such lines at all. \[75e891ed9]
* **Uploads into upload collections work with a change-owner workflow**: When a workflow running on insert changes the owner of new records, uploading into an upload collection failed with a server error (`Unable to set _owner … when using "collection"`). The workflow-set owner is now accepted and takes precedence over the owner the upload collection would assign. \[def1ba39f]

## Plugins

### Deprecated CMS integration plugins

These hybrid plugins for easydb5 and fylr are deprecated:

* `easydb-wordpress-plugin`
* `easydb-typo3-plugin`
* `easydb-drupal-plugin`

Make sure to replace them with their corresponding fylr-only version:

* `fylr-plugin-wordpress`
* `fylr-plugin-typo3`
* `fylr-plugin-drupal`

### fylr-plugin-ai-metadata

Version `v0.10.9` makes the `url` setting introduced with `v0.10.2` work across the whole range of OpenAI-compatible providers. Besides the default OpenAI API and proxies such as [LiteLLM](https://www.litellm.ai/), the plugin now also connects to the [Azure OpenAI Service](https://azure.microsoft.com/en-us/products/ai-foundry/models/openai/), to Anthropic's Claude models (through their OpenAI-compatible endpoint), and to local models served by [Ollama](https://ollama.com/), so metadata can be generated fully on-premise. The plugin first requests a structured tool call and, on endpoints whose model cannot answer one — typical for local vision models — retries with the answer constrained to a JSON schema; a model that returns nothing usable is now reported with an actionable error message instead of failing cryptically. The list of selectable OpenAI models has been refreshed to the current generation (`gpt-5.5`, `gpt-5.4-mini`, `gpt-5.4-nano`).

## Frontend

### New

* **Custom rendition presets**: The presets configured in the base config appear as on-demand options in the download dialog and the export version select (image class), grouped under their own heading and showing the configured format. A dedicated editor for the *Custom Rendition Presets* block reuses the download dialog's custom-version option fields with the same conditional inputs, trims whitespace from the preset name as it is typed, and shows a settings summary per preset; Apply is disabled for invalid presets (no display name or no effective setting). See [Custom Version Presets](https://docs.fylr.io/for-administrators/readme/file-worker/custom-version-presets#configuration). (aad526dfc) (b66c3ddf2)
* **Download manager — custom renditions on the fly**: The download dialog lets the user decide exactly how each image comes out instead of only picking from the fixed versions. One or more custom renditions (format, size mode, colorspace, DPI, color profile, watermark) can be added on the fly and downloaded right away, side by side with the admin's custom rendition presets (those stay read-only but remain croppable). Every rendition can be framed in the new crop editor (see below), and across a multi-image selection each file keeps its own crop and rotation, or can be excluded from a given rendition — the export request is resolved per asset so every file is produced exactly as it was framed. Linked objects referenced inside a nested table are now gathered for download as well. (700ad3879) (1d2071cfb)
* **New crop editor**: A reusable crop editor, opened per rendition, that frames each image individually. Draw and resize the crop and pick or lock an aspect ratio — a preset ratio or an exact width × height. A new *Straighten* tool levels a tilted image: arm it and drag a line along an edge that should be horizontal or vertical, and the image rotates so that edge becomes level; alongside it a degree dial fine-tunes the angle by hand, so the rotation is no longer limited to the existing 90° steps. Large images load high-resolution tiles to stay sharp at any zoom, the editor warns when a rendition would upscale the source, and while a large preview loads it shows a blurhash placeholder and a short-delayed spinner. See [Custom renditions and crop tool](https://docs.fylr.io/for-users/additional-features/custom-renditions-and-crop-tool). (700ad3879) (b47ac7985)
* **Consolidate duplicate linked objects**: A new `consolidate_objects` background-task module and an editor shortcut for merging duplicates of linked objects such as keywords, actors or places. In the task manager the sources and target are picked with the editor's linked-object popover — sources multi-select, the target restricted to the sources' objecttype and disabled until sources are picked — and the picker offers every objecttype used as a linked object anywhere in the datamodel, remembers the picked records when a saved task is reopened (in their saved objecttype, showing the now-trashed sources of an executed run), and can be prefilled from a hitlist selection. The object editor gains a *Replace by another record…* item in its 3-dot menu: it counts the records referencing the edited object, confirms via the task module's own rights-aware preview, runs the consolidation immediately (source = the edited object, target = the picked one, delete the source) and reloads the editor in place on the surviving target. The item appears only for a saved record whose objecttype is a link target, and only for users who may create tasks and hold the delete right. (78a8b33c9) (581f4eebe) (ba9b28c54)
* **Two-factor authentication**: The base-config editor gains a *Two-factor authentication* section (enable + method selection), and the group editor gains a "2FA required" toggle. See [Enabling Two-Factor Authentication](https://docs.fylr.io/help/tutorials/for-administrators/enabling-two-factor-authentication). (e3d078528)
* **Two-factor authentication — authenticator app (TOTP) and passkey**: The user editor shows per-method enrollment status (authenticator app / passkey) with an admin reset link, and users can see and reset their own enrollments in their settings. The login-time method picker, enrollment and verification are server-rendered pages. See [Enabling Two-Factor Authentication](https://docs.fylr.io/help/tutorials/for-administrators/enabling-two-factor-authentication). (ce74ed4ce)
* **Passwordless sign-in with a passkey**: The login form offers "Sign in with passkey" — the passkey is suggested in the login-field autofill (conditional UI) or via the explicit link (which also covers signing in from a phone). No username or password needed. See [Enabling Two-Factor Authentication](https://docs.fylr.io/help/tutorials/for-administrators/enabling-two-factor-authentication). (34e459241)
* **External share URL per collection preset**: The collection preset editor gains an *External URL for sharing links* field, and the share-link buttons of share entries using such a preset copy/open the configured portal URL instead of the fylr link. See [Presets](https://docs.fylr.io/for-administrators/permissions/presets). (b68667c26)

### Improved

* **License manager**: The license info shows the grace-period end date next to the valid-until date, and lists the full set of license capabilities (including two-factor) instead of only the ones a license happens to carry. See [License management](https://docs.fylr.io/for-administrators/readme/license-management). (9b0c96093) (5bdc1d66f)
* **Password fields hidden for non-password user types**: Email, LDAP and SSO users have no fylr-managed password, so the user editor's password tab now hides the password input, the "set new password at next login" flag and the login-validity window for them (leaving only "login disabled") and strips those fields on save, and the session tray's "change password" item is hidden as well. The system root user keeps the fields. (a510ae73d) (5019a15da)
* **Sign-out on a broadcast logout**: The web app listens for `USER_LOGOUT` on the event stream: a "user"-scoped logout signs the current user out on every device, a "browser"-scoped one only in the browser whose token was actually revoked. Older servers that do not broadcast the event are unaffected. (cb43a2611) (436c8828b)
* **Faster, less blank startup**: A spinner and a subtle fylr logo are now shown inline in the served HTML before the app JS loads, so a slow boot no longer shows a blank screen, and the welcome splash appears as soon as the theme is ready; the connector loading modal waits for the theme so it is styled correctly. (05feacfd0) (fe9568d10)
* **Result grid rendering performance**: Rendering large result pages no longer freezes the main thread — already-loaded standard-view cards render in time-bounded batches that yield between them, the uniform tile size is cached instead of forcing a reflow per card, view-size changes are coalesced and drained in batches (skipping far-off-screen cards), the scroll-load debounce is lowered so fresh regions fill in quickly, and the loading placeholder skeleton is back. A set of lower-level optimizations cuts repeated work across mask and result rendering. (9c92c648e) (2988dfabc)
* **Lazy loading of heavy editor lookups**: The new-record dialog loads pool selectors for linked fields only when "Select pools for linked records" is enabled, and dedups the remaining `db_info` requests (one per linked table, reusing the main record's for self-links); the reverse-linked tool button no longer makes an expensive eager request just to decide whether to show itself. (814e0950c) (0113a08b3)
* **Background task manager**: The task log gets a "show more" button to page beyond the first 100 entries and live-loads while its popover is open (the polling that used to break the layout and orphan the popover is fixed), the task list gains an inspect button to open a task in `/inspect`, the module description is shown under the module select, and the email-recipient form now sends the right format so email users are persisted with the task. (404982748) (df37ad301) (cc7de04e7) (2a682f70c) (ace3078b1)
* **Plugin manager**: Saving is disabled until a zip upload finishes and the upload modal stays open on a save error, the build-info section is enhanced, and a commit URL is built automatically from the pattern declared in the plugin manifest. (3fb22e04e) (5da1f2f4b) (1c37d9726)
* **Nested table pagination**: The table-mode view of a nested column is now paginated. (42ec45eb2)
* **Fulltext file search behind its right**: The "search in file fulltext" checkbox is now gated by the `allow_fulltext_file_search` right, not merely the presence of asset fields, and the search filter itself is gated too so a stale saved preference cannot search asset fulltext without permission. (340cd0f70)
* **Background tasks skip connector data**: Connector instances are not supported in background tasks — the create-task tool is hidden when the selection contains connector objects, connector tags are dropped from the set/unset-tags selector, and the base-config and task search popovers are local-only. (8bdf82b61)
* **Custom XMP tags in export metadata mappings**: The export metadata-mapping editor now accepts a custom `XMP-<group>:<tag>` path typed into the tag selector — previously only import mappings accepted custom paths — restricted to XMP because only XMP is extensible on the server. A deselectable "Type" radio replaces the plain list checkbox so an export tag is a list, localized (loca) or a single value, never an impossible combination; invalid tag input is flagged and blocks saving, the tag's real capability drives which list/loca options are offered, custom tags are stored with a capitalized local name, and each tag's list / loca / printable markers render in the mapping list view. (814954362) (04667a1af) (72df3ed74) (742800b7a) (f73dd28d3) (af0789af1) (1fb9a05d7) (04f48a9cf) (665c87a97) (ea65a32f6)
* **DateTime — BC dates**: Year-month and date-time inputs accept BC dates (for example `-06.2026` or `11.02.-2026 10:18`), and BC-date conversion is corrected. (via CUI update)
* **Metadata mapping — Skip Empty Mapping**: The option is offered only for export (merge) mappings. (7cd9b02cd)

### Fixed

* **Metadata mapping: custom fields no longer share an `_id`**: Two custom fields could end up with the same `_id`, so they shared one drag-wrapper array and editing or removing one leaked into the other and got duplicated on save. New custom fields now get a unique `_id`, and a mapping loaded with a duplicate custom `_id` heals itself on the next save. (0568a9837)
* **Linked-object suggestions in the main search**: Clicking a suggestion now builds the filter from the actual (direct, nested or reverse) link fields instead of a flat objecttype filter, so objecttypes linked only indirectly match instead of returning nothing; the request restricts suggestions to objects some searched-objecttype record actually links to (no more dead-end suggestions), a suggestion that is a hierarchy ancestor filters the whole subtree, the over-eager "skip same objecttype" logic was removed, and the linked-object filters configured in the objecttype manager are applied to the suggestion dropdown. (4fdf79361) (b2189c966) (467cf0f92) (74e841e71) (7f6980415)
* **Required-field message for link, nested and asset fields**: Mandatory linked, nested and asset fields left empty now show the specific "… is required" message instead of the generic "A problem with field …" text. (89a316fec)
* **Connector search and facets**: A remote fylr date-range facet or search no longer doubles the `.range` suffix (which produced `.range.range` and empty results), and objecttypes are no longer missing from the main search-type selector when the connector finishes loading after the initial search. (03327ed84) (6e313a802)
* **Facet popover search with special characters**: The popover search escapes the Lucene-only operators (`& ~ < > @ # "`) that the JS escaping leaves bare, so filtering for terms containing them returns results. (d2ac01929)
* **Expert search with no objecttype selected**: Fixed the expert-search state when no objecttype is selected in the search type selector. (80ec07815)
* **Mask editor default display name**: A new non-default mask now gets the correct default display name. (9332cb90e) (ffd620014)
* **Multilang columns in the group editor**: The save button is no longer disabled when the append mode is selected. (13d17fafd)
* **Geo-JSON multipolygons**: Multipolygon geometries are supported again on `geo_json` fields. (d24710282)
* **Detail hierarchy list**: Closing and reopening a root node no longer expands all of its child nodes. (709d5a87b)
* **Base config `show_script`**: The option now also shows and hides the "tag filter" and "set/unset tags" fields, which previously stayed visible regardless. (f3b4bb049)
* **Firefox autocomplete on the collection filter**: The collection filter input is typed as a search field so Firefox no longer treats it as a username autocomplete. (dc06478f1)
* **Webhook workflow name field**: The empty "Name" value no longer shows "Missing: Undefined". (a3e0a04ad)
* **Text-field overflow**: Added an overflow check for regular text fields. (f0705fe74)
* **Asset browser auto-scroll**: Showing an asset auto-scrolls to the asset browser when the browser is not pinned. (e3d9a2f78)

### Styling

Small CSS refinements from the design team:

* List-view tree handles are offset with CSS variables so their indentation adapts to the parent container or component. (CUI 8e75ea8a)
* Fixed a CSS-variable typo that set the min-width of the main-right column when it holds the hierarchy browser. (795bd8e01)

### Docker images

```
docker.fylr.io/fylr/fylr:v6.34.0
docker.fylr.io/fylr/fylr-server:v6.34.0
```
