plutonium 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
@@ -0,0 +1,572 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Plutonium App — Installation, Packages, Portals, Routes
7
+
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
+
10
+ For the resources themselves (model + definition + scaffold options), see [[plutonium-resource]]. For controllers/policies/interactions, see [[plutonium-behavior]]. For multi-tenancy, see [[plutonium-tenancy]].
11
+
12
+ ## 🚨 Critical (read first)
13
+
14
+ - **Use the generators for everything.** `pu:core:install`, `pu:rodauth:install`, `pu:pkg:portal`, `pu:pkg:package`, `pu:res:scaffold`, `pu:res:conn`. Never hand-write base controllers, engine files, layouts, or portal route registration.
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
+ - **Pass `--dest`, `--auth`, `--force`, `--skip-bundle`** etc. for unattended runs so generators don't block on prompts.
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.
21
+
22
+ ---
23
+
24
+ # Part 1 — Installation
25
+
26
+ ## Fresh Rails app (recommended)
27
+
28
+ ```bash
29
+ rails new myapp -a propshaft -j esbuild -c tailwind \
30
+ -m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb
31
+ ```
32
+
33
+ Configures Rails + Propshaft + esbuild + TailwindCSS + Plutonium in one shot.
34
+
35
+ ## Existing Rails app
36
+
37
+ ⚠️ Use `base.rb`, **not** `plutonium.rb`.
38
+
39
+ ```bash
40
+ # Option 1 — template
41
+ bin/rails app:template \
42
+ LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb
43
+
44
+ # Option 2 — manual
45
+ # Add `gem "plutonium"` to Gemfile, then:
46
+ bundle install
47
+ rails generate pu:core:install
48
+ ```
49
+
50
+ ## Full setup workflow
51
+
52
+ ```bash
53
+ # 1. Core install (base controllers/policies/definitions/layouts)
54
+ rails generate pu:core:install
55
+
56
+ # 2. Auth (if needed)
57
+ rails generate pu:rodauth:install
58
+ rails generate pu:rodauth:account user
59
+
60
+ # 3. Portal
61
+ rails generate pu:pkg:portal admin --auth=user
62
+
63
+ # 4. First resource
64
+ rails generate pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
65
+ rails db:migrate
66
+
67
+ # 5. Connect resource to portal
68
+ rails generate pu:res:conn Post --dest=admin_portal
69
+
70
+ # 6. Mount portal in config/routes.rb
71
+ # mount AdminPortal::Engine, at: "/admin"
72
+
73
+ # 7. Start
74
+ rails server
75
+ ```
76
+
77
+ ## What `pu:core:install` creates
78
+
79
+ ```
80
+ app/
81
+ ├── controllers/
82
+ │ ├── plutonium_controller.rb # non-resource base
83
+ │ └── resource_controller.rb # CRUD base — see plutonium-behavior
84
+ ├── definitions/resource_definition.rb
85
+ ├── interactions/resource_interaction.rb
86
+ ├── models/resource_record.rb # abstract model — includes Plutonium::Resource::Record
87
+ ├── policies/resource_policy.rb
88
+ └── views/layouts/resource.html.erb
89
+
90
+ config/
91
+ ├── initializers/plutonium.rb
92
+ └── packages.rb # auto-loads packages/**/lib/engine.rb
93
+
94
+ packages/.keep
95
+ ```
96
+
97
+ The base classes (`ResourceController`, `ResourcePolicy`, `ResourceDefinition`, `ResourceRecord`, `ResourceInteraction`) are where you put app-wide defaults; resource-specific subclasses come from `pu:res:scaffold`.
98
+
99
+ ## Converting an existing model to a resource
100
+
101
+ ```ruby
102
+ # 1. Include the module on your model
103
+ class Post < ApplicationRecord
104
+ include Plutonium::Resource::Record
105
+ end
106
+ ```
107
+
108
+ ```bash
109
+ # 2. Generate supporting files (skips model + migration)
110
+ rails g pu:res:scaffold Post --no-migration --dest=main_app
111
+
112
+ # 3. Connect to portal
113
+ rails g pu:res:conn Post --dest=admin_portal
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ ```ruby
119
+ # config/initializers/plutonium.rb
120
+ Plutonium.configure do |config|
121
+ config.load_defaults 1.0
122
+
123
+ # Page chrome. Default :modern (topbar + icon rail).
124
+ # :classic preserves the legacy header + sidebar (only when upgrading).
125
+ # config.shell = :classic
126
+
127
+ # Custom assets
128
+ # config.assets.stylesheet = "custom_stylesheet"
129
+ # config.assets.script = "custom_script"
130
+ # config.assets.logo = "custom_logo.png"
131
+ end
132
+ ```
133
+
134
+ ---
135
+
136
+ # Part 2 — The Package System
137
+
138
+ Two kinds, hard split:
139
+
140
+ | Type | Purpose | Generator | Examples |
141
+ |---|---|---|---|
142
+ | **Feature** | Business logic (models, policies, definitions, interactions, migrations) | `pu:pkg:package NAME` | `blogging`, `billing`, `inventory` |
143
+ | **Portal** | Web interface (controllers, views, routes, auth) | `pu:pkg:portal NAME` | `admin_portal`, `customer_portal`, `public_portal` |
144
+
145
+ ## Feature packages
146
+
147
+ ```bash
148
+ rails g pu:pkg:package blogging
149
+ ```
150
+
151
+ Structure:
152
+
153
+ ```
154
+ packages/blogging/
155
+ ├── app/
156
+ │ ├── models/blogging/ # Blogging::Post
157
+ │ ├── definitions/blogging/ # Blogging::PostDefinition
158
+ │ ├── policies/blogging/ # Blogging::PostPolicy
159
+ │ └── interactions/blogging/ # Blogging::PublishPostInteraction
160
+ ├── db/migrate/
161
+ └── lib/engine.rb
162
+ ```
163
+
164
+ Engine:
165
+
166
+ ```ruby
167
+ module Blogging
168
+ class Engine < Rails::Engine
169
+ include Plutonium::Package::Engine
170
+ end
171
+ end
172
+ ```
173
+
174
+ Auto-namespacing: every file under `app/<kind>/blogging/` resolves to `Blogging::*`.
175
+
176
+ ### Creating resources in a feature package
177
+
178
+ ```bash
179
+ rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
180
+ ```
181
+
182
+ `--dest=<package_name>` puts model/migration in the package. Cross-package references use the full namespace:
183
+
184
+ ```bash
185
+ rails g pu:res:scaffold Comment user:belongs_to blogging/post:belongs_to body:text --dest=comments
186
+ ```
187
+
188
+ ## Portal packages
189
+
190
+ ```bash
191
+ rails g pu:pkg:portal admin
192
+ ```
193
+
194
+ Structure:
195
+
196
+ ```
197
+ packages/admin_portal/
198
+ ├── app/
199
+ │ ├── controllers/admin_portal/
200
+ │ │ ├── concerns/controller.rb # auth + shared filters
201
+ │ │ ├── dashboard_controller.rb
202
+ │ │ ├── plutonium_controller.rb
203
+ │ │ └── resource_controller.rb
204
+ │ ├── definitions/admin_portal/ # per-portal overrides
205
+ │ ├── policies/admin_portal/ # per-portal overrides
206
+ │ └── views/layouts/admin_portal.html.erb
207
+ ├── config/routes.rb
208
+ └── lib/engine.rb
209
+ ```
210
+
211
+ See Part 3 for engine configuration and Part 5 for resource connection.
212
+
213
+ ## Package loading
214
+
215
+ `config/packages.rb` (created by `pu:core:install`):
216
+
217
+ ```ruby
218
+ Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
219
+ load package
220
+ end
221
+ ```
222
+
223
+ This is loaded from `config/application.rb`. Migrations from all packages are picked up by `rails db:migrate` automatically.
224
+
225
+ ## When to use which
226
+
227
+ **Feature packages** — domain logic that:
228
+ - Could be reused across multiple portals (admin and customer both edit `Blogging::Post`)
229
+ - Has no inherent UI / auth (it's just behavior)
230
+ - You want to keep isolated from other domains (`billing` should not depend on `blogging`)
231
+
232
+ **Portal packages** — user-facing surfaces that:
233
+ - Have a specific auth flow (admin vs customer vs public)
234
+ - Render different views of the same underlying resources
235
+ - Need different policies / definitions per audience
236
+
237
+ ---
238
+
239
+ # Part 3 — Portal Engines
240
+
241
+ A portal is a Rails engine mixing in `Plutonium::Portal::Engine`. It defines its own routes, controller concern, and (optionally) entity scoping.
242
+
243
+ ## Generator
244
+
245
+ ```bash
246
+ rails g pu:pkg:portal <name>
247
+ ```
248
+
249
+ ### Options
250
+
251
+ | Option | Description |
252
+ |---|---|
253
+ | `--auth=NAME` | Rodauth account to use (e.g. `--auth=user`) |
254
+ | `--public` | Public access — no auth |
255
+ | `--byo` | Bring your own auth |
256
+ | `--scope=CLASS` | Entity class for multi-tenancy (e.g. `--scope=Organization`) |
257
+
258
+ ```bash
259
+ rails g pu:pkg:portal admin --auth=admin
260
+ rails g pu:pkg:portal api --public
261
+ rails g pu:pkg:portal custom --byo
262
+ rails g pu:pkg:portal admin --auth=admin --scope=Organization
263
+ ```
264
+
265
+ Without flags, the generator prompts interactively.
266
+
267
+ ## Engine file
268
+
269
+ ```ruby
270
+ # packages/admin_portal/lib/engine.rb
271
+ module AdminPortal
272
+ class Engine < Rails::Engine
273
+ include Plutonium::Portal::Engine
274
+
275
+ config.after_initialize do
276
+ # Optional: multi-tenancy. See plutonium-tenancy for strategies.
277
+ scope_to_entity Organization, strategy: :path
278
+ end
279
+ end
280
+ end
281
+ ```
282
+
283
+ ## Controller concern (auth)
284
+
285
+ Every portal has a `Concerns::Controller` mixed into its `ResourceController`. The generator wires this up; you customize it for auth / before_action hooks.
286
+
287
+ ### Rodauth
288
+
289
+ ```ruby
290
+ module AdminPortal::Concerns::Controller
291
+ extend ActiveSupport::Concern
292
+ include Plutonium::Portal::Controller
293
+ include Plutonium::Auth::Rodauth(:user)
294
+ end
295
+ ```
296
+
297
+ ### Public access
298
+
299
+ ```ruby
300
+ module AdminPortal::Concerns::Controller
301
+ extend ActiveSupport::Concern
302
+ include Plutonium::Portal::Controller
303
+ include Plutonium::Auth::Public
304
+ end
305
+ ```
306
+
307
+ ### BYO auth
308
+
309
+ ```ruby
310
+ module AdminPortal::Concerns::Controller
311
+ extend ActiveSupport::Concern
312
+ include Plutonium::Portal::Controller
313
+ include Plutonium::Auth::Public # disables Rodauth requirement
314
+
315
+ def current_user
316
+ @current_user ||= User.find_by(api_key: request.headers["X-API-Key"])
317
+ end
318
+ end
319
+ ```
320
+
321
+ ## Mounting
322
+
323
+ ```ruby
324
+ # config/routes.rb
325
+ Rails.application.routes.draw do
326
+ # Authenticated mount
327
+ constraints Rodauth::Rails.authenticate(:user) do
328
+ mount AdminPortal::Engine, at: "/admin"
329
+ end
330
+
331
+ # Unconstrained (portal handles its own auth)
332
+ mount PublicPortal::Engine, at: "/public"
333
+ end
334
+ ```
335
+
336
+ ## Controller hierarchy
337
+
338
+ Portal controllers inherit from the feature-package controller if one exists, OR from the portal's `ResourceController` otherwise.
339
+
340
+ ```ruby
341
+ # Feature controller exists → inherit from it AND include portal concern
342
+ class AdminPortal::PostsController < ::PostsController
343
+ include AdminPortal::Concerns::Controller
344
+ end
345
+
346
+ # No feature controller → inherit from portal's ResourceController
347
+ class AdminPortal::PostsController < AdminPortal::ResourceController
348
+ end
349
+ ```
350
+
351
+ For non-resource portal pages (dashboard, settings):
352
+
353
+ ```ruby
354
+ module AdminPortal
355
+ class DashboardController < PlutoniumController
356
+ def index; end
357
+ end
358
+ end
359
+ ```
360
+
361
+ ## Per-portal overrides
362
+
363
+ ```ruby
364
+ # Definition
365
+ class AdminPortal::PostDefinition < ::PostDefinition
366
+ input :internal_notes, as: :text # admins see this; customers don't
367
+ scope :pending_review
368
+ end
369
+
370
+ # Policy
371
+ class AdminPortal::PostPolicy < ::PostPolicy
372
+ include AdminPortal::ResourcePolicy
373
+ def destroy? = true
374
+ def permitted_attributes_for_create = %i[title content featured internal_notes]
375
+ end
376
+
377
+ # Controller
378
+ module AdminPortal
379
+ class PostsController < ResourceController
380
+ private
381
+ def preferred_action_after_submit = "index"
382
+ end
383
+ end
384
+ ```
385
+
386
+ ---
387
+
388
+ # Part 4 — Routes & `register_resource`
389
+
390
+ Portal routes live in `packages/<name>_portal/config/routes.rb`:
391
+
392
+ ```ruby
393
+ AdminPortal::Engine.routes.draw do
394
+ root to: "dashboard#index"
395
+
396
+ register_resource ::Post
397
+ register_resource Blogging::Comment
398
+
399
+ # Non-resource pages
400
+ get "settings", to: "settings#index"
401
+ end
402
+ ```
403
+
404
+ ## `register_resource` — what it does
405
+
406
+ For each call, Plutonium auto-generates:
407
+
408
+ - Top-level CRUD routes (`/posts`, `/posts/:id`, etc.)
409
+ - Nested routes for every registered `has_many` / `has_one` parent (prefixed `nested_`)
410
+ - Route names that `resource_url_for` can resolve
411
+
412
+ 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.
413
+
414
+ ## Singular (singleton) resources
415
+
416
+ For resources with no collection — a single per-user `Profile`, app-wide `Settings`, etc.:
417
+
418
+ ```ruby
419
+ register_resource ::Profile, singular: true
420
+ ```
421
+
422
+ Generates singular routes (no `:id`, no index):
423
+
424
+ - `GET /profile` → show
425
+ - `GET /profile/new` → new
426
+ - `GET /profile/edit` → edit
427
+ - `POST /profile` → create
428
+ - `PATCH /profile` → update
429
+ - `DELETE /profile` → destroy
430
+
431
+ Use the `--singular` flag on `pu:res:conn`:
432
+
433
+ ```bash
434
+ rails g pu:res:conn Profile --dest=customer_portal --singular
435
+ ```
436
+
437
+ ## Custom member / collection routes
438
+
439
+ ```ruby
440
+ register_resource ::Post do
441
+ member do
442
+ get :preview, as: :preview
443
+ get :analytics, as: :analytics
444
+ post :publish, as: :publish
445
+ end
446
+ collection do
447
+ get :archived, as: :archived
448
+ post :bulk_publish, as: :bulk_publish
449
+ end
450
+ end
451
+ ```
452
+
453
+ **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.
454
+
455
+ 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.
456
+
457
+ ## Cross-package and nested URLs
458
+
459
+ See [[plutonium-behavior]] for full `resource_url_for` signature and [[plutonium-tenancy]] for nested routing semantics.
460
+
461
+ ---
462
+
463
+ # Part 5 — Connecting Resources to Portals (`pu:res:conn`)
464
+
465
+ A resource is invisible until connected to at least one portal. The generator wires up the portal-specific controller, policy, definition, and route registration.
466
+
467
+ ## Command syntax
468
+
469
+ ```bash
470
+ rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME [--singular]
471
+ ```
472
+
473
+ Pass resources directly — avoids interactive prompts. No `--src` needed.
474
+
475
+ ## Usage
476
+
477
+ ```bash
478
+ # Main app resources
479
+ rails g pu:res:conn Post Comment Tag --dest=admin_portal
480
+
481
+ # Namespaced (from a feature package)
482
+ rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
483
+
484
+ # Singular (profile, settings, dashboard)
485
+ rails g pu:res:conn Profile --dest=customer_portal --singular
486
+ ```
487
+
488
+ **Run after migrations** — the generator reads model columns to seed the policy's `permitted_attributes_for_*`.
489
+
490
+ ## What gets generated
491
+
492
+ For `Post` connected to `admin_portal`:
493
+
494
+ ```
495
+ packages/admin_portal/app/
496
+ ├── controllers/admin_portal/posts_controller.rb
497
+ ├── policies/admin_portal/post_policy.rb
498
+ └── definitions/admin_portal/post_definition.rb
499
+ ```
500
+
501
+ Plus route registration appended to `packages/admin_portal/config/routes.rb`:
502
+
503
+ ```ruby
504
+ register_resource ::Post
505
+ register_resource ::Profile, singular: true # if --singular
506
+ ```
507
+
508
+ ### Generated controller
509
+
510
+ ```ruby
511
+ class AdminPortal::PostsController < ::PostsController
512
+ include AdminPortal::Concerns::Controller
513
+ end
514
+ ```
515
+
516
+ ### Generated policy (seeded from model columns)
517
+
518
+ ```ruby
519
+ class AdminPortal::PostPolicy < ::PostPolicy
520
+ include AdminPortal::ResourcePolicy
521
+
522
+ def permitted_attributes_for_create
523
+ [:title, :content, :user_id]
524
+ end
525
+
526
+ def permitted_attributes_for_read
527
+ [:title, :content, :user_id, :created_at, :updated_at]
528
+ end
529
+
530
+ def permitted_associations
531
+ %i[]
532
+ end
533
+ end
534
+ ```
535
+
536
+ 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.
537
+
538
+ ---
539
+
540
+ ## Generator reference
541
+
542
+ | Generator | Purpose |
543
+ |---|---|
544
+ | `pu:core:install` | Initial Plutonium setup (base classes, config, layouts) |
545
+ | `pu:rodauth:install` | Set up Rodauth auth |
546
+ | `pu:rodauth:account NAME` | Create user account type |
547
+ | `pu:rodauth:admin NAME` | Admin account with 2FA, lockout, audit |
548
+ | `pu:saas:setup` | User + entity + membership in one shot |
549
+ | `pu:saas:user NAME` | SaaS user account |
550
+ | `pu:saas:entity NAME` | Entity model |
551
+ | `pu:saas:membership` | Membership join table |
552
+ | `pu:pkg:package NAME` | Feature package |
553
+ | `pu:pkg:portal NAME` | Portal package |
554
+ | `pu:res:scaffold NAME` | Resource (model, migration, controller, policy, definition) |
555
+ | `pu:res:conn NAME` | Connect resource to portal |
556
+ | `pu:invites:install` | Invite system (see [[plutonium-tenancy]]) |
557
+ | `pu:invites:invitable NAME` | Mark a model as invitable |
558
+ | `pu:eject:layout` | Eject layouts for customization |
559
+ | `pu:eject:shell` | Eject topbar/sidebar partials |
560
+ | `pu:core:update` | Update plutonium gem + npm |
561
+ | `pu:skills:sync` | Sync Claude Code skills to project |
562
+
563
+ ---
564
+
565
+ ## Related skills
566
+
567
+ - [[plutonium-resource]] — what a resource IS (model + definition + scaffold options)
568
+ - [[plutonium-behavior]] — controllers, policies, interactions
569
+ - [[plutonium-tenancy]] — entity scoping, nested resources, invites
570
+ - [[plutonium-auth]] — Rodauth account configuration
571
+ - [[plutonium-ui]] — layouts, page classes, custom Phlex components, assets
572
+ - [[plutonium-testing]] — testing portals, packages, controllers