plutonium 0.65.0 → 0.66.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +43 -43
  3. data/.claude/skills/plutonium-app/SKILL.md +101 -59
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
  5. data/.claude/skills/plutonium-auth/SKILL.md +119 -53
  6. data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
  7. data/.claude/skills/plutonium-dashboard/SKILL.md +11 -4
  8. data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
  9. data/.claude/skills/plutonium-resource/SKILL.md +144 -133
  10. data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
  11. data/.claude/skills/plutonium-testing/SKILL.md +130 -33
  12. data/.claude/skills/plutonium-ui/SKILL.md +151 -96
  13. data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
  14. data/CHANGELOG.md +21 -0
  15. data/README.md +9 -9
  16. data/SECURITY.md +1 -1
  17. data/app/assets/plutonium.css +1 -1
  18. data/docs/.vitepress/sync-skills.mjs +6 -3
  19. data/docs/blog/introducing-plutonium-dashboards.md +4 -5
  20. data/docs/blog/introducing-plutonium-i18n.md +4 -5
  21. data/docs/getting-started/installation.md +5 -5
  22. data/docs/getting-started/tutorial/02-first-resource.md +3 -3
  23. data/docs/getting-started/tutorial/03-authentication.md +7 -7
  24. data/docs/getting-started/tutorial/04-authorization.md +21 -4
  25. data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
  26. data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
  27. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  28. data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
  29. data/docs/getting-started/tutorial/index.md +1 -1
  30. data/docs/guides/adding-resources.md +10 -7
  31. data/docs/guides/authentication.md +25 -25
  32. data/docs/guides/authorization.md +24 -24
  33. data/docs/guides/creating-packages.md +17 -17
  34. data/docs/guides/custom-actions.md +32 -32
  35. data/docs/guides/customizing-ui.md +29 -26
  36. data/docs/guides/dashboards.md +1 -1
  37. data/docs/guides/index.md +3 -3
  38. data/docs/guides/kanban.md +55 -55
  39. data/docs/guides/multi-tenancy.md +35 -22
  40. data/docs/guides/nested-resources.md +21 -21
  41. data/docs/guides/performance.md +3 -3
  42. data/docs/guides/search-filtering.md +13 -13
  43. data/docs/guides/testing.md +16 -12
  44. data/docs/guides/theming.md +32 -17
  45. data/docs/guides/troubleshooting.md +2 -2
  46. data/docs/guides/user-invites.md +17 -17
  47. data/docs/guides/user-profile.md +51 -24
  48. data/docs/guides/wizards.md +55 -55
  49. data/docs/reference/app/generators.md +22 -22
  50. data/docs/reference/app/index.md +15 -18
  51. data/docs/reference/app/packages.md +8 -8
  52. data/docs/reference/app/portals.md +75 -29
  53. data/docs/reference/auth/accounts.md +15 -15
  54. data/docs/reference/auth/index.md +12 -12
  55. data/docs/reference/auth/profile.md +67 -29
  56. data/docs/reference/behavior/async-interactions.md +24 -24
  57. data/docs/reference/behavior/controllers.md +28 -28
  58. data/docs/reference/behavior/index.md +5 -5
  59. data/docs/reference/behavior/interactions.md +44 -44
  60. data/docs/reference/behavior/policies.md +48 -28
  61. data/docs/reference/configuration.md +6 -6
  62. data/docs/reference/dashboard/dsl.md +2 -2
  63. data/docs/reference/dashboard/index.md +1 -1
  64. data/docs/reference/generators/lite.md +7 -7
  65. data/docs/reference/i18n.md +23 -0
  66. data/docs/reference/index.md +1 -1
  67. data/docs/reference/kanban/authorization.md +9 -9
  68. data/docs/reference/kanban/dsl.md +32 -32
  69. data/docs/reference/kanban/index.md +1 -1
  70. data/docs/reference/kanban/positioning.md +17 -15
  71. data/docs/reference/resource/actions.md +51 -51
  72. data/docs/reference/resource/definition.md +73 -73
  73. data/docs/reference/resource/export.md +6 -6
  74. data/docs/reference/resource/index.md +16 -16
  75. data/docs/reference/resource/model.md +24 -24
  76. data/docs/reference/resource/positioning.md +78 -76
  77. data/docs/reference/resource/query.md +13 -13
  78. data/docs/reference/tenancy/entity-scoping.md +65 -35
  79. data/docs/reference/tenancy/index.md +11 -11
  80. data/docs/reference/tenancy/invites.md +20 -20
  81. data/docs/reference/tenancy/nested-resources.md +13 -13
  82. data/docs/reference/testing/index.md +116 -22
  83. data/docs/reference/ui/assets.md +57 -25
  84. data/docs/reference/ui/components.md +20 -20
  85. data/docs/reference/ui/displays.md +14 -14
  86. data/docs/reference/ui/forms.md +35 -35
  87. data/docs/reference/ui/index.md +17 -15
  88. data/docs/reference/ui/layouts.md +21 -21
  89. data/docs/reference/ui/pages.md +22 -22
  90. data/docs/reference/ui/tables.md +7 -7
  91. data/docs/reference/wizard/anchoring-resume.md +33 -32
  92. data/docs/reference/wizard/dsl.md +44 -44
  93. data/docs/reference/wizard/index.md +6 -6
  94. data/docs/reference/wizard/one-time.md +18 -18
  95. data/docs/reference/wizard/registration-launch.md +32 -32
  96. data/docs/reference/wizard/storage-config.md +23 -23
  97. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  98. data/lib/generators/pu/profile/conn_generator.rb +6 -0
  99. data/lib/plutonium/resource/record/associated_with.rb +23 -2
  100. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
  101. data/lib/plutonium/version.rb +1 -1
  102. data/package.json +1 -1
  103. data/src/css/components.css +10 -10
  104. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '008433e7fc21f308028d5d047ddfa2c01c26efe13258f8af39ca99b7b72f0e0d'
4
- data.tar.gz: d3c4ecbec9fd85ee133c4e0bd4b28fcf4fcc66ee632e0b5ea84d277fa00316a1
3
+ metadata.gz: b131f7d0e5b02982f4696e7d1db781f45b17f8b28f17da5fe5cc3b0a4480c7a0
4
+ data.tar.gz: f700d3d65a3fc368e73315c436a7f8d2afb5244cd04b4b0be5618efbc738d26f
5
5
  SHA512:
6
- metadata.gz: cc40c769d7dc3b30e2f5a2cd88aeb07d41c52bb37f1e28fb6946c88705d1dfb04adf19c19cdbc2889d4356f722a4d5705b4f0b3e1142ba3b58dbe9c9cbb8f30e
7
- data.tar.gz: 0a772b07eee9784de0012559814134515a3fb228f9b9bf51a75b94f4161a5b05d913434a24cbb7991fdc0722f415a69b87f725990603ed7557af5b1afdd0205b
6
+ metadata.gz: 7dc926b3cf2fb69f5dfa4644d10c47e88a39e74f3359be2572a4ea6ea46ad216a5c0447959b0e1596f0ee6ae67190140f46acc53afa332a508e63f275440bdad
7
+ data.tar.gz: 4d9f3fba2b94d24ef0db3d5b24b35b30d6c058acc46ac20107d37c1a507f804dac7d47b3a38556f02b16ac229ce9b2a4140e73216152c9e56ae7189a5c71bc00
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: plutonium
3
- description: Use BEFORE starting any Plutonium work — new app, new feature, or first edit in an unfamiliar area. Routes you to the right skill and bootstraps greenfield work.
3
+ description: 'Use BEFORE starting any Plutonium work: new app, new feature, or first edit in an unfamiliar area. Routes you to the right skill and bootstraps greenfield work.'
4
4
  ---
5
5
 
6
- # Plutonium — Router & Bootstrapper
6
+ # Plutonium: Router & Bootstrapper
7
7
 
8
8
  Entry point for all Plutonium work. Does three things:
9
9
 
@@ -14,57 +14,57 @@ Entry point for all Plutonium work. Does three things:
14
14
  ## 🚨 Critical (read first)
15
15
 
16
16
  - **Plutonium is generator-driven.** Almost every file you'd hand-write has a `pu:*` generator. Hand-written files drift from conventions and break future generator runs.
17
- - **For greenfield** (new app, substantial new feature, first resource in a new domain) — load the **bootstrap bundle** below before writing code.
18
- - **For targeted edits** — use the **router table** to jump to the right skill.
19
- - **For anything touching tenant scoping** — load `plutonium-tenancy`. Don't reach for `where(organization: ...)` in a policy; fix the model instead.
17
+ - **For greenfield** (new app, substantial new feature, first resource in a new domain), load the **bootstrap bundle** below before writing code.
18
+ - **For targeted edits**: use the **router table** to jump to the right skill.
19
+ - **For anything touching tenant scoping**: load `plutonium-tenancy`. Don't reach for `where(organization: ...)` in a policy; fix the model instead.
20
20
  - **Unattended execution:** always pass `--dest=`, `--force` (when re-running meta-generators), `--auth=`, `--skip-bundle`, `--quiet` so generators don't block on prompts. See [Unattended execution](#unattended-execution).
21
- - **Inspect before you act.** Every targeted skill now opens with a CHECK gate — read the relevant files yourself before scaffolding or editing. Don't ask the user to describe their app when you can read it.
21
+ - **Inspect before you act.** Every targeted skill now opens with a CHECK gate: read the relevant files yourself before scaffolding or editing. Don't ask the user to describe their app when you can read it.
22
22
 
23
- ## The mental model (read once — it decides what you should write)
23
+ ## The mental model (read once: it decides what you should write)
24
24
 
25
- Plutonium applies Rails' bargain — follow the convention and the framework carries you; reach for an escape hatch when you need one — to the layer **above CRUD**: auth, authorization, multi-tenancy, admin UI, business operations. Four consequences change what you should actually type.
25
+ Plutonium applies Rails' bargain (follow the convention and the framework carries you; reach for an escape hatch when you need one) to the layer **above CRUD**: auth, authorization, multi-tenancy, admin UI, business operations. Four consequences change what you should actually type.
26
26
 
27
27
  ### 1. Everything is derived from something you already declared
28
28
 
29
- Not "defaults someone picked for you" — **computed from existing declarations**:
29
+ Not "defaults someone picked for you": **computed from existing declarations**:
30
30
 
31
31
  | Derived | From |
32
32
  |---|---|
33
33
  | Field types, required markers, select choices | model columns, associations, attachments, enums, **and validations** (`presence: true` → required; `inclusion:` → select choices) |
34
- | A collection's preloads (index, kanban, export) | the policy's permitted field set — there is **no `includes` list to write or maintain** |
35
- | Tenant scope | your associations — direct `belongs_to`, then `has_one`/`has_one :through`, then reverse `has_many` |
34
+ | A collection's preloads (index, kanban, export) | the policy's permitted field set; there is **no `includes` list to write or maintain** |
35
+ | Tenant scope | your associations: direct `belongs_to`, then `has_one`/`has_one :through`, then reverse `has_many` |
36
36
  | Action type (record / bulk / resource) | whether the interaction declares `:resource`, `:resources`, or neither |
37
37
  | An association input's typeahead | the **target resource's own `search` block** |
38
38
  | CRUD, nested and action routes | one `register_resource` line |
39
39
 
40
- ⇒ **Declare only what differs.** A `field :title` matching the detected type is dead code — and one more line to fall out of step when the column changes. This is the single most common way generated-looking code goes wrong.
40
+ ⇒ **Declare only what differs.** A `field :title` matching the detected type is dead code, and one more line to fall out of step when the column changes. This is the single most common way generated-looking code goes wrong.
41
41
 
42
42
  ### 2. Definition and policy answer different questions
43
43
 
44
44
  - **Definition** = *how* a field renders.
45
45
  - **Policy** = *whether it appears at all*.
46
46
 
47
- "Only admins see this field" is `permitted_attributes_for_*`. Never a definition declaration, and never a `condition:` (that only hides UI — the route stays live).
47
+ "Only admins see this field" is `permitted_attributes_for_*`. Never a definition declaration, and never a `condition:` (that only hides UI; the route stays live).
48
48
 
49
49
  ### 3. Overrides are plain Ruby inheritance
50
50
 
51
- `AdminPortal::PostDefinition < ::PostDefinition`, and the same for policies and controllers. App-level default, portal-level subclass. No registry of overrides, no precedence DSL, no merge semantics — so "why does this field show here but not there" is always readable as a class hierarchy.
51
+ `AdminPortal::PostDefinition < ::PostDefinition`, and the same for policies and controllers. App-level default, portal-level subclass. No registry of overrides, no precedence DSL, no merge semantics, so "why does this field show here but not there" is always readable as a class hierarchy.
52
52
 
53
53
  ### 4. Climb the escape-hatch ladder only as far as the problem requires
54
54
 
55
- 1. **Change an option** — `input :content, as: :markdown`
56
- 2. **Render inline** — `display :priority do |f| … end` (a block, `instance_exec`ed in Phlex, emits markup directly and gives you `f.object`)
57
- 3. **Write a component** — a *field* component (subclasses the Phlexi base) plugs into `as:`; anything with its own constructor goes through a block (`display :card do |field| … end`)
58
- 4. **Implement a hook** — controller hooks instead of reopening `create`/`update`; page `render_before_*` / `render_after_*` instead of `view_template`
59
- 5. **Replace the page** — `view_template` on the nested class, or an ERB view at the controller path (ERB wins when both exist)
55
+ 1. **Change an option**: `input :content, as: :markdown`
56
+ 2. **Render inline**: `display :priority do |f| … end` (a block, `instance_exec`ed in Phlex, emits markup directly and gives you `f.object`)
57
+ 3. **Write a component**: a *field* component (subclasses the Phlexi base) plugs into `as:`; anything with its own constructor goes through a block (`display :card do |field| … end`)
58
+ 4. **Implement a hook**: controller hooks instead of reopening `create`/`update`; page `render_before_*` / `render_after_*` instead of `view_template`
59
+ 5. **Replace the page**: `view_template` on the nested class, or an ERB view at the controller path (ERB wins when both exist)
60
60
 
61
61
  Reaching for rung 5 on a rung-1 problem is how you end up owning breadcrumbs, the header and turbo frame wiring you never meant to touch.
62
62
 
63
63
  **Underneath all of it, it stays Rails.** Models are plain ActiveRecord, controllers inherit from Rails controllers, views resolve through Rails view paths. A Plutonium resource and a hand-written controller coexist in one app.
64
64
 
65
- ## ✅ Orient before you route (CHECK — read the app, don't assume)
65
+ ## ✅ Orient before you route (CHECK: read the app, don't assume)
66
66
 
67
- A one-line request rarely says whether this is a new app, a half-built one, or a multi-tenant one — and those change which path you take. Spend 30 seconds reading the app **before** loading a bundle or running anything:
67
+ A one-line request rarely says whether this is a new app, a half-built one, or a multi-tenant one, and those change which path you take. Spend 30 seconds reading the app **before** loading a bundle or running anything:
68
68
 
69
69
  | Read | Tells you |
70
70
  |---|---|
@@ -80,16 +80,16 @@ This is the global "look before you leap"; each targeted skill carries its own A
80
80
  | Skill | Covers |
81
81
  |---|---|
82
82
  | **[[plutonium-app]]** | Installation, packages (feature + portal), portal engines, mounting, `register_resource` (including singular and custom routes), `pu:res:conn` |
83
- | **[[plutonium-resource]]** | The resource itself — `pu:res:scaffold`, field types, model layer (`Plutonium::Resource::Record`, `has_cents`, SGID, routing), definition layer (fields/inputs/displays/columns, search/filters/scopes/sorting, custom actions, bulk actions, index views, page customization) |
83
+ | **[[plutonium-resource]]** | The resource itself: `pu:res:scaffold`, field types, model layer (`Plutonium::Resource::Record`, `has_cents`, SGID, routing), definition layer (fields/inputs/displays/columns, search/filters/scopes/sorting, custom actions, bulk actions, index views, page customization) |
84
84
  | **[[plutonium-behavior]]** | Controllers (hooks, key methods, presentation), policies (action methods, `permitted_attributes_for_*`, `permitted_associations`), interactions (structure, outcomes, chaining, URL generation) |
85
- | **[[plutonium-async-interactions]]** | Async interactions — `async`, the Run STI model, failure policies (`halt`/`continue`/`transactional`), authorization re-derivation at perform time, registering AsyncRun as a resource (progress page + running banner), scheduling `ReapJob` |
85
+ | **[[plutonium-async-interactions]]** | Async interactions: `async`, the Run STI model, failure policies (`halt`/`continue`/`transactional`), authorization re-derivation at perform time, registering AsyncRun as a resource (progress page + running banner), scheduling `ReapJob` |
86
86
  | **[[plutonium-ui]]** | Page classes, forms, displays, tables, custom Phlex components, layouts, modals & tabs, Tailwind config, Stimulus, design tokens, `.pu-*` classes, Phlexi themes |
87
- | **[[plutonium-kanban]]** | `kanban do…end` DSL in a Definition — columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy, quick-add, static vs dynamic boards |
88
- | **[[plutonium-dashboard]]** | Dashboards — `Plutonium::Dashboard::Base`, `metric` / `chart` / `card`, `register_dashboard`, lazy turbo-frame cards, `refresh`, `condition:`, `authorize?`, `pu:dashboard` |
87
+ | **[[plutonium-kanban]]** | `kanban do…end` DSL in a Definition, columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy, quick-add, static vs dynamic boards |
88
+ | **[[plutonium-dashboard]]** | Dashboards, `Plutonium::Dashboard::Base`, `metric` / `chart` / `card`, `register_dashboard`, lazy turbo-frame cards, `refresh`, `condition:`, `authorize?`, `pu:dashboard` |
89
89
  | **[[plutonium-auth]]** | Rodauth install, account types (basic / admin / SaaS), profile resource, security section |
90
90
  | **[[plutonium-tenancy]]** | Entity scoping (`associated_with`, `default_relation_scope`, three model shapes), nested resources, invites |
91
91
  | **[[plutonium-testing]]** | `pu:test:install`, `pu:test:scaffold`, `ResourceCrud`/`ResourcePolicy`/`ResourceDefinition`/`ResourceModel`/`NestedResource`/`PortalAccess`/`ResourceInteraction`, `AuthHelpers` |
92
- | **[[plutonium-wizard]]** | Multi-step flows — the wizard DSL (`step`/`review`/`using:`/`condition:`, per-step `on_submit`/`persist`/`on_rollback`, `execute`), anchoring & resume, one-time wizards + gate, registration (`wizard` macro + `register_wizard`), storage/config + SweepJob |
92
+ | **[[plutonium-wizard]]** | Multi-step flows: the wizard DSL (`step`/`review`/`using:`/`condition:`, per-step `on_submit`/`persist`/`on_rollback`, `execute`), anchoring & resume, one-time wizards + gate, registration (`wizard` macro + `register_wizard`), storage/config + SweepJob |
93
93
 
94
94
  ## Greenfield bootstrap bundle
95
95
 
@@ -97,10 +97,10 @@ Triggers: installing Plutonium, building a new app, adding the first resource in
97
97
 
98
98
  **Load these before writing code:**
99
99
 
100
- 1. **`plutonium-app`** — install, portals, packages, routes.
101
- 2. **`plutonium-resource`** — scaffold, model, definition (the bulk of the work).
102
- 3. **`plutonium-behavior`** — controllers, policies, interactions.
103
- 4. **`plutonium-tenancy`** — only if multi-tenant; load before declaring entity scoping.
100
+ 1. **`plutonium-app`**: install, portals, packages, routes.
101
+ 2. **`plutonium-resource`**: scaffold, model, definition (the bulk of the work).
102
+ 3. **`plutonium-behavior`**: controllers, policies, interactions.
103
+ 4. **`plutonium-tenancy`**: only if multi-tenant; load before declaring entity scoping.
104
104
 
105
105
  Add when relevant:
106
106
  - **`plutonium-auth`** for login / accounts / profile.
@@ -114,7 +114,7 @@ Add when relevant:
114
114
  | Install Plutonium, create a portal or package, mount engines, register routes (incl. singular / custom routes) | **[[plutonium-app]]** |
115
115
  | Run `pu:res:scaffold`, pick field types, set scaffold options | **[[plutonium-resource]]** |
116
116
  | Edit a model, add associations, use `has_cents`, override `to_param` / `to_label` | **[[plutonium-resource]]** |
117
- | Edit a definition — fields, inputs, displays, columns, search, filters, scopes, custom actions, bulk actions, index views, modal/slideover, page titles | **[[plutonium-resource]]** |
117
+ | Edit a definition, fields, inputs, displays, columns, search, filters, scopes, custom actions, bulk actions, index views, modal/slideover, page titles | **[[plutonium-resource]]** |
118
118
  | Override a controller action, hook, redirect, or `resource_params` | **[[plutonium-behavior]]** |
119
119
  | Write `relation_scope`, `permitted_attributes_for_*`, `permitted_associations`, action methods, or any policy override | **[[plutonium-behavior]]** (+ **[[plutonium-tenancy]]** if scoping) |
120
120
  | Write an interaction class for business logic | **[[plutonium-behavior]]** |
@@ -122,24 +122,24 @@ Add when relevant:
122
122
  | Scope a model to a tenant, write `associated_with`, set portal entity strategy | **[[plutonium-tenancy]]** |
123
123
  | Configure parent/child nested routes, custom parent resolution | **[[plutonium-tenancy]]** |
124
124
  | Set up user invitations or entity membership | **[[plutonium-tenancy]]** |
125
- | Build or customize a kanban board view — `kanban do…end`, columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy | **[[plutonium-kanban]]** |
126
- | Build a dashboard, KPI overview or chart page — `pu:dashboard`, `metric` / `chart` / `card`, `register_dashboard`, refresh, per-card conditions | **[[plutonium-dashboard]]** |
125
+ | Build or customize a kanban board view, `kanban do…end`, columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy | **[[plutonium-kanban]]** |
126
+ | Build a dashboard, KPI overview or chart page, `pu:dashboard`, `metric` / `chart` / `card`, `register_dashboard`, refresh, per-card conditions | **[[plutonium-dashboard]]** |
127
127
  | Build a custom page (override `ShowPage`/`IndexPage`/`NewPage`/`EditPage`), custom form, custom display, custom table, custom Phlex component | **[[plutonium-ui]]** |
128
128
  | Configure Tailwind, register Stimulus controllers, edit design tokens, theme forms/displays/tables, write a custom layout | **[[plutonium-ui]]** |
129
129
  | Install Rodauth, set up accounts, configure login flow, add the profile resource | **[[plutonium-auth]]** |
130
130
  | Write tests for a resource, run `pu:test:scaffold`, include `Plutonium::Testing::*` concerns | **[[plutonium-testing]]** |
131
- | Build a multi-step flow — onboarding, checkout, branching create — register a `wizard` / `register_wizard`, gate a one-time wizard | **[[plutonium-wizard]]** |
131
+ | Build a multi-step flow, onboarding, checkout, branching create, register a `wizard` / `register_wizard`, gate a one-time wizard | **[[plutonium-wizard]]** |
132
132
 
133
133
  ## Resource architecture at a glance
134
134
 
135
- A **resource** is four cooperating layers — Plutonium auto-fills defaults from the model, so you only declare overrides:
135
+ A **resource** is four cooperating layers; Plutonium auto-fills defaults from the model, so you only declare overrides:
136
136
 
137
137
  | Layer | File | Purpose |
138
138
  |---|---|---|
139
139
  | **Model** | `app/models/post.rb` | Data, validations, associations |
140
- | **Definition** | `app/definitions/post_definition.rb` | UI — fields, filters, actions |
141
- | **Policy** | `app/policies/post_policy.rb` | Authorization — who, what |
142
- | **Controller** | `app/controllers/posts_controller.rb` | Request handling (rarely edited — use hooks) |
140
+ | **Definition** | `app/definitions/post_definition.rb` | UI, fields, filters, actions |
141
+ | **Policy** | `app/policies/post_policy.rb` | Authorization: who, what |
142
+ | **Controller** | `app/controllers/posts_controller.rb` | Request handling (rarely edited; use hooks) |
143
143
 
144
144
  Plus one optional fifth layer:
145
145
 
@@ -191,8 +191,8 @@ Meta-generators (`pu:saas:setup`) propagate flags to the generators they chain.
191
191
  ## Workflow summary
192
192
 
193
193
  1. **Load the bootstrap bundle** (or the targeted skill from the router table).
194
- 2. **Generate** — `rails g pu:res:scaffold Model field:type ... --dest=main_app`.
195
- 3. **Migrate** — `rails db:prepare`.
196
- 4. **Connect** — `rails g pu:res:conn Model --dest=portal_name`.
197
- 5. **Customize** — edit definition / policy as needed.
198
- 6. **Verify** — hit the route in the browser.
194
+ 2. **Generate**: `rails g pu:res:scaffold Model field:type ... --dest=main_app`.
195
+ 3. **Migrate**: `rails db:prepare`.
196
+ 4. **Connect**: `rails g pu:res:conn Model --dest=portal_name`.
197
+ 5. **Customize**: edit definition / policy as needed.
198
+ 6. **Verify**: hit the route in the browser.
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: plutonium-app
3
- description: Use BEFORE installing Plutonium, creating a portal or feature package, mounting an engine, or registering resources/routes. Covers initial setup, the package system, portal engines, route registration (including singular and custom routes), and resource-to-portal wiring.
3
+ description: 'Use BEFORE installing Plutonium, creating a portal or feature package, mounting an engine, or registering resources/routes. Covers initial setup, the package system, portal engines, route registration (including singular and custom routes), and resource-to-portal wiring.'
4
4
  ---
5
5
 
6
- # Plutonium App — Installation, Packages, Portals, Routes
6
+ # Plutonium App: Installation, Packages, Portals, Routes
7
7
 
8
8
  How a Plutonium app is assembled: the install bootstrap, the package system (feature vs portal), portal engines, and the routing surface that exposes resources to the web.
9
9
 
@@ -15,54 +15,54 @@ For the resources themselves (model + definition + scaffold options), see [[plut
15
15
  - **Existing app → `base.rb`. New app → `plutonium.rb`.** The `plutonium.rb` template re-runs full bootstrap (dotenv, annotate, solid_*, asset config) and creates generic "initial commit" commits that clobber history. For any pre-existing app use `base.rb`.
16
16
  - **Pass `--dest`, `--auth`, `--force`, `--skip-bundle`** etc. for unattended runs so generators don't block on prompts.
17
17
  - **Feature vs portal is a hard split.** Feature packages hold models/policies/definitions/interactions. Portal packages hold controllers/views/routes/auth. Don't mix.
18
- - **Package classes are auto-namespaced** — `packages/blogging/app/models/blogging/post.rb` → `Blogging::Post`. Don't fight it.
19
- - **Always connect resources with `pu:res:conn`** — until connected, a resource has no portal routes and is invisible.
20
- - **For custom routes on a registered resource, pass `as:`** — otherwise `resource_url_for` can't build URLs.
18
+ - **Package classes are auto-namespaced**: `packages/blogging/app/models/blogging/post.rb` → `Blogging::Post`. Don't fight it.
19
+ - **Always connect resources with `pu:res:conn`**: until connected, a resource has no portal routes and is invisible.
20
+ - **For custom routes on a registered resource, pass `as:`**: otherwise `resource_url_for` can't build URLs.
21
21
 
22
22
  ---
23
23
 
24
- ## 🛑 Before you install or scaffold structure: confirm the shape (ASK — don't infer)
24
+ ## 🛑 Before you install or scaffold structure: confirm the shape (ASK: don't infer)
25
25
 
26
- "Set up Plutonium" / "make an admin area" / "create a billing package" each hide a high-blast-radius decision. Get one wrong and you **clobber a year of git history**, build the wrong kind of package, or ship a portal nobody can log into. Resolve each — confirming by inspection (next section), not assumption:
26
+ "Set up Plutonium" / "make an admin area" / "create a billing package" each hide a high-blast-radius decision. Get one wrong and you **clobber a year of git history**, build the wrong kind of package, or ship a portal nobody can log into. Resolve each, confirming by inspection (next section), not assumption:
27
27
 
28
- 1. **Fresh app or existing one?** Existing ⇒ `bundle add plutonium` + `pu:core:install` (the `base.rb` path). **NEVER the `plutonium.rb` fresh-app template on an existing app** — it re-bootstraps (dotenv/annotate/solid_*/assets) and drops "initial commit" commits that clobber history. This is the single most dangerous mistake in this skill — confirm it's greenfield *before* reaching for `plutonium.rb`.
29
- 2. **Feature package or portal package?** Business logic (models/policies/definitions/interactions) ⇒ `pu:pkg:package` (feature, no UI). A web surface (controllers/views/routes/auth) ⇒ `pu:pkg:portal`. Hard split — "billing" is a *feature*; "admin area" is a *portal*. A feature package is invisible until its resources are `pu:res:conn`'d into a portal.
30
- 3. **Auth per portal.** `--auth=<account>` / `--public` / `--byo` / `--scope=<Entity>` (multi-tenant). Unguessable from "admin area" — decide, don't default silently.
31
- 4. **Don't stop half-wired.** A resource reaches the browser only after: scaffold → migrate → `pu:res:conn --dest=portal` → portal engine `mount`ed in `config/routes.rb` → registered (conn does the last). Name the whole chain before you start.
28
+ 1. **Fresh app or existing one?** Existing ⇒ `bundle add plutonium` + `pu:core:install` (the `base.rb` path). **NEVER the `plutonium.rb` fresh-app template on an existing app**: it re-bootstraps (dotenv/annotate/solid_*/assets) and drops "initial commit" commits that clobber history. This is the single most dangerous mistake in this skill; confirm it's greenfield *before* reaching for `plutonium.rb`.
29
+ 2. **Feature package or portal package?** Business logic (models/policies/definitions/interactions) ⇒ `pu:pkg:package` (feature, no UI). A web surface (controllers/views/routes/auth) ⇒ `pu:pkg:portal`. Hard split: "billing" is a *feature*; "admin area" is a *portal*. A feature package is invisible until its resources are `pu:res:conn`'d into a portal.
30
+ 3. **Auth per portal.** `--auth=<account>` / `--public` / `--byo` / `--scope=<Entity>` (multi-tenant). Unguessable from "admin area"; decide, don't default silently.
31
+ 4. **Don't stop half-wired.** A resource reaches the browser only after: scaffold → migrate → `pu:res:conn --dest=portal` → registered in the portal's routes (conn does this) → portal engine mounted (`pu:pkg:portal` already did this). Name the whole chain before you start.
32
32
 
33
- **Never ship a guessed schema, portal name, or auth flag as applied commands** — read them off the app first; fall back to `AskUserQuestion` only for genuine product choices (separate staff accounts vs shared, which payment backend). The decisions compound: *existing app ⇒ base.rb path*; *feature package ⇒ needs a portal to be visible*; *new portal ⇒ pick auth + mount it*.
33
+ **Never ship a guessed schema, portal name, or auth flag as applied commands**; read them off the app first; fall back to `AskUserQuestion` only for genuine product choices (separate staff accounts vs shared, which payment backend). The decisions compound: *existing app ⇒ base.rb path*; *feature package ⇒ needs a portal to be visible*; *new portal ⇒ pick auth + mount it*.
34
34
 
35
- ## ✅ Before you run a generator: verify the ground truth (CHECK — read it, don't ask for it)
35
+ ## ✅ Before you run a generator: verify the ground truth (CHECK: read it, don't ask for it)
36
36
 
37
- You have file access — **inspect**; don't ask the user to describe their app.
37
+ You have file access, **inspect**; don't ask the user to describe their app.
38
38
 
39
39
  | Check | How | Why it matters |
40
40
  |---|---|---|
41
- | Greenfield vs existing | `git log --oneline \| head`; is there a populated `Gemfile`/`app/`? | An existing app must use the `base.rb` path — **never** `plutonium.rb` |
41
+ | Greenfield vs existing | `git log --oneline \| head`; is there a populated `Gemfile`/`app/`? | An existing app must use the `base.rb` path, **never** `plutonium.rb` |
42
42
  | Plutonium already installed | grep `Gemfile` for `plutonium`; `ls config/packages.rb app/controllers/resource_controller.rb` | Avoid re-installing / double bootstrap |
43
- | Package/portal already exists | `ls packages/<name>` | Don't duplicate — connect to / extend the existing one |
43
+ | Package/portal already exists | `ls packages/<name>` | Don't duplicate; connect to / extend the existing one |
44
44
  | Existing auth | grep `Gemfile`/`app/models` for `rodauth`/`devise`/`has_secure_password` | Drives `--auth` vs `--byo` |
45
- | Portal engine mounted | grep `config/routes.rb` for `mount <Portal>::Engine` | An unmounted portal 404s |
45
+ | Portal engine mounted | grep `packages/<portal>/config/routes.rb` for `mount <Portal>::Engine` | `pu:pkg:portal` writes the mount there. Don't add a second one to `config/routes.rb` (duplicate route name error) |
46
46
  | Resource registered | grep the portal's `config/routes.rb` for `register_resource ::<X>` | Unregistered ⇒ no URLs (`resource_url_for` fails) |
47
47
  | Migrations applied | `rails db:migrate:status` before `pu:res:conn` | `conn` seeds the policy from columns |
48
48
 
49
49
  Inspect with your own tools **before** running any generator.
50
50
 
51
- ## 🛠 Use the generator — pick the right install path
51
+ ## 🛠 Use the generator: pick the right install path
52
52
 
53
53
  Never hand-write base controllers, engine files, layouts, or route registration. Pass `--dest`/`--auth`/`--force`/`--skip-bundle` for unattended runs.
54
54
 
55
55
  | Task | Generator | Verify first |
56
56
  |---|---|---|
57
- | Install — **existing** app | `bundle add plutonium` + `pu:core:install` | It's an existing app (use `base.rb`, **not** `plutonium.rb`) |
58
- | Install — **fresh** app | `rails new … -m …/plutonium.rb` | Brand-new app **only** |
57
+ | Install, **existing** app | `bundle add plutonium` + `pu:core:install` | It's an existing app (use `base.rb`, **not** `plutonium.rb`) |
58
+ | Install, **fresh** app | `rails new … -m …/plutonium.rb` | Brand-new app **only** |
59
59
  | Feature package | `pu:pkg:package <name>` | Not already present |
60
- | Portal package | `pu:pkg:portal <name> --auth=…/--public/--byo/--scope=…` | Auth strategy decided; then `mount` the engine by hand |
60
+ | Portal package | `pu:pkg:portal <name> --auth=…/--public/--byo/--scope=…` | Auth strategy decided. The generator also mounts the engine (see Mounting) |
61
61
  | Connect a resource | `pu:res:conn <Res> --dest=portal` | Migrated; target portal exists |
62
62
 
63
63
  ---
64
64
 
65
- # Part 1 — Installation
65
+ # Part 1: Installation
66
66
 
67
67
  ## Fresh Rails app (recommended)
68
68
 
@@ -78,11 +78,11 @@ Configures Rails + Propshaft + esbuild + TailwindCSS + Plutonium in one shot.
78
78
  ⚠️ Use `base.rb`, **not** `plutonium.rb`.
79
79
 
80
80
  ```bash
81
- # Option 1 — template
81
+ # Option 1: template
82
82
  bin/rails app:template \
83
83
  LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb
84
84
 
85
- # Option 2 — manual
85
+ # Option 2: manual
86
86
  # Add `gem "plutonium"` to Gemfile, then:
87
87
  bundle install
88
88
  rails generate pu:core:install
@@ -108,10 +108,7 @@ rails db:prepare
108
108
  # 5. Connect resource to portal
109
109
  rails generate pu:res:conn Post --dest=admin_portal
110
110
 
111
- # 6. Mount portal in config/routes.rb
112
- # mount AdminPortal::Engine, at: "/admin"
113
-
114
- # 7. Start
111
+ # 6. Start (step 3 already mounted the portal at /admin)
115
112
  rails server
116
113
  ```
117
114
 
@@ -121,10 +118,10 @@ rails server
121
118
  app/
122
119
  ├── controllers/
123
120
  │ ├── plutonium_controller.rb # non-resource base
124
- │ └── resource_controller.rb # CRUD base — see plutonium-behavior
121
+ │ └── resource_controller.rb # CRUD base; see plutonium-behavior
125
122
  ├── definitions/resource_definition.rb
126
123
  ├── interactions/resource_interaction.rb
127
- ├── models/resource_record.rb # abstract model — includes Plutonium::Resource::Record
124
+ ├── models/resource_record.rb # abstract model that includes Plutonium::Resource::Record
128
125
  ├── policies/resource_policy.rb
129
126
  └── views/layouts/resource.html.erb
130
127
 
@@ -174,7 +171,7 @@ end
174
171
 
175
172
  ---
176
173
 
177
- # Part 2 — The Package System
174
+ # Part 2: The Package System
178
175
 
179
176
  Two kinds, hard split:
180
177
 
@@ -275,19 +272,19 @@ Switch locale with a normal Rails `around_action` in the portal's controller con
275
272
 
276
273
  ## When to use which
277
274
 
278
- **Feature packages** — domain logic that:
275
+ **Feature packages** are domain logic that:
279
276
  - Could be reused across multiple portals (admin and customer both edit `Blogging::Post`)
280
277
  - Has no inherent UI / auth (it's just behavior)
281
278
  - You want to keep isolated from other domains (`billing` should not depend on `blogging`)
282
279
 
283
- **Portal packages** — user-facing surfaces that:
280
+ **Portal packages** are user-facing surfaces that:
284
281
  - Have a specific auth flow (admin vs customer vs public)
285
282
  - Render different views of the same underlying resources
286
283
  - Need different policies / definitions per audience
287
284
 
288
285
  ---
289
286
 
290
- # Part 3 — Portal Engines
287
+ # Part 3: Portal Engines
291
288
 
292
289
  A portal is a Rails engine mixing in `Plutonium::Portal::Engine`. It defines its own routes, controller concern, and (optionally) entity scoping.
293
290
 
@@ -302,7 +299,7 @@ rails g pu:pkg:portal <name>
302
299
  | Option | Description |
303
300
  |---|---|
304
301
  | `--auth=NAME` | Rodauth account to use (e.g. `--auth=user`) |
305
- | `--public` | Public access — no auth |
302
+ | `--public` | Public access, no auth |
306
303
  | `--byo` | Bring your own auth |
307
304
  | `--scope=CLASS` | Entity class for multi-tenancy (e.g. `--scope=Organization`) |
308
305
 
@@ -333,7 +330,9 @@ end
333
330
 
334
331
  ## Controller concern (auth)
335
332
 
336
- Every portal has a `Concerns::Controller` mixed into its `ResourceController`. The generator wires this up; you customize it for auth / before_action hooks.
333
+ Every portal has a `Concerns::Controller`, included by both its `ResourceController` (resource pages) and its `PlutoniumController` (dashboard and other non-resource pages). The generator wires this up; you customize it for auth / before_action hooks.
334
+
335
+ Portal-wide helpers the layout calls belong here, declared with `helper_method`, so they work on the dashboard as well as resource pages. Defining one on a single resource controller (or on the app's `::ResourceController`) leaves the dashboard without it. The common case is `profile_url`: `Plutonium::Auth::Rodauth` defines it as `nil`, and the avatar menu only shows a Profile link when it is truthy. `pu:profile:conn --dest=<portal>` writes the override into this concern; it reads `current_user.profile`, the `has_one :profile` association that `pu:profile:install` adds (see [[plutonium-auth]]). A plain `pu:res:conn --singular` registers the route but does not define `profile_url`. When testing the link, request both the portal root and a resource page.
337
336
 
338
337
  ### Rodauth
339
338
 
@@ -371,19 +370,62 @@ end
371
370
 
372
371
  ## Mounting
373
372
 
373
+ `pu:pkg:portal` writes the mount at the bottom of the portal's own `packages/<name>_portal/config/routes.rb`, at `/<name>`, wrapped in an auth constraint when `--auth` is given:
374
+
374
375
  ```ruby
375
- # config/routes.rb
376
+ # packages/admin_portal/config/routes.rb (after the engine's routes.draw block)
376
377
  Rails.application.routes.draw do
377
- # Authenticated mount
378
378
  constraints Rodauth::Rails.authenticate(:user) do
379
379
  mount AdminPortal::Engine, at: "/admin"
380
380
  end
381
+ end
382
+ ```
383
+
384
+ To change the path, edit `at:` in place. Don't mount the engine again in `config/routes.rb`: the second `mount` reuses the route name (`admin_portal`) and Rails raises `ArgumentError: Invalid route name, already in use`.
385
+
386
+ Route order matters: the app's `config/routes.rb` is drawn first, then each package's routes file, then gem engines (Active Storage, Turbo).
387
+
388
+ ### Mounting a portal at `/`
389
+
390
+ Two things change when a portal is mounted at `"/"`.
391
+
392
+ **Drop the `Rodauth::Rails.authenticate` constraint and authenticate in the controller concern instead.** The constraint does not fail the match for an anonymous visitor; it calls `rodauth.require_account`, which redirects to login. A constrained mount at `/` matches every path the app's own routes did not claim, so it redirects anonymous requests meant for routes drawn after it (other portals, Active Storage) and turns unknown URLs into login redirects.
393
+
394
+ ```ruby
395
+ # packages/desk_portal/config/routes.rb
396
+ Rails.application.routes.draw do
397
+ mount DeskPortal::Engine, at: "/"
398
+ end
399
+
400
+ # packages/desk_portal/app/controllers/desk_portal/concerns/controller.rb
401
+ module DeskPortal
402
+ module Concerns
403
+ module Controller
404
+ extend ActiveSupport::Concern
405
+ include Plutonium::Portal::Controller
406
+ include Plutonium::Auth::Rodauth(:user)
407
+ # add concerns above.
408
+
409
+ included do
410
+ before_action { rodauth.require_account }
411
+ end
412
+ end
413
+ end
414
+ end
415
+ ```
416
+
417
+ **Move the engine's root off `/` but keep the name.** The app's `root` is drawn first, so the generated `root to: "dashboard#index"` is unreachable and the portal's `root_path` points at the app's home page. Plutonium's header, icon rail, breadcrumbs and wizard exits all link to `root_path`, so the portal still needs a route named `root`:
381
418
 
382
- # Unconstrained (portal handles its own auth)
383
- mount PublicPortal::Engine, at: "/public"
419
+ ```ruby
420
+ DeskPortal::Engine.routes.draw do
421
+ get "dashboard", to: "dashboard#index", as: :root
422
+ register_resource ::Comment
423
+ # register resources above.
384
424
  end
385
425
  ```
386
426
 
427
+ The same applies to `register_dashboard ..., at: "/"`. Confirm with `Rails.application.routes.recognize_path("/")` (still the app's home) and `recognize_path("/dashboard")`.
428
+
387
429
  ## Controller hierarchy
388
430
 
389
431
  Portal controllers inherit from the feature-package controller if one exists, OR from the portal's `ResourceController` otherwise.
@@ -412,13 +454,13 @@ end
412
454
  ## Per-portal overrides
413
455
 
414
456
  ```ruby
415
- # Definition — how fields render (NOT whether they appear)
457
+ # Definition: how fields render (NOT whether they appear)
416
458
  class AdminPortal::PostDefinition < ::PostDefinition
417
459
  scope :pending_review
418
460
  input :internal_notes, hint: "Not shown to the author"
419
461
  end
420
462
 
421
- # Policy — whether a field appears at all
463
+ # Policy: whether a field appears at all
422
464
  class AdminPortal::PostPolicy < ::PostPolicy
423
465
  include AdminPortal::ResourcePolicy
424
466
  def destroy? = true
@@ -436,7 +478,7 @@ end
436
478
 
437
479
  ---
438
480
 
439
- # Part 4 — Routes & `register_resource`
481
+ # Part 4: Routes & `register_resource`
440
482
 
441
483
  Portal routes live in `packages/<name>_portal/config/routes.rb`:
442
484
 
@@ -452,7 +494,7 @@ AdminPortal::Engine.routes.draw do
452
494
  end
453
495
  ```
454
496
 
455
- ## `register_resource` — what it does
497
+ ## `register_resource`: what it does
456
498
 
457
499
  For each call, Plutonium auto-generates:
458
500
 
@@ -460,11 +502,11 @@ For each call, Plutonium auto-generates:
460
502
  - Nested routes for every registered `has_many` / `has_one` parent (prefixed `nested_`)
461
503
  - Route names that `resource_url_for` can resolve
462
504
 
463
- You list every resource the portal exposes. If a resource isn't registered, it has no URLs in that portal — `resource_url_for` will fail.
505
+ You list every resource the portal exposes. If a resource isn't registered, it has no URLs in that portal, so `resource_url_for` will fail.
464
506
 
465
507
  ## Singular (singleton) resources
466
508
 
467
- For resources with no collection — a single per-user `Profile`, app-wide `Settings`, etc.:
509
+ For resources with no collection, a single per-user `Profile`, app-wide `Settings`, etc.:
468
510
 
469
511
  ```ruby
470
512
  register_resource ::Profile, singular: true
@@ -505,9 +547,9 @@ register_resource ::Post do
505
547
  end
506
548
  ```
507
549
 
508
- **Always pass `as:`.** Without it, `resource_url_for(@post, action: :preview)` fails because there's no named route to look up — especially critical for nested resources.
550
+ **Always pass `as:`.** Without it, `resource_url_for(@post, action: :preview)` fails because there's no named route to look up, which is especially critical for nested resources.
509
551
 
510
- For most operations with business logic, prefer **interactive actions** (definition + interaction — see [[plutonium-resource]] › Actions) over custom controller routes. The action routes are wired automatically with no `register_resource` block needed.
552
+ For most operations with business logic, prefer **interactive actions** (definition + interaction, see [[plutonium-resource]] › Actions) over custom controller routes. The action routes are wired automatically with no `register_resource` block needed.
511
553
 
512
554
  ## Cross-package and nested URLs
513
555
 
@@ -515,7 +557,7 @@ See [[plutonium-behavior]] for full `resource_url_for` signature and [[plutonium
515
557
 
516
558
  ---
517
559
 
518
- # Part 5 — Connecting Resources to Portals (`pu:res:conn`)
560
+ # Part 5: Connecting Resources to Portals (`pu:res:conn`)
519
561
 
520
562
  A resource is invisible until connected to at least one portal. The generator wires up the portal-specific controller, policy, definition, and route registration.
521
563
 
@@ -525,7 +567,7 @@ A resource is invisible until connected to at least one portal. The generator wi
525
567
  rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME [--singular]
526
568
  ```
527
569
 
528
- Pass resources directly — avoids interactive prompts. No `--src` needed.
570
+ Pass resources directly; this avoids interactive prompts. No `--src` needed.
529
571
 
530
572
  ## Usage
531
573
 
@@ -540,7 +582,7 @@ rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
540
582
  rails g pu:res:conn Profile --dest=customer_portal --singular
541
583
  ```
542
584
 
543
- **Run after migrations** — the generator reads model columns to seed the policy's `permitted_attributes_for_*`.
585
+ **Run after migrations**: the generator reads model columns to seed the policy's `permitted_attributes_for_*`.
544
586
 
545
587
  ## What gets generated
546
588
 
@@ -560,7 +602,7 @@ register_resource ::Post
560
602
  register_resource ::Profile, singular: true # if --singular
561
603
  ```
562
604
 
563
- Re-running `pu:res:conn` for the same resource is **idempotent** — already-registered entries report `identical` and are not duplicated. Insertion falls back gracefully when the conventional `# register resources above` marker is missing (uses the `routes.draw do` opening), and warns clearly if it can't find any anchor.
605
+ Re-running `pu:res:conn` for the same resource is **idempotent**: already-registered entries report `identical` and are not duplicated. Insertion falls back gracefully when the conventional `# register resources above` marker is missing (uses the `routes.draw do` opening), and warns clearly if it can't find any anchor.
564
606
 
565
607
  ### Generated controller
566
608
 
@@ -590,7 +632,7 @@ class AdminPortal::PostPolicy < ::PostPolicy
590
632
  end
591
633
  ```
592
634
 
593
- Review and trim — the generator is liberal. Especially: drop `_id` fields when the form uses the association name, and add `:price` (not `:price_cents`) for `has_cents` fields.
635
+ Review and trim: the generator is liberal. Especially: drop `_id` fields when the form uses the association name, and add `:price` (not `:price_cents`) for `has_cents` fields.
594
636
 
595
637
  ---
596
638
 
@@ -621,9 +663,9 @@ Review and trim — the generator is liberal. Especially: drop `_id` fields when
621
663
 
622
664
  ## Related skills
623
665
 
624
- - [[plutonium-resource]] — what a resource IS (model + definition + scaffold options)
625
- - [[plutonium-behavior]] — controllers, policies, interactions
626
- - [[plutonium-tenancy]] — entity scoping, nested resources, invites
627
- - [[plutonium-auth]] — Rodauth account configuration
628
- - [[plutonium-ui]] — layouts, page classes, custom Phlex components, assets
629
- - [[plutonium-testing]] — testing portals, packages, controllers
666
+ - [[plutonium-resource]]: what a resource IS (model + definition + scaffold options)
667
+ - [[plutonium-behavior]]: controllers, policies, interactions
668
+ - [[plutonium-tenancy]]: entity scoping, nested resources, invites
669
+ - [[plutonium-auth]]: Rodauth account configuration
670
+ - [[plutonium-ui]]: layouts, page classes, custom Phlex components, assets
671
+ - [[plutonium-testing]]: testing portals, packages, controllers