plutonium 0.67.0 → 0.69.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-dashboard/SKILL.md +17 -2
- data/.claude/skills/plutonium-resource/SKILL.md +3 -2
- data/.claude/skills/plutonium-ui/SKILL.md +2 -0
- data/CHANGELOG.md +33 -0
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +3 -1
- data/config/locales/en/async.yml +11 -1
- data/config/locales/en/dashboard.yml +1 -0
- data/config/locales/en/ui.yml +3 -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/reference/behavior/async-interactions.md +62 -5
- data/docs/reference/dashboard/dsl.md +54 -3
- data/docs/reference/generators/lite.md +30 -6
- data/docs/reference/i18n.md +1 -1
- data/docs/reference/resource/definition.md +2 -0
- data/docs/reference/ui/forms.md +23 -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/dashboard/base.rb +80 -1
- data/lib/plutonium/dashboard/card.rb +12 -13
- data/lib/plutonium/dashboard/controller.rb +28 -10
- data/lib/plutonium/dashboard/dsl.rb +59 -2
- data/lib/plutonium/dashboard/evaluation.rb +30 -0
- data/lib/plutonium/dashboard/filter.rb +114 -0
- data/lib/plutonium/dashboard/link.rb +35 -0
- data/lib/plutonium/dashboard/route_resolution.rb +6 -2
- 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/resource/query_object.rb +6 -0
- data/lib/plutonium/routing/dashboard_registration.rb +5 -0
- data/lib/plutonium/ui/avatar.rb +2 -2
- data/lib/plutonium/ui/breadcrumbs.rb +26 -8
- data/lib/plutonium/ui/dashboard/board.rb +4 -1
- data/lib/plutonium/ui/dashboard/frame.rb +1 -1
- data/lib/plutonium/ui/dashboard/metric.rb +18 -5
- data/lib/plutonium/ui/dashboard/toolbar.rb +76 -0
- data/lib/plutonium/ui/form/base.rb +20 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +31 -8
- data/lib/plutonium/ui/form/components/secure_association_checkboxes.rb +30 -0
- data/lib/plutonium/ui/form/components/secure_association_choices.rb +123 -0
- data/lib/plutonium/ui/form/components/secure_association_radio_buttons.rb +45 -0
- data/lib/plutonium/ui/form/components/secure_association_toggles.rb +18 -0
- data/lib/plutonium/ui/form/theme.rb +15 -1
- data/lib/plutonium/ui/grid/card.rb +12 -4
- 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/page/dashboard.rb +1 -0
- data/lib/plutonium/ui/page/index.rb +23 -4
- data/lib/plutonium/ui/table/components/scopes_bar.rb +1 -1
- data/lib/plutonium/ui/table/components/scopes_pills.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/plutonium.gemspec +1 -1
- data/src/css/components.css +9 -0
- data/src/css/font_awesome.css +2328 -0
- data/src/css/plutonium.css +1 -0
- metadata +16 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec1cce1809eadd51ea67c024d0b3634ce5484c84f8399cf8eaa33c5eb0c2e000
|
|
4
|
+
data.tar.gz: fca80361dc22fcb0ff768c9ff3c4a4e6128aceff31425c6e1f02d4e3df6d958c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7aeac86ceb3d02a7528c685aa7b1a3e4ae1069dc30610cbeea384cc9e777191b7ed6b81903153351feb9919163b1be8e4e19cf902d12a10b8457b31458948d80
|
|
7
|
+
data.tar.gz: cb967b6a1a24dcfeda965994c2ed0395a30f1446c78128b3b51dcf2f6e3daa2465a12b95e13b9c07ec2f40cb2f314917a6e0ba4443700c7c51a81d5287dc3211
|
|
@@ -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
|
|
|
@@ -77,9 +77,24 @@ end
|
|
|
77
77
|
|
|
78
78
|
## metric
|
|
79
79
|
|
|
80
|
-
Return a value, or a hash: `value` (required), `previous` (computes
|
|
80
|
+
Return a value, or a hash: `value` (required), `previous` (computes the change), `change` (a number or a verbatim string), `trend` (`:up`/`:down`/`:flat`, inferred), `change_label`.
|
|
81
81
|
|
|
82
|
-
Options: `format:` (`:number` default, `:currency`, `:percentage`, `:human`, or `->(v) { ... }`), `precision:`, `unit:` (currency symbol), `prefix:`, `suffix:`, `positive:` (`:up` default, `:down` when falling is good), `change_label
|
|
82
|
+
Options: `format:` (`:number` default, `:currency`, `:percentage`, `:human`, or `->(v) { ... }`), `precision:`, `unit:` (currency symbol), `prefix:`, `suffix:`, `positive:` (`:up` default, `:down` when falling is good), `change_label:`, `change_unit:` (`:percent` default; `:points` for a value that is itself a percentage, so 50% to 55.6% reads `+5.6 pts`).
|
|
83
|
+
|
|
84
|
+
## Filters and toolbar links
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
filter :period, choices: {"7" => "Last 7 days", "30" => "Last 30 days"}, default: "30"
|
|
88
|
+
link :settings, icon: Phlex::TablerIcons::Settings, href: -> { resource_url_for(Setting) }
|
|
89
|
+
|
|
90
|
+
metric(:orders) { orders.where(created_at: filter_value(:period).to_i.days.ago..).count }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- A filter's value comes from the query string; an unknown value reads as the default. Read it with `filter_value(:key)`, never `params[:key]` directly.
|
|
94
|
+
- Don't name a filter after a card-route param or `url_for` option (`card`, `format`, `anchor`, `host`, `use_route`, ...) or a path segment of the dashboard's route (entity param, `locale`); it raises.
|
|
95
|
+
- To filter by a resource's scopes, use `filter :status, scopes: Post` (choices = "All" + the definition's scopes) with `filtered(Post)` in card blocks and `href: -> { filtered_url(Post) }`. Do not hand-map filter values to `where` clauses or `q[scope]` URLs; `filtered` runs the definition's scope, so the count matches the index. One scope filter per resource. The scope block runs on the dashboard's controller, so it cannot call resource-controller helpers like `current_parent`.
|
|
96
|
+
- Every lazy card's frame URL carries the filter values, so lazy cards see the page's choice. Do not hand-roll query forwarding or a custom `DashboardsController` for this.
|
|
97
|
+
- Filters render as segmented links and links as buttons in a toolbar above the cards; neither needs a custom page class.
|
|
83
98
|
|
|
84
99
|
## chart
|
|
85
100
|
|
|
@@ -622,8 +622,9 @@ end
|
|
|
622
622
|
|
|
623
623
|
- `input :tag_ids` uses Rails' collection-ids setter. Permit `:tag_ids`; values are plain ids. Simplest for a scoped checkbox list.
|
|
624
624
|
- `input :tags, as: :secure_association` is the native picker. It sets input_param `tag_sgids`, scopes via `choices:` (or the target policy), and validates submitted SGIDs by decoded model-id.
|
|
625
|
+
- `input :tags, as: :association_checkboxes` (or `:association_toggles`) is the same picker rendered as one checkbox/switch per record. `input :category, as: :association_radio_buttons` does the same for a belongs_to/has_one. They are capped by `choice_limit:` like the select but have no typeahead, so keep them to short lists.
|
|
625
626
|
|
|
626
|
-
⚠️ Do **not** hand-roll `collection_checkboxes_tag(choices: [[label, record.to_sgid.to_s]])`. SGIDs are non-deterministic (`to_sgid` embeds a timestamp), the component re-derives `choices` at submit and intersects the posted value against the freshly-minted SGIDs, so it never matches and the value is dropped. Use stable ids in custom checkboxes, or `
|
|
627
|
+
⚠️ Do **not** hand-roll `collection_checkboxes_tag(choices: [[label, record.to_sgid.to_s]])`. SGIDs are non-deterministic (`to_sgid` embeds a timestamp), the component re-derives `choices` at submit and intersects the posted value against the freshly-minted SGIDs, so it never matches and the value is dropped. Use stable ids in custom checkboxes, or `as: :association_checkboxes` (which validates by model-id).
|
|
627
628
|
|
|
628
629
|
## Conditional Rendering
|
|
629
630
|
|
|
@@ -1116,7 +1117,7 @@ Only declare `index_views` explicitly if you want to **disable** one (e.g. `inde
|
|
|
1116
1117
|
| `grid_layout :media` | `:compact` (image left) or `:media` (image on top). |
|
|
1117
1118
|
| `grid_columns 3` | Override responsive column count. |
|
|
1118
1119
|
|
|
1119
|
-
All grid slots are optional; slots pointing at unpermitted fields collapse silently.
|
|
1120
|
+
All grid slots are optional; slots pointing at unpermitted fields collapse silently. In the `:media` layout a record whose image does not resolve keeps the cover's space, with a neutral icon, so cards in a row stay one height. An image may be an attachment, an `http` or root-relative URL, or a `data:image/` URI.
|
|
1120
1121
|
|
|
1121
1122
|
---
|
|
1122
1123
|
|
|
@@ -324,6 +324,8 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
324
324
|
| `uppy_tag` / `file_tag` | Uppy file upload |
|
|
325
325
|
| `secure_association_tag` | Association with policy-checked options |
|
|
326
326
|
| `belongs_to_tag` / `has_many_tag` / `has_one_tag` | Association selects |
|
|
327
|
+
| `association_checkboxes_tag` / `association_toggles_tag` | has_many / habtm as checkboxes or switches (`as: :association_checkboxes` / `:association_toggles`); same policy scoping and SGID validation as the select, capped by `choice_limit:` like the select (selected records always render), no typeahead |
|
|
328
|
+
| `association_radio_buttons_tag` | belongs_to / has_one as a radio group (`as: :association_radio_buttons`); optional associations get a "None" choice |
|
|
327
329
|
| `key_value_store_tag` | Key/value pairs editor |
|
|
328
330
|
| `currency_tag` | Money input (number field + unit prefix) |
|
|
329
331
|
| `rich_text_tag` | Action Text editor (Trix, or Lexxy once the app installs it); default for `has_rich_text` |
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.69.0] - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Link collection pages without a record back to the resource index in breadcrumbs
|
|
10
|
+
- Vendor Font Awesome 4 so its .sr-only stops overriding Tailwind utilities
|
|
11
|
+
- Answer turbo-stream redirects with a stream visit, held to the open-redirect check
|
|
12
|
+
- Perform opaque runs dispatched from a resource index
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- Drop the Interaction suffix from default labels
|
|
17
|
+
- Name runs by their interaction, show progress as one cell
|
|
18
|
+
- Check a run's input before processing with pre_check
|
|
19
|
+
- Clear finished SolidQueue jobs and gate VACUUM on free pages in pu:lite:maintenance
|
|
20
|
+
|
|
21
|
+
### Miscellaneous Tasks
|
|
22
|
+
|
|
23
|
+
- Update gemfiles
|
|
24
|
+
|
|
25
|
+
## [0.68.0] - 2026-10-14
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
- Send card links to the whole page ([#183](https://github.com/radioactive-labs/plutonium-core/issues/183))
|
|
30
|
+
- Keep the grid card cover's space when a record has no image ([#184](https://github.com/radioactive-labs/plutonium-core/issues/184))
|
|
31
|
+
|
|
32
|
+
### Features
|
|
33
|
+
|
|
34
|
+
- Filters carried to card frames, toolbar links, point changes ([#182](https://github.com/radioactive-labs/plutonium-core/issues/182))
|
|
35
|
+
- Checkbox, toggle and radio variants of the association input ([#185](https://github.com/radioactive-labs/plutonium-core/issues/185))
|
|
36
|
+
- Scope filters, plus fixes from the #182 review ([#186](https://github.com/radioactive-labs/plutonium-core/issues/186))
|
|
37
|
+
|
|
5
38
|
## [0.67.0] - 2026-10-09
|
|
6
39
|
|
|
7
40
|
### Bug Fixes
|
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.69.x`) | :white_check_mark: |
|
|
16
16
|
| Older releases | :x: |
|
|
17
17
|
|
|
18
18
|
## Reporting a Vulnerability
|