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
@@ -4,9 +4,9 @@ How a Plutonium app is assembled: installation, the package system (feature vs p
4
4
 
5
5
  ## Sub-pages
6
6
 
7
- - [Packages](./packages) — feature vs portal packages, structure, namespacing, package loading
8
- - [Portals](./portals) — portal engines, mounting, controller concerns, `register_resource` (including singular and custom routes), connecting resources via `pu:res:conn`
9
- - [Generators](./generators) — full `pu:*` generator catalog
7
+ - [Packages](./packages): feature vs portal packages, structure, namespacing, package loading
8
+ - [Portals](./portals): portal engines, mounting, controller concerns, `register_resource` (including singular and custom routes), connecting resources via `pu:res:conn`
9
+ - [Generators](./generators): full `pu:*` generator catalog
10
10
 
11
11
  ## Installation
12
12
 
@@ -30,7 +30,7 @@ The `plutonium.rb` template re-runs the full app bootstrap (dotenv, annotate, so
30
30
  bin/rails app:template \
31
31
  LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb
32
32
 
33
- # Or manual — add `gem "plutonium"` to Gemfile, then:
33
+ # Or manual: add `gem "plutonium"` to Gemfile, then:
34
34
  bundle install
35
35
  rails generate pu:core:install
36
36
  ```
@@ -38,7 +38,7 @@ rails generate pu:core:install
38
38
  ## Full setup workflow
39
39
 
40
40
  ```bash
41
- # 1. Core install — base controllers, policies, definitions, layouts
41
+ # 1. Core install: base controllers, policies, definitions, layouts
42
42
  rails generate pu:core:install
43
43
 
44
44
  # 2. Auth (if needed)
@@ -55,10 +55,7 @@ rails db:prepare
55
55
  # 5. Connect resource to portal
56
56
  rails generate pu:res:conn Post --dest=admin_portal
57
57
 
58
- # 6. Mount portal in config/routes.rb
59
- # mount AdminPortal::Engine, at: "/admin"
60
-
61
- # 7. Start
58
+ # 6. Start (step 3 already mounted the portal at /admin)
62
59
  bin/dev # uses Procfile to run Rails + CSS watcher
63
60
  ```
64
61
 
@@ -70,10 +67,10 @@ Visit `http://localhost:3000/admin`.
70
67
  app/
71
68
  ├── controllers/
72
69
  │ ├── plutonium_controller.rb # non-resource base
73
- │ └── resource_controller.rb # CRUD base — see Behavior › Controllers
70
+ │ └── resource_controller.rb # CRUD base; see Behavior › Controllers
74
71
  ├── definitions/resource_definition.rb
75
72
  ├── interactions/resource_interaction.rb
76
- ├── models/resource_record.rb # abstract model — includes Plutonium::Resource::Record
73
+ ├── models/resource_record.rb # abstract model: includes Plutonium::Resource::Record
77
74
  ├── policies/resource_policy.rb
78
75
  └── views/layouts/resource.html.erb
79
76
 
@@ -103,7 +100,7 @@ Plutonium.configure do |config|
103
100
  # :classic preserves the legacy header + sidebar (only when upgrading).
104
101
  # config.shell = :classic
105
102
 
106
- # Custom assets — see UI › Assets
103
+ # Custom assets; see UI › Assets
107
104
  # config.assets.stylesheet = "custom_stylesheet"
108
105
  # config.assets.script = "custom_script"
109
106
  # config.assets.logo = "custom_logo.png"
@@ -150,9 +147,9 @@ Meta-generators (`pu:saas:setup`) propagate these flags to the generators they c
150
147
 
151
148
  ## Related
152
149
 
153
- - [Packages](./packages) — feature vs portal package structure
154
- - [Portals](./portals) — portal engines, routing, resource connection
155
- - [Generators](./generators) — full generator reference
156
- - [Auth](/reference/auth/) — Rodauth setup and account types
157
- - [UI › Assets](/reference/ui/assets) — Tailwind, Stimulus, design tokens
158
- - [Tutorial](/getting-started/tutorial/) — step-by-step walkthrough
150
+ - [Packages](./packages): feature vs portal package structure
151
+ - [Portals](./portals): portal engines, routing, resource connection
152
+ - [Generators](./generators): full generator reference
153
+ - [Auth](/reference/auth/): Rodauth setup and account types
154
+ - [UI › Assets](/reference/ui/assets): Tailwind, Stimulus, design tokens
155
+ - [Tutorial](/getting-started/tutorial/): step-by-step walkthrough
@@ -1,6 +1,6 @@
1
1
  # Packages
2
2
 
3
- Plutonium apps are organized into **packages** — Rails engines with stricter conventions. Two flavors, hard split:
3
+ Plutonium apps are organized into **packages**: Rails engines with stricter conventions. Two flavors, hard split:
4
4
 
5
5
  | Type | Purpose | Generator | Examples |
6
6
  |---|---|---|---|
@@ -60,7 +60,7 @@ Each feature package gets its own base classes:
60
60
  - `Blogging::ResourceDefinition`
61
61
  - `Blogging::ResourceInteraction`
62
62
 
63
- These inherit from the main app's base classes — extend them for package-wide defaults.
63
+ These inherit from the main app's base classes; extend them for package-wide defaults.
64
64
 
65
65
  ### Creating resources inside a feature package
66
66
 
@@ -111,13 +111,13 @@ This is loaded from `config/application.rb`. Migrations from all packages are pi
111
111
 
112
112
  ## When to use which
113
113
 
114
- **Feature packages** — domain logic that:
114
+ **Feature packages**: domain logic that:
115
115
 
116
116
  - Could be reused across multiple portals (admin and customer both edit `Blogging::Post`).
117
117
  - Has no inherent UI / auth (it's just behavior).
118
118
  - You want isolated from other domains (`billing` should not depend on `blogging`).
119
119
 
120
- **Portal packages** — user-facing surfaces that:
120
+ **Portal packages**: user-facing surfaces that:
121
121
 
122
122
  - Have a specific auth flow (admin vs customer vs public).
123
123
  - Render different views of the same underlying resources.
@@ -137,10 +137,10 @@ packages/
137
137
  └── controllers, views, routes
138
138
  ```
139
139
 
140
- The portals expose the features. A single feature can be exposed by multiple portals — usually with different policies and definitions per portal.
140
+ The portals expose the features. A single feature can be exposed by multiple portals, usually with different policies and definitions per portal.
141
141
 
142
142
  ## Related
143
143
 
144
- - [Portals](./portals) — portal-specific configuration (mounting, auth, route registration)
145
- - [Generators](./generators) — `pu:pkg:package` and `pu:pkg:portal` flags
146
- - [Guide: Creating Packages](/guides/creating-packages) — task-oriented walkthrough
144
+ - [Portals](./portals): portal-specific configuration (mounting, auth, route registration)
145
+ - [Generators](./generators): `pu:pkg:package` and `pu:pkg:portal` flags
146
+ - [Guide: Creating Packages](/guides/creating-packages): task-oriented walkthrough
@@ -5,7 +5,7 @@ A portal is a Rails engine mixing in `Plutonium::Portal::Engine`. It defines its
5
5
  ## 🚨 Critical
6
6
 
7
7
  - **Use `pu:pkg:portal` for everything.** Never hand-write the engine file, controller concern, or layout.
8
- - **Pass `--auth=<name>`, `--public`, or `--byo`** for unattended runs — without one of these flags, the generator prompts.
8
+ - **Pass `--auth=<name>`, `--public`, or `--byo`** for unattended runs; without one of these flags, the generator prompts.
9
9
  - **Always connect resources with `pu:res:conn`.** Until connected, a resource has no portal routes and is invisible.
10
10
  - **For custom routes on a registered resource, pass `as:`.** Without it, `resource_url_for` can't build URLs.
11
11
 
@@ -20,7 +20,7 @@ rails g pu:pkg:portal <name>
20
20
  | Option | Description |
21
21
  |---|---|
22
22
  | `--auth=NAME` | Rodauth account to authenticate with (e.g. `--auth=user`) |
23
- | `--public` | Public access — no authentication |
23
+ | `--public` | Public access, no authentication |
24
24
  | `--byo` | Bring your own authentication |
25
25
  | `--scope=CLASS` | Entity class for multi-tenancy (e.g. `--scope=Organization`) |
26
26
 
@@ -51,7 +51,9 @@ end
51
51
 
52
52
  ## Controller concern (auth)
53
53
 
54
- Every portal has a `Concerns::Controller` mixed into its `ResourceController`. The generator wires this up; you customize for auth flow and shared before_action hooks.
54
+ 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 for auth flow and shared before_action hooks.
55
+
56
+ Portal-wide helpers the layout calls belong here, declared with `helper_method`, so they work on the dashboard as well as resource pages. The common case is `profile_url`: `Plutonium::Auth::Rodauth` defines it as `nil`, and the avatar menu only shows a Profile link when it returns a URL. `pu:profile:conn --dest=<portal>` writes the override into this concern (see [Auth › Profile](/reference/auth/profile)).
55
57
 
56
58
  ### Rodauth
57
59
 
@@ -89,19 +91,63 @@ end
89
91
 
90
92
  ## Mounting
91
93
 
94
+ `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:
95
+
92
96
  ```ruby
93
- # config/routes.rb
97
+ # packages/admin_portal/config/routes.rb (after the engine's routes.draw block)
94
98
  Rails.application.routes.draw do
95
- # Authenticated mount
96
99
  constraints Rodauth::Rails.authenticate(:user) do
97
100
  mount AdminPortal::Engine, at: "/admin"
98
101
  end
102
+ end
103
+ ```
104
+
105
+ With `--public` or `--byo` the mount is unconstrained and the portal handles its own auth.
106
+
107
+ 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`.
108
+
109
+ Route order matters: the app's `config/routes.rb` is drawn first, then each package's routes file, then gem engines (Active Storage, Turbo).
110
+
111
+ ### Mounting a portal at `/`
112
+
113
+ Two things change when a portal is mounted at `"/"`.
114
+
115
+ **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.
116
+
117
+ ```ruby
118
+ # packages/desk_portal/config/routes.rb
119
+ Rails.application.routes.draw do
120
+ mount DeskPortal::Engine, at: "/"
121
+ end
122
+
123
+ # packages/desk_portal/app/controllers/desk_portal/concerns/controller.rb
124
+ module DeskPortal
125
+ module Concerns
126
+ module Controller
127
+ extend ActiveSupport::Concern
128
+ include Plutonium::Portal::Controller
129
+ include Plutonium::Auth::Rodauth(:user)
130
+
131
+ included do
132
+ before_action { rodauth.require_account }
133
+ end
134
+ end
135
+ end
136
+ end
137
+ ```
138
+
139
+ **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`:
99
140
 
100
- # Unconstrained — the portal handles its own auth
101
- mount PublicPortal::Engine, at: "/public"
141
+ ```ruby
142
+ DeskPortal::Engine.routes.draw do
143
+ get "dashboard", to: "dashboard#index", as: :root
144
+ register_resource ::Comment
145
+ # register resources above.
102
146
  end
103
147
  ```
104
148
 
149
+ The same applies to `register_dashboard ..., at: "/"`. Confirm with `Rails.application.routes.recognize_path("/")` (still the app's home) and `recognize_path("/dashboard")`.
150
+
105
151
  ## Routes & `register_resource`
106
152
 
107
153
  Portal routes live in `packages/<name>_portal/config/routes.rb`:
@@ -126,7 +172,7 @@ For each call, Plutonium auto-generates:
126
172
  - Nested routes for every registered `has_many` / `has_one` parent (prefixed `nested_`)
127
173
  - Route names that `resource_url_for` can resolve
128
174
 
129
- 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.
175
+ 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.
130
176
 
131
177
  ### Choosing nested associations
132
178
 
@@ -136,11 +182,11 @@ Name the associations that get nested routes, and the rest are not drawn:
136
182
  register_resource ::Post, associations: %i[comments post_detail]
137
183
  ```
138
184
 
139
- `associations: []` draws none, and a name that is not a routable association fails the boot. Set `config.nested_association_routes = :declared` to make naming them the rule — see [Tenancy › Nested resources](../tenancy/nested-resources#declaring-which-associations-get-routes).
185
+ `associations: []` draws none, and a name that is not a routable association fails the boot. Set `config.nested_association_routes = :declared` to make naming them the rule; see [Tenancy › Nested resources](../tenancy/nested-resources#declaring-which-associations-get-routes).
140
186
 
141
187
  ### Singular (singleton) resources
142
188
 
143
- For resources with no collection — a single per-user `Profile`, app-wide `Settings`, etc.:
189
+ For resources with no collection: a single per-user `Profile`, app-wide `Settings`, etc.:
144
190
 
145
191
  ```ruby
146
192
  register_resource ::Profile, singular: true
@@ -178,12 +224,12 @@ end
178
224
  ```
179
225
 
180
226
  ::: warning Always pass `as:`
181
- Without `as:`, `resource_url_for(@post, action: :preview)` fails because there's no named route — especially critical for nested resources.
227
+ Without `as:`, `resource_url_for(@post, action: :preview)` fails because there's no named route, which is especially critical for nested resources.
182
228
  :::
183
229
 
184
- For most operations with business logic, prefer **interactive actions** (definition + interaction — see [Resource › Actions](/reference/resource/actions)) over custom controller routes. Action routes wire automatically with no `register_resource` block needed.
230
+ For most operations with business logic, prefer **interactive actions** (definition + interaction; see [Resource › Actions](/reference/resource/actions)) over custom controller routes. Action routes wire automatically with no `register_resource` block needed.
185
231
 
186
- ## Connecting resources — `pu:res:conn`
232
+ ## Connecting resources: `pu:res:conn`
187
233
 
188
234
  A resource is invisible until connected to at least one portal. The generator wires up the portal-specific controller, policy, definition, and route registration.
189
235
 
@@ -191,7 +237,7 @@ A resource is invisible until connected to at least one portal. The generator wi
191
237
  rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME [--singular]
192
238
  ```
193
239
 
194
- Pass resources directly — avoids interactive prompts. No `--src` needed.
240
+ Pass resources directly to avoid interactive prompts. No `--src` needed.
195
241
 
196
242
  ```bash
197
243
  # Main app resources
@@ -286,13 +332,13 @@ end
286
332
  ## Per-portal overrides
287
333
 
288
334
  ```ruby
289
- # Definition — how fields render per portal
335
+ # Definition: how fields render per portal
290
336
  class AdminPortal::PostDefinition < ::PostDefinition
291
337
  scope :pending_review
292
338
  input :internal_notes, hint: "Not shown to the author"
293
339
  end
294
340
 
295
- # Policy — which fields exist, and who may act
341
+ # Policy: which fields exist, and who may act
296
342
  # `internal_notes` appears for admins because THIS permits it,
297
343
  # not because the definition above mentions it.
298
344
  class AdminPortal::PostPolicy < ::PostPolicy
@@ -302,7 +348,7 @@ class AdminPortal::PostPolicy < ::PostPolicy
302
348
  def permitted_attributes_for_create = %i[title content featured internal_notes]
303
349
  end
304
350
 
305
- # Controller — different redirect after submit
351
+ # Controller: different redirect after submit
306
352
  module AdminPortal
307
353
  class PostsController < ResourceController
308
354
  private
@@ -321,7 +367,7 @@ config.after_initialize do
321
367
  end
322
368
  ```
323
369
 
324
- Strategies: `:path` (entity id in URL — default) or a custom method name on the portal controller concern.
370
+ Strategies: `:path` (entity id in URL, the default) or a custom method name on the portal controller concern.
325
371
 
326
372
  For the full multi-tenancy story, see [Tenancy › Entity scoping](/reference/tenancy/entity-scoping).
327
373
 
@@ -330,13 +376,13 @@ For the full multi-tenancy story, see [Tenancy › Entity scoping](/reference/te
330
376
  The generated `dashboard#index` is a plain page listing the registered resources. To replace it with a [dashboard](/guides/dashboards) of metric and chart cards, run `rails g pu:dashboard Home --dest=<portal> --at=/`, which swaps the `root to:` line for a `register_dashboard ... at: "/"` registration.
331
377
 
332
378
  ```ruby
333
- # config/routes.rb
379
+ # packages/admin_portal/config/routes.rb
334
380
  AdminPortal::Engine.routes.draw do
335
381
  root to: "dashboard#index"
336
382
  get "settings", to: "settings#index"
337
383
  end
338
384
 
339
- # Controller — inherit from PlutoniumController, NOT ResourceController
385
+ # Controller: inherit from PlutoniumController, NOT ResourceController
340
386
  module AdminPortal
341
387
  class DashboardController < PlutoniumController
342
388
  def index
@@ -351,7 +397,7 @@ See [UI › Pages](/reference/ui/pages) for custom Phlex page classes.
351
397
  ## Multiple portals
352
398
 
353
399
  ```ruby
354
- # Admin — full access, entity-scoped
400
+ # Admin: full access, entity-scoped
355
401
  module AdminPortal
356
402
  class Engine < Rails::Engine
357
403
  include Plutonium::Portal::Engine
@@ -362,7 +408,7 @@ module AdminPortal
362
408
  end
363
409
  end
364
410
 
365
- # Customer dashboard — entity-scoped to the customer's organization
411
+ # Customer dashboard: entity-scoped to the customer's organization
366
412
  module DashboardPortal
367
413
  class Engine < Rails::Engine
368
414
  include Plutonium::Portal::Engine
@@ -373,7 +419,7 @@ module DashboardPortal
373
419
  end
374
420
  end
375
421
 
376
- # Public — no auth, no entity scoping
422
+ # Public: no auth, no entity scoping
377
423
  module PublicPortal
378
424
  class Engine < Rails::Engine
379
425
  include Plutonium::Portal::Engine
@@ -383,9 +429,9 @@ end
383
429
 
384
430
  ## Related
385
431
 
386
- - [Packages](./packages) — feature vs portal split, structure, namespacing
387
- - [Generators](./generators) — full `pu:pkg:portal` / `pu:res:conn` option reference
388
- - [Behavior › Controllers](/reference/behavior/controllers) — controller key methods, hooks, customizations
389
- - [Tenancy › Entity scoping](/reference/tenancy/entity-scoping) — multi-tenancy mechanics
390
- - [Auth](/reference/auth/) — Rodauth account types referenced by `--auth=`
391
- - [UI › Layouts](/reference/ui/layouts) — customizing portal chrome
432
+ - [Packages](./packages): feature vs portal split, structure, namespacing
433
+ - [Generators](./generators): full `pu:pkg:portal` / `pu:res:conn` option reference
434
+ - [Behavior › Controllers](/reference/behavior/controllers): controller key methods, hooks, customizations
435
+ - [Tenancy › Entity scoping](/reference/tenancy/entity-scoping): multi-tenancy mechanics
436
+ - [Auth](/reference/auth/): Rodauth account types referenced by `--auth=`
437
+ - [UI › Layouts](/reference/ui/layouts): customizing portal chrome
@@ -1,8 +1,8 @@
1
1
  # Accounts
2
2
 
3
- Rodauth account types. Pick one (or several — apps can have multiple side-by-side).
3
+ Rodauth account types. Pick one (or several; apps can have multiple side-by-side).
4
4
 
5
- ## Basic account — `pu:rodauth:account`
5
+ ## Basic account: `pu:rodauth:account`
6
6
 
7
7
  ```bash
8
8
  rails generate pu:rodauth:account user [options]
@@ -59,7 +59,7 @@ rails g pu:rodauth:account api_user --api_only --jwt --jwt_refresh
59
59
  rails g pu:rodauth:account user --kitchen_sink
60
60
  ```
61
61
 
62
- ## Admin account — `pu:rodauth:admin`
62
+ ## Admin account: `pu:rodauth:admin`
63
63
 
64
64
  Pre-configured secure admin with multi-phase login, **required** TOTP, recovery codes, lockout, active session tracking, audit logging, role-based access, invite interaction, and **no public signup**.
65
65
 
@@ -74,7 +74,7 @@ rails g pu:rodauth:admin admin --extra-attributes=name:string,department:string
74
74
  | `--roles` | `super_admin,admin` | Comma-separated roles (positional enum) |
75
75
  | `--extra_attributes` | | Additional model attributes (e.g. `name:string`) |
76
76
 
77
- **Role-ordering convention:** index 0 is the most privileged. Generated invite interaction defaults new invitees to `roles[1]` — the order in `--roles=` matters.
77
+ **Role-ordering convention:** index 0 is the most privileged. Generated invite interaction defaults new invitees to `roles[1]`, so the order in `--roles=` matters.
78
78
 
79
79
  ```ruby
80
80
  enum :role, super_admin: 0, admin: 1
@@ -82,12 +82,12 @@ enum :role, super_admin: 0, admin: 1
82
82
 
83
83
  **Invite + resend.** The admin resource gets two actions:
84
84
 
85
- - **Invite** — invite a new admin by email; Rodauth sends a verification link and the invitee sets their own password through the verify flow.
86
- - **Resend invitation** — re-send the verification email. Only shown for admins who haven't verified yet.
85
+ - **Invite**: invite a new admin by email; Rodauth sends a verification link and the invitee sets their own password through the verify flow.
86
+ - **Resend invitation**: re-send the verification email. Only shown for admins who haven't verified yet.
87
87
 
88
88
  This uses Rodauth account verification, separate from the [Tenancy › Invites](/reference/tenancy/invites) system.
89
89
 
90
- Rake task for direct admin creation (generated alongside the account — namespace is `rodauth`, task name is the account name):
90
+ Rake task for direct admin creation (generated alongside the account; namespace is `rodauth`, task name is the account name):
91
91
 
92
92
  ```bash
93
93
  EMAIL=admin@example.com rails rodauth:admin
@@ -96,7 +96,7 @@ EMAIL=admin@example.com rails rodauth:admin
96
96
 
97
97
  The task creates the account and triggers a verification email; the admin sets their own password via that flow. No password is passed on the command line.
98
98
 
99
- ## SaaS setup — `pu:saas:setup` (meta-generator) {#saas-setup}
99
+ ## SaaS setup: `pu:saas:setup` (meta-generator) {#saas-setup}
100
100
 
101
101
  Creates the User + Entity + Membership trio AND runs:
102
102
 
@@ -152,7 +152,7 @@ class OrganizationCustomer < ApplicationRecord
152
152
  end
153
153
  ```
154
154
 
155
- ## API client — `pu:saas:api_client`
155
+ ## API client: `pu:saas:api_client`
156
156
 
157
157
  For machine-to-machine authentication. HTTP Basic Auth with auto-generated password.
158
158
 
@@ -233,12 +233,12 @@ end
233
233
  Emitted by the generators; both parts are required for concurrent portal logins.
234
234
 
235
235
  ```ruby
236
- # app/rodauth/rodauth_plugin.rb — the shared base, once
236
+ # app/rodauth/rodauth_plugin.rb: the shared base, once
237
237
  enable :session_isolation
238
238
  ```
239
239
 
240
240
  ```ruby
241
- # app/rodauth/<name>_rodauth_plugin.rb — once per account type
241
+ # app/rodauth/<name>_rodauth_plugin.rb: once per account type
242
242
  session_key_prefix "admin_" # namespaces EVERY key, account id included
243
243
  remember_cookie_key "_admin_remember"
244
244
  ```
@@ -249,7 +249,7 @@ Do **not** also set `session_key`: explicit values bypass `convert_session_key`
249
249
 
250
250
  ## Related
251
251
 
252
- - [Profile](./profile) — profile resource + SecuritySection component
253
- - [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth) — wiring accounts into portal controllers
254
- - [Tenancy › Invites](/reference/tenancy/invites) — invitation system on top of Rodauth signup
255
- - [App › Generators › Authentication generators](/reference/app/generators#authentication-generators) — full generator catalog
252
+ - [Profile](./profile): profile resource + SecuritySection component
253
+ - [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth): wiring accounts into portal controllers
254
+ - [Tenancy › Invites](/reference/tenancy/invites): invitation system on top of Rodauth signup
255
+ - [App › Generators › Authentication generators](/reference/app/generators#authentication-generators): full generator catalog
@@ -4,18 +4,18 @@ Plutonium uses [Rodauth](http://rodauth.jeremyevans.net/) via [rodauth-rails](ht
4
4
 
5
5
  ## Sub-pages
6
6
 
7
- - [Accounts](./accounts) — Rodauth install, basic accounts, admin accounts, SaaS setup, account customization
8
- - [Profile](./profile) — profile resource generator, the SecuritySection component
7
+ - [Accounts](./accounts): Rodauth install, basic accounts, admin accounts, SaaS setup, account customization
8
+ - [Profile](./profile): profile resource generator, the SecuritySection component
9
9
 
10
10
  ## 🚨 Critical
11
11
 
12
12
  - **Use the generators.** `pu:rodauth:install`, `pu:rodauth:account`, `pu:rodauth:admin`, `pu:saas:setup`, `pu:profile:install`, `pu:profile:conn`. Never hand-write Rodauth plugin files, account models, or profile resources.
13
- - **Role index 0 is the most privileged** (`owner`, `super_admin`). Invite interactions default new invitees to **index 1** — the order in `--roles=` matters.
13
+ - **Role index 0 is the most privileged** (`owner`, `super_admin`). Invite interactions default new invitees to **index 1**, so the order in `--roles=` matters.
14
14
  - **`pu:saas:setup --roles=...` always prepends `owner` as index 0.** Don't include `owner` in the option.
15
15
  - **`pu:saas:setup` is a meta-generator.** It also runs `pu:saas:portal`, `pu:profile:setup`, `pu:saas:welcome`, and `pu:invites:install`. Don't re-run those manually.
16
- - **Profile association is always `:profile`** regardless of the model class — `current_user.profile`, `build_profile`, `params.require(:profile)`.
17
- - **Profile needs `pu:profile:conn` to be visible** — without it, the singular `/profile` route and `profile_url` helper don't exist.
18
- - **Every user needs a profile row.** Add an `after_create` callback or `find_or_create_by` — otherwise `current_user.profile` is nil.
16
+ - **Profile association is always `:profile`** regardless of the model class: `current_user.profile`, `build_profile`, `params.require(:profile)`. `pu:profile:conn` hardcodes `current_user.profile`.
17
+ - **Profile needs `pu:profile:conn` to be visible.** Without it, there is no singular `/profile` route and `profile_url` stays `nil` (no menu link).
18
+ - **Check an existing profile policy for `update?`.** Older `pu:profile:conn` output lacks `def update? = true`, so the profile can't be edited once it exists. See [Profile](./profile#what-pu-profile-conn-generates).
19
19
 
20
20
  ## Install Rodauth
21
21
 
@@ -34,7 +34,7 @@ class ResourceController < PlutoniumController
34
34
  end
35
35
  ```
36
36
 
37
- Multiple account types — include the matching `:name`:
37
+ Multiple account types: include the matching `:name`:
38
38
 
39
39
  ```ruby
40
40
  class AdminController < PlutoniumController
@@ -80,9 +80,9 @@ Authorization: Bearer <access_token>
80
80
 
81
81
  ## Related
82
82
 
83
- - [Accounts](./accounts) — account types and feature flags
84
- - [Profile](./profile) — profile resource + SecuritySection
85
- - [Tenancy › Invites](/reference/tenancy/invites) — invitation system on top of Rodauth signup
86
- - [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth) — portal-side wiring
87
- - [Guides › Authentication](/guides/authentication) — task-oriented walkthrough
83
+ - [Accounts](./accounts): account types and feature flags
84
+ - [Profile](./profile): profile resource + SecuritySection
85
+ - [Tenancy › Invites](/reference/tenancy/invites): invitation system on top of Rodauth signup
86
+ - [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth): portal-side wiring
87
+ - [Guides › Authentication](/guides/authentication): task-oriented walkthrough
88
88
  - [Guides › User profile](/guides/user-profile)