trek 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.node-version +1 -1
  3. data/AGENTS.md +7 -4
  4. data/CHANGELOG.md +37 -0
  5. data/Gemfile +5 -0
  6. data/Gemfile.lock +16 -3
  7. data/Makefile +2 -0
  8. data/app/components/trek/dashboard/tile_component/tile_component.css +9 -0
  9. data/app/components/trek/dashboard/tile_component/tile_component.html.slim +4 -2
  10. data/app/components/trek/dashboard/tile_component.rb +12 -2
  11. data/app/components/trek/form/content_editor_component/content_editor_component.css +5 -0
  12. data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +19 -93
  13. data/app/components/trek/form/content_editor_component/content_editor_component.js +94 -251
  14. data/app/components/trek/form/content_editor_component/extensions/block_view.js +40 -0
  15. data/app/components/trek/form/content_editor_component/extensions/image_block.js +28 -26
  16. data/app/components/trek/form/content_editor_component/extensions/prompts_block.js +15 -26
  17. data/app/components/trek/form/content_editor_component/extensions/table_of_contents.js +1 -3
  18. data/app/components/trek/form/content_editor_component.rb +15 -0
  19. data/app/components/trek/form/image_field_component/image_field_component.css +40 -1
  20. data/app/components/trek/form/image_field_component/image_field_component.html.slim +0 -9
  21. data/app/components/trek/form/image_field_component.rb +36 -9
  22. data/app/content/trek/content/cleaner.rb +53 -0
  23. data/app/content/trek/content/document.rb +2 -2
  24. data/app/content/trek/content/prompts_block.rb +1 -2
  25. data/app/content/trek/content.rb +6 -0
  26. data/app/controllers/concerns/trek/reorderable.rb +24 -6
  27. data/app/controllers/trek/csp_reports_controller.rb +14 -1
  28. data/app/controllers/trek/panels/prompts_controller.rb +1 -1
  29. data/app/controllers/trek/resource_controller.rb +5 -5
  30. data/app/jobs/trek/purge_expired_sessions_job.rb +4 -3
  31. data/app/models/concerns/trek/contentable.rb +15 -1
  32. data/app/models/concerns/trek/users/sign_in_code.rb +70 -0
  33. data/app/models/concerns/trek/users/sign_in_lockout.rb +4 -2
  34. data/app/views/trek/panels/links/_form.html.slim +2 -4
  35. data/app/views/trek/panels/prompts/create.turbo_stream.slim +0 -1
  36. data/config/locales/trek.en.yml +26 -11
  37. data/config/locales/trek.fr.yml +26 -11
  38. data/docs/.vitepress/config.mjs +4 -1
  39. data/docs/background-jobs.md +69 -0
  40. data/docs/content-editor.md +219 -0
  41. data/docs/customize.md +2 -0
  42. data/docs/deploy.md +2 -2
  43. data/docs/getting-started.md +1 -1
  44. data/docs/index.md +1 -1
  45. data/docs/reference/components.md +2 -2
  46. data/docs/reference/concerns/contentable.md +10 -4
  47. data/docs/reference/concerns/formattable.md +1 -1
  48. data/docs/reference/environment-variables.md +1 -0
  49. data/docs/reference/formatters.md +2 -30
  50. data/docs/reference/forms.md +1 -1
  51. data/docs/reference/generators/audit-log.md +2 -2
  52. data/docs/reference/generators/install.md +3 -2
  53. data/docs/reference/generators/public-accounts.md +141 -0
  54. data/docs/reference/generators/security-headers.md +1 -1
  55. data/docs/reference/index.md +2 -1
  56. data/docs/reference/models/user.md +3 -1
  57. data/docs/what-is-trek.md +1 -1
  58. data/lib/generators/trek/audit_log_generator.rb +54 -0
  59. data/lib/generators/trek/install/admin/users_generator.rb +6 -1
  60. data/lib/generators/trek/install/audit_log_generator.rb +9 -45
  61. data/lib/generators/trek/install/good_job_generator.rb +161 -0
  62. data/lib/generators/trek/install/model_specs_generator.rb +3 -3
  63. data/lib/generators/trek/install/ruby_generator.rb +1 -1
  64. data/lib/generators/trek/install/scalingo_generator.rb +7 -45
  65. data/lib/generators/trek/install_generator.rb +15 -0
  66. data/lib/generators/trek/public_accounts_generator.rb +267 -0
  67. data/lib/generators/trek/scalingo_generator.rb +55 -0
  68. data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
  69. data/lib/generators/trek/templates/controllers/admin/password_renewals_controller.rb +11 -0
  70. data/lib/generators/trek/templates/controllers/admin/user_sessions_controller.rb +1 -1
  71. data/lib/generators/trek/templates/controllers/admin/users_controller.rb +0 -8
  72. data/lib/generators/trek/templates/initializers/content_security_policy.rb +3 -2
  73. data/lib/generators/trek/templates/initializers/good_job.rb.tt +38 -0
  74. data/lib/generators/trek/templates/jobs/mail_delivery_job.rb +6 -0
  75. data/lib/generators/trek/templates/layouts/admin.html.slim +5 -0
  76. data/lib/generators/trek/templates/mailers/admin_mailer.rb +4 -2
  77. data/lib/generators/trek/templates/package.json.tt +1 -1
  78. data/lib/generators/trek/templates/public_accounts/api/authentication.rb.tt +76 -0
  79. data/lib/generators/trek/templates/public_accounts/api/controllers/accounts_controller.rb.tt +22 -0
  80. data/lib/generators/trek/templates/public_accounts/api/controllers/active_sessions_controller.rb.tt +21 -0
  81. data/lib/generators/trek/templates/public_accounts/api/controllers/base_controller.rb.tt +71 -0
  82. data/lib/generators/trek/templates/public_accounts/api/controllers/email_changes_controller.rb.tt +55 -0
  83. data/lib/generators/trek/templates/public_accounts/api/controllers/email_confirmations_controller.rb.tt +19 -0
  84. data/lib/generators/trek/templates/public_accounts/api/controllers/password_resets_controller.rb.tt +28 -0
  85. data/lib/generators/trek/templates/public_accounts/api/controllers/passwords_controller.rb.tt +17 -0
  86. data/lib/generators/trek/templates/public_accounts/api/controllers/registrations_controller.rb.tt +17 -0
  87. data/lib/generators/trek/templates/public_accounts/api/controllers/sessions_controller.rb.tt +45 -0
  88. data/lib/generators/trek/templates/public_accounts/api/controllers/sign_in_codes_controller.rb.tt +15 -0
  89. data/lib/generators/trek/templates/public_accounts/jobs/sign_in_code_job.rb.tt +13 -0
  90. data/lib/generators/trek/templates/public_accounts/locales/accounts.en.yml.tt +205 -0
  91. data/lib/generators/trek/templates/public_accounts/locales/accounts.fr.yml.tt +205 -0
  92. data/lib/generators/trek/templates/public_accounts/mailers/account_mailer.rb.tt +85 -0
  93. data/lib/generators/trek/templates/public_accounts/migrations/create_accounts.rb.tt +46 -0
  94. data/lib/generators/trek/templates/public_accounts/models/account/session.rb.tt +47 -0
  95. data/lib/generators/trek/templates/public_accounts/models/account/sign_in_code.rb.tt +8 -0
  96. data/lib/generators/trek/templates/public_accounts/models/account.rb.tt +73 -0
  97. data/lib/generators/trek/templates/public_accounts/spec/api/accounts_spec.rb.tt +90 -0
  98. data/lib/generators/trek/templates/public_accounts/spec/api/sign_ins_spec.rb.tt +158 -0
  99. data/lib/generators/trek/templates/public_accounts/spec/models/account/sign_in_code_spec.rb.tt +56 -0
  100. data/lib/generators/trek/templates/public_accounts/spec/models/account_spec.rb.tt +109 -0
  101. data/lib/generators/trek/templates/public_accounts/spec/web/accounts_spec.rb.tt +126 -0
  102. data/lib/generators/trek/templates/public_accounts/spec/web/authentication_spec.rb.tt +43 -0
  103. data/lib/generators/trek/templates/public_accounts/spec/web/sign_ins_spec.rb.tt +215 -0
  104. data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.html.slim.tt +10 -0
  105. data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.text.erb.tt +8 -0
  106. data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.html.slim.tt +13 -0
  107. data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.text.erb.tt +8 -0
  108. data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.html.slim.tt +9 -0
  109. data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.text.erb.tt +11 -0
  110. data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.html.slim.tt +11 -0
  111. data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.text.erb.tt +11 -0
  112. data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.html.slim.tt +10 -0
  113. data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.text.erb.tt +8 -0
  114. data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.html.slim.tt +10 -0
  115. data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.text.erb.tt +8 -0
  116. data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.html.slim.tt +10 -0
  117. data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.text.erb.tt +8 -0
  118. data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.html.slim.tt +13 -0
  119. data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.text.erb.tt +8 -0
  120. data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.html.slim.tt +9 -0
  121. data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.text.erb.tt +11 -0
  122. data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.html.slim.tt +9 -0
  123. data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.text.erb.tt +11 -0
  124. data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.html.slim.tt +23 -0
  125. data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.text.erb.tt +14 -0
  126. data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.html.slim.tt +24 -0
  127. data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.text.erb.tt +14 -0
  128. data/lib/generators/trek/templates/public_accounts/web/authentication.rb.tt +97 -0
  129. data/lib/generators/trek/templates/public_accounts/web/controllers/accounts_controller.rb.tt +33 -0
  130. data/lib/generators/trek/templates/public_accounts/web/controllers/active_sessions_controller.rb.tt +15 -0
  131. data/lib/generators/trek/templates/public_accounts/web/controllers/base_controller.rb.tt +52 -0
  132. data/lib/generators/trek/templates/public_accounts/web/controllers/email_changes_controller.rb.tt +85 -0
  133. data/lib/generators/trek/templates/public_accounts/web/controllers/email_confirmations_controller.rb.tt +19 -0
  134. data/lib/generators/trek/templates/public_accounts/web/controllers/password_resets_controller.rb.tt +41 -0
  135. data/lib/generators/trek/templates/public_accounts/web/controllers/passwords_controller.rb.tt +23 -0
  136. data/lib/generators/trek/templates/public_accounts/web/controllers/registrations_controller.rb.tt +24 -0
  137. data/lib/generators/trek/templates/public_accounts/web/controllers/sessions_controller.rb.tt +78 -0
  138. data/lib/generators/trek/templates/public_accounts/web/controllers/sign_in_codes_controller.rb.tt +27 -0
  139. data/lib/generators/trek/templates/public_accounts/web/views/_flash.html.slim.tt +5 -0
  140. data/lib/generators/trek/templates/public_accounts/web/views/accounts/show.html.slim.tt +52 -0
  141. data/lib/generators/trek/templates/public_accounts/web/views/email_changes/edit.html.slim.tt +17 -0
  142. data/lib/generators/trek/templates/public_accounts/web/views/email_changes/new.html.slim.tt +15 -0
  143. data/lib/generators/trek/templates/public_accounts/web/views/password_resets/edit.html.slim.tt +26 -0
  144. data/lib/generators/trek/templates/public_accounts/web/views/password_resets/new.html.slim.tt +15 -0
  145. data/lib/generators/trek/templates/public_accounts/web/views/passwords/edit.html.slim.tt +25 -0
  146. data/lib/generators/trek/templates/public_accounts/web/views/registrations/new.html.slim.tt +28 -0
  147. data/lib/generators/trek/templates/public_accounts/web/views/sessions/new.html.slim.tt +38 -0
  148. data/lib/generators/trek/templates/public_accounts/web/views/sign_in_codes/new.html.slim.tt +17 -0
  149. data/lib/generators/trek/templates/spec/jobs/mail_delivery_job_spec.rb +11 -0
  150. data/lib/generators/trek/templates/spec/mailers/admin_mailer_spec.rb +10 -0
  151. data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +1 -1
  152. data/lib/generators/trek/templates/spec/requests/admin/good_job_spec.rb +34 -0
  153. data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +1 -1
  154. data/lib/generators/trek/templates/spec/requests/admin/panels_spec.rb +16 -0
  155. data/lib/generators/trek/templates/spec/requests/admin/password_renewal_spec.rb +2 -2
  156. data/lib/generators/trek/templates/spec/requests/admin/sign_in_lockout_spec.rb +1 -1
  157. data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +2 -2
  158. data/lib/generators/trek/templates/views/admin/users/index.html.slim +1 -1
  159. data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.html.slim +1 -1
  160. data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.text.erb +1 -1
  161. data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.html.slim +1 -1
  162. data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.text.erb +1 -1
  163. data/lib/trek/csp_reports.rb +22 -4
  164. data/lib/trek/typography_formatter.rb +35 -4
  165. data/lib/trek/version.rb +1 -1
  166. data/package.json +12 -8
  167. data/trek.gemspec +1 -1
  168. data/vitest.config.mjs +24 -0
  169. data/yarn.lock +1004 -288
  170. metadata +90 -3
@@ -0,0 +1,69 @@
1
+ # Background jobs
2
+
3
+ Trek apps run Active Job on [GoodJob](https://github.com/bensheldon/good_job), installed by `rails g trek:install` (or on its own with `rails g trek:install:good_job`). GoodJob stores its queue in the application's own PostgreSQL database, so there is no second database to provision — which is why the installer removes the Solid Queue setup `rails new` generates.
4
+
5
+ ## What the installer sets up
6
+
7
+ | Where | What |
8
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
9
+ | `Gemfile` | `good_job` replaces `solid_queue` (Solid Cache stays: its table lives in the primary database) |
10
+ | `config/application.rb` | `config.active_job.queue_adapter = :good_job` and `config.action_mailer.delivery_job = "MailDeliveryJob"` |
11
+ | `config/environments/production.rb` | `config.good_job.execution_mode = :async` and `config.good_job.shutdown_timeout = 25` |
12
+ | `config/environments/test.rb` | `config.active_job.queue_adapter = :test`, so specs can check what is enqueued (`have_enqueued_job`) |
13
+ | `config/initializers/good_job.rb` | queues, cron, error reporting, and the dashboard guard |
14
+ | `config/database.yml` | a connection pool sized for the web threads **and** the job threads |
15
+ | `app/jobs/mail_delivery_job.rb` | the delivery job every `deliver_later` email goes through |
16
+ | `config/routes.rb` | `mount GoodJob::Engine => "good_job"` inside `namespace :admin` |
17
+
18
+ ## Async in Puma, or a dedicated worker
19
+
20
+ In production the jobs run in the web container's own threads (`execution_mode = :async`): nothing else to deploy, nothing else to pay for. `shutdown_timeout = 25` lets a restart drain the jobs in flight instead of dropping them.
21
+
22
+ `GOOD_JOB_MAX_THREADS` sizes the job pool. It also feeds the database pool, since `config/database.yml` reserves connections for the web threads and the job threads separately:
23
+
24
+ ```yml
25
+ max_connections: <%= web = ENV.fetch("RAILS_MAX_THREADS", 5).to_i; 1 + web + ENV.fetch("GOOD_JOB_MAX_THREADS", web).to_i %>
26
+ ```
27
+
28
+ When the jobs outgrow the web container — long-running work, or a queue that must not compete with requests — move them to their own process: set `config.good_job.execution_mode = :external` and uncomment the worker line in the `Procfile` (`worker: bundle exec good_job start`). Note that in `:external` mode only the worker runs a scheduler, so cron then runs there and nowhere else.
29
+
30
+ ## Queues
31
+
32
+ One scheduler serves every queue by default (`config.good_job.queues = "*"`). Give a queue its own lane when it must not wait behind the others:
33
+
34
+ ```ruby
35
+ config.good_job.queues = "mailers:2;*:3"
36
+ ```
37
+
38
+ The thread counts have to stay within `GOOD_JOB_MAX_THREADS` — they are database connections too.
39
+
40
+ ## Scheduled jobs (cron)
41
+
42
+ Recurring jobs are declared in the initializer, and only run in production (a development machine should not fire them):
43
+
44
+ ```ruby
45
+ config.good_job.enable_cron = Rails.env.production?
46
+ config.good_job.cron = {
47
+ purge_old_sessions: {
48
+ cron: "0 3 * * *", # every day at 3am
49
+ class: "PurgeOldSessionsJob",
50
+ description: "Delete the user sessions that expired more than a month ago"
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## The dashboard
56
+
57
+ GoodJob's dashboard is mounted at `/admin/good_job`. It sits outside `Admin::BaseController`, so it is guarded in the initializer by the same signed session cookie the admin uses: anyone who is not an administrator gets a 404 rather than a redirect, which keeps the route invisible. The installed request spec covers the three cases (administrator, privileged non-administrator, visitor).
58
+
59
+ ## Mail deliveries are retried
60
+
61
+ GoodJob discards a failed job rather than retrying it, so a transient error from the mail service would lose the email. Every `deliver_later` goes through `MailDeliveryJob` instead, which retries five times with a growing delay:
62
+
63
+ ```ruby
64
+ class MailDeliveryJob < ActionMailer::MailDeliveryJob
65
+ retry_on StandardError, wait: :polynomially_longer, attempts: 5
66
+ end
67
+ ```
68
+
69
+ An error raised in a scheduler thread, outside a job's own execution, has no retry handler to catch it; `config.good_job.on_thread_error` reports it through `Rails.error` like the rest of the app.
@@ -0,0 +1,219 @@
1
+ # Content editor
2
+
3
+ Trek's content editor is where the admin writes the content of pages, page versions and fragments: headings, lists, quotes, links, images and calls to action, laid out as they will read.
4
+
5
+ It's built on [Tiptap](https://tiptap.dev), an editor framework on top of [ProseMirror](https://prosemirror.net). That's why the editor doesn't save HTML. It saves a structured document: a tree of typed nodes (paragraphs, headings, images…) and marks (bold, links…), each with typed attributes. The site turns that document into HTML when it shows it, with the [`prosemirror`](https://github.com/etaminstudio/prosemirror) gem.
6
+
7
+ Keeping structure rather than markup lets Trek:
8
+
9
+ - **render the content safely:** the HTML is built from known types, so content can't inject markup;
10
+ - **change how a block looks** on the site without touching stored content, for example images rendered by the app's own component;
11
+ - **keep links to pages working** when they move: a link stores the page, not its URL;
12
+ - **read the content as data:** extract its text, search it, check it.
13
+
14
+ This guide follows content from end to end:
15
+
16
+ 1. **Editing:** [using the editor](#using-the-editor) in admin forms, and its panels.
17
+ 2. **Storage:** [how content is stored](#how-content-is-stored), as JSON in a `jsonb` column.
18
+ 3. **Rendering:** [rendering on the site](#rendering-on-the-site), through `Trek::Content.schema`, and [customizing it](#customizing-the-rendering).
19
+
20
+ ## Using the editor
21
+
22
+ Add it to an admin form with `f.content_editor`:
23
+
24
+ ```slim
25
+ = form_with model: [:admin, @book] do |f|
26
+ = f.group :content
27
+ = f.content_editor :content
28
+ ```
29
+
30
+ | Option | Default | Effect |
31
+ | --- | --- | --- |
32
+ | `nodes` | `true` | the toolbar's text buttons: bold, italic, link, clear formatting, line break |
33
+ | `blocks` | `true` | the toolbar's block buttons: paragraph, headings 2 to 4, bullet list, quote, horizontal rule |
34
+ | `floating` | `true` | the menu on an empty line, inserting an image or a prompt |
35
+ | `autofocus` | `false` | puts the cursor at the end of the content on load |
36
+
37
+ Undo and redo are always there. For a short field, such as an introduction, turn the rest off:
38
+
39
+ ```slim
40
+ = f.content_editor :intro, blocks: false, floating: false
41
+ ```
42
+
43
+ Tiptap's Markdown shortcuts work too: typing `1. ` starts an ordered list, and `~~text~~` strikes text through, even though the toolbar has no button for them. The site renders them like the rest.
44
+
45
+ ### Links
46
+
47
+ The link button opens a panel to pick a page of the site, or type any URL. A link to a page is stored as its GlobalID (`gid://app/Page/12`) rather than its URL. It's turned into a URL when the content is rendered, so the link survives the page moving or changing its slug (see [Links to records with GlobalID](https://github.com/etaminstudio/prosemirror/blob/main/COOKBOOK.md#links-to-records-with-globalid) in the `prosemirror` gem's cookbook).
48
+
49
+ ### Images
50
+
51
+ The image entry of the floating menu opens a panel to upload an image (with Uppy, stored by Shrine's `ModelImageUploader`). The panel also sets its alternative text, caption, credit and size (`default` or `larger`). The node keeps the uploaded file's data, so the image belongs to the content, not to a record.
52
+
53
+ ### Prompts
54
+
55
+ The prompt entry inserts a call to action: a text, a link (a page or a URL, as for links), where it opens, and a title.
56
+
57
+ ### Panels
58
+
59
+ The link, image and prompt panels are the `Admin::Panels::*` controllers that the installer generates (see [Controllers](/reference/controllers)).
60
+
61
+ ## How content is stored
62
+
63
+ The editor saves a ProseMirror document in the form's field, as JSON, with Tiptap's camelCase type names:
64
+
65
+ ```json
66
+ {
67
+ "type": "doc",
68
+ "content": [
69
+ { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Opening hours" }] },
70
+ {
71
+ "type": "paragraph",
72
+ "content": [
73
+ { "type": "text", "text": "See the " },
74
+ { "type": "text", "text": "map", "marks": [{ "type": "link", "attrs": { "href": "gid://app/Page/12" } }] }
75
+ ]
76
+ },
77
+ { "type": "imageBlock", "attrs": { "file_data": "{\"id\":\"…\",\"storage\":\"store\"}", "alt": "The entrance", "text": "Main entrance", "credit": "Photo: A. Smith", "size": "larger" } }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ - **Column:** Trek's models keep it in a `jsonb` `content` column. [Translatable](/reference/concerns/translatable) models hold one document per locale.
83
+ - **Cleaning:** before saving, [Contentable](/reference/concerns/contentable) removes what rendering skips: empty paragraphs, empty list items, and lists left empty. An empty document keeps one paragraph, and whatever the schema doesn't know is kept. The editor submits the content as a JSON string, and it's stored as a document. Empty paragraphs used for spacing are removed for good.
84
+ - **Typography:** [Formattable](/reference/concerns/formattable) works on documents, changing only their text nodes. Add `:content` to the generated `format_attributes Trek::TypographyFormatter, :title, :description` to apply French typography to it.
85
+ - **Search:** to find content by its words, search the text nodes rather than the JSON, or every document matches `paragraph`. `content_text` gives the plain text, and `Fragment.search` shows a query in SQL (see [Fragment](/reference/models/fragment)).
86
+
87
+ ## Rendering on the site
88
+
89
+ A model with a `content` column includes [Contentable](/reference/concerns/contentable):
90
+
91
+ ```slim
92
+ / ERB or Slim
93
+ = @page.formatted_content
94
+ ```
95
+
96
+ ```ruby
97
+ # Phlex
98
+ render @page.content_document.to_phlex
99
+ ```
100
+
101
+ For a [versioned](/reference/concerns/versionable) page, render its live version's content, `current_content`, which comes from a `PageVersion`: `Trek::Content::Document.parse(@page.current_version || @page, @page.current_content).to_html`.
102
+
103
+ `formatted_content` only reads the `content` column. To render another column, such as a book's `intro`, parse it with `Trek::Content::Document`. Its first argument is the record holding the content: when that content is invalid, the error reports name the record (`trek_content: "Book#3"`, see [Invalid content](#invalid-content)), so you know which one to fix.
104
+
105
+ ```ruby
106
+ Trek::Content::Document.parse(book, book.intro).to_html
107
+ ```
108
+
109
+ ### What the HTML contains
110
+
111
+ - **Escaping:** HTML is escaped as it is built. Nothing a user types can open a tag.
112
+ - **Attributes** are typed: a value that doesn't fit, such as a heading level of 9, is replaced by its default, or removed.
113
+ - **URLs:** links and images only allow `http`, `https`, `mailto`, `tel` and relative URLs.
114
+ - **Links to pages** become the page's URL, through `url_for`. The records a document links to load together, with one query per model. A link to a deleted record is removed, and its text kept.
115
+ - **Empty nodes:** empty paragraphs, empty list items, and lists with only empty items render nothing ([Skip empty paragraphs](https://github.com/etaminstudio/prosemirror/blob/main/COOKBOOK.md#skip-empty-paragraphs)). Saving removes them from the stored content too ([cleaning](#how-content-is-stored)), but content saved before Trek did is only cleaned when it's saved again.
116
+ - **Images** render as a `<figure>` with a `data-size` attribute, with the caption and the credit in its `<figcaption>`.
117
+ - **Prompts** render as a link.
118
+ - **Unknown attributes,** such as heading ids added by the editor, are kept in the JSON but never rendered. To give headings anchors, see [Heading anchors](https://github.com/etaminstudio/prosemirror/blob/main/COOKBOOK.md#heading-anchors) in the `prosemirror` gem's cookbook.
119
+
120
+ ### Invalid content
121
+
122
+ A document that doesn't fit the schema is fixed as it's read, and each problem is reported to `Rails.error` (`handled: true`):
123
+
124
+ - an attribute of the wrong type is converted when the meaning is clear, or replaced by its default, or its node or mark is removed (a link keeps its text);
125
+ - a node of an unknown type renders only its content;
126
+ - links to deleted records, reported as `Prosemirror::UnresolvedURL`.
127
+
128
+ The report's context names the record holding the content (`trek_content: "Page#12"`), which AppSignal shows as a tag, so a problem leads back to the page to fix.
129
+
130
+ ## Customizing the rendering
131
+
132
+ `Trek::Content.schema` is Tiptap's schema (`Prosemirror::Schema.tiptap(case: :camel)`), with Trek's own node types:
133
+
134
+ | Node type | Role |
135
+ | --- | --- |
136
+ | `Trek::Content::Paragraph` | skips empty paragraphs |
137
+ | `Trek::Content::ListItem` | skips empty list items, with no content or only empty paragraphs |
138
+ | `Trek::Content::BulletList`, `OrderedList` | skip lists with only empty items |
139
+ | `Trek::Content::ImageBlock` | renders image blocks as a `<figure>`: the image, its caption and credit |
140
+ | `Trek::Content::PromptsBlock` | renders prompt blocks as a link |
141
+
142
+ Node and mark types are [Phlex](https://www.phlex.fun) components with [Literal](https://literal.fun) properties (see the [`prosemirror` README](https://github.com/etaminstudio/prosemirror#custom-nodes-and-marks)). To change how one renders, subclass it, and put it in the schema from an initializer. The cookbook's [Render your own components](https://github.com/etaminstudio/prosemirror/blob/main/COOKBOOK.md#render-your-own-components) shows the pattern.
143
+
144
+ ### Images with the app's component
145
+
146
+ For example, images at the app's own sizes:
147
+
148
+ ```ruby
149
+ # app/content/image_block.rb
150
+ class ImageBlock < Trek::Content::ImageBlock
151
+ def view_template
152
+ return unless image
153
+
154
+ render Components::Figure.new(
155
+ src: image.derivation_url(:thumbnail, 930, 1200),
156
+ alt: @alt,
157
+ caption: @text,
158
+ credit: @credit
159
+ )
160
+ end
161
+ end
162
+ ```
163
+
164
+ ```ruby
165
+ # config/initializers/trek.rb
166
+ Rails.application.config.to_prepare do
167
+ Trek::Content.schema = Trek::Content.default_schema.with(ImageBlock)
168
+ end
169
+ ```
170
+
171
+ - **Registering:** `with` replaces the type of the same name, and keeps the others.
172
+ - **Reloading:** the default schema is rebuilt after code reloading, so `to_prepare` sees the reloaded classes in development.
173
+ - **Inside a node type:**
174
+ - the node's attributes are instance variables (`@alt`, `@text`);
175
+ - `image` is the uploaded file, or `nil` when the data is missing or invalid;
176
+ - `safe_url(url)` checks a URL against the allowed schemes.
177
+
178
+ ### Prompts as the app's buttons
179
+
180
+ ```ruby
181
+ class PromptsBlock < Trek::Content::PromptsBlock
182
+ def view_template
183
+ href = @href.present? && safe_url(@href)
184
+ return if !href || @text.blank?
185
+
186
+ render Components::Button.new(href:, target: @target, title: @title) { @text }
187
+ end
188
+ end
189
+ ```
190
+
191
+ Register it the same way: `Trek::Content.default_schema.with(ImageBlock, PromptsBlock)`.
192
+
193
+ ### Node types of the app's own
194
+
195
+ A node of a type the schema doesn't know renders only its content, and is reported. To render it, add a class of that name to the schema (`Callout` for `callout`):
196
+
197
+ ```ruby
198
+ class Callout < Prosemirror::NodeType
199
+ group :block
200
+ content "block+"
201
+
202
+ prop :tone, _Union?("info", "warning")
203
+
204
+ def view_template(&) = aside(data_tone: @tone, &)
205
+ end
206
+ ```
207
+
208
+ Trek's editor has a fixed set of Tiptap extensions, so it can't insert such a node yet. It renders nodes that come from elsewhere: imported content, or JSON written by a script.
209
+
210
+ ## Importing content
211
+
212
+ The `prosemirror` gem builds documents from HTML (with the `herb` gem), Markdown (with `commonmarker`) or plain text, using the schema's parse rules. To fill pages from an older site:
213
+
214
+ ```ruby
215
+ document = Prosemirror.from_html(old_html, schema: Trek::Content.schema)
216
+ page.update!(content: document.to_h)
217
+ ```
218
+
219
+ `to_h` gives the JSON the editor reads, with invalid parts fixed. See the [`prosemirror` README](https://github.com/etaminstudio/prosemirror#html-import) for what each import keeps.
data/docs/customize.md CHANGED
@@ -48,6 +48,8 @@ Trek's UI components accept options where it matters:
48
48
  f.content_editor :intro, nodes: false, floating: false
49
49
  ```
50
50
 
51
+ To render content blocks with your own components, see [Customizing the rendering](/content-editor#customizing-the-rendering).
52
+
51
53
  - **`Trek::LayoutComponent`** — use the `panel` slot to add your own side panel to admin screens.
52
54
 
53
55
  ## Models
data/docs/deploy.md CHANGED
@@ -17,7 +17,7 @@ Compared with the stock buildpacks, deploys typically go from around 3 minutes t
17
17
  Generate the Scalingo files (an `Aptfile` and a `Procfile` with the web process and post-deploy migrations):
18
18
 
19
19
  ```sh
20
- rails g trek:install:scalingo
20
+ rails g trek:scalingo
21
21
  ```
22
22
 
23
23
  Then configure the Scalingo app's environment, pinning the buildpack to a release through its archive:
@@ -57,7 +57,7 @@ Before going live, check what Trek expects from the hosting: backups, a shared c
57
57
 
58
58
  Apps deployed with the stock `.buildpacks` (apt + ruby) migrate in three steps:
59
59
 
60
- 1. delete `.buildpacks` (re-running `rails g trek:install:scalingo` does it) and check that `.node-version` exists — apps generated with `rails new -j esbuild` have it
60
+ 1. delete `.buildpacks` (re-running `rails g trek:scalingo` does it) and check that `.node-version` exists — apps generated with `rails new -j esbuild` have it
61
61
  2. set `BUILDPACK_URL` as above, and map `DATABASE_URL` if the app doesn't read it yet
62
62
  3. deploy — the first build fills the caches, the next ones benefit from them
63
63
 
@@ -132,4 +132,4 @@ Next steps:
132
132
 
133
133
  - [Customize Trek](/customize) — branding, favicon, components
134
134
  - [Reference manual](/reference/) — generators, models, components and more
135
- - [Deploying to Scalingo](/deploy) — `trek:install:scalingo`, a few environment variables, and the dedicated trek-buildpack handles the rest
135
+ - [Deploying to Scalingo](/deploy) — `trek:scalingo`, a few environment variables, and the dedicated trek-buildpack handles the rest
data/docs/index.md CHANGED
@@ -23,7 +23,7 @@ features:
23
23
  details: One installer sets up authentication, authorization, file uploads, emails, i18n and a polished admin UI — with sensible conventions you can override later.
24
24
  - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 6H30V30A12 12 0 0 1 18 42H6ZM12 14H24V18H12ZM12 22H20V26H12Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M34 6H42V32A4 4 0 0 1 34 32Z"/></svg>'
25
25
  title: Modern content editing
26
- details: A Tiptap-based rich text editor, hierarchical pages with versioning and SEO-friendly paths, plus reusable content fragments.
26
+ details: A Tiptap-based content editor, hierarchical pages with versioning and SEO-friendly paths, plus reusable content fragments.
27
27
  - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 18H30V30H42V42H6Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M6 18V6A12 12 0 0 1 18 18Z"/></svg>'
28
28
  title: Scaffold-driven development
29
29
  details: One command generates the model, admin panel, views, policy, routes and locales for any resource — just like Rails scaffolding, but for your back-office.
@@ -47,10 +47,10 @@ Rendered by the [form builder](/reference/forms) helpers, under `Trek::Form::`:
47
47
 
48
48
  ## Content editor
49
49
 
50
- `Trek::Form::ContentEditorComponent` wraps the [Tiptap](https://tiptap.dev) editor and accepts three options — `nodes`, `blocks` and `floating` (all enabled by default) — to toggle the node toolbar, block formatting and floating menu:
50
+ `Trek::Form::ContentEditorComponent` wraps the [Tiptap](https://tiptap.dev) editor and accepts `nodes`, `blocks` and `floating` (all enabled by default) to toggle the text buttons, the block buttons and the floating menu, and `autofocus`:
51
51
 
52
52
  ```ruby
53
53
  f.content_editor :intro, nodes: false, floating: false
54
54
  ```
55
55
 
56
- Content is stored as ProseMirror JSON and rendered with [`formatted_content`](/reference/concerns/contentable). Image uploads (Uppy), link insertion and prompts are served by the [panel controllers](/reference/controllers).
56
+ Content is stored as ProseMirror JSON and rendered with [`formatted_content`](/reference/concerns/contentable). Image uploads (Uppy), link insertion and prompts are served by the [panel controllers](/reference/controllers). See the [Content editor](/content-editor) guide for its toolbar, how content is stored and rendered, and how to customize it.
@@ -1,6 +1,6 @@
1
1
  # Trek::Contentable
2
2
 
3
- Utilities for rich text stored as ProseMirror JSON in a `content` column — the format produced by Trek's [content editor](/reference/components#content-editor).
3
+ Utilities for content stored as ProseMirror JSON in a `content` column — the format produced by Trek's [content editor](/content-editor).
4
4
 
5
5
  ## Methods
6
6
 
@@ -8,8 +8,8 @@ Utilities for rich text stored as ProseMirror JSON in a `content` column — the
8
8
  | --- | --- |
9
9
  | `parsed_content` | the content as a hash (parsing JSON strings if needed) |
10
10
  | `content_document` | the content as a `Prosemirror::Document`, to query or render it |
11
- | `formatted_content` | safe HTML, rendered as described in [Rich text rendering](/reference/formatters#rich-text-rendering) |
12
- | `content_text` | plain text extracted from all text nodes |
11
+ | `formatted_content` | safe HTML, rendered as described in [Rendering on the site](/content-editor#rendering-on-the-site) |
12
+ | `content_text` | the plain text of the content: blocks separated by a blank line, hard breaks by a newline |
13
13
  | `prosemirror_content?` | whether `content` is a valid ProseMirror document |
14
14
 
15
15
  ## Usage
@@ -23,7 +23,13 @@ page.formatted_content # => "<p>Hello <strong>world</strong></p>"
23
23
  page.content_text # => "Hello world"
24
24
  ```
25
25
 
26
+ Content in another column renders through the same schema: `Trek::Content::Document.parse(record, record.intro).to_html`. See the [Content editor](/content-editor) guide for the whole path, from the editor to the HTML.
27
+
28
+ ## Cleaning before saving
29
+
30
+ Before validation, empty paragraphs, empty list items and lists left empty are removed from the content, as rendering skips them (`Trek::Content::Cleaner`). The document stays one the editor opens: an empty document keeps one paragraph, a list item keeps the first paragraph it needs, and node types or attributes the schema doesn't know are kept. Content submitted as a JSON string, as the content editor sends it, is stored as a document. Translated content is cleaned in the current locale.
31
+
26
32
  ## Requirements
27
33
 
28
34
  - A `content` jsonb column
29
- - Used by [Page](/reference/models/page), [PageVersion](/reference/models/page-version) and [Fragment](/reference/models/fragment)
35
+ - Used by [Page](/reference/models/page) and [Fragment](/reference/models/fragment)
@@ -21,4 +21,4 @@ page.valid?
21
21
  page.title # => "Un titre : voici…"
22
22
  ```
23
23
 
24
- Works on plain strings **and** on ProseMirror documents (only `text` nodes are touched; the structure is preserved).
24
+ Works on plain strings **and** on ProseMirror documents (only `text` nodes are touched; the structure is preserved), including a document submitted by the content editor as a JSON string, which comes back as a Hash.
@@ -9,5 +9,6 @@
9
9
  | `APPSIGNAL_APP_ENV` | environment name reported to AppSignal (optional, defaults to `RAILS_ENV`) |
10
10
  | `DEEPL_AUTH_KEY` | automatic locale translation in the [scaffold generator](/reference/generators/scaffold) |
11
11
  | `FEATURE_AUDIT_LOG` | `false` stops the [audit log](/reference/generators/audit-log) recording requests (on by default) |
12
+ | `GOOD_JOB_MAX_THREADS` | number of job threads GoodJob runs, and the share of the database pool reserved for them (optional, defaults to `RAILS_MAX_THREADS`) — see [Background jobs](/background-jobs) |
12
13
  | `FEATURE_STORAGE` | `local` (development) or `s3` (production) |
13
14
  | `S3_BUCKET`, `S3_REGION`, `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_BASE_URL` | S3 storage configuration for [uploads](/reference/uploaders) |
@@ -18,36 +18,8 @@ format_attributes Trek::TypographyFormatter, :title
18
18
 
19
19
  ## Rich text rendering
20
20
 
21
- `formatted_content` (from [Contentable](/reference/concerns/contentable)) renders the content editor's ProseMirror JSON to HTML with the [`prosemirror`](https://github.com/etaminstudio/prosemirror) gem. HTML is escaped as it is built, attributes are typed, and links and images only allow `http`, `https`, `mailto`, `tel` and relative URLs. Invalid content is fixed and reported to `Rails.error`.
22
-
23
- `Trek::Content.schema` is Tiptap's schema, with:
24
-
25
- | Node type | Role |
26
- | --- | --- |
27
- | `Trek::Content::Paragraph` | skips empty paragraphs |
28
- | `Trek::Content::ListItem` | skips empty list items, with no content or only empty paragraphs |
29
- | `Trek::Content::BulletList`, `OrderedList` | skip lists with only empty items |
30
- | `Trek::Content::ImageBlock` | renders image blocks as a `<figure>`: the image, its caption and credit |
31
- | `Trek::Content::PromptsBlock` | renders prompt blocks as a link |
32
-
33
- Links to records are stored as GlobalIDs (`gid://app/Page/1`). `Trek::Content::LinkResolver` turns them into the record's URL with `url_for`, loading the records of a document's links at once, with one query per model; a link to a deleted record is removed and its text kept, and reported to `Rails.error` as a `Prosemirror::UnresolvedURL`. Problems in content are reported with the record holding it in their context (`trek_content: "Page#12"`), which AppSignal shows as a tag.
21
+ `formatted_content` (from [Contentable](/reference/concerns/contentable)) renders the content editor's ProseMirror JSON to HTML with the [`prosemirror`](https://github.com/etaminstudio/prosemirror) gem, through `Trek::Content.schema`: HTML is escaped as it is built, attributes are typed, links and images only allow safe URLs, and links to pages are resolved. See [Rendering on the site](/content-editor#rendering-on-the-site) for what the HTML contains, and how invalid content is reported.
34
22
 
35
23
  ### Custom blocks
36
24
 
37
- Node types are [Phlex](https://www.phlex.fun) components with [Literal](https://literal.fun) properties (see the [`prosemirror` README](https://github.com/etaminstudio/prosemirror#custom-nodes-and-marks)). To render the image block with the app's own component, subclass it and register it in an initializer:
38
-
39
- ```ruby
40
- # app/content/image_block.rb
41
- class ImageBlock < Trek::Content::ImageBlock
42
- def view_template
43
- render Components::Figure.new(src: image&.url, alt: @alt, caption: @text, credit: @credit)
44
- end
45
- end
46
-
47
- # config/initializers/trek.rb
48
- Rails.application.config.to_prepare do
49
- Trek::Content.schema = Trek::Content.default_schema.with(ImageBlock)
50
- end
51
- ```
52
-
53
- The same works for a node type of your own, added to the editor with a Tiptap extension.
25
+ Trek's node types (`Trek::Content::ImageBlock`, `PromptsBlock`…) can be subclassed to render the app's own components, and registered with `Trek::Content.schema = Trek::Content.default_schema.with(…)`: see [Customizing the rendering](/content-editor#customizing-the-rendering).
@@ -5,7 +5,7 @@ Trek ships `Trek::FormBuilder`, an extension of [`ViewComponent::Form`](https://
5
5
  | Helper | Renders |
6
6
  | --- | --- |
7
7
  | `f.group` | a label + input wrapper with errors |
8
- | `f.content_editor` | the Tiptap rich text editor |
8
+ | `f.content_editor` | the [content editor](/content-editor), based on Tiptap |
9
9
  | `f.image_field` | an image uploader with thumbnail preview (Uppy + Shrine) |
10
10
  | `f.sound_field` | an audio file uploader |
11
11
  | `f.switch_box` | a toggle switch |
@@ -1,7 +1,7 @@
1
1
  # Audit log
2
2
 
3
3
  ```sh
4
- rails g trek:install:audit_log
4
+ rails g trek:audit_log
5
5
  bin/rails db:migrate
6
6
  ```
7
7
 
@@ -108,6 +108,6 @@ The rows hold personal data: the author, the IP address, the user agent, and wha
108
108
 
109
109
  The `audit_logs` table uses the column names of the audit logs some apps wrote themselves (`actor`, `auditable`, `action`, `http_method`, `path`, `http_status`, `params`, `ip_address`, `user_agent`, `request_id`, `error_class`, `created_at`), so existing rows carry over. In such an app, skip the migration and replace the app's own files:
110
110
 
111
- 1. Run `rails g trek:install:audit_log`, delete the generated `db/migrate/*_trek_audit_logs.rb`, and let the generator overwrite the model, controller, views and policy.
111
+ 1. Run `rails g trek:audit_log`, delete the generated `db/migrate/*_trek_audit_logs.rb`, and let the generator overwrite the model, controller, views and policy.
112
112
  2. Delete the app's `Auditable` concern, its initializer and its purge job, and update the job's cron entry to `Trek::AuditLog::PurgeJob`.
113
113
  3. Move app-specific columns (e.g. a request duration, or a denormalized foreign key) to `audit_attributes`, and their scopes and associations to the `AuditLog` model.
@@ -15,6 +15,8 @@ Trek provides an install generator, that will:
15
15
  - create the relevant policies
16
16
  - generate and run the relevant migrations
17
17
 
18
+ It ends with what to do next: create an admin user, start the server, and the generators it leaves for later, to run as the app needs them: [`trek:scaffold`](/reference/generators/scaffold), [`trek:taxonomies`](/reference/generators/taxonomies), [`trek:audit_log`](/reference/generators/audit-log), [`trek:public_accounts`](/reference/generators/public-accounts) and [`trek:scalingo`](/deploy).
19
+
18
20
  ## Sub-generators
19
21
 
20
22
  The installer orchestrates dedicated sub-generators, all under the `trek:install:*` namespace. Each one can also be run individually, e.g. `rails g trek:install:healthcheck`.
@@ -32,12 +34,11 @@ The installer orchestrates dedicated sub-generators, all under the `trek:install
32
34
  | Tooling | esbuild, PostCSS, EditorConfig, Procfiles, Makefile, health check endpoint |
33
35
  | Linting | Standard, ESLint, Stylelint, Prettier, Slim Lint, Lefthook git hooks |
34
36
  | Security | Brakeman, bundler-audit, ruby_audit; HTTPS only (`force_ssl`, with HSTS), a Content Security Policy and the Permissions-Policy header (`trek:install:security_headers`) — see [Security headers](/reference/generators/security-headers) |
37
+ | Jobs | GoodJob as the Active Job backend, replacing Solid Queue — dashboard at `/admin/good_job`, cron in `config/initializers/good_job.rb`, pool sized by `GOOD_JOB_MAX_THREADS` — see [Background jobs](/background-jobs) |
35
38
  | Monitoring | AppSignal error tracking and performance monitoring — `config/appsignal.rb`, the push API key in the credentials, and the `AppsignalContext` concern tagging transactions per area and user |
36
39
  | Specs | RSpec, and specs for the models Trek installs (`trek:install:model_specs`): `spec/models/page_spec.rb` and `spec/models/user_spec.rb`, with their test data as [Oaken](https://github.com/kaspth/oaken) seed scripts — defaults in `db/seeds/setup/`, scenarios in `db/seeds/test/cases/` loaded with `seed "cases/…"`. They check Trek's behaviour in your app — slugs and paths, user validations and roles — so a customization that breaks it fails your suite; edit or remove the ones you deliberately change. Trek's own suite runs the same files |
37
40
  | N+1 detection | Prosopite (development/test), scanning every request — see [Controllers › N+1 detection](/reference/controllers#n-1-detection) |
38
41
  | CI | GitLab CI configuration: specs, linters, Brakeman, bundler-audit, and, commented out, a [Software Bill of Materials](#software-bill-of-materials) |
39
- | Deployment | Scalingo files (`trek:install:scalingo`, run on demand) — see [Deploying to Scalingo](/deploy) |
40
- | Audit log | The `AuditLog` model recording the admin's requests, and its admin list (`trek:install:audit_log`, run on demand) — see [Audit log](/reference/generators/audit-log) |
41
42
 
42
43
  ## ENV variables read by the installer
43
44