Move Rails Markup from a single hardcoded admin gate to a host-defined capability model for internal staff: grant view, create, and/or manage so people can file bugs/change requests and track their own tickets — without becoming superadmins.
manage may edit, delete, reply, or change status. One shared base_controller_class for dashboard + toolbar API.
| Question | Decision |
|---|---|
| Audience | Internal staff only — not end-customers / multi-tenant public reporters in this track |
| Seeing others’ tickets | No, unless the user has manage (then see all) |
| Edit / delete / reply / status after create | Manage only — reporters cannot mutate their own tickets post-create |
| Auth parent | One base_controller_class for dashboard + toolbar API |
Today the product story is “admins annotate, agents consume.” Hosts hard-code something like current_user.admin? in both the layout gate and authorize_rails_markup!. That blocks letting broader internal staff file feedback and follow their tickets, while only a smaller set triages the whole queue.
| Surface | Gate today | Implication |
|---|---|---|
| Layout / toolbar chrome | Install injects current_user.admin? |
Non-admins never see FAB / pins |
| Dashboard + toolbar API | Generated authorize_rails_markup! → admin? |
Binary allow/deny; read = write |
| Page annotation pull | Annotation.for_page(url) — global among authorized users |
No ownership scoping |
fab_visible = false |
UI-only “read-only-ish” | Not enforced server-side |
| Attribution | user_id + metadata.author on create |
Identity exists; unused for ACL |
| Notifications | on_create_callback only |
No status-change / per-user notify |
| MCP / CLI / external API | Shared bearer token | Out of scope for human roles |
fix|change|question|approve, statuses pending → acknowledged → resolved|dismissed, toolbar panel, dashboard list/board, threads. Gap is authorization + scoping.
| Persona | Needs | Capabilities |
|---|---|---|
| Reporter internal staff |
File bugs/CRs; track status of own tickets only; no post-create edits | view create |
| Viewer optional |
Track own tickets only; no filing | view |
| Triager / Admin today’s admin path |
See everyone’s queue; edit/status/reply/delete/export; usually also create | view create manage |
| Agent MCP / CLI token |
Unchanged service principal | separate auth track |
Keep Rails Markup policy-thin: the gem names capabilities; the host decides who gets them.
| Capability | Allows | Does not allow |
|---|---|---|
view |
Toolbar chrome; GET pull of own annotations; “My tickets” dashboard | Seeing others’ pins/tickets; any mutation |
create |
FAB + create new annotations only (initial upsert/create) | Subsequent content edit, delete, reply, status change — even on own tickets |
manage |
See all annotations; edit content; status transitions; reply; delete; board/bulk/export | — |
manage → scope :all; otherwise scope :own (user_id == current_user.id). manage implies view. Host still maps roles onto the three capabilities.
| Action | view | create | manage |
|---|---|---|---|
| Pull / list own | ✓ | ✓ | ✓ |
| Pull / list others | — | — | ✓ |
| Create new ticket | — | ✓ | ✓ (if also granted create, or manage includes create in host mapping) |
| Edit content / upsert after create | — | — | ✓ |
| Acknowledge / resolve / dismiss | — | — | ✓ |
| Reply / delete / bulk / export | — | — | ✓ |
admin? → [:view, :create, :manage] for backwards compatibility. Reporters get [:view, :create]. Recommend treating manage as also granting create in the resolver so triagers keep the FAB.
One auth parent; extend hooks rather than replacing them.
RailsMarkup.configure do |config|
config.base_controller_class = "RailsMarkupAuthController"
# Returns capabilities for the current user.
config.authorize = ->(user, _request) {
return [] unless user
return [:view, :create, :manage] if user.admin?
return [:view, :create] if user.staff_reporter?
[]
}
# Visibility: derived — manage? :all : :own
end
base_controller_class for dashboard and toolbar API.create for new records; manage for all other writes; scoped reads.authorize_rails_markup! for hosts that prefer a method over a Proc.:view.create; hide status/delete/edit without manage).user_id / metadata.author on create via assign_current_user + author_name_method.user_id — treat missing user_id as invisible to non-manage users.view+create): FAB to file; pins/panel show own only; status/content read-only after sync.view): own pins/panel only; no FAB.manage.on_create_callback for triage alerts.Authorize resolver; action checks; scope pulls/lists by own-vs-all from manage; reject non-manage mutations after create; toolbar boot flags. Default admin? → full caps.
Layout gate on :view; FAB on :create; read-only chrome without :manage; My tickets dashboard mode.
README examples for staff role helpers; dual-gate sync (layout ↔ API); migration from binary admin?.
Transition/reply callbacks; host-owned delivery.
| Piece | Path | Change |
|---|---|---|
| Auth template | lib/generators/.../auth_controller.rb.erb |
Capability-aware entry gate |
| Install layout gate | install_generator.rb |
Gate on :view |
| Configuration | configuration.rb |
authorize proc; derive visibility from manage |
| Annotations API | annotations_controller.rb |
Create vs manage checks; scoped for_page |
| Dashboard | dashboard_controller.rb |
Own vs all default scope; hide write UI |
| Toolbar boot | _toolbar.html.erb + toolbar.js |
Pass capabilities; enforce read-only client UX |
| Annotation model | annotation.rb |
scope :for_user |
assignee_id later?manage