plutonium 0.68.0 → 0.70.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-async-interactions/SKILL.md +41 -2
  3. data/.claude/skills/plutonium-resource/SKILL.md +13 -0
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +37 -0
  6. data/CLAUDE.md +11 -0
  7. data/SECURITY.md +1 -1
  8. data/app/assets/plutonium.css +3 -1
  9. data/app/assets/plutonium.png +0 -0
  10. data/config/locales/en/async.yml +11 -1
  11. data/config/locales/en/ui.yml +3 -0
  12. data/docs/blog/introducing-plutonium-dashboards.md +4 -4
  13. data/docs/blog/the-slowest-query-is-the-count.md +109 -0
  14. data/docs/guides/performance.md +14 -0
  15. data/docs/public/favicon-16x16.png +0 -0
  16. data/docs/public/favicon-32x32.png +0 -0
  17. data/docs/public/images/blog/i18n-es-admin.png +0 -0
  18. data/docs/public/images/blog/pagination-capped.png +0 -0
  19. data/docs/public/images/blog/pagination-countless.png +0 -0
  20. data/docs/public/images/components/avatar.png +0 -0
  21. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  22. data/docs/public/images/guides/dashboard-card-error.png +0 -0
  23. data/docs/public/images/guides/dashboard-loading.png +0 -0
  24. data/docs/public/images/guides/dashboard-overview.png +0 -0
  25. data/docs/public/images/guides/dashboard-sidebar.png +0 -0
  26. data/docs/public/images/guides/kanban-after-move.png +0 -0
  27. data/docs/public/images/guides/kanban-board-light.png +0 -0
  28. data/docs/public/images/guides/kanban-board.png +0 -0
  29. data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
  30. data/docs/public/images/guides/kanban-wip-toast.png +0 -0
  31. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  32. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  33. data/docs/public/images/guides/nested-inputs.png +0 -0
  34. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  35. data/docs/public/images/guides/search-filtering-index.png +0 -0
  36. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  37. data/docs/public/images/guides/theming-after.png +0 -0
  38. data/docs/public/images/guides/theming-before.png +0 -0
  39. data/docs/public/images/guides/user-invites-landing.png +0 -0
  40. data/docs/public/images/guides/user-profile-edit.png +0 -0
  41. data/docs/public/images/guides/user-profile-show.png +0 -0
  42. data/docs/public/images/guides/wizards-chooser.png +0 -0
  43. data/docs/public/images/guides/wizards-completed.png +0 -0
  44. data/docs/public/images/guides/wizards-index-action.png +0 -0
  45. data/docs/public/images/guides/wizards-repeater.png +0 -0
  46. data/docs/public/images/guides/wizards-review.png +0 -0
  47. data/docs/public/images/guides/wizards-step.png +0 -0
  48. data/docs/public/images/home/tour-actions.png +0 -0
  49. data/docs/public/images/home/tour-async.png +0 -0
  50. data/docs/public/images/home/tour-kanban.png +0 -0
  51. data/docs/public/images/home/tour-tenancy.png +0 -0
  52. data/docs/public/images/home/tour-wizard.png +0 -0
  53. data/docs/public/images/home-index.png +0 -0
  54. data/docs/public/images/home-new.png +0 -0
  55. data/docs/public/images/home-show.png +0 -0
  56. data/docs/public/images/reference/async-progress-page.png +0 -0
  57. data/docs/public/images/reference/async-running-banner.png +0 -0
  58. data/docs/public/images/reference/structured-inputs-removed.png +0 -0
  59. data/docs/public/images/reference/structured-inputs.png +0 -0
  60. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  61. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  62. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  63. data/docs/public/images/tutorial/02-new-form.png +0 -0
  64. data/docs/public/images/tutorial/03-create-account.png +0 -0
  65. data/docs/public/images/tutorial/03-login.png +0 -0
  66. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  67. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  68. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  69. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  70. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  71. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  72. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  73. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  74. data/docs/public/og-image.png +0 -0
  75. data/docs/public/plutonium.png +0 -0
  76. data/docs/public/tutorial/plutonium-association-panel.png +0 -0
  77. data/docs/public/tutorial/plutonium-dashboard.png +0 -0
  78. data/docs/public/tutorial/plutonium-login-page.png +0 -0
  79. data/docs/public/tutorial/plutonium-nested-form.png +0 -0
  80. data/docs/public/tutorial/plutonium-posts-dashboard-customized.png +0 -0
  81. data/docs/public/tutorial/plutonium-posts-dashboard.png +0 -0
  82. data/docs/public/tutorial/plutonium-posts-detail-customized.png +0 -0
  83. data/docs/public/tutorial/plutonium-posts-detail.png +0 -0
  84. data/docs/public/tutorial/plutonium-publish-post.png +0 -0
  85. data/docs/reference/behavior/async-interactions.md +62 -5
  86. data/docs/reference/configuration.md +1 -0
  87. data/docs/reference/generators/lite.md +30 -6
  88. data/docs/reference/i18n.md +2 -2
  89. data/docs/reference/resource/definition.md +35 -0
  90. data/docs/reference/ui/layouts.md +13 -0
  91. data/docs/superpowers/plans/2026-10-15-async-run-pre-check.md +1056 -0
  92. data/docs/superpowers/plans/2026-10-15-async-run-pre-check.md.tasks.json +49 -0
  93. data/docs/superpowers/specs/2026-10-15-async-run-pre-check-design.md +208 -0
  94. data/gemfiles/rails_7.gemfile.lock +5 -5
  95. data/gemfiles/rails_8.0.gemfile.lock +3 -3
  96. data/gemfiles/rails_8.1.gemfile.lock +3 -3
  97. data/lib/generators/pu/lite/maintenance/maintenance_generator.rb +20 -1
  98. data/lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt +18 -1
  99. data/lib/plutonium/configuration.rb +17 -0
  100. data/lib/plutonium/definition/base.rb +22 -0
  101. data/lib/plutonium/definition/presentable.rb +4 -1
  102. data/lib/plutonium/interaction/async/configuration.rb +15 -0
  103. data/lib/plutonium/interaction/async/context.rb +12 -2
  104. data/lib/plutonium/interaction/async/executor.rb +54 -0
  105. data/lib/plutonium/interaction/async/run.rb +128 -14
  106. data/lib/plutonium/interaction/async/run_definition.rb +5 -4
  107. data/lib/plutonium/interaction/async/run_policy.rb +5 -5
  108. data/lib/plutonium/interaction/response/redirect.rb +19 -7
  109. data/lib/plutonium/pagination.rb +35 -0
  110. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +27 -1
  111. data/lib/plutonium/ui/breadcrumbs.rb +26 -8
  112. data/lib/plutonium/ui/grid/card.rb +11 -2
  113. data/lib/plutonium/ui/grid/resource.rb +2 -2
  114. data/lib/plutonium/ui/interaction/async/run_progress.rb +25 -11
  115. data/lib/plutonium/ui/interaction/async/running_banner.rb +21 -5
  116. data/lib/plutonium/ui/layout/base.rb +0 -9
  117. data/lib/plutonium/ui/layout/icon_rail.rb +42 -4
  118. data/lib/plutonium/ui/page/index.rb +23 -4
  119. data/lib/plutonium/ui/table/components/filter_pills.rb +8 -2
  120. data/lib/plutonium/ui/table/components/pagy_info.rb +30 -10
  121. data/lib/plutonium/ui/table/resource.rb +2 -2
  122. data/lib/plutonium/version.rb +1 -1
  123. data/package.json +1 -1
  124. data/plutonium.gemspec +3 -1
  125. data/src/css/components.css +12 -1
  126. data/src/css/font_awesome.css +2328 -0
  127. data/src/css/plutonium.css +1 -0
  128. metadata +11 -7
  129. data/docs/public/images/blog/dashboards-card-error.png +0 -0
  130. data/docs/public/images/blog/dashboards-loading.png +0 -0
  131. data/docs/public/images/blog/dashboards-overview.png +0 -0
  132. data/docs/public/images/blog/dashboards-sidebar.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a01423b0e39c2e985ec24ea8663cf53377817ff24814c95ce89270806f51b878
4
- data.tar.gz: cbe95fbf1cd307e3b07cccf24cda7db7293dc15676c0ee288fe076d37102cb15
3
+ metadata.gz: 36351840fe45e3c1d51694397c480dc641bb6edb4b7d32b38149b2af742d317a
4
+ data.tar.gz: dad970de51f6f04a961cae9bf4c50ea35f6b76601371cae3787b3c5e3031dfbf
5
5
  SHA512:
6
- metadata.gz: a2034d1af622ec4bfbc4c6ac00c46ed63f4a7ea6ed878ebc78b9666b109cf572dff20f60da0bc9f5e5adafdb992e76deb292c4ec08146684e3828732daf2ced7
7
- data.tar.gz: '09f8b0ef419d7adf5df3057a271ed4659140e4308c4b7ea5ea5965554256e2c609314ecb6cd33893629773b9fab97f735b1e16872f08bb8784445cc80049a214'
6
+ metadata.gz: d42e71c6b6c9a94643af9f394f9467283d0465ef25cb1862a3be8dc0a9f8b94f4592398f55a1816a55fd9e1d1412b6112a0cb11fc842c1366ecba27963f7b34a
7
+ data.tar.gz: 88f941e7429c0ef0469d104de914a6e872153a64317f2177329c7f34acbc9b90895846f351fe8d38f77f52fa8198e70ea4512b3eebe728710835f1849b556c8f
@@ -19,7 +19,7 @@ For everything about the interaction itself (inputs, validation, outcomes, `exec
19
19
  - **Permissions are re-derived at perform time, never replayed from dispatch.** The job rebuilds `(initiator, tenant)` from the row and re-checks the policy scope and predicate per target, immediately before each `perform_on`. A permission revoked mid-run stops applying to what's left. Both failure directions (scope, predicate) fail closed (refuse/report), never open.
20
20
  - **Register `Run` as a resource per portal.** Its show page IS the progress page, and other resources' index pages get a "runs in progress" banner for free. Nothing renders without registration.
21
21
  - **Read `outcome`, never bare `state`, when displaying a run's result.** A `:continue` run that under-applied still has `state == "completed"`; only `outcome` says `"completed_with_errors"`.
22
- - **Long work must call `heartbeat!`.** `stall_after` measures SILENCE, and the executor only writes per target, so opaque `perform` writes nothing at all between claim and finish. An opaque run longer than `stall_after` is reaped and, having no `handled_target_ids`, re-run from scratch. Call `heartbeat!` inside the loop. It also raises `StaleObjectError` if another worker took the run over, which for opaque work is the only way to find out.
22
+ - **Long work must call `heartbeat!`.** `stall_after` measures SILENCE, and the executor only writes per target, so opaque `perform` writes nothing at all between claim and finish. The same holds for `pre_check`. An opaque run longer than `stall_after` is reaped and, having no `handled_target_ids`, re-run from scratch. Call `heartbeat!` inside the loop. It also raises `StaleObjectError` if another worker took the run over, which for opaque work is the only way to find out.
23
23
  - **A crashed/stalled run does not auto-heal.** Nothing revisits a `"running"` row on its own. `pu:async_interactions:install` schedules `Plutonium::Interaction::Async::ReapJob` for you when Solid Queue is in the bundle; otherwise (or on another scheduler) you must schedule it yourself. Unscheduled, a crash mid-batch leaves that row stuck forever.
24
24
 
25
25
  ---
@@ -114,6 +114,44 @@ end
114
114
 
115
115
  `backend:` and `uploader:` come off the attribute's `input` declaration, exactly as in a wizard step: `input :import_file, as: :uppy, uploader: Catalog::ImportUploader`. The uploader's `Attacher.validate` rules run when the interaction validates, so a bad file **fails the form** rather than surfacing as a run failure the submitter never sees. Where no `backend:` is declared: `config.async_interactions.attachment_backend` → `config.attachment_backend` → auto-detect.
116
116
 
117
+ ## Checking input before processing (`pre_check`)
118
+
119
+ For checks too slow for the request (every row of a large file, lookups per row), define `pre_check` in the run. It runs in the job before any work; `reject` records a problem and keeps going (`return` yourself where nothing after it makes sense):
120
+
121
+ ```ruby
122
+ # require "csv" at the top of the file
123
+ async do
124
+ def pre_check # targeted runs: def pre_check(records)
125
+ each_row do |row, line|
126
+ reject "line #{line}: title is missing" if row["title"].blank?
127
+ end
128
+ rescue CSV::MalformedCSVError => e
129
+ reject "the file is not valid CSV: #{e.message}"
130
+ end
131
+
132
+ def perform # one transaction: an opaque run restarts from scratch after a reap
133
+ Task.transaction { each_row { |row, _line| Task.create!(title: row["title"]) } }
134
+ end
135
+
136
+ private
137
+
138
+ # Whole file, UTF-8, BOM stripped: the stored file comes back as binary.
139
+ def each_row
140
+ text = attachment(:import_file).download.force_encoding(Encoding::UTF_8).delete_prefix("\uFEFF")
141
+ CSV.parse(text, headers: true).each.with_index(2) do |row, line| # header is line 1
142
+ yield row, line unless row.fields.all?(&:blank?)
143
+ end
144
+ end
145
+ end
146
+ ```
147
+
148
+ - Any rejection: run ends `failed`, `outcome == "rejected"`, problems on the progress page, no work done.
149
+ - An exception in `pre_check` is a plain `failed`, not `rejected`. A wrong-arity `pre_check` fails the run naming the expected signature.
150
+ - `config.async_interactions.max_rejections` (default 100) caps stored problems; the rest become one "and N more problems were found" entry.
151
+ - It re-runs on resume, so it must only read (a resumed targeted run skips it once a target was applied). Long checks must `heartbeat!`.
152
+ - `reject` outside `pre_check` raises.
153
+ - Full version: `docs/reference/behavior/async-interactions.md`, "Checking input before processing".
154
+
117
155
  ## Registering the Run resource
118
156
 
119
157
  Use the generator, per portal. Never hand-write the route/controller:
@@ -144,7 +182,8 @@ Registering gets you, for free:
144
182
 
145
183
  - **Progress page.** The show page IS the progress page, self-polling while `pending`/`running`, and stops carrying the poll once settled (a finished run is a static page, not an eternal background request per viewer).
146
184
  - **Self-refreshing index.** The runs index polls on the same terms, one frame around the whole collection, armed only while some run is still working. One request per interval whatever the page size, and it re-fetches the current URL so filters, sort and page survive. No action needed on your part.
147
- - **Running banner.** Any OTHER registered resource's index lists in-progress runs targeting it, above the collection, scoped through the same `authorized_resource_scope` every cross-resource read uses (a run in another tenant can't surface). If a `resource_class` is registered in a portal that never registered `Run`, the banner is skipped there instead of raising while building a link to a nonexistent route. No action needed on your part.
185
+ - **Running banner.** Any OTHER registered resource's index lists in-progress runs targeting it, above the collection, plus the viewer's own runs that finished within `config.async_interactions.recent_runs_window` (default 10 minutes, at most 5) with their outcome, so a fast or rejected run is still visible after the dispatch redirects back to the index. Both are scoped through the same `authorized_resource_scope` every cross-resource read uses (a run in another tenant can't surface). If a `resource_class` is registered in a portal that never registered `Run`, the banner is skipped there instead of raising while building a link to a nonexistent route. No action needed on your part.
186
+ - **`interaction_label`.** A block-declared run (`<Interaction>::Run`) is titled by its interaction's label (`"Archive all (async) #19"`): its `presents label:`, else the class name without the `Interaction` suffix. A standalone run class falls back to its own name. An action-level `label:` or `plutonium.actions.*` translation does NOT reach the run (it does not know which action started it), so label the interaction with `presents label:` when the button and the run should match. The show page/table read `interaction_label` rather than the STI `type`.
148
187
  - **`target_label`.** The show page/table read `run.target_label` (`model_name.human`, e.g. `"Post"`) rather than the raw `target_type` string (`"Blogging::Post"`).
149
188
  - **Tenant scoping via `associated_with`.** Filters on the tenant the run was dispatched in (recorded on the row), because the two tenant columns are polymorphic and the generic object-graph scope can't walk them.
150
189
 
@@ -1358,6 +1358,19 @@ default_sort :created_at, :desc
1358
1358
  default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
1359
1359
  ```
1360
1360
 
1361
+ ## Pagination count
1362
+
1363
+ Each index page load runs a `COUNT(*)` for the total. On a table with millions of rows that count can take longer than the page query itself. Cap it or turn it off per resource:
1364
+
1365
+ ```ruby
1366
+ class TransactionDefinition < ResourceDefinition
1367
+ pagination count: 10_000 # count at most 10,000 rows; past that the total reads "10000+"
1368
+ pagination count: false # no COUNT query; previous/next navigation only
1369
+ end
1370
+ ```
1371
+
1372
+ The global default is `config.pagination = {count: :exact}`. A cap stops counting early only when many rows match: a filter that matches few rows still scans for them, so index the filtered column. Pages past the cap are out of range. To decide per request, override the private `pagination_count` method in the controller.
1373
+
1361
1374
  ## URL Parameters
1362
1375
 
1363
1376
  ```
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.1
1
+ 4.0.9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.70.0] - 2026-10-26
6
+
7
+ ### Bug Fixes
8
+
9
+ - Color meta badges from the field's declared colors
10
+
11
+ ### Features
12
+
13
+ - Pin links above the icon rail's footer with footer_menu
14
+ - Show a menu item's trailing badge in the icon rail
15
+ - Cap or skip the index COUNT with pagination count: ([#188](https://github.com/radioactive-labs/plutonium-core/issues/188))
16
+
17
+ ### Miscellaneous Tasks
18
+
19
+ - Move to Ruby 4.0.9 and test on 3.4 and 4.0 ([#187](https://github.com/radioactive-labs/plutonium-core/issues/187))
20
+ - Compress docs images and shrink the logo ([#189](https://github.com/radioactive-labs/plutonium-core/issues/189))
21
+
22
+ ## [0.69.0] - 2026-10-15
23
+
24
+ ### Bug Fixes
25
+
26
+ - Link collection pages without a record back to the resource index in breadcrumbs
27
+ - Vendor Font Awesome 4 so its .sr-only stops overriding Tailwind utilities
28
+ - Answer turbo-stream redirects with a stream visit, held to the open-redirect check
29
+ - Perform opaque runs dispatched from a resource index
30
+
31
+ ### Features
32
+
33
+ - Drop the Interaction suffix from default labels
34
+ - Name runs by their interaction, show progress as one cell
35
+ - Check a run's input before processing with pre_check
36
+ - Clear finished SolidQueue jobs and gate VACUUM on free pages in pu:lite:maintenance
37
+
38
+ ### Miscellaneous Tasks
39
+
40
+ - Update gemfiles
41
+
5
42
  ## [0.68.0] - 2026-10-14
6
43
 
7
44
  ### Bug Fixes
data/CLAUDE.md CHANGED
@@ -323,6 +323,17 @@ mode, icon rail collapsed (`localStorage.theme = 'light'`,
323
323
  generated titles read as broken data). Crop dead space with `magick`. Store under
324
324
  `docs/public/images/<section>/`.
325
325
 
326
+ Compress every PNG before committing: `docs/` ships inside the gem, so each
327
+ screenshot is in every install.
328
+
329
+ ```bash
330
+ pngquant --quality=80-95 --speed 1 --strip --skip-if-larger --ext .png --force <file>.png
331
+ ```
332
+
333
+ Reuse an existing image rather than copying it into a second folder. Full-size
334
+ brand sources live in `brand/` (excluded from the gem); `app/assets/plutonium.png`
335
+ is the 512px compressed copy.
336
+
326
337
  ## Settled Decisions (do not re-litigate)
327
338
 
328
339
  - **SQLite for the dummy app** across all environments; production is multi-db
data/SECURITY.md CHANGED
@@ -12,7 +12,7 @@ please upgrade before reporting an issue to confirm it still reproduces.
12
12
 
13
13
  | Version | Supported |
14
14
  | ------- | ------------------ |
15
- | Latest release (`0.68.x`) | :white_check_mark: |
15
+ | Latest release (`0.70.x`) | :white_check_mark: |
16
16
  | Older releases | :x: |
17
17
 
18
18
  ## Reporting a Vulnerability