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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium-async-interactions/SKILL.md +41 -2
- data/.claude/skills/plutonium-resource/SKILL.md +13 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +37 -0
- data/CLAUDE.md +11 -0
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +3 -1
- data/app/assets/plutonium.png +0 -0
- data/config/locales/en/async.yml +11 -1
- data/config/locales/en/ui.yml +3 -0
- data/docs/blog/introducing-plutonium-dashboards.md +4 -4
- data/docs/blog/the-slowest-query-is-the-count.md +109 -0
- data/docs/guides/performance.md +14 -0
- data/docs/public/favicon-16x16.png +0 -0
- data/docs/public/favicon-32x32.png +0 -0
- data/docs/public/images/blog/i18n-es-admin.png +0 -0
- data/docs/public/images/blog/pagination-capped.png +0 -0
- data/docs/public/images/blog/pagination-countless.png +0 -0
- data/docs/public/images/components/avatar.png +0 -0
- data/docs/public/images/guides/custom-actions-bulk.png +0 -0
- data/docs/public/images/guides/dashboard-card-error.png +0 -0
- data/docs/public/images/guides/dashboard-loading.png +0 -0
- data/docs/public/images/guides/dashboard-overview.png +0 -0
- data/docs/public/images/guides/dashboard-sidebar.png +0 -0
- data/docs/public/images/guides/kanban-after-move.png +0 -0
- data/docs/public/images/guides/kanban-board-light.png +0 -0
- data/docs/public/images/guides/kanban-board.png +0 -0
- data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
- data/docs/public/images/guides/kanban-wip-toast.png +0 -0
- data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
- data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
- data/docs/public/images/guides/nested-inputs.png +0 -0
- data/docs/public/images/guides/nested-resources-tab.png +0 -0
- data/docs/public/images/guides/search-filtering-index.png +0 -0
- data/docs/public/images/guides/search-filtering-panel.png +0 -0
- data/docs/public/images/guides/theming-after.png +0 -0
- data/docs/public/images/guides/theming-before.png +0 -0
- data/docs/public/images/guides/user-invites-landing.png +0 -0
- data/docs/public/images/guides/user-profile-edit.png +0 -0
- data/docs/public/images/guides/user-profile-show.png +0 -0
- data/docs/public/images/guides/wizards-chooser.png +0 -0
- data/docs/public/images/guides/wizards-completed.png +0 -0
- data/docs/public/images/guides/wizards-index-action.png +0 -0
- data/docs/public/images/guides/wizards-repeater.png +0 -0
- data/docs/public/images/guides/wizards-review.png +0 -0
- data/docs/public/images/guides/wizards-step.png +0 -0
- data/docs/public/images/home/tour-actions.png +0 -0
- data/docs/public/images/home/tour-async.png +0 -0
- data/docs/public/images/home/tour-kanban.png +0 -0
- data/docs/public/images/home/tour-tenancy.png +0 -0
- data/docs/public/images/home/tour-wizard.png +0 -0
- data/docs/public/images/home-index.png +0 -0
- data/docs/public/images/home-new.png +0 -0
- data/docs/public/images/home-show.png +0 -0
- data/docs/public/images/reference/async-progress-page.png +0 -0
- data/docs/public/images/reference/async-running-banner.png +0 -0
- data/docs/public/images/reference/structured-inputs-removed.png +0 -0
- data/docs/public/images/reference/structured-inputs.png +0 -0
- data/docs/public/images/tutorial/02-empty-index.png +0 -0
- data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
- data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
- data/docs/public/images/tutorial/02-new-form.png +0 -0
- data/docs/public/images/tutorial/03-create-account.png +0 -0
- data/docs/public/images/tutorial/03-login.png +0 -0
- data/docs/public/images/tutorial/04-admin-index.png +0 -0
- data/docs/public/images/tutorial/05-actions-menu.png +0 -0
- data/docs/public/images/tutorial/05-row-actions.png +0 -0
- data/docs/public/images/tutorial/06-comments-tab.png +0 -0
- data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
- data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
- data/docs/public/images/tutorial/07-author-portal.png +0 -0
- data/docs/public/images/tutorial/08-customized-index.png +0 -0
- data/docs/public/og-image.png +0 -0
- data/docs/public/plutonium.png +0 -0
- data/docs/public/tutorial/plutonium-association-panel.png +0 -0
- data/docs/public/tutorial/plutonium-dashboard.png +0 -0
- data/docs/public/tutorial/plutonium-login-page.png +0 -0
- data/docs/public/tutorial/plutonium-nested-form.png +0 -0
- data/docs/public/tutorial/plutonium-posts-dashboard-customized.png +0 -0
- data/docs/public/tutorial/plutonium-posts-dashboard.png +0 -0
- data/docs/public/tutorial/plutonium-posts-detail-customized.png +0 -0
- data/docs/public/tutorial/plutonium-posts-detail.png +0 -0
- data/docs/public/tutorial/plutonium-publish-post.png +0 -0
- data/docs/reference/behavior/async-interactions.md +62 -5
- data/docs/reference/configuration.md +1 -0
- data/docs/reference/generators/lite.md +30 -6
- data/docs/reference/i18n.md +2 -2
- data/docs/reference/resource/definition.md +35 -0
- data/docs/reference/ui/layouts.md +13 -0
- data/docs/superpowers/plans/2026-10-15-async-run-pre-check.md +1056 -0
- data/docs/superpowers/plans/2026-10-15-async-run-pre-check.md.tasks.json +49 -0
- data/docs/superpowers/specs/2026-10-15-async-run-pre-check-design.md +208 -0
- data/gemfiles/rails_7.gemfile.lock +5 -5
- data/gemfiles/rails_8.0.gemfile.lock +3 -3
- data/gemfiles/rails_8.1.gemfile.lock +3 -3
- data/lib/generators/pu/lite/maintenance/maintenance_generator.rb +20 -1
- data/lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt +18 -1
- data/lib/plutonium/configuration.rb +17 -0
- data/lib/plutonium/definition/base.rb +22 -0
- data/lib/plutonium/definition/presentable.rb +4 -1
- data/lib/plutonium/interaction/async/configuration.rb +15 -0
- data/lib/plutonium/interaction/async/context.rb +12 -2
- data/lib/plutonium/interaction/async/executor.rb +54 -0
- data/lib/plutonium/interaction/async/run.rb +128 -14
- data/lib/plutonium/interaction/async/run_definition.rb +5 -4
- data/lib/plutonium/interaction/async/run_policy.rb +5 -5
- data/lib/plutonium/interaction/response/redirect.rb +19 -7
- data/lib/plutonium/pagination.rb +35 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +27 -1
- data/lib/plutonium/ui/breadcrumbs.rb +26 -8
- data/lib/plutonium/ui/grid/card.rb +11 -2
- data/lib/plutonium/ui/grid/resource.rb +2 -2
- data/lib/plutonium/ui/interaction/async/run_progress.rb +25 -11
- data/lib/plutonium/ui/interaction/async/running_banner.rb +21 -5
- data/lib/plutonium/ui/layout/base.rb +0 -9
- data/lib/plutonium/ui/layout/icon_rail.rb +42 -4
- data/lib/plutonium/ui/page/index.rb +23 -4
- data/lib/plutonium/ui/table/components/filter_pills.rb +8 -2
- data/lib/plutonium/ui/table/components/pagy_info.rb +30 -10
- data/lib/plutonium/ui/table/resource.rb +2 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/plutonium.gemspec +3 -1
- data/src/css/components.css +12 -1
- data/src/css/font_awesome.css +2328 -0
- data/src/css/plutonium.css +1 -0
- metadata +11 -7
- data/docs/public/images/blog/dashboards-card-error.png +0 -0
- data/docs/public/images/blog/dashboards-loading.png +0 -0
- data/docs/public/images/blog/dashboards-overview.png +0 -0
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36351840fe45e3c1d51694397c480dc641bb6edb4b7d32b38149b2af742d317a
|
|
4
|
+
data.tar.gz: dad970de51f6f04a961cae9bf4c50ea35f6b76601371cae3787b3c5e3031dfbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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.
|
|
15
|
+
| Latest release (`0.70.x`) | :white_check_mark: |
|
|
16
16
|
| Older releases | :x: |
|
|
17
17
|
|
|
18
18
|
## Reporting a Vulnerability
|