Extending the dashboard
The dashboard isn't a walled garden. A host controller can wear the engine's layout and build pages from the same component helpers the engine's own screens use — so the admin surfaces you build around your agents (an audit page over the refunds Sundae issued, a browser for whatever memory your resolvers read) sit inside the same console, matching its look, theme toggle, and keyboard chrome, without forking or patching the engine.
The surface is deliberately small — a layout, a helper module, and one gotcha — not a plugin system.
The three lines
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Opt your own views into Protege's component helpers (page_container, card, table, …).
helper Protege::ApplicationHelper
# The engine injects its engine-scoped url_helpers into every view context (so its Turbo-broadcast
# partials resolve paths under the /protege mount). Re-including the host's url_helpers afterwards
# makes yours win in your own views — without this, path helpers can resolve against the engine.
helper Rails.application.routes.url_helpers
end# app/controllers/refunds_controller.rb
class RefundsController < ApplicationController
layout "protege/application" # wear the dashboard chrome: header, nav, theme, hotkeys
def index
@refunds = Refund.includes(:order).order(created_at: :desc)
end
endThat's the whole mechanism: layout "protege/application" puts your page inside the dashboard shell, and Protege::ApplicationHelper gives your views the engine's UI toolkit.
A worked example
The Scoop wants an audit page over every refund Sundae has issued — a first-class dashboard screen, built entirely in the host app:
/ app/views/refunds/index.html.slim
- pagy, refunds = paginate(@refunds)
ruby:
rows = refunds.map do |refund|
[
ui_link(refund.order.number, refund_path(refund)),
number_to_currency(refund.amount_cents / 100.0),
truncate(refund.reason, length: 80),
refund.created_at.to_date.iso8601
]
end
= page_container do
= page_header("Refunds", "Every refund Sundae has issued, newest first.")
= card do
= table(\
headers: ["Order", "Amount", "Reason", "Issued"],
rows: rows,
empty: "No refunds yet — Sundae hasn't issued one.")
= sidebar_pagination(pagy)Routes are ordinary host routes (resources :refunds) — put them behind your own authentication, exactly like the engine mount itself.
The component toolkit
Everything below comes with helper Protege::ApplicationHelper and is what the engine's own screens are built from. Each is RDoc'd in engine/app/helpers/protege/components/:
| Group | Helpers |
|---|---|
| Layout & typography | page_container, page_header(title, subtitle), heading_1 / heading_2, body_text, hint |
| Surfaces | card, info_card(title, pairs), grid_container(cols:) + grid_card(title, description, href:) |
| Data | table(headers:, rows:, empty:), badge(text, color, size:), paginate(scope) + sidebar_pagination(pagy) |
| Actions & forms | button, ui_link, destructive_link(confirm:), dialog, form_errors, form_field, form_select, form_checkbox, form_checklist |
Table cells accept already-rendered markup, so a ui_link or badge drops straight into a row — that's the pattern above, and the one the engine's own index screens use.
The limits
This is limited extensibility, and honestly so:
- The top nav is fixed. There's no registration hook for adding a section to the engine's header (or a
g-number shortcut). Link to your pages from your own navigation, or from wherever in your app the workflow starts. - It's the engine's design system, not a frozen API. The helpers are versioned with the engine and may change across alpha releases — breakage lands in the CHANGELOG.
- Prefer the helpers over raw utility classes. The layout loads the engine's precompiled stylesheet, which includes only the classes the engine itself uses — an arbitrary Tailwind class in your view won't exist unless your own asset pipeline provides it.
- Auth is yours. Engine pages sit behind whatever you wrapped the mount in; pages you add sit behind whatever you wrap them in.
Related
- Invoking from your app — the code-level counterpart: handing agents work from host code.
- Configuration —
nav_titlere-brands the dashboard header. - Observing your agent — the tour of the built-in screens.