---
url: /changelog.md
---

# Changelog

What changed in each release of Trek, newest first, with upgrade notes when an app needs to act. **Unreleased** lists what's merged but not yet published.

## \[Unreleased]

### Added

* `rails g trek:public_accounts Member --invite-only`, for a private site where only the accounts the app created sign in: a code is emailed only to an email that has an account, with the same answer for every email, and signing in never creates an account. Which accounts may sign in is `Member.for_sign_in(email)`, to narrow in the app (e.g. `active.find_by(email:)`). Codes only. See [Public accounts](https://trek.etamin.studio/reference/generators/public-accounts#invite-only).
* Generated apps check their npm packages for known vulnerabilities, as bundler-audit does their gems: `make scan` and a new `npm-audit` CI job run `yarn npm audit`, failing on any advisory or deprecation in the packages the app bundles (`dependencies`, not `devDependencies`). See [Vulnerable dependencies](https://trek.etamin.studio/reference/generators/install#vulnerable-dependencies) for how to resolve one. **Upgrade note:** move the PostCSS packages to `devDependencies` first (see below), then add the `npm-audit` job next to `bundler-audit` in `.gitlab-ci.yml`, and `yarn npm audit --recursive --environment production` to `make scan` (see `lib/generators/trek/templates/.gitlab-ci.yml`).
* The [Controllers](https://trek.etamin.studio/reference/controllers#localization) reference documents `Trek::Localization`: where a request's locale comes from, and how to add a source by overriding `locale_for_request`.
* `bin/rails trek:security:check` checks a deployed app's configuration against Trek's recommended settings: HTTPS and HSTS, the Content Security Policy, the cache store, the proxies and `ADMIN_ALLOWED_IPS`, the audit log and the scheduled purges, the admin users (password only, dormant, password expiry) and Active Record Encryption's keys. It prints each check with ✓, •, ⚠ or ✗ and a link to the docs, in a dated report to keep as evidence, and exits with status 1 when a check fails. See [Self-check](https://trek.etamin.studio/security#self-check).
* A **Settings › Security** page in the admin, for admins only, shows the same checks, dated, and checks the request that opened it too: that it reached the app over HTTPS, and that the IP address Rails sees is the one a CDN forwards. See [In the admin](https://trek.etamin.studio/security#in-the-admin). **Upgrade note:** run `rails g trek:install:admin:security_check`, and add the page to the Settings section of `app/views/layouts/admin.html.slim`, after the background jobs: `s.with_item(text: t("admin.security_checks.show.title"), href: %i[admin security_check], allowed_to: Current.user.admin?)`. In apps with Trek's host specs, copy `spec/requests/admin/security_checks_spec.rb` from Trek's templates (`lib/generators/trek/templates/spec/requests/admin/`).
* A [Personal data](https://trek.etamin.studio/personal-data) page lists the personal data Trek's generators create, table by table: why it's kept, for how long, and what deletes it; what deleting a user or an account leaves; and the third parties a default app sends some of it to. For an app's record of processing activities, and security questionnaires.
* The [Deploying to Scalingo](https://trek.etamin.studio/deploy) guide covers what a first deploy still needed: keeping Solid Cache in the app's single database, the environment variables the generated code reads, domains and their certificates, deploying from GitLab CI, and `> /dev/null` on `env-set` so secrets aren't printed.
* `Trek::Auditable`'s `audit_read_actions` lists actions that only read although they're not a GET, such as the chips hotwire_combobox requests with a POST, to keep them out of the audit log. See [Customizing](https://trek.etamin.studio/reference/generators/audit-log#customizing). **Upgrade note:** an app overriding `read_only_request?` for this can set `self.audit_read_actions = %i[chips]` instead.
* `Trek::PurgeExpiredSignInCodesJob` deletes a public account model's expired sign-in codes, which each hold an email: only a new code deleted them, so the last ones requested were kept for good. `rails g trek:public_accounts` schedules it every night in GoodJob's cron, with codes (3:45am). See [Public accounts](https://trek.etamin.studio/reference/generators/public-accounts#sessions). **Upgrade note:** in apps signing in with codes, add to `config.good_job.cron`, for each public account model (here `Member`): `purge_expired_member_sign_in_codes: { cron: "45 3 * * *", class: "Trek::PurgeExpiredSignInCodesJob", args: ["Member::SignInCode"] }`.
* `bin/rails trek:attachments:strip_image_metadata` strips the metadata of the images stored before `ImageUploader` stripped it on upload (`DRY_RUN=1` only lists them). Each image is re-encoded and replaced in place, so every record, page version and content editor block pointing to it keeps working. See [Image metadata](https://trek.etamin.studio/reference/uploaders#image-metadata). **Upgrade note:** once the app's `ImageUploader` strips metadata (see below), back up the bucket, or enable its versioning, then run the task once in production.

### Changed

* A paragraph holding only line breaks (Shift+Enter) counts as empty, as a paragraph with no content does: it renders nothing (`Trek::Content::Paragraph.empty?`) instead of a blank space, a list item holding only such a paragraph is skipped, and content is cleaned of them before it is saved (`Trek::Contentable`). A line break between words stays. **Upgrade note:** existing content is cleaned the next time it is saved; apps that pruned these paragraphs before saving can drop that code.
* The `trek_known_users` cookie, which recognizes a browser that already signed in as a user so that only a new one emails them, is encrypted and kept 13 months after the last sign-in, instead of signed and kept 20 years: someone using the browser can no longer read who else signed in from it. Browsers recognized by the former signed cookie still are. **Upgrade note:** apps have their own copy of `app/controllers/concerns/trek/authentication.rb`: replace its `signed_in_from_this_browser?` and `remember_sign_in_from_this_browser`, and add `known_user_ids`, from Trek's (`lib/generators/trek/templates/controllers/concerns/trek/authentication.rb`). In apps with Trek's host specs, copy `spec/requests/admin/sign_in_notices_spec.rb` from Trek's templates.
* **Security:** the `ImageUploader` `trek:install:uploaders` copies strips an uploaded image's metadata (EXIF, with the GPS location and the camera's serial number, XMP, IPTC), which was published with the file. It re-encodes the image in its own format after applying its orientation (a JPEG or a WebP at quality 90, every frame of an animated WebP), and keeps its color profile with libvips 8.15 or later. It also accepts WebP images, besides JPEG and PNG. Thumbnails and other sizes are on by default: the derivation endpoint is enabled and mounted at `/derivations/image`, and makes every size a WebP at quality 80, without metadata. An image's sizes are deleted with it. See [Images: types and sizes](https://trek.etamin.studio/reference/uploaders#images-types-and-sizes). **Upgrade note:** apps have their own copy of `app/uploaders/image_uploader.rb`: re-run `rails g trek:install:uploaders`, which offers to replace it with Trek's, then bring back the app's own changes, and mount the endpoint in `config/routes.rb` if it's commented out: `mount ImageUploader.derivation_endpoint => "derivations/image"`. Images uploaded before keep their metadata until they're uploaded again, but their sizes don't.
* The generators schedule Trek's purges every night in GoodJob's cron, which they left to each app, and most never did: expired sessions and old audit log rows were kept for good. `rails g trek:install` schedules `Trek::PurgeExpiredSessionsJob` (the admin's sessions, 3am), `trek:audit_log` adds `Trek::AuditLog::PurgeJob` (4am), and `trek:public_accounts` the purge of its accounts' sessions (3:30am). Without GoodJob's initializer, the generators say what to schedule instead. See [Scheduled jobs](https://trek.etamin.studio/background-jobs#scheduled-jobs-cron). **Upgrade note:** in `config/initializers/good_job.rb`, add to `config.good_job.cron` the entries the app is missing: `purge_expired_sessions: { cron: "0 3 * * *", class: "Trek::PurgeExpiredSessionsJob" }`, `purge_audit_logs: { cron: "0 4 * * *", class: "Trek::AuditLog::PurgeJob" }` with the audit log, and `purge_expired_member_sessions: { cron: "30 3 * * *", class: "Trek::PurgeExpiredSessionsJob", args: ["Member::Session"] }` for each public account model (here `Member`). An app on another Active Job backend schedules the same jobs with it.
* The admin's image and sound fields upload with Uppy 6, up from Uppy 3, which brought nanoid 4 and its four advisories (three high) that `yarn npm audit` reported in every app (no nanoid 4 fixes them). The fields no longer bundle Uppy's stylesheet: it only styles Uppy's own interface, which Trek doesn't use.
* `rails g trek:install:postcss` adds the PostCSS packages to `devDependencies`, as the other build and lint tools, rather than to `dependencies`, and no longer adds `postcss-inline-svg`, which Trek's stylesheets don't use any more: it brought a css-what with a high advisory. **Upgrade note:** in `package.json`, move `postcss`, `postcss-flexbugs-fixes`, `postcss-import`, `postcss-modules` and `postcss-preset-env` to `devDependencies`, and run `yarn install`. Unless the app's own CSS uses `@svg-load` or `svg-load()`, remove `postcss-inline-svg` from `package.json` and its `require("postcss-inline-svg")(…)` from `postcss.config.js`.
* `rails g trek:scalingo` and the [Deploying to Scalingo](https://trek.etamin.studio/deploy) guide pin [trek-buildpack](https://github.com/etaminstudio/trek-buildpack) 0.1.4, up from 0.1.0. Since 0.1.0, the buildpack installs the Bundler version of `Gemfile.lock` (apps whose lockfile asked for a newer one died on start with `Could not find 'bundler'`) and forces its install past the default gem, and it now preloads jemalloc: the `libjemalloc2` of the `Aptfile` was installed but never used, as rv's Rubies aren't built with it. **Upgrade note:** on each Scalingo app, run `scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.4.tar.gz`; it applies from the next deploy.
* Trek no longer requires `json` < 3, which made `bundle add trek` on a fresh Rails 8.1.4 app silently install Trek 2.2.0. json 3 needs Rails 8.1.4 or later: with an older Rails, the app now fails at boot saying so. **Upgrade note:** apps on Rails 8.0 or 8.1.0 to 8.1.3.1 keep the json 2 in their `Gemfile.lock`; add `gem "json", "< 3"` to the Gemfile so a `bundle update` doesn't bring json 3.
* `rails g trek:scalingo` comments out the `kamal` gem `rails new` adds, since a Scalingo app doesn't deploy with it, and runs `bundle install`: Kamal's SSH dependencies (`net-ssh`, `net-scp`, `net-sftp`, `sshkit`, `bcrypt_pbkdf`, `ed25519`) leave the bundle, and with them the advisories that kept failing bundler-audit. `config/deploy.yml`, `.kamal/secrets` and `bin/kamal` stay in place. **Upgrade note:** apps already on Scalingo can re-run `rails g trek:scalingo`, or comment out `gem "kamal"` by hand and run `bundle install`.
* `bin/rails g trek:scaffold` gives the new model a `to_s` returning its first string attribute (`title.presence || super`), which the admin shows records by: they were listed as `#<Book:0x…>` until its commented-out `to_s` was filled in. Without a string attribute, it stays a TODO. The [Getting started](https://trek.etamin.studio/getting-started#scaffold-your-first-resource) guide and the [Scaffold](https://trek.etamin.studio/reference/generators/scaffold) page now say to run the migration, and list the model's TODOs.
* The docs, the generators' messages and the security checks write commands as `bin/rails …`, the app's own Rails, instead of a mix of `rails g …` and `bin/rails …`.

### Removed

* DeepL: the scaffold generator no longer translates the new locale files with `DEEPL_AUTH_KEY`, Trek no longer depends on `deepl-rb`, and `rails g trek:install:translations` no longer adds it, nor a DeepL `translation:` section to `config/i18n-tasks.yml`. **Upgrade note:** apps can remove `gem "deepl-rb"` from their Gemfile, the `translation:` section from `config/i18n-tasks.yml`, and `DEEPL_AUTH_KEY` from their environment; an app that still translates with `i18n-tasks translate-missing --backend=deepl` keeps the gem and the section.

### Fixed

* Password managers such as Bitwarden didn't offer to save the password after signing in to the admin, until the page was reloaded: Turbo replaced the page's body, and the prompt they had added to it. The forms that take a password (the admin's sign-in and password reset, and `trek:public_accounts`' sign-in, sign-up, password reset and password change) now submit without Turbo, and load the next page in full. **Upgrade note:** add `data: { turbo: false }` to the `form_with` of `app/views/admin/user_sessions/new.html.slim` and `app/views/admin/user_password_resets/edit.html.slim`, and, in apps that ran `trek:public_accounts`, of its `sessions/new` (the password form), `registrations/new`, `password_resets/edit` and `passwords/edit` views.
* A `locale` param that isn't one of the app's locales (`?locale=xx`) made every controller including `Trek::Localization` answer with a 500 (`I18n::InvalidLocale`), the admin's included. It's now ignored, and the locale comes from the user or the browser as without it.
* The password strength meter no longer makes the form grow when its label appears, as a password is typed.
* An app that resolved Prism 2, as a fresh one has since rubocop-ast 1.51.0, failed at boot wherever it eager loads, in production and on CI (`uninitialized constant Prism::MissingNode`): refract, which `prosemirror` brings through Phlex, uses a node Prism 2 removed. Trek now requires Prism < 2. **Upgrade note:** if the app's `Gemfile.lock` has Prism 2 (`bundle info prism`), `bundle update trek` brings it back to 1.x.
* The arrows around a larger image in the content editor didn't show in host apps: their build looked for the icons in the app's own `app/assets/icons`, and left an invalid rule. The icons are now inlined in the stylesheet.
* The generated `.gitlab-ci.yml` ran on the `ci-ruby/node-24` image whatever Node the app pins: `rails g trek:install:ci` now picks the image of the major in `.node-version` (20, 22, 24 or 26), and falls back to Node 24, with a warning, without the file or for a major `ci-ruby` has no image for. **Upgrade note:** apps pinning another Node than 24 can set `node-<major>` in the `image:` of `.gitlab-ci.yml`.
* `rails g trek:public_accounts` added the account's attributes to `Current` without indenting them. **Upgrade note:** apps that ran it on Trek 2.6.0 can indent the three lines it added to `app/models/current.rb`.
* The sign-in concern `rails g trek:public_accounts` generates for a website (`app/controllers/concerns/<accounts>/authentication.rb`) failed the generated CI's Brakeman job (HTTP verb confusion): it now remembers the page to return to for `HEAD` requests too, as for `GET`. **Upgrade note:** apps that ran it on Trek 2.6.0 can replace `if request.get?` with `if request.get? || request.head?` in `require_<account>`.
* The Getting started guide and the README installed Yarn with `corepack enable`, which Node 25 and later no longer ship (`corepack: command not found`). They now install corepack first, with `npm install --global corepack`, or Yarn with mise.
* The admin's edit form of a page made of sections (`sectioned`) but not versioned failed with a `NameError` (`vf`). **Upgrade note:** in `app/views/admin/pages/_form.html.slim`, replace `vf.object.sections_instances` with `@object.sections_instances` in the branch for pages without versions (the second one).
* Docs that no longer matched the code: the Fragmentable configuration, the scaffold's form fields and route, icon keys (`trek/search`), `Trek::Search`, Sectionable, Orderable's `ordered`, the SEO fallbacks, the cryptography table and self-check levels in Security, CSP reports in Personal data, `S3_BASE_URL`, the admin's per-IP sign-in limit, the installer's linters, mailers and specs, `media-src`, `FEATURE_STORAGE`, `TREK_LOCALES`, and the json pin Rails before 8.1.4 needs.
* Deleting a public account left the sign-in codes waiting for its email, which hold that email: they're keyed by email, not linked to the account, so only their expiry and the nightly purge deleted them. `rails g trek:public_accounts` (with codes) now deletes them with the account. **Upgrade note:** in apps signing in with codes, add to the account model (here `Member`), after `has_many :sign_in_codes`: `after_destroy { Member::SignInCode.where(email:, purpose: "sign_in").delete_all }`.
* Small inaccuracies in the reference docs: `GateComponent` and the toast component's name, what `f.group` renders, the formatter's narrow no-break spaces, `User#last_activity_at`, `Current`'s attributes, `page.versions.current`, and the generators list, which now has `trek:scalingo` and `trek:upgrade:v2`.
* `bin/rails g trek:scaffold` left the new model's entry in `Admin::Dashboard::MODELS_IN_MENU` unindented.

## \[2.6.0] - 2026-10-28

### Added

* The content editor's link, image and prompt panels give hints under their fields: what to write in an image's alternative text, credit and caption, that a link goes to a page or a URL, and what a prompt's title is for. **Upgrade note:** you can now drop your own `helpers.hint.trek/content_editor/*` translations.
* The content editor's link panel asks for the link's title, shown when hovering it, as its prompt panel does: Tiptap 3 keeps it on the link, where Tiptap 2 dropped it, and the site renders it.
* CSP violations reported to AppSignal (`Trek::CspReportsController`) tell what was blocked: they're tagged with the browser's user agent, where it happened (the source file and line) and, for an inline script, its first 40 characters, which the generated policy now asks for with `'report-sample'`. The log line gives the source and sample too. Tags are cut to 255 characters, as AppSignal silently drops longer ones. **Upgrade note:** add `:report_sample` to `policy.script_src` in `config/initializers/content_security_policy.rb` to get the samples.
* `rails g trek:install:good_job`, a new installer step (also runnable alone) making [GoodJob](https://github.com/bensheldon/good_job) the Active Job backend, as every Trek app was doing by hand: it swaps `solid_queue` for `good_job` in the Gemfile (Solid Cache stays), removes the Solid Queue setup `rails new` leaves behind (`config/queue.yml`, `config/recurring.yml`, `db/queue_schema.rb`, `bin/jobs`, the Puma plugin, the `queue:` database and Kamal's `SOLID_QUEUE_IN_PUMA`/`JOB_CONCURRENCY` variables), and configures the adapter (Active Job' test adapter in `config/environments/test.rb`, so specs can check what is enqueued), `:async` execution in production, the queues, cron and error reporting in `config/initializers/good_job.rb`. The dashboard is mounted at `/admin/good_job` with a menu entry in the admin, guarded by the Trek session: anyone who is not an administrator gets a 404 rather than a redirect. Two specs come with it (dashboard access per role, and the mail delivery job). Because GoodJob discards a failed job, every `deliver_later` email now goes through an installed `MailDeliveryJob` that retries five times with a growing delay, and `config/database.yml` reserves connections for the job threads on top of the web threads (`GOOD_JOB_MAX_THREADS`, which GoodJob defaults to `RAILS_MAX_THREADS`). See [Background jobs](https://trek.etamin.studio/background-jobs). **Upgrade note:** apps installed before this version can run `rails g trek:install:good_job` — it is idempotent and skips what is already in place — then add the dashboard entry to `app/views/layouts/admin.html.slim` (an item with `text: t("admin.jobs")`, `href: "/admin/good_job"`, `allowed_to: Current.user.admin?` in the settings section) and run the migration it generates. Apps already on GoodJob only need the Gemfile left alone and their `config/initializers/good_job.rb` compared with the generated one; apps still on Solid Queue must drop the `queue` database from their deploy target as well.
* Dashboard tiles (`Trek::Dashboard::TileComponent`) opening in a new tab (`target: "_blank"`) show an external link icon after their text, and a tile's color is the accent of what it holds (`data-accent-color`), which can use `var(--accent-*)`.
* Public accounts: `rails g trek:public_accounts Customer` gives the site's or the mobile app's users (customers, members…) a model of their own, apart from the admin's `User`, with its own table, sessions and sign-in, so nothing public can reach a staff account. They sign in either with a code emailed to them, which creates the account the first time, or with `--sign-in=password` with a password, the email confirmed and the password reset by a link; a website gets pages and a cookie, `--api` JSON endpoints and a JWT naming a session, so it can be revoked, and `--model-only` neither, for an app whose sign-in is its own. See [Public accounts](https://trek.etamin.studio/reference/generators/public-accounts).
* `Trek::Users::SignInLockout` sends its email through `sign_in_lockout_mailer` (`"AdminMailer"` by default), and `Trek::PurgeExpiredSessionsJob` takes the session model to purge (`User::Session` by default), for public accounts.

### Changed

* Trek requires the `prosemirror` gem 1.0 or later (was 0.7.1), and works with 1.1: rendering content (`formatted_content`) raised `ArgumentError` with it. `Trek::Content::Document#to_html` takes 1.1's `heading_ids: true`.
* `rails g trek:install` ends by listing the generators it doesn't run, to run as the app needs them: `trek:scaffold`, `trek:taxonomies`, `trek:audit_log`, `trek:public_accounts` and `trek:scalingo`.
* `trek:install:audit_log` and `trek:install:scalingo` are now `trek:audit_log` and `trek:scalingo`, as `trek:install` doesn't run them: `trek:install:*` is left to its own steps. The former names still work, with a deprecation warning, until Trek 3. **Upgrade note:** update the generator names in your docs and scripts.
* `trek:install:ruby` now pins **Ruby 4.0.9** (was 4.0.7), and the CI images (repo + generated `.gitlab-ci.yml` template) use the matching `4.0.9` tag. **Upgrade note:** existing apps can set `4.0.9` in `.ruby-version` and bump the `ci-ruby` image tag in `.gitlab-ci.yml`.
* Asking every user for a new password goes through its own controller, `Admin::PasswordRenewalsController#create` (`POST /admin/password_renewal`), instead of a `require_password_renewal` action of `Admin::UsersController`. **Upgrade note:** copy `app/controllers/admin/password_renewals_controller.rb` from Trek's templates (`lib/generators/trek/templates/controllers/admin/`), add `resource :password_renewal, only: :create` to the `admin` namespace of `config/routes.rb`, remove `post :require_password_renewal, on: :collection` from `resources :users` and the `require_password_renewal` action from `Admin::UsersController`, and point the button of `app/views/admin/users/index.html.slim` to `[:admin, :password_renewal]`.
* The new sign-in email (`AdminMailer#new_sign_in`) gives the IP address's host name next to it, as the active sessions do, looked up when the email is sent. **Upgrade note:** to get it, copy `new_sign_in` from Trek's `lib/generators/trek/templates/mailers/admin_mailer.rb`, and replace `@ip` with `@address` in `app/views/admin_mailer/new_sign_in.*`.
* Content is cleaned before it's saved (`Trek::Contentable`): empty paragraphs, empty list items and lists left empty are removed, as rendering already skipped them, so the stored JSON, `content_text` and searches no longer carry them. The document stays one the content editor opens, and node types or attributes the schema doesn't know are kept. The editor's JSON string is now stored as a document, which `Fragment.search` can search. **Upgrade note:** existing content is cleaned the next time it's saved; empty paragraphs authors used for spacing are removed for good. Apps that pruned empty paragraphs before saving can drop that code.
* The content editor's panels label their fields more explicitly: « Largeur d'affichage », « Légende de l'image », « Lien vers », « Ouvrir ce lien dans… », « Texte du lien », « Texte du bouton », and « Dépasse sur les côtés » for the wider image size (in English: "Display width", "Image caption", "Link to", "Open this link in…", "Link text", "Button text", "Extends past the sides").
* Trek is now developed and tested against **prosemirror 1.0**, its first stable release, which imports HTML and Markdown about a quarter faster. Trek already accepted it (`>= 0.7.1, < 2`), and doesn't use what 1.0 renames. **Upgrade note:** an app calling `Schema#parse_html`, `#parse_markdown` or `#parse_text`, or `Prosemirror::TextAlign`, renames them `#from_html`, `#from_markdown`, `#from_text` and `Prosemirror::Extensions::TextAlign` when it runs `bundle update prosemirror` (see [its changelog](https://github.com/etaminstudio/prosemirror/blob/main/CHANGELOG.md)).
* Failed form submissions answer with `:unprocessable_content` (422), Rack's name for it since 3.1, instead of the deprecated `:unprocessable_entity`, in `Trek::ResourceController`, the sign-in controller and the generated specs, which printed a deprecation warning in every run. **Upgrade note:** replace `:unprocessable_entity` with `:unprocessable_content` in the app's own controllers and specs to silence the warning.
* **Security:** the content editor runs on **Tiptap 3** (was 2), which fixes `mergeAttributes()` turning an own `__proto__` key into DOM attributes ([GHSA-cp6q-959q-f8rh](https://github.com/advisories/GHSA-cp6q-959q-f8rh)). Its floating menu uses Floating UI instead of Tippy. The editor behaves as before: no underline, and no empty paragraph added after a final block. **Upgrade note:** run `yarn up trek`, and update the app's own Tiptap extensions, if any, to Tiptap 3 (see [Upgrade from Tiptap v2](https://tiptap.dev/docs/guides/upgrade-tiptap-v2)).
* `Trek::Form::ImageFieldComponent` (`f.image_field`) shows two icons over its thumbnail, on hover and focus, opening the original image in a new tab or downloading it, instead of a « See original » button under « Replace » and « Remove », which wrapped onto two lines in French.

### Fixed

* `Trek::TypographyFormatter` broke content saved from the content editor: given the editor's JSON string, it formatted it as plain text, and the non-breaking spaces it added before colons made the JSON invalid. It now parses the document, and formats its text nodes. **Upgrade note:** an app that added `:content` to `format_attributes` may hold records whose content no longer parses (`formatted_content` raises `JSON::ParserError`), which the editor can't open either: restore them from a backup.
* `Trek::TypographyFormatter` no longer adds a space before a colon, semicolon, `!`, `?`, `%`, `€` or `$` inside a word: times (`10:30`), URLs (`https://…?a=1`) and ratios (`3:1`) are left alone, and one space goes before a group of signs (`Quoi ?!`). Apostrophes and the space after `«` now work next to accented letters (`l’été`, `« Été »`). Spaces earlier versions put inside a word (`10 :30`, `https ://`) are removed. **Upgrade note:** formatted titles and content are repaired the next time they're saved.
* `content_text` ran the content's paragraphs and lines together (`Line oneLine two`), which shows in the excerpts of the admin's fragments index: it now separates blocks with a blank line, and hard breaks with a newline (`Prosemirror::Document#to_text`).
* Reordering a list in the admin (`Trek::Reorderable`) could leave records out of place: it saved them one at a time while acts_as_list, and closure_tree for the trees ordered with `numeric_order`, renumbered the other records at each save. A list numbered from 1, acts_as_list's default, lost a record on most moves, as the list posts positions from 0. The posted order is now applied as a whole: in an `acts_as_list_no_update` block, from the list's `top_of_list`, or with closure_tree's `prepend_sibling` and `append_sibling` for a tree. Records missing from the list (deleted meanwhile, or left out by `reorderable_objects`) are skipped. **Upgrade note:** you can now drop your own `reorder` actions written to work around it.
* Adding a call to action from the content editor failed with `NoMethodError: undefined method 'color'`: the prompt panel's response read a `color` that `Trek::ContentEditor::Prompt` never had, and the panel has no field for. Prompts no longer have a color: the editor's block, the panel and `Trek::Content::PromptsBlock` drop it, and the site's link loses its `data-color`. A color already stored in content stays in the JSON, unrendered. **Upgrade note:** an app styling prompts with `[data-color]`, or reading `@color` in its own `PromptsBlock`, can drop that code.
* **Security:** the content editor showed image and call-to-action blocks by writing their attributes into HTML: an alternative text, caption or button text containing `"` or `<` broke the block, or injected markup into the admin. They are now built as DOM nodes, their texts as text.
* The content editor's toolbar buttons show the styles and the block where the cursor is right after a click: the bold, italic and heading buttons marked their icon instead of the button until the cursor moved.

## \[2.5.0] - 2026-10-25

### Changed

* **Security:** rich text is rendered with the [`prosemirror`](https://github.com/etaminstudio/prosemirror) gem instead of `prosemirror_to_html`, which had two XSS advisories: HTML is escaped as it is built, attributes are typed, and links and images only allow `http`, `https`, `mailto`, `tel` and relative URLs. The content editor's image and prompt blocks render out of the box (`Trek::Content::ImageBlock`, `Trek::Content::PromptsBlock`), empty list items and lists are skipped like empty paragraphs, invalid content is reported to `Rails.error`, with the record holding it in the context (`trek_content: "Page#12"`, a tag in AppSignal), and the records that a page's links point to load at once, with one query per model. See [Rich text rendering](https://trek.etamin.studio/reference/formatters#rich-text-rendering). **Upgrade note:** the `Prosemirror` model and the `ProsemirrorToHtml` formatters are removed: call `formatted_content`, or `Trek::Content.schema.parse(content).to_html`, instead of `Prosemirror.new.format_content`, and rewrite the app's `ProsemirrorToHtml` nodes as node types registered in `Trek::Content.schema` (see [Custom blocks](https://trek.etamin.studio/reference/formatters#custom-blocks)).
* Trek requires **Rails 8.0** or later (was 6.0) and **Ruby 3.3** (was 3.1: Rails 8 needs 3.2, and the `prosemirror` gem 3.3). Its authentication already relied on Rails 8: `has_secure_password`'s `:password_reset` token and `password_reset_token`, without which a password reset fails on Rails 7.2 (`KeyError: key not found: :password_reset`) and so does a password renewal (`NoMethodError`). **Upgrade note:** an app on Rails 7 upgrades Rails before Trek (see the [Rails 8.0 upgrade guide](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-7-2-to-rails-8-0)); meanwhile, it can declare the token in `app/models/user.rb`, after `has_secure_password`: `generates_token_for(:password_reset, expires_in: 15.minutes) { password_salt&.last(10) }` and `def password_reset_token = generate_token_for(:password_reset)`.
* Trek is now developed and tested against **css_parser 3.3** (was 3.0), which Lookbook, a Trek dependency, brings in: 3.2 and 3.3 fix a ReDoS on an unclosed CSS function ([GHSA-84w7-hpvm-rxx2](https://github.com/premailer/css_parser/security/advisories/GHSA-84w7-hpvm-rxx2)) and a local file read through `@import` ([GHSA-w3mx-4vv9-hjpg](https://github.com/premailer/css_parser/security/advisories/GHSA-w3mx-4vv9-hjpg)). **Upgrade note:** run `bundle update css_parser` if the app's `Gemfile.lock` has an older one.
* Trek is now developed and tested against **crass 1.0.8** (was 1.0.7), which Loofah and rails-html-sanitizer bring in to sanitize CSS: it fixes a CSS sanitizer bypass through bad URLs ([GHSA-pq4g-8rxf-4g29](https://github.com/rgrove/crass/security/advisories/GHSA-pq4g-8rxf-4g29)). **Upgrade note:** run `bundle update crass` if the app's `Gemfile.lock` has an older one.
* The generators no longer print RuboCop's report when they autocorrect the files they inject code into, only an `autocorrect` line per file; an offense RuboCop can't correct is still shown, and stops the install.
* Trek's own CI is faster: its linters and security checks run in one job (`make -k lint security i18n-check`, with a `make` target per tool), the tests no longer wait for them, and the generated app's install keeps its gems and Rails in a cache, one per Ruby version, and is skipped for changes to the docs or the repository's tooling; it no longer installs Trek's own gems and packages first. Nothing changes for host apps.
* The admin's fragments index shows each fragment's title, its key (`namespace.key`) and the start of its content, with `Trek::EntriesComponent`, instead of only its key, and has a search box matching the key, the title or the content's text (`Fragment.search`). **Upgrade note:** to get them, copy from Trek's templates (`lib/generators/trek/templates/`) `views/admin/fragments/index.html.slim`, the `search` scope of `models/fragment.rb`, and `controllers/admin/fragments_controller.rb`.

### Fixed

* Since 0.1.18, rendering rich text that links to a page raised `NoMethodError: undefined method 'page_url'`: page links are turned into URLs with `url_for`, which needs `Trek::PagePathHelper`, no longer included. An app's own `direct :page` route still takes precedence. **Upgrade note:** you can now drop initializers that include `Trek::PagePathHelper` in `ProsemirrorToHtml::GlobalIdToLinksFormatter`.
* Since 2.4.0, the site's links to pageable records (`Trek::Pageable`) used their id instead of their page's slug: `to_param` took the admin IP allowlist middleware, in the stack of every request, for an admin caller.
* The content editor's link, image and prompt panels opened empty ("Content missing"): the installer routed them to `Admin::Panels` controllers it didn't generate. It now generates them, subclasses of `Trek::Panels::*` that serve Trek's panels under the app's routes. The panels' titles, field labels and image sizes are translated, and the link panel no longer needs `OpenStruct`, which Ruby doesn't load by default since 3.5. **Upgrade note:** run `rails g trek:install:admin` and keep only its new `app/controllers/admin/panels/*_controller.rb`, or copy them from Trek's templates (`lib/generators/trek/templates/controllers/admin/panels`); you can then drop your own translations of `trek.panels.*.form.title`, of `activemodel.attributes.trek/content_editor/*` and of the image sizes, and the `ostruct` gem if nothing else needs it. Re-run `rails g trek:install:model_specs` for the new `spec/requests/admin/panels_spec.rb`.
* `Trek::Reorderable` gives the views `reorder_path`, which the scaffolded index's commented-out reorder form uses, and reorders without Prosopite taking acts_as_list's repeated queries for an N+1. **Upgrade note:** you can now drop your own `helper_method :reorder_path` and Prosopite pause around `reorder`.
* The admin's lists, the pages' tree and the content editor's image panel raised `NoMethodError: undefined method 'derivation_url'` on an image when the image uploader doesn't load Shrine's derivation endpoint, as the generated `ImageUploader` doesn't: they show the image itself instead, as the image field already did (`Trek::ThumbnailHelper.thumbnail_url`). **Upgrade note:** you can now drop the derivation endpoint if you enabled it only for these thumbnails.
* The specs installed by `trek:install:model_specs` pass in an app whose default locale isn't English: the three examples reading browser descriptions ("Firefox on Windows") use an English-speaking user. **Upgrade note:** re-run `rails g trek:install:model_specs`, or drop your own locale workarounds in `spec/mailers/admin_mailer_spec.rb`, `spec/requests/admin/sign_in_notices_spec.rb` and `spec/requests/admin/active_sessions_spec.rb`.
* `trek:scaffold` passes the attributes to the model generator as given: it dropped their indexes (`slug:string:uniq`, `isbn:string:index`), references' options (`author:references{polymorphic}`) and decimals' precision (`price:decimal{10,2}`).
* The generated `Makefile` lints every Slim template: unquoted, `make`'s shell expanded `app/**/*.slim` one directory deep only, so `make lint` skipped what CI caught. **Upgrade note:** quote the pattern in your `Makefile`'s `lint` target: `bundle exec slim-lint "app/**/*.slim"`.
* `trek:install:specs` includes the matchers of rspec-html-matchers (`have_tag`, `with_tag`), which it adds to the Gemfile, in `spec/rails_helper.rb`. **Upgrade note:** add `config.include RSpecHtmlMatchers` to the `RSpec.configure` block of `spec/rails_helper.rb` to use them.
* `Trek::SearchComponent` labels its field in the admin's language by default (it was always « Rechercher »), and its label is tied to the field, which has an id of its own: clicking the label focuses the field, and screen readers announce the field by its label.

## \[2.4.0] - 2026-10-16

### Added

* `Trek::Form::ImageFieldComponent` (`f.image_field`) shows a « See original » button opening the uploaded image in a new tab.
* An audit log: `rails g trek:install:audit_log` installs an `AuditLog` model recording each mutation in the admin (author, request, target record, status), sign-ins included, and an admin list for the `admin` role; `Trek::AuditLog::PurgeJob` deletes rows past 6 months. See [Audit log](https://trek.etamin.studio/reference/generators/audit-log). **Upgrade note:** an app with its own audit log can drop its copy for Trek's, keeping its `audit_logs` table (see the docs' migration steps).
* `Trek::Filters` exposes `filter_params` to views, as `Trek::Scopes` does `scope_params`.
* Users can't reuse one of their last 5 passwords (`Trek::Users::PasswordHistory`, configurable with `password_history_size`). **Upgrade note:** copy `app/models/user/previous_password.rb` (`User::PreviousPassword`) and the `user_previous_passwords` migration from Trek's templates (`lib/generators/trek/templates/{models,migrations}`), run it, add `include Trek::Users::PasswordHistory` after `has_secure_password` in `app/models/user.rb`, and re-run `rails g trek:install:model_specs` for the new specs.
* Password expiry and renewal (`Trek::Users::PasswordRenewal`): an admin can ask every user for a new password after a suspected compromise, and `password_expires_after` (none by default) makes passwords expire. A user concerned is signed out and sent to the new password form, which states the rules and rates the password as it is typed (`Trek::Form::PasswordStrengthComponent`). **Upgrade note:** copy the `user_password_renewal` migration from Trek's templates (`lib/generators/trek/templates/migrations`) and run it, add `include Trek::Users::PasswordRenewal` after `has_secure_password` in `app/models/user.rb`, and take the changes to `require_authentication` in `app/controllers/concerns/trek/authentication.rb`, the `require_password_renewal` action of `Admin::UsersController`, its route (`post :require_password_renewal, on: :collection` in `resources :users`), and the `admin/users/index` and `admin/user_password_resets/edit` views from Trek's templates.
* Sessions expire after 48 hours without a request and 30 days after signing in (`Trek::Users::SessionLifetime`, configurable on `User::Session`), and users see their active sessions in a tab of their profile, to sign out any other or all of them; an IP address shows its host name on hover. Session activity is written at most every 5 minutes instead of on every request. **Upgrade note:** copy the `user_session_tracking` migration from Trek's templates (`lib/generators/trek/templates/migrations`) and run it; add `include Trek::Users::SessionLifetime` to `app/models/user/session.rb` (or `app/models/user_session.rb`); take Trek's changes to `app/controllers/concerns/trek/authentication.rb` (`find_session_by_cookie`, `start_new_session_for`, `track_last_activity`), to `Admin::UsersController#show` and to `app/views/admin/users/show.html.slim` (with its new `_properties` and `_sessions` partials); copy `app/controllers/admin/active_sessions_controller.rb` from Trek's templates and add its route (`resources :active_sessions, only: :destroy do delete :others, action: :destroy_others, on: :collection end` in the admin namespace); schedule `Trek::PurgeExpiredSessionsJob` daily.
* Users see their previous sign-in (date, browser, IP address) on their profile's Active sessions tab, and a sign-in from a browser that never signed in as them emails them (`Trek::Users::SignIns`, `new_sign_in_email`). **Upgrade note:** copy the `user_sign_ins` migration and the `admin_mailer/new_sign_in` views from Trek's templates and run the migration; add `include Trek::Users::SignIns` to `app/models/user.rb`; take Trek's changes to `app/controllers/concerns/trek/authentication.rb` (`record_sign_in_of` and the methods after it), `app/controllers/admin/user_sessions_controller.rb` (`create`, `sign_in_notice`), `app/mailers/admin_mailer.rb` (`new_sign_in`) and `app/views/admin/users/_sessions.html.slim`.
* `Trek::SectionComponent`: a titled section of a page (an `h2` labelling it), with an optional hint under the heading.
* `NotDisposableValidator` refuses email addresses from disposable providers, on any model: `validates :email, not_disposable: true`. The domains come from the `valid_email2` gem, which keeps them up to date. See [Validators](https://trek.etamin.studio/reference/validators).
* Failed sign-ins are throttled per account and per IP address over a longer window: after 5 failures in a row, an account's sign-in locks for 1 minute, doubled on each further failure up to 1 hour, and its user is emailed (`Trek::Users::SignInLockout`); an IP address gets 20 failures an hour, whatever the account. A locked account gets the same answer as a wrong password, and a failed or throttled sign-in now renders the form (422, 429) instead of redirecting, so the audit log counts it as a failure, on the account tried. See [Failed sign-ins](https://trek.etamin.studio/reference/models/user#failed-sign-ins), including why `Rails.cache` must be shared between servers. **Upgrade note:** copy the `user_sign_in_lockout` migration and the `admin_mailer/sign_in_locked` views from Trek's templates and run the migration; add `include Trek::Users::SignInLockout` to `app/models/user.rb`; take Trek's `app/controllers/admin/user_sessions_controller.rb` and the `sign_in_locked` method of `app/mailers/admin_mailer.rb`.
* Passkeys: users add and delete passkeys on a new tab of their profile, which names each after its password manager, and sign in with one instead of their password, from a button on the sign-in form or the email field's autofill (`Trek::Users::Passkeys`, `User::Passkey`, through the `webauthn` gem). The password keeps working. See [Passkeys](https://trek.etamin.studio/reference/models/user#passkeys). **Upgrade note:** copy `app/models/user/passkey.rb` and the `user_passkeys` migration from Trek's templates and run it; add `include Trek::Users::Passkeys` to `app/models/user.rb`; copy `app/controllers/admin/passkeys_controller.rb` and `app/controllers/admin/passkey_sessions_controller.rb`, and add their routes in the admin namespace (`resources :passkeys, only: %i[create destroy] do get :options, on: :collection end` and `resource :passkey_session, only: :create do get :options end`); take Trek's changes to `app/views/admin/user_sessions/new.html.slim`, `app/views/admin/users/show.html.slim` (with the new `_passkeys` partial) and `Admin::UsersController#show`; with the audit log, add `Admin::PasskeySessionsController` to `config/initializers/audit_log.rb`.
* A [Security and compliance](https://trek.etamin.studio/security) docs page maps Trek's security measures to ISO 27001's controls and NIS2's measures, and lists what stays with the app, its hosting and the organization.
* The [Security and compliance](https://trek.etamin.studio/security#cryptography) page lists the cryptography a generated app uses, and how each algorithm stands against a quantum computer.
* The generated GitLab CI has an `sbom` job, commented out, making a Software Bill of Materials (CycloneDX, with cdxgen) of the app's gems and npm packages, for a client who asks for one. See [Software Bill of Materials](https://trek.etamin.studio/reference/generators/install#software-bill-of-materials). **Upgrade note:** when a client asks for an SBOM, copy the `sbom` job from Trek's `lib/generators/trek/templates/.gitlab-ci.yml` into the app's `.gitlab-ci.yml`, uncommented.
* `ADMIN_ALLOWED_IPS` restricts the admin to IP addresses and ranges (e.g. `203.0.113.0/24, 2001:db8::/32`): from anywhere else, `/admin` and everything below it answer like an unknown URL, a 404. Off when unset. Behind a CDN, list its ranges in `trusted_proxies` first. See [Restricting the admin to IP addresses](https://trek.etamin.studio/proxies#restricting-the-admin-to-ip-addresses).
* Security headers: `rails g trek:install:security_headers`, which `trek:install` runs, sets up a Content Security Policy, report-only at first, allowing no inline script, and a `Permissions-Policy` header turning off the browser features the app doesn't use (camera, microphone, geolocation…). Violations are reported to `/csp-reports` (`Trek::CspReportsController`), which sends them to AppSignal, one incident per directive and blocked origin, and to the log, throttled against floods. It replaces Rails' commented-out example, but leaves an app's own policy. HSTS, from `force_ssl`, already covers subdomains; its `preload` option is documented. See [Security headers](https://trek.etamin.studio/reference/generators/security-headers). **Upgrade note:** run `rails g trek:install:security_headers`, browse the app with the console open and watch AppSignal's `csp` namespace, add the front office's third-party hosts, then set `content_security_policy_report_only` to `false`.

### Changed

* Trek pins **Node 24.22.0** (was 24.18.0) in `.node-version`. **Upgrade note:** existing apps can set `24.22.0` in their `.node-version`.
* `trek:install:yarn` installs **Yarn 4.18.1** (was 4.18.0), and Trek's own development moves to it from Yarn 3, with its defaults: no install scripts, and no package published less than a day ago. **Upgrade note:** existing apps can upgrade with `yarn set version 4.18.1` followed by `yarn install`.
* Passwords follow the ANSSI's recommendations for a privileged access (`Trek::Users::PasswordPolicy`, included by `Trek::Users::Validations`): at least 12 characters, with a digit and a symbol below 20, and neither a common password, a keyboard or number sequence, nor the user's name or email; a Have I Been Pwned check is opt-in. Existing passwords keep working: the rules apply when one is set. **Upgrade note:** the rules are configurable in `app/models/user.rb` (`password_min_length`, `passphrase_length`, `password_check_pwned`); update seeds and specs whose passwords no longer pass.
* The generated signed-in browser model is `User::Session` (`user.sessions`), in the same `user_sessions` table. Apps generated before keep working with `UserSession`. **Upgrade note:** to follow, move `app/models/user_session.rb` to `app/models/user/session.rb` as `class User::Session`, replace `has_many :user_sessions` with `has_many :sessions` in `app/models/user.rb`, and update `UserSession.find_by` and `user.user_sessions` in `app/controllers/concerns/trek/authentication.rb` and `app/controllers/admin/user_password_resets_controller.rb`; no migration is needed.
* The admin's Inter font comes with Trek's npm package ([`@fontsource/inter`](https://fontsource.org/fonts/inter)), bundled into `admin.css`, instead of Google Fonts: visitors' browsers no longer contact Google, and the Content Security Policy needs no Google host. **Upgrade note:** remove `admin_webfonts_link_tag` from `app/helpers/application_helper.rb` and `app/views/layouts/admin.html.slim`, and the Google Fonts `link` tags from both admin layouts (`admin.html.slim`, `admin/auth.html.slim`); `esbuild.config.js` must have the `.woff` and `.woff2` file loaders, as Trek's has.

### Deprecated

* `Trek::UserSession`, the sign-in form object Trek 2 no longer uses, warns and will be removed in Trek 3. **Upgrade note:** in a sign-in controller still building one, pass the params to `User.authenticate_by(email:, password:)` instead, as Trek's generated `Admin::UserSessionsController` does.

### Changed

* The install generators no longer add `esbuild-plugin-import-glob`, `postcss-cli`, `postcss-nesting` and `postcss-url`, which no generated config uses. **Upgrade note:** you can drop them from the app's `package.json` (`yarn remove …`), except those its own `postcss.config.js`, `esbuild.config.js` or `package.json` scripts still use (e.g. `postcss-url` to rebase the URLs of `@import`-ed CSS).
* `rails g trek:install` runs Trek's own generators in its process instead of starting `rails generate` for each, which saves about two minutes.

### Fixed

* The generated `.gitlab-ci.yml`'s commented-out deploy jobs point at `example.com` instead of a real site.
* A tag's key is unique within its category, as `Tag` validates it and the docs said: the generated `tags` migration's unique index on `key` alone refused the same key in two categories with `ActiveRecord::RecordNotUnique`. A tag category with tags can no longer be deleted, which left them without a category, and the admin now says why a deletion is refused instead of reporting it done. **Upgrade note:** in `app/models/tag_category.rb`, replace `dependent: :nullify` with `dependent: :restrict_with_error`. Check that no category has two tags with the same key, nor two tags without a category, then generate a migration (`bin/rails g migration TagKeyUniquePerCategory`) with `remove_index :tags, name: "index_tags_on_key", if_exists: true` (apps generated with Trek 0.1.9 or later have it), `add_index :tags, %i[category_id key], unique: true` and `add_index :tags, :key, unique: true, where: "category_id IS NULL", name: "index_tags_on_key_without_category"`. Copy `spec/models/tag_spec.rb` from Trek's templates for the specs.
* The notice after signing in is in the user's language, rather than the app's default one.
* The password reset request no longer reveals whether an email has an account: it answers every email the same way, and queues `Admin::PasswordResetJob` for each, which emails privileged users only, so the response takes as long either way. **Upgrade note:** copy `app/jobs/admin/password_reset_job.rb` from Trek's templates (`lib/generators/trek/templates/jobs/admin`), and in `Admin::UserPasswordResetsController#create`, replace the user lookup and `deliver_later` with `Admin::PasswordResetJob.perform_later(params[:email].to_s)`.
* Icons drawn in black (`stroke="#000000"`, such as the content editor's toolbar icons) stayed black in the dark theme, and in any colored context: since the sprite was sanitized in two passes, black was turned into `currentColor` before the icon's paths were inserted, so it never reached them. The substitution now runs once the paths are in.
* `trek:install:esbuild` installed esbuild 0.15, too old for the `esbuild.config.js` it copies: `yarn build --watch` (and `bin/dev`) crashed with `esbuild.context is not a function`. It now installs esbuild 0.28, as Trek does. **Upgrade note:** in an app generated by an earlier Trek 2, run `yarn add --dev esbuild@^0.28.0`.
* The generated invitation and password reset emails formatted the link's expiry (« 7 days ») in the app's default locale instead of the recipient's. `rails g trek:install:model_specs` now also installs mailer specs covering it. **Upgrade note:** in `app/mailers/admin_mailer.rb` and `app/mailers/admin/passwords_mailer.rb`, move the `@expiration_delay = …` line inside the `I18n.with_locale` block, before `mail`; re-run `rails g trek:install:model_specs` for the new specs.

## \[2.3.2] - 2026-10-02

### Fixed

* **Security:** `Trek::Scopes#apply_scopes` called any name found in `params[:scopes]` that the relation answered to, not only the controller's `available_scopes`, which let a request run unintended methods on the listed records. Only the names in `available_scopes` are applied now; others are ignored. **Upgrade note:** update Trek as soon as possible. A controller passing `apply_scopes` a scope name it does not list in `available_scopes` must add it there.
* The generated admin sign-in, password reset request and new password pages rendered their actions with the default aspect of `Trek::Form::ActionsComponent`, whose sticky action bar (top margin, padding, background) is meant for edit forms: inside those small cards, the button and the « forgotten password » link sat in a padded bar. They now use the `seamless` aspect. **Upgrade note:** in apps generated before this version, pass `aspect: :seamless` to `Trek::Form::ActionsComponent.new` in `app/views/admin/user_sessions/new.html.slim` and `app/views/admin/user_password_resets/{new,edit}.html.slim`.

## \[2.3.1] - 2026-10-02

### Changed

* `Trek::Filters` only filters on the columns a controller lists in its new `available_filters` (empty by default), read through strong params (`filter_params`): other keys of `params[:filters]`, and values that aren't a single value, are ignored. Until now any key became a `where` condition, so a request could filter an index on columns it was never meant to expose. **Upgrade note:** in each controller including `Trek::Filters`, define `available_filters` returning the filtered columns (e.g. `def available_filters = %i[country_code]`), or its filters stop applying.
* `Trek::Users::Validations` stores emails stripped and in lower case (sign-in and password resets now ignore case), checks them with `email_validator`'s strict mode, and refuses a spaces-only password; the password reset form refuses an empty password instead of pretending to change it. **Upgrade note:** resolve users whose emails differ only by case (`User.group("lower(email)").having("count(*) > 1").count`), then downcase the rest (`User.find_each { |user| user.update_column(:email, user.email.strip.downcase) }`); in `Admin::UserPasswordResetsController#update`, redirect back with an error when `params[:password]` is blank (see Trek's template); re-run `rails g trek:install:model_specs` for the new specs.

### Fixed

* The header's theme switcher now hovers like the links next to it, and the whole pill is clickable, not just the icon. `Trek::DropdownComponent`'s trigger inherits its context's color, and a new `trigger_classnames:` option styles the trigger button. **Upgrade note:** if you reuse `Trek::DropdownComponent`, move trigger styling (padding, hover) from `classnames:` to `trigger_classnames:`.

## \[2.3.0] - 2026-10-01

### Added

* More admin request specs from `rails g trek:install:model_specs`, run by Trek's own suite as well: `fragments_spec.rb` (index, new, create, duplicate key, edit, update, destroy), `users_spec.rb` (an admin lists privileged users only, creates a user and sends the invitation, changes a role, deletes a user; an editor is sent back from the users list and other profiles, gets a 403 in JSON, edits their own profile but not their role, switches the admin theme) and `user_password_resets_spec.rb` (the reset request answers known and unknown emails alike and only mails privileged users, reset and invitation links open the form, an invalid link goes back to the request form, a new password signs the user out everywhere, a password under 8 characters is refused). A `db/seeds/setup/fragments.rb` makes `fragments` available to seeds and specs. **Upgrade note:** re-run `rails g trek:install:model_specs` to add them.
* `Trek::Dashboard::TileComponent` accepts `method:` and `data:`. With `method: :post` (or `:delete`…) the tile issues that request through Turbo, as a `data-turbo-method` link like `Trek::ButtonComponent`'s `to:` links, so a dashboard shortcut can create a record in one click (e.g. a pre-filled draft) rather than only open a form; `data:` adds attributes to the link, such as `turbo_confirm:`. Tiles without them still render a plain GET link. **Upgrade note:** you can now turn a tile pointing to a form into a direct action, and drop buttons kept elsewhere only because a tile couldn't POST.
* Policy specs for host apps, run by Trek's own suite as well: `rails g trek:install:model_specs` installs `spec/policies/admin/` specs for the page, fragment, user, preview and external link policies (what each role may do, which attributes it may change, which records it sees), and `rails g trek:taxonomies` installs the tag and tag category ones (a tag or category in use can't be deleted). **Upgrade note:** re-run `rails g trek:install:model_specs` (and `rails g trek:taxonomies` if you use tags) to add them.
* Sound uploads from the admin: `Trek::Form::SoundFieldComponent` (`f.sound_field`) posted to `/admin/upload/audio`, which no generated app routed or handled. `trek:install:uploaders` now installs an `AudioUploader` (MP3, AAC/M4A, Ogg, WAV, WebM, FLAC, up to the field's 200 MB), the generated `Admin::UploadsController` gains an `audio` action, and `trek:install:admin:routes` adds `post "upload/audio", to: "uploads#audio"` to the admin namespace. Attach it with `include AudioUploader::Attachment(:audio)` on a record with an `audio_data` jsonb column. **Upgrade note:** copy `app/uploaders/audio_uploader.rb` from Trek (or re-run `rails g trek:install:uploaders`), add the `audio` action to `app/controllers/admin/uploads_controller.rb` (`set_rack_response AudioUploader.upload_response(:cache, request.env)`), and add `post "upload/audio", to: "uploads#audio"` inside `namespace :admin` in `config/routes.rb`; you can then drop a hand-written audio uploader, action and route that did the same.
* `rails g trek:install:model_specs` also installs request specs for the admin, run by Trek's own suite as well: `spec/requests/admin/user_sessions_spec.rb` (sign-in form, signing in and out, refusing a wrong password or a user who isn't privileged), `pages_spec.rb` (index, new, create, invalid create, edit, update, destroy as an editor), `uploads_spec.rb` (the image upload endpoint) and the existing `n_plus_one_spec.rb`, with an image fixture in `spec/fixtures/files`. The Oaken users setup gains a `users.password` helper to sign in with. **Upgrade note:** re-run `rails g trek:install:model_specs` to add them, then `bin/rails spec`.
* `rails g trek:install:model_specs`, a new installer step (also runnable alone) giving host apps specs for the models Trek installs: `spec/models/page_spec.rb` (slugs, full paths through the page tree, path history when a page moves) and `spec/models/user_spec.rb` (validations, roles, `privileged`, `to_s`). Their test data uses [Oaken](https://github.com/kaspth/oaken) seed scripts rather than factories: `db/seeds/setup/users.rb` sets defaults for `users.create`/`users.new` (a unique email, a valid password), and `db/seeds/test/cases/page_tree.rb` lays out a named page tree the Page spec loads with `seed "cases/page_tree"`. The step adds the `oaken` gem (in every group, since `db/seeds.rb` now calls `Oaken.seed`, which also makes these seeds available to `bin/rails db:seed`) and `require "oaken/rspec_setup"` in `spec/rails_helper.rb`, which seeds the database before the suite. The specs check Trek's behaviour inside the app, so a customization that breaks it fails the app's own suite; edit or remove the ones covering behaviour you deliberately changed. Trek's own suite runs the very same files against its dummy app, so they stay green with each Trek release. Oaken needs Ruby 3.2+. **Upgrade note:** run `rails g trek:install:model_specs` to add them (it skips the gem, the `db/seeds.rb` line and the `rails_helper` line when already there, and asks before overwriting an existing file), then `bundle exec rspec spec/models`. `oaken/rspec_setup` truncates every table and runs `db/seeds.rb` before the suite, so make sure your `db/seeds.rb` is fine to run in the test environment.
* The header's theme button is now a working theme switcher. It used to toggle `light-theme`/`dark-theme` on the page without saving the choice (a `TODO` logged it to the console), so the theme reset on the next page load. It now opens a menu offering **Auto**, **Light** and **Dark**, applies the choice at once and saves it to the user's `theme` through `PATCH /admin/users/theme`. The action answers with a turbo stream that refreshes the header icon. Logged-out pages (login, password reset) now render with `auto-theme` instead of `light-theme`. **Auto** follows the system setting (`prefers-color-scheme`) without JavaScript: a small PostCSS plugin in `postcss.config.js` copies Radix's `.dark-theme` variables into a `@media (prefers-color-scheme: dark)` block scoped to `.auto-theme`. `color-scheme` is now set for each theme too, so native controls and scrollbars follow it. The menu is a new `Trek::DropdownComponent` (a trigger slot plus `with_item` entries, with keyboard navigation), which host apps can reuse. **Upgrade note:** in `app/controllers/admin/users_controller.rb`, replace `head :no_content` in the `theme` action with `respond_to { |format| format.turbo_stream; format.html { head :no_content } }`, and add `app/views/admin/users/theme.turbo_stream.slim` from Trek's generator templates (or re-run `rails g trek:install:admin:users`). Without them the theme is still saved, but the header icon only updates on the next page load. For **Auto** to follow the system setting, copy the `autoDarkTheme` plugin from Trek's `postcss.config.js` into yours and add it to `plugins` right after `postcss-import`; without it, **Auto** renders the light theme.
* `phonable_attributes` accepts a `strictness:` option — `:possible` (the default, unchanged) or `:valid`. `:possible` only asks Phonelib whether the digits could form a number for the country; `:valid` asks whether they fall in a range the regulator has actually assigned. The gap bites on input errors: with a French default country, `"0012345678"` is possible, so it is accepted and normalized to `"+12345678"` — the stray leading zero read as an international prefix, the number silently rehomed to country code +1, and the original input gone since normalization happens on save. Apps that intend to call or text the numbers they collect want `phonable_attributes :phone, strictness: :valid`.
* Plain file inputs are now styled: inside a `Trek::Form::GroupComponent` (`f.group`), a standard `f.file_field` gets its native `::file-selector-button` styled like a `Trek::ButtonComponent` with `size: :narrow`, using the accent variables so `data-accent-color` applies as on buttons. The browser's built-in "file chosen" feedback is kept and no JS is involved — useful for one-off uploads (e.g. an .xlsx import) next to Trek's styled fields. The Shrine-backed `image_field`/`sound_field` are unaffected (their file inputs are visually hidden). **Upgrade note:** you can now drop custom `::file-selector-button` styles that mirrored Trek's buttons for plain file inputs.
* The confirm dialog (`Trek::DialogComponent` wired to `Turbo.config.forms.confirm`) now takes its title, confirm button label and button color per action from the confirming element's data attributes — `data-turbo-confirm-title`, `data-turbo-confirm-button` and `data-turbo-confirm-color` alongside `data-turbo-confirm` — falling back to the current delete wording ("Are you sure?" / "Delete" / tomato) when absent. A non-destructive confirm (a "Validate" flow, a scheduled job trigger) no longer shows a **Delete** button, and one page can host a delete confirm and an action confirm side by side. Works on form submit buttons and on `data-turbo-method` links (the clicked element is remembered, since Turbo only forwards `data-turbo-confirm` to the form it builds for a link). **Upgrade note:** you can now drop `content_for(:dialog)`-style overrides that swapped the whole dialog just to rename the button — pass the data attributes on the confirming button instead.
* `Trek::MenuComponent` items accept a `target:` option forwarded to the link — e.g. `s.with_item(text: …, href: …, target: "_blank")` for external tools mounted in the admin, like a GoodJob dashboard. A `_blank` item automatically gets `rel="noopener"` and the `trek/external` new-tab icon after its text, as the toast link already did. `Trek::Dashboard::TileComponent` (which already accepted `target:`/`rel:`) now also defaults `rel` to `noopener` when `target` is `_blank`. **Upgrade note:** you can now drop initializer patches that reopened `ItemComponent` to add a `target` option.
* `Trek::ButtonComponent` accepts a `name:` option, rendered on the plain-button branch (paired with `value:`) so a `type: :submit` button can carry a param — needed when a form has two submit actions with different outcomes (e.g. a "save" button next to a "save then preview" one submitting `then_preview`). **Upgrade note:** you can now drop initializer patches that reopened the component to add a `name` option.
* `rails g trek:install:appsignal`, a new installer step (also runnable alone) that sets up [AppSignal](https://appsignal.com) error tracking and performance monitoring, until now wired by hand in every app: it adds the `appsignal` gem, injects `APPSIGNAL_PUSH_API_KEY` into the Rails credentials (as the Postmark generator does with its API token — `bundle exec appsignal install` would write the key in plain text and needs a terminal), and writes a `config/appsignal.rb` that reads the key from the credentials, activates the agent in production as soon as a key exists, and ignores the `/healthcheck` action. It also wraps the boot in `config.ru` so a broken initializer or a missing credential is reported, and installs an `AppsignalContext` controller concern that sets the `admin`/`web` namespace and tags transactions with the acting user — included in `ApplicationController` and, through an initializer, in `Trek::BaseController`. `APPSIGNAL_APP_ENV` is added to `.env.example`; the rescued Postmark delivery in `ApplicationMailer` now reports through `Rails.error` rather than only logging, which keeps app code independent of the monitoring vendor. Check an install with `bundle exec appsignal diagnose`. **Upgrade note:** existing apps can run `rails g trek:install:appsignal` — it skips the gem and the credentials when AppSignal is already set up, but review the generated `config/appsignal.rb` against your own (it takes the push API key from the credentials, so drop a plain-text key or an `APPSIGNAL_PUSH_API_KEY` environment variable you no longer need), and remove your hand-written `AppsignalContext` equivalent if you have one.
* `bin/rails trek:attachments:promote_cached`, a rake task finding the records whose Shrine attachments still point to the temporary `cache` storage — for every model and attachment, whatever the uploader — and promoting those whose cached file still exists (backed by `Trek::CachedAttachments`). It prints one line per attachment with its status: `promoted`, `missing` when the cached file is gone (it has to be uploaded again), or `changed` when the record changed meanwhile; `DRY_RUN=1` only lists them, as `promotable`. Documented in the [uploaders reference](/reference/uploaders).

### Changed

* Trek's dummy app now generates and serves the whole admin (authentication, authorization, the admin generator's controllers and layout, routes, the pages, fragments, users, sessions and password-reset panels, mailers), so request and component specs run against the real admin. Line coverage goes from 49% to 73%; the minimum is raised to 72%.
* Every Trek generator is now run by Trek's own suite (`spec/trek/generators_spec.rb`): on a fresh `rails new` app set up as in the Getting started guide, then a second time to check that re-running it changes nothing (host apps re-run generators when upgrading). Shell-outs (bundle, yarn, migrations, rubocop) are recorded rather than run; the few other gems' generators whose output a later step edits (`rails g model`, `action_policy:install`, `mobility:install`, `rspec:install`) run for real, in-process. `trek:admin:user` has its own spec against the dummy app's database. Line coverage goes from 35% to 49%; the minimum is raised to 49%.
* `trek:install:ruby` now pins **Ruby 4.0.7** (was 4.0.6), and the CI images (repo + generated `.gitlab-ci.yml` template) use the matching `4.0.7` tag. **Upgrade note:** existing apps can set `4.0.7` in `.ruby-version` and bump the `ci-ruby` image tag in `.gitlab-ci.yml`.
* Trek's own specs now run in random order (`--seed` replays an order, `--bisect` finds the spec leaking state), and no longer depend on it: `page_path_helper_spec` switched the default locale to French inside `I18n.with_locale`, which left `I18n.locale` set to `:fr` for the specs that ran after it, so `localization_spec` and `i18n_helper_spec` failed under some orders.
* Trek's own suite can now test code that needs Rails and a database. Specs that `require "rails_helper"` boot `spec/dummy`, a minimal host app on PostgreSQL. Its models, migrations and Shrine initializer are not copied by hand: they are produced on each boot by running Trek's own generator steps (`trek:install:page`, `:user`, `:authentication`, `:fragment`…), so they cannot drift from what `rails g trek:install` gives a real app, and a spec fails when a model or migration template is added without the suite generating it. First `User` and `Page` model specs come with it. Line coverage goes from 9% to 35%, most of it from the app's code now being loaded; the minimum is raised to 34%.
* Trek's own spec suite runs in CI again, in a `tests` job that had been commented out — so nothing flagged that `spec/helpers/trek/page_path_helper_spec.rb` required a `rails_helper` the repository does not have, which stopped the whole suite from loading. That spec is rewritten in the suite's isolated style (no Rails app, stubbed routes). `COVERAGE=1` turns on SimpleCov, which reports line and branch coverage over every Ruby file in `app/` and `lib/` (generator templates excluded), writes a Cobertura report for GitLab's merge-request diff coverage, and fails the run below a minimum line coverage, currently 9%, to be raised as coverage grows.
* Trek is now developed and tested against **Shrine 3.10** (was 3.6). The gemspec constraint stays `~> 3.0`, so this changes nothing for host apps by itself — they already resolve 3.10 on Ruby ≥ 3.2 — but Trek's own suite and the generated app in CI no longer run against a version nobody installs. Worth picking up: 3.8 fixes a path traversal outside the storage directory in the `file_system` storage (the development and test storage), 3.7 stops `column` from failing on an empty string, 3.9 fixes `remove_invalid` leaving the attribute set after an invalid file is deassigned and stops the `activerecord` plugin from deleting the original attachment of a duplicated record. Shrine 3.7 dropped Ruby < 3.2, so an app still on Ruby 3.1 keeps resolving 3.6. **Upgrade note:** run `bundle update shrine` (nothing else to change).

### Fixed

* A signed-in user opening what their role doesn't allow (an editor on the users panel, or on another user's profile) got a 500 error page: `ActionPolicy::Unauthorized` was raised and handled nowhere. `Trek::Authorization` now rescues it: back to the previous page, or to the dashboard, with a translated alert (`notices.unauthorized`); Turbo Stream and JSON requests get a 403.
* The new fragment form (and any fragment saved without content) raised `undefined method '[]' for nil`: the generated `fragments` migration created `content` as a jsonb column without the `{}` default its translated `title` has, and Mobility's jsonb backend reads a translation as `column[locale]`. The migration now defaults `content` to `{}`. **Upgrade note:** in apps generated before this version, add a migration with `change_column_default :fragments, :content, from: nil, to: {}` and `up_only { execute "UPDATE fragments SET content = '{}' WHERE content IS NULL" }`.
* The generated `Admin::ExternalLinkPolicy` scoped external links for non-privileged users with `relation.where(user:)`, a leftover from an example: `external_links` has no `user` column, so applying the scope raised a SQL error. Non-privileged users now get none, as in Trek's other policies. **Upgrade note:** in `app/policies/admin/external_link_policy.rb`, replace `relation.where(user:)` with `relation.none`.
* With `rails g trek:taxonomies`, asking whether a tag could be deleted (`Admin::TagPolicy#destroy?`) raised `HasManyThroughAssociationPolymorphicSourceError`: it called `Tag#taggables`, a `has_many :taggables, through: :taggings` that Rails can't build over the polymorphic `Tagging#taggable`. The policy now checks `taggings`, and `Tag#taggables` is a method returning the tagged records. **Upgrade note:** in `app/models/tag.rb`, replace `has_many :taggables, through: :taggings` with `def taggables = taggings.includes(:taggable).map(&:taggable)`, and in `app/policies/admin/tag_policy.rb`, replace `record.taggables.empty?` with `record.taggings.empty?`.
* The docs and README still described [Sorcery](https://github.com/Sorcery/sorcery) as Trek's authentication, and the `User` reference listed its `crypted_password` column. Trek authenticates with Rails' `has_secure_password` and database-backed `UserSession` records; the Getting started guide, the introduction, the home page, the customization guide, the installer and `User` references now say so, and the `User` columns table lists `password_digest` and `last_activity_at`. The README's ClosureTree link, which pointed to Sorcery, now points to ClosureTree.
* Deleting a page (or a page version) raised `NameError: Missing model class Section for the Page#sections association`: `Trek::Sectionable`, included in the generated `Page` and `PageVersion`, declared `has_many :sections, dependent: :destroy`, but no generator creates a `Section` model. The association is now declared only when the app defines one, so apps with their own sections keep them.
* Editing a page raised `undefined method 'page_path' for an instance of Trek::HeaderComponent`: the admin header's "See on website" link called `page_path`, which components don't include. The header now includes `Trek::PagePathHelper`, and leaves the link out when the app has no front-office route for the page (a freshly generated app has none), instead of raising.
* Image uploads from the admin returned 404: `trek:install:admin:routes` wrote `post "upload", to: "uploads#image", namespace: :admin` at the top of the routes (the `namespace:` ended up inside the route string), while image fields post to `/admin/upload/image`. It now writes `post "upload/image", to: "uploads#image"` inside `namespace :admin`. **Upgrade note:** in `config/routes.rb`, replace `post "upload", to: "uploads#image", namespace: :admin` with `post "upload/image", to: "uploads#image"` inside your `namespace :admin do … end` block.
* Trek's components use `Turbo::FramesHelper`, but the gemspec didn't declare `turbo-rails`: it only worked because `rails new` adds it by default. Trek now depends on `turbo-rails` (>= 1.0) and requires it.
* `Trek::Users::Validations` only enforced the 8-character password minimum when a user was created: the check for a later change looked at `crypted_password`, a column from Trek's former authentication library that `has_secure_password` users don't have, so an existing user could set a 1-character password (e.g. through the password reset form). It now also applies whenever `password_digest` changes, and still skips saves that leave the password alone. The generated `spec/models/user_spec.rb` covers both cases. **Upgrade note:** nothing to change; to get the new examples in an existing app, re-run `rails g trek:install:model_specs` and accept overwriting `spec/models/user_spec.rb` (or copy the two examples).
* `rails g trek:upgrade:v2` left two blank lines in `.yarnrc.yml` where it removed the `git.etamin.studio` registry, right before the `npmPreapprovedPackages` block it adds, so running it again changed the file. It now leaves a single one.
* `rails g trek:scaffold` wrote an invalid `config/locales/models/<model>.en.yml`: the attribute labels ran together on one line (`name: Name        description: Description`), so Rails failed to load the translations (`Psych::SyntaxError: mapping values are not allowed in this context`) and every page rendering a translation raised. Each attribute is on its own line again. Trek's CI now generates its test app the way the Getting started guide does (`rails new -j esbuild`) and runs that app's own suite (`bin/rails spec`) after `trek:install` and `trek:scaffold`, which is how this surfaced. **Upgrade note:** in models scaffolded before this version, put each attribute of `config/locales/models/*.en.yml` on its own line.
* Freshly generated apps got a broken `db/schema.rb`: they resolve `json` 3, whose `JSON.parse` takes its options as keywords only, while ActiveSupport (up to 8.1.3.1, the latest release) passes them positionally. The schema dumper then fails on every jsonb column with a default and writes `# Could not dump table "pages" because of following ArgumentError` in place of `pages`, `page_paths`, `fragments` and `menu_nodes`, so the test database, loaded from `schema.rb`, lacks those tables and the app's specs cannot run. Trek now requires `json` < 3 until Rails supports json 3, and its suite dumps the generated schema to catch a regression. **Upgrade note:** run `bundle update trek` (it moves `json` back to 2.x if you had 3), then `bin/rails db:schema:dump` and check that `db/schema.rb` has no "Could not dump" comment left.
* Three generated migrations tripped RuboCop in a freshly generated app: `trek_tag_categories` and `trek_taggings` (from `rails g trek:taxonomies`) indented `create_table` by one space, and `trek_menu_nodes` ended with a trailing blank line. The templates are fixed, and Trek's suite now runs RuboCop on every generated migration. **Upgrade note:** apps generated before this version can run `bundle exec rubocop --autocorrect db/migrate` to fix them.
* `Trek::PagePathHelper#page_path` (and `page_url`) raised `NoMethodError: undefined method 'shop/product_path'` for a page attached to a namespaced pageable: the route name was built from `pageable.class.name.underscore`, which keeps the `/` of the namespace. It now uses `pageable.model_name.singular_route_key`, the key Rails' polymorphic routes use — `shop_product_path` for `Shop::Product`, and `post_path` for an isolated engine's `Blog::Post`. Non-namespaced pageables are unaffected.
* `Trek::PagePathHelper#page_url` returned a path, not a URL, for a page with a named route (a keyed page such as `about`): that branch always called `#{key}_path` and ignored the `url` suffix the other branches honour. It now calls `about_url`, so mailers, sitemaps and canonical links built with `page_url` get an absolute URL. **Upgrade note:** you can now drop workarounds that made `page_url` absolute for keyed pages (e.g. prefixing `request.base_url`).
* `trek:install:scalingo` pinned the buildpack with `BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0`, and Scalingo resolves a `#ref` suffix as a **branch**: the tag was looked up as `archive/refs/heads/v0.1.0`, which does not exist, and every build failed with `fail to download and extract the custom buildpack: Invalid status code: 404 Not Found`. The generator and the [Deploying to Scalingo](https://trek.etamin.studio/deploy) guide now point at the release archive, `https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz`, which Scalingo downloads as is and which stays pinned to the release. **Upgrade note:** on each Scalingo app, run `scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz` — an app still on the `#v0.1.0` form cannot deploy, and nothing surfaces it until the next deploy fails.
* The English locale gave the content editor's prompt panel its target labels in French ("un nouvel onglet" / "le même onglet"): `admin.content_editor.prompt.targets` had been copied from `trek.fr.yml` rather than translated. An English-locale admin inserting a Turbo-frame prompt now reads "a new tab" / "the same tab", as the link panel right above already did.
* The generated `trek_users` migration tripped RuboCop's `Rails/NotNullColumn` on `add_column :users, :password_digest, :string, null: false`, so `make lint` (and the `lint` CI job) failed in a freshly generated app — the installer's own closing `make fix` already exited non-zero on it. The column is fine as it stands: the preceding `trek_create_users` migration creates the table empty, so there is nothing to backfill and no default is wanted on a password digest. The line now carries an inline `rubocop:disable Rails/NotNullColumn` with a comment saying why. **Upgrade note:** apps generated before this version can add the same disable comment to `db/migrate/*_trek_users.rb`.
* The generated `.slim-lint.yml` listed an `AllCops:` key (`NewCops: disable`) among its linters. slim-lint validates linter names since 0.34 ("Add check for invalid keys in config files"), so `make lint` in a freshly generated app died with `No such linter: AllCops. Available: CommentControlStatement, …` before checking a single template; Trek's own lockfile pins 0.33, where the unknown key was ignored, which is why it went unnoticed. The key is gone — it configured nothing (the embedded RuboCop is configured under the `RuboCop` linter's `ignored_cops`). **Upgrade note:** remove the two `AllCops:` / `NewCops: disable` lines from your `.slim-lint.yml` if it still has them.
* The generated `config/appsignal.rb` now sets `config.log_path = "/tmp"` in production, so the AppSignal agent stops announcing `appsignal: Unable to log to '/app/log'. Logging to '/tmp' instead.` on every boot and every one-off container. Hosts with a read-only filesystem (Scalingo, Heroku and the like) cannot let the agent write its diagnostic log next to the app, and it falls back to `/tmp` by itself — nothing was lost, but the message reads like a misconfiguration. **Upgrade note:** add `config.log_path = "/tmp" if Rails.env.production?` to a `config/appsignal.rb` written before this version, or regenerate it with `rails g trek:install:appsignal`.
* `Trek::Phonable#normalize_phone` normalized every declared attribute from the value of `phone`: a model declaring `phonable_attributes :phone, :fax` — the concern's own documented example — overwrote the fax with the phone number on every save, and wiped it when the phone was blank. Each attribute is now parsed from its own value. Models declaring a single attribute are unaffected.
* `Trek::ButtonComponent`'s `disabled` option is now actually rendered — it was declared but ignored by every branch, so a "disabled" button stayed clickable with no visual cue. A disabled button renders `<button disabled>`; a disabled link (`href:` or `to:`) renders an inert anchor — no `href`, no Turbo method — with `aria-disabled="true"`, since `<a>` has no `disabled` attribute. Both get a dimmed, non-interactive style (`opacity: 0.5`, `pointer-events: none`, `cursor: not-allowed`). **Upgrade note:** you can now drop `classnames: "is-disabled"`-style workarounds and their CSS, and pass `disabled:` with a real `href` instead of blanking the href conditionally.
* The icon of a `Trek::ButtonComponent` no longer shrinks next to a long label: as a flex item it was squeezed (and, with a `min-width`, distorted) when the text ran out of room. It keeps its size, the text wraps or ellipsises as before. **Upgrade note:** you can now drop a `.c-trek--button-icon { flex-shrink: 0 }` override.
* The login, forgotten-password and password-reset forms labelled their fields with the humanized attribute name ("Email", "Password") whatever the locale. They post to a URL rather than to a record, so `Trek::Form::GroupComponent` had no model to resolve `activerecord.attributes.user.*` against and fell back to the attribute name; the translations shipped in Trek's locales were never looked up. The three generated views now pass `model_name: User.model_name` to `f.group`, which drives the i18n lookup alone and leaves field names (and therefore the params the controllers read) untouched. **Upgrade note:** views generated before this version keep the old labels — add `model_name: User.model_name` to the `f.group` calls in `app/views/admin/user_sessions/new.html.slim` and `app/views/admin/user_password_resets/{new,edit}.html.slim`, or regenerate them.
* The toast link's new-tab icon rendered as an empty `<svg><use></svg>`: `Trek::Toaster::ToastComponent::LinkComponent` referenced an `external` icon that didn't ship with the gem (it only resolved when the host app provided its own `app/assets/icons/external.svg`). Trek now bundles `trek/external` ([Lucide](https://lucide.dev)'s `external-link`, ISC) and the toast link uses it; the icon is also available to host apps as `Trek::IconComponent.new("trek/external")`. **Upgrade note:** you can now drop an `app/assets/icons/external.svg` that only existed to back the toast link (keep it if your own code renders the bare `external` key).
* `Trek::MenuComponent` items pointing at a namespaced model doubled the namespace in the path used for the active-state check (`href: [:admin, :missive, Missive::List]` compared against `admin/missive/missive/lists`), so those items never got `is-active`. The namespace segments already covered by the model's collection are now deduplicated, and the item highlights on its own controller. **Upgrade note:** you can now drop `active:` overrides that only worked around this (e.g. `active: "admin/#{model.model_name.collection}"`).
* `make release` now chains its steps with `&&`, so a failing step halts the run. Previously the steps were separated by `;`: when `rake release` failed (e.g. RubyGems asking for an MFA code), `npm publish` still ran and its output buried the error, leaving the gem unpublished while the tag and npm package were out.
* `make release` prompts for the RubyGems and npm one-time passwords right before each publish step (passed as `GEM_HOST_OTP_CODE` and `npm publish --otp`), so accounts with MFA no longer fail the push; leave a prompt empty when MFA is off for that registry.
* A file attached to a record on a numerically ordered closure tree — the generated `Page` and its image — stayed in Shrine's temporary `cache` storage forever when attached on create or together with a parent change. closure_tree reloads the record in its `after_save` callback to reorder siblings, and Shrine's `activerecord` plugin resets the attacher on reload, so the `after_commit` promotion found nothing to promote: the record kept `"storage": "cache"` data, and its derivation URLs broke with "Source file not found" as soon as the cached file was deleted. Updating an existing page's image was unaffected. `Trek::SavedAttachmentsReload`, a Shrine plugin loaded by the generated `RecordImageUploader`, now keeps across a reload the attachers whose file was saved with the record (an unsaved attachment is still discarded), so the file is promoted and the replaced one deleted after commit. **Upgrade note:** add `plugin Trek::SavedAttachmentsReload` under `plugin :activerecord` in `app/uploaders/record_image_uploader.rb` (it covers every model using that uploader, not only Trek's). Records saved before that may still point to cached files — run `bin/rails trek:attachments:promote_cached` to promote those whose file still exists, and have the ones it reports as `missing` uploaded again.

## \[2.2.0] - 2026-08-28

### Added

* `Trek::Dashboard::ChartComponent`, a dashboard card wrapping [Chartkick](https://chartkick.com) — pass `title`, `data`, a `type` (`:line_chart`, `:bar_chart`, …), optional `info`/`options`, and a `color` (default `:slate`); it renders `public_send(type, data, **options)` inside a coloured card matching the tiles. Chartkick is an **optional** dependency: the component only includes `Chartkick::Helper` when it is loaded, so Trek keeps working without it. `Trek::Dashboard::AnalyticsComponent` gained a `renders_many :charts` slot (alongside the existing `section`) so charts can be dropped straight into the dashboard with `a.with_chart(...)`. **Upgrade note:** to use `ChartComponent`, add `gem "chartkick"` to your app and `import "chartkick/chart.js"` in your admin JS bundle (Trek does not pull Chartkick in for you).
* `Trek::Dashboard::TileComponent` gained a `title` option (a large heading above the text, coloured per the tile's `color`) and a `wide` option (default `false`) that spans two tile columns. Its `href` is now optional: without one the tile renders as a plain `div` instead of a link, for read-only stat tiles.
* The layout gains a **stacked panel**: a second panel frame (`trek--layout-stacked-panel`) that opens on top of the primary one. The content editor's link/image/prompt inserters now open there, so an editor hosted *inside* a panel (e.g. a Sectionable payload edited in a side panel) no longer loses its unsaved content when inserting a link — the primary panel stays put underneath and the inserted element lands back in the live editor. Closing the primary panel also closes the stacked one; *Annuler* in the stacked panel closes only that one. The stacked panel is 1rem narrower than the primary panel (only while it stacks on an open one). **Upgrade note:** host apps that override the panel views (`app/views/<engine>/panels/{links,images,prompts}/*`) must retarget them to the `trek--layout-stacked-panel` frame and the `stackedPanelCloser` target, as Trek's own views now do.

### Fixed

* Internal links inserted through the content editor (stored as `gid://` URLs) rendered as literal `gid://…` hrefs — or were dropped by the sanitizer — in formatted content: `Prosemirror#format_content` now resolves GlobalIDs to page paths before rendering, and the editor's Link extension accepts the `gid` protocol so reopening such a link shows the linked page instead of an empty field.
* `Trek::Form::ActionsComponent`: the sticky footer styling (sticky positioning, `--accent-1` background, `4rem` vertical padding) now only applies to the `default` aspect. It was set on the root rule, so the `seamless` aspect — used by `Trek::DialogComponent` — was also sticky and padded inside dialogs.

## \[2.1.0] - 2026-08-25

### Added

* Docs: a [Deploying to Scalingo](https://trek.etamin.studio/deploy) guide covering the trek-buildpack setup, the environment to configure, and how to migrate an app from the stock buildpacks.
* `trek:install:prosopite` generator (wired into `trek:install`) sets up [Prosopite](https://github.com/charkost/prosopite) N+1 query detection (#211): adds `prosopite` and `pg_query` to the `development`/`test` Gemfile group and a `config/initializers/prosopite.rb` that mounts the Rack middleware — so every request is scanned, Trek's engine controllers included — raising `Prosopite::NPlusOneQueriesError` in test and logging in development. It also generates `spec/requests/admin/n_plus_one_spec.rb`, a smoke request spec rendering the dashboard, pages index and fragments index as an admin (extend it with the app's other admin routes). Documented under [Controllers › N+1 detection](https://trek.etamin.studio/reference/controllers#n-1-detection) with the `Prosopite.pause` escape hatch. **Upgrade note:** existing apps can run `rails g trek:install:prosopite`; apps that already wired Prosopite by hand can keep their setup (the generator skips the gems when `prosopite` is already in the Gemfile and won't overwrite an existing initializer).
* `make lint` and `make fix` targets in Trek's own Makefile, mirroring the ones generated for host apps (RuboCop, slim-lint, ESLint, Stylelint, Prettier — the `fix` variant autocorrects all but slim-lint). A `.prettierignore` excludes the gitignored esbuild output in `app/assets/builds/`, which made the Prettier check fail on a local checkout that had run `yarn build`.
* `Trek::MenuComponent` items accept an `active:` option for menu entries whose pages live under other controllers: a `String` is matched as a `controller_path` prefix, a `Regexp` against the whole path, and a boolean forces (or clears) the state — e.g. `s.with_item(text: …, href: [:admin, Shop], active: "admin/shop")` keeps *Shop* highlighted while editing its products. Without it, the item stays active when its href matches the current controller, as before.
* `content_editor` accepts an `autofocus:` option (default `false`): when true, the editor grabs focus on mount with the cursor at the end of the content — useful when the editor is the primary field of a side panel.

### Changed

* The shared `.rubocop.yml` (Trek's own, and the one `trek:install:rubocop` copies into host apps) re-enables `Layout/IndentationWidth` and `Layout/IndentationConsistency` with `EnforcedStyle: normal`. rubocop-rails-omakase disables both to allow its house style of indenting methods under `private`; Trek's style keeps methods at the class-body level, and a plain `rubocop --autocorrect` now fixes indentation — in generated files too. **Upgrade note:** copy the two cop blocks from the template (`rails g trek:install:rubocop` will prompt to overwrite) into your app's `.rubocop.yml`, then run `bundle exec rubocop --autocorrect` once; methods indented under `private` and mis-indented generated files get fixed.
* `trek:install:scalingo` now targets the dedicated [trek-buildpack](https://github.com/etaminstudio/trek-buildpack) (#193) instead of the stock apt + ruby buildpack pair: it no longer generates `.buildpacks` (and removes an existing one) and prints the `env-set` commands to run (`BUILDPACK_URL` pinned to the buildpack's release tag, and the `DATABASE_URL=$SCALINGO_POSTGRESQL_URL` mapping). The buildpack pins Ruby/Node/Yarn from `.ruby-version`, the `.node-version` that `rails new` already generates, and `package.json`'s `packageManager`, runs `yarn install` once, and ships a slimmed image (~40% smaller, measured deploys down from ~3min to ~1min35) — see the buildpack README for details. **Upgrade note:** on each Scalingo app, set `BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0`, delete `.buildpacks`, make sure `.node-version` exists (apps generated with `rails new -j esbuild` have it), and check that `config/database.yml` reads `DATABASE_URL` in production (mapped with `scalingo env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'`).
* `Trek::Form::FieldsetComponent`: the fieldset title (`.c-trek--form--fieldset-title`) now shares the form group label style (`--slate-12`, semibold, `1.2rem` bottom margin) so a titled fieldset reads like a labelled group; the `section` modifier only adds its larger font size on top. Hosts that restyled the plain fieldset title themselves can drop that override.
* `trek:install:yarn` now installs **Yarn 4.18.0** (was 4.17.0). **Upgrade note:** existing apps can upgrade with `yarn set version 4.18.0` followed by `yarn install`.

### Fixed

* `trek:scaffold` now references the model top-level qualified (`::Shop::Product`) in the generated controller's `model` and in the `Admin::Dashboard::MODELS_IN_MENU` entry: once the scaffold creates the `Admin::Shop` namespace, a bare `Shop` under `module Admin` resolves to that module instead of the model — the scaffolded controller raised `NameError`, and previously generated code referencing a `Shop` model silently got the module. The generator also warns when a namespace segment matches an existing model file that `rails g model` would replace with a namespace module. (#201) **Upgrade note:** if a scaffold introduced a namespace named like an existing model, qualify that model as `::Shop` in the existing `Admin::…Controller#model` and `MODELS_IN_MENU` entries.
* `Trek::Entries::ItemComponent`: the details list (`.c-trek--entries--item-details`) now wraps (`flex-wrap: wrap`) instead of overflowing or squeezing its items when an entry has many details or a narrow container.
* `trek:install:postmark` is idempotent again: re-running it on an app where it had already run appended a second `config.action_mailer.delivery_method = :postmark` / `postmark_settings` block to `config/environments/production.rb` (the gem, credentials and mailer steps were already guarded). The config injection is now skipped when the file already sets `delivery_method = :postmark`. (#210)
* Namespaced models (e.g. `Shop::Product`, routed under `namespace :shop`) now work out of the box with `Trek::ResourceController` (#212): `object_params` requires `model_name.param_key` (`shop_product`) instead of the demodulized element, so create/update no longer 400; `model_element_route` no longer prefixes the namespace when the route key already carries it (`shop_product`), so the delete button and list item links build `admin_shop_product_path` instead of the double-namespaced `admin_shop_shop_product_path` — models from isolated engines with relative naming (e.g. an engine's `Blog::Post`, route key `post`) keep the `[:blog, :post]` form; the controller defines `implicit_authorization_target` (`@object || model`) so `authorized(params)` and `Trek::Reorderable#reorder`'s bare `authorize!` find the policy; and `i18n_model_gender` reads `activerecord.models.<i18n_key>.gender` (`shop/product`), so `i18n_resource_label` picks the declared gender instead of the neutral fallback. **Upgrade note:** hosts that worked around this with a controller concern overriding `implicit_authorization_target`/`object_params`/`model_element_route`, or a prepend on `Trek::I18nHelper#i18n_model_gender`, can drop them.
* `Trek::Form::GroupComponent` now styles `input[type="time"]` like `input[type="date"]` (field frame, 16rem max width, lighter picker colour); `time_field`s used to render as bare browser inputs.
* Removed the dangling `sourceMappingURL` comment from the vendored `tom_select/css/tom-select.custom.css`: the referenced `.map` file is not shipped, so every host-app esbuild build (including `assets:precompile` on deploy) logged a `missing-source-map` debug warning. (#209)
* `trek:scaffold` now re-indents the generated controller and policy: for a namespaced model the nested `module` wrappers were emitted without indenting the class body, tripping the host app's linters. The generator runs a targeted `rubocop --autocorrect --only Layout/IndentationWidth,Layout/IndentationConsistency` on both files (forced with `--only` since rubocop-rails-omakase leaves those cops disabled). (#199)
* The `config/initializers/host.rb` generated by `trek:install:config` no longer references `ActionMailer::Base` during the initializer phase: doing so fires Action Mailer's load hook before the app's autoloader can resolve app classes, so an app configuring a custom `config.action_mailer.delivery_job` (e.g. `"MailDeliveryJob"`) crashed with a `NameError` on every production boot — breaking `assets:precompile` on deploy. The assignment is now deferred with `ActiveSupport.on_load(:action_mailer)`. (#208) **Upgrade note:** apply the same change to your app's `config/initializers/host.rb` — replace the `ActionMailer::Base.default_url_options = … if Rails.env.production?` line with the deferred block (see the generator template).
* `trek:scaffold` now generates the right controller class for a namespaced model (e.g. `rails g trek:scaffold Shop::Product`): the template used the still-namespaced `controller_class_name` inside the per-namespace `module` wrappers, defining `Admin::Shop::Shop::ProductsController` while the generated route expected `Admin::Shop::ProductsController` — every request 404ed with an uninitialized constant. The class name is now demodulized, like the policy template already did. (#200)
* The `show.html.slim` generated by `trek:scaffold` no longer ends with a trailing blank line: the attribute loop emitted its separator after every item instead of between items, so slim-lint failed in host apps with `TrailingBlankLines: Multiple empty lines in the end of file`. (#205)
* `trek:upgrade:v2` now upgrades Yarn to 4.18.0 first (applying the 0.1.26 upgrade note: `yarn set version` + dropping the `interactive-tools` plugin) when the app still runs Yarn 3. Previously the generator wrote `npmPreapprovedPackages` — a Yarn 4-only setting — unconditionally, which under Yarn 3 is a hard error that breaks every yarn command, including the generator's own `yarn add` step.
* Clarified the 0.1.26 changelog upgrade note about the Yarn minimal-age-gate exemption: it only applied to the pre-2.0.0 private GitLab distribution — since 2.0.0 the (Yarn 4-only) `npmPreapprovedPackages` entry, set up automatically by `trek:install` and `trek:upgrade:v2`, replaces it.
* Fixed whitespace in `model.en.yml.tt` which caused prettier to fail.

## \[2.0.1] - 2026-08-18

### Fixed

* The npm tarball ships `vendor/` again: the 2.0.0 slimming dropped it, but the three select form components (`collection_select`, `grouped_collection_select`, `link_select`) import `vendor/tom_select/css/tom-select.custom.css` from their JS sidecars, so host-app esbuild builds failed with "Could not resolve" errors.
* `AGENTS.md` now describes the public distribution (RubyGems + npmjs.com, npm package renamed to `trek`) instead of the retired private GitLab packages.

## \[2.0.0] - 2026-08-18

### Changed

* Trek's packages are now **public-only** (#197): the gem is consumed from [RubyGems](https://rubygems.org/gems/trek) and the npm package — renamed from `@etaminstudio/trek` to the unscoped [`trek`](https://www.npmjs.com/package/trek) — is published to npmjs.com. Nothing is published to the private GitLab registries any more, and no deploy token is needed. Because the previous owner of the npm `trek` name had already published versions up to `1.0.0` (npm never frees version numbers), Trek jumps straight to **2.0.0**, keeping gem and npm versions in lockstep. **Upgrade note:** existing apps upgrade by moving to 0.1.27 first, then running `rails g trek:upgrade:v2` (see 0.1.27's changelog entry) — the generator now also adds the `npmPreapprovedPackages` exemption below. Afterwards, remove the now-unneeded CI variables — `NPM_AUTH_TOKEN` and `BUNDLE_GIT__ETAMIN__STUDIO` (spelled `BUNDLE_GIT__ETAMINSTUD__IO` on apps created before the domain migration) — along with any local `TREK_DEPLOY_TOKEN` export, and revoke the project's Trek deploy token on GitLab.
* `trek:install` now wires new apps to the public packages: the generated `package.json` depends on `trek`, the JS entrypoint imports `"trek"`, and the generated `.yarnrc.yml` no longer configures the GitLab registry/scope — instead it lists `trek` under `npmPreapprovedPackages`, exempting it from Yarn's package gates (minimal age, …) so fresh releases install without the quarantine delay.
* The published npm tarball now only contains what host apps consume — `app/javascript`, `app/assets/{icons,images,stylesheets}` and the components' JS/CSS sidecars — instead of the whole repository (Ruby engine code, specs, docs, local config…).
* Package metadata refresh for the public listings: description "A modern content management & back-office system for Ruby on Rails" and homepage <https://trek.etamin.studio> on both the gemspec and `package.json`, plus an explicit MIT license on both.
* `make release` now publishes the npm package with `npm publish` (requires `npm login` as a `trek` maintainer) instead of `yarn npm publish` to the GitLab registry.

## \[0.1.27] - 2026-08-18

### Added

* `trek:upgrade:v2` generator: upgrades a host app to Trek 2.0 by switching it from the private GitLab packages to the public ones — the [`trek` gem on RubyGems](https://rubygems.org/gems/trek) and the unscoped [`trek` npm package](https://www.npmjs.com/package/trek). It rewrites the Gemfile (`git:` source → `gem "trek", "~> 2.0"`), drops the `git.etamin.studio` Bundler credentials, swaps the `@etaminstudio/trek` npm dependency and imports for `trek`, cleans the GitLab registry/scope out of `.yarnrc.yml`, and lists the CI credentials to remove manually. (#198) **Upgrade note:** this ships in 0.1.27 — the last version published to the private GitLab sources — so upgrade to it, then run `rails g trek:upgrade:v2` to move to Trek 2.0 and stop depending on the private GitLab sources and their deploy token.

### Changed

* `trek:install:ruby` now pins **Ruby 4.0.6** (was 4.0.5), and the CI images (repo + generated `.gitlab-ci.yml` template) use the matching `4.0.6` tag. **Upgrade note:** existing apps can set `4.0.6` in `.ruby-version` and bump the `ci-ruby` image tag in `.gitlab-ci.yml`.

## \[0.1.26] - 2026-07-02

### Changed

* `Trek::Localization` now resolves the request locale through an overridable `locale_for_request` method (still `params` → `current_user` → `headers` by default). Apps that need another source can override it and fall back to the default chain with `super` instead of forking the whole concern — e.g. `def locale_for_request = Current.event&.locale.presence || super`. No behavior change for existing consumers.
* `trek:install:authentication` no longer injects `generates_token_for` declarations into the generated `User`: `:password_reset` (15 min) is provided automatically by Rails' `has_secure_password`, and `:invite` (1 week) now lives in the `Trek::Users::Invitable` concern (which owns the rest of the invite behaviour). **Upgrade note:** existing apps can delete both `generates_token_for` blocks from their `User` — Rails and the concern provide them; customise the reset window via `has_secure_password reset_token: { expires_in: … }` if needed.
* `trek:install:ruby` now pins **Ruby 4.0.5** (was 4.0.0), and the CI images (repo + generated `.gitlab-ci.yml` template) use the matching `4.0.5` tag. **Upgrade note:** existing apps can set `4.0.5` in `.ruby-version` and bump the `ci-ruby` image tag in `.gitlab-ci.yml`.
* `trek:install:yarn` now installs **Yarn 4.17.0** (was 3.8.7) and no longer imports the `interactive-tools` plugin, whose `upgrade-interactive` command is built into Yarn 4. Yarn 4 is also what supports the minimal-age-gate exception below. **Upgrade note:** existing apps can upgrade with `yarn set version 4.17.0` followed by `yarn install`, and drop the now-redundant `interactive-tools` plugin.
* Bumped **Node to 24.18.0** (was 22.21.1) in `.node-version` and the CI images (repo + generated `.gitlab-ci.yml` template, now `ci-ruby/node-24`). **Upgrade note:** existing apps can set `24.18.0` in `.node-version` and bump the `ci-ruby/node-*` image in `.gitlab-ci.yml`.
* CI now runs `i18n-tasks missing -t diff` (config in `config/i18n-tasks.yml`) so a base-locale key with no translation in another locale fails the build. (Full `health` isn't used: as an engine, Trek's keys are consumed by host apps and many components/templates use relative keys that only resolve there.)

### Fixed

* Added the missing French translations that fell back to English: `admin.dialog_component.{title,text}` (the confirmation dialog shown on every destroy action) and `admin.{pages,fragments}.{show.intro,reorder.success,search.alert}`. Also added the English `admin.section`/`admin.sections` flash messages that previously existed only in French. (#194)
* `trek:install:yarn` now exempts the `@etaminstudio` scope from Yarn's minimal-age gate (`npmMinimalAgeGate: 0` under `npmScopes.etaminstudio` in the generated `.yarnrc.yml`), so a freshly published `@etaminstudio/trek` release installs without being quarantined; the gate still applies to public packages. **Upgrade note:** existing apps can add `npmMinimalAgeGate: 0` under `npmScopes.etaminstudio` in their `.yarnrc.yml`. *(Only relevant to the private GitLab distribution used before 2.0.0: the `@etaminstudio` scope is retired since then, so this scope exemption does nothing for the public unscoped `trek` package. From 2.0.0 the equivalent is `trek` listed under `npmPreapprovedPackages` — a Yarn 4-only setting, hence the Yarn upgrade above — which `trek:install` generates and `trek:upgrade:v2` adds during migration, so no manual step is needed.)*
* `trek:install:locales` no longer generates an invalid `config/initializers/locales.rb` (`I18n.available_locales = [:]` / `I18n.default_locale = :`) when passed no or blank locales. A blank option/env now falls back to the default (`en, fr`), blank entries are dropped, and the list can never end up empty. (#169)

## \[0.1.25] - 2026-07-01

### Added

* Invitation and password-reset emails now tell the recipient when the link expires. Invitations get their own `:invite` token purpose (1 week) — distinct from the 15-minute `:password_reset` token — and the generated `Admin::UserPasswordResetsController` accepts either purpose, so the shared "set your password" form works from both emails. Each mailer reads its window from the model's token definition (`User.token_definitions[:purpose].expires_in`) rather than a hardcoded constant. **Upgrade note:** add `generates_token_for :invite, expires_in: 1.week do password_salt&.last(10) end` to your `User`; have `AdminMailer#invite` issue an `:invite` token (and `include ActionView::Helpers::DateHelper`); and update `Admin::UserPasswordResetsController#set_user_by_token` to try both purposes, e.g. `User.privileged.find_by_token_for(:invite, @token) || User.privileged.find_by_token_for!(:password_reset, @token)`.
* `trek:install` now generates an `AGENTS.md` (onboarding guidance for AI coding agents: commands, conventions, and a "read the changelog on every upgrade" checklist) plus a `CLAUDE.md` that imports it via `See @AGENTS.md`, so Claude Code and other agents share one source of truth. **Upgrade note:** existing apps can add both files by running `rails g trek:install:agents`.
* `Trek::Filters` controller concern: filter the index by `params[:filters]` (attribute equality via `where`). Call `apply_filters!` in your `index`.
* Pedagogical `NotAppliedError` guard on `Trek::Filters` and `Trek::Scopes`: raises (outside production) when the concern is included but `index` never calls `apply_filters!` / `apply_scopes!`.
* `trek:install:scalingo` now adds `libjemalloc2` to the generated `Aptfile`, enabling the jemalloc allocator to reduce Ruby memory usage on Scalingo.

### Fixed

* The generated dashboard's "search a user" tile now links to the users index (`href: %i[admin users]`) instead of a dead `#`, and gates on `allowed_to: allowed_to?(:index?, User)` so roles that can't reach user management no longer see it (matching the sidebar's Users entry). **Upgrade note:** existing apps can set `href: %i[admin users]` and add `allowed_to: allowed_to?(:index?, User)` on that tile in `app/views/admin/dashboards/index.html.slim`.
* `trek:install` now points Action Mailer at the same host as the routes' `default_url_options` (driven by `DEFAULT_HOSTNAME`) in the generated `config/initializers/host.rb`, in production only (development and test keep the host from their environment files). Previously the mailer kept Rails' `example.com` default, so invitation and password-reset email links pointed to `https://example.com/...` in production. **Upgrade note:** existing apps can add `ActionMailer::Base.default_url_options = Rails.application.routes.default_url_options if Rails.env.production?` to their `config/initializers/host.rb` (and drop any hardcoded `config.action_mailer.default_url_options` host).
* The generated `Admin::UsersController` (`trek:install`) seeds a random password in `create_hook`, so users created via the admin form — which never collects one — satisfy `has_secure_password` and the non-null `password_digest` column. Invitees set their real password via the invite email; others via password reset. (Password seeding lives in the controller rather than `Trek::Users::Invitable` so it also covers users created without an invite.) **Upgrade note:** ensure your `Admin::UsersController#create_hook` seeds a password when blank: `@object.password = SecureRandom.alphanumeric(24) if @object.password.blank?`.
* `trek:install:ci` now sets `POSTGRES_DB` in `.gitlab-ci.yml` from the application name (e.g. `myapp_test`) instead of the hardcoded `project_test`.
* `trek:install:yarn` now rewrites `bin/setup`'s `yarn install --check-files` to `yarn install`, since `--check-files` is not a valid flag under Yarn berry and was breaking `bin/setup` (and therefore CI).
* `trek:install` now adds a `!/.env.example` exception to `.gitignore` so the generated `.env.example` template stays tracked (Rails ignores all `/.env*` files by default).
* `trek:install:postmark` now matches the production.rb SMTP comment with or without the `bin/` prefix, so the Postmark config is injected on Rails 8.0 as well as 8.1 (previously the injection silently did nothing when the anchor did not match).

## \[0.1.8] - 2025-09-03

### Fixed

* *(documented retroactively, 2026-07-27)* Removed the dead `option :items` from `Trek::ListComponent`, which collided with its `renders_many :items` slot. This turned out to fix a serious incompatibility: since view_component **3.15.0**, slot readers are defined in an included module, so the dry-initializer option reader (defaulting to `[]`) shadows the slot reader — `items` always returns `[]` and **every `ListComponent` list renders its empty state**, i.e. all admin index pages look empty regardless of data or permissions (tree-based indexes like pages are unaffected). With view_component ≤ 3.14 the slot reader happened to overwrite the option reader, masking the collision. **Upgrade note:** apps pinned to Trek < 0.1.8 that bump view_component to ≥ 3.15 (e.g. for its security fixes) hit this immediately; until they can upgrade Trek, they can restore the slot reader with an initializer: `Rails.application.config.to_prepare { Trek::ListComponent.class_eval { def items = get_slot(:items) } }`.

## \[0.0.1] - 2023-01-24

* Initial release
