Admin Scaffold Generator
Generate a complete admin panel with CRUD operations, filtering, pagination, dashboard, exports, and bulk actions for any Rails 6.1+ application.
Overview
This skill creates a fully functional admin interface by:
- •Auto-detecting your project's CSS framework, pagination gem, and test framework
- •Analyzing all models for fields, associations, enums, and attachments
- •Asking targeted questions to customize the output
- •Generating controllers, views, routes, and optional tests
- •Providing clear next steps for verification
Phase 1: Detection
Before asking any questions, automatically detect the project configuration.
1.1 Detect Existing Admin Dashboards
Scan for existing admin implementations and identify which paths/namespaces they use.
Read config/routes.rb and extract:
# 1. Rails Admin - extract mount path # mount RailsAdmin::Engine => '/admin', as: 'rails_admin' # mount RailsAdmin::Engine, at: '/backend' # → Extract path: "/admin" or "/backend" etc. # 2. ActiveAdmin - extract namespace from config or routes # ActiveAdmin.routes(self) → default namespace is "admin" # Also check config/initializers/active_admin.rb for: # config.default_namespace = :backend # → Extract namespace: "admin" or custom # 3. Administrate - check for namespace block # namespace :admin do # resources :users, controller: "users" # → Usually uses "admin" namespace # 4. Custom admin - check for namespace blocks # namespace :admin do ... end # namespace :backend do ... end # → Extract all admin-like namespaces # 5. Check controller directories # app/controllers/admin/ → "admin" # app/controllers/backend/ → "backend"
Store results as:
existing_admins = [
{ type: "rails_admin", path: "/admin" },
{ type: "activeadmin", path: "/backend" },
{ type: "custom", path: "/management" }
]
# List of taken namespaces (for Question 1)
taken_namespaces = ["admin", "backend", "management"]
Common namespace options to check:
- •
admin,backend,dashboard,management,panel,console,staff
1.2 Detect CSS Framework
Read these files to determine CSS framework:
# Priority order: # 1. Tailwind: tailwind.config.js or config/tailwind.config.js exists # 2. Bootstrap: Gemfile contains 'bootstrap' or package.json contains 'bootstrap' # 3. Bulma: Gemfile contains 'bulma' or package.json contains 'bulma' # 4. Default: Tailwind (will need to be added)
Detection logic:
- •Read
tailwind.config.jsorconfig/tailwind.config.js→ Tailwind - •Read
Gemfileforgem 'bootstrap'orgem 'cssbundling-rails'+package.jsonforbootstrap→ Bootstrap - •Read
Gemfileorpackage.jsonforbulma→ Bulma
Store result as: css_framework = "tailwind" | "bootstrap" | "bulma"
1.3 Detect Pagination Gem
Read Gemfile:
# Look for: gem 'pagy' # → pagination_gem = "pagy" (recommended, ~40x faster) gem 'kaminari' # → pagination_gem = "kaminari" gem 'will_paginate' # → pagination_gem = "will_paginate" # None found # → pagination_gem = "pagy" (default, fastest option)
1.4 Detect Test Framework
# Check directory structure: # spec/ exists → test_framework = "rspec" # test/ exists → test_framework = "minitest"
1.5 Detect Stimulus
# Check for Stimulus: # package.json contains "@hotwired/stimulus" → has_stimulus = true # config/importmap.rb contains "stimulus" → has_stimulus = true # Otherwise → has_stimulus = false
1.6 Detect File Uploader
Check Gemfile and model files for file upload solution:
# Check Gemfile: gem 'activestorage' # → file_uploader = "activestorage" (Rails 5.2+ built-in) gem 'carrierwave' # → file_uploader = "carrierwave" gem 'shrine' # → file_uploader = "shrine" gem 'paperclip' # → file_uploader = "paperclip" (deprecated) gem 'dragonfly' # → file_uploader = "dragonfly" # Also check model files for: has_one_attached # → Active Storage has_many_attached # → Active Storage mount_uploader # → CarrierWave include ImageUploader # → Shrine has_attached_file # → Paperclip dragonfly_accessor # → Dragonfly
Preview syntax by uploader:
<%# Active Storage %>
<%% if record.avatar.attached? %>
<%%= image_tag record.avatar.variant(resize_to_limit: [100, 100]) %>
<%% end %>
<%# CarrierWave %>
<%% if record.avatar.present? %>
<%%= image_tag record.avatar.thumb.url %>
<%% end %>
<%# Shrine %>
<%% if record.avatar_data.present? %>
<%%= image_tag record.avatar_url(:thumb) %>
<%% end %>
<%# Paperclip (legacy) %>
<%% if record.avatar.present? %>
<%%= image_tag record.avatar.url(:thumb) %>
<%% end %>
<%# Dragonfly %>
<%% if record.avatar_stored? %>
<%%= image_tag record.avatar.thumb('100x100#').url %>
<%% end %>
Form input syntax by uploader:
<%# Active Storage %>
<%%= form.file_field :avatar, accept: "image/*", class: "{CSS: file-input}" %>
<%% if record.avatar.attached? %>
<div class="{CSS: file-preview}">
<%%= image_tag record.avatar.variant(resize_to_limit: [200, 200]) %>
<%%= form.check_box :remove_avatar, label: "Remove" %>
</div>
<%% end %>
<%# CarrierWave %>
<%%= form.file_field :avatar, class: "{CSS: file-input}" %>
<%% if record.avatar.present? %>
<div class="{CSS: file-preview}">
<%%= image_tag record.avatar.thumb.url %>
<%%= form.check_box :remove_avatar %>
</div>
<%% end %>
<%# Shrine %>
<%%= form.hidden_field :avatar, value: record.cached_avatar_data %>
<%%= form.file_field :avatar, class: "{CSS: file-input}" %>
<%# Paperclip %>
<%%= form.file_field :avatar, class: "{CSS: file-input}" %>
<%# Dragonfly %>
<%%= form.file_field :avatar, class: "{CSS: file-input}" %>
<%%= form.hidden_field :retained_avatar %>
1.7 Discover Models
# List all model files ls app/models/*.rb
Parse each model file to extract:
- •Model name (from filename and class definition)
- •Skip abstract models, concerns, ApplicationRecord
Store as: models = ["User", "Post", "Comment", ...]
1.7 Detect Existing Admin Panels
Check for existing admin implementations to understand current patterns:
# Check Gemfile for admin gems: gem 'activeadmin' # → existing_admin = "activeadmin" gem 'rails_admin' # → existing_admin = "rails_admin" gem 'administrate' # → existing_admin = "administrate"
If existing admin gem found:
- •
ActiveAdmin - Check
app/admin/*.rbfor:- •Custom form blocks
- •Custom show blocks
- •Filters configuration
- •Batch actions
- •Sidebar sections
- •
RailsAdmin - Check
config/initializers/rails_admin.rbfor:- •Custom actions
- •Field configurations
- •Navigation labels
- •
Administrate - Check
app/dashboards/*_dashboard.rbfor:- •COLLECTION_ATTRIBUTES
- •SHOW_PAGE_ATTRIBUTES
- •FORM_ATTRIBUTES
If custom admin exists (app/controllers/admin/ without gems):
Analyze existing controllers and views:
# List existing admin controllers ls app/controllers/admin/*_controller.rb # List existing admin views ls -la app/views/admin/*/
For each existing admin resource, extract:
- •Custom actions (beyond standard CRUD)
- •Custom filters or scopes
- •Special display logic in show views
- •Custom form fields or nested forms
- •Bulk actions
- •Export functionality
Store as:
existing_admin_features = {
"User" => {
custom_actions: ["impersonate", "ban", "export_csv"],
custom_filters: ["by_role", "by_status", "date_range"],
nested_forms: ["profile", "addresses"],
show_sections: ["activity_log", "permissions"],
bulk_actions: ["activate", "deactivate"]
},
# ... other models
}
Phase 2: Model Analysis
For each discovered model, extract detailed information.
2.1 Schema Information
Look for Annotate gem comments at top of model:
# == Schema Information
#
# Table name: users
#
# id :bigint not null, primary key
# email :string default(""), not null
# encrypted_password :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
If no annotation, read db/schema.rb for the table definition.
2.2 Extract Field Types
Map each column to form input and display type using reference/field-mappings.md.
2.3 Associations
Look for:
belongs_to :author, class_name: 'User' has_many :comments has_one :profile has_and_belongs_to_many :tags
2.4 Enums
enum status: { draft: 0, published: 1, archived: 2 }
enum :role, { admin: 0, user: 1 }, prefix: true # Rails 7+
2.5 Active Storage Attachments
has_one_attached :avatar has_many_attached :images
2.6 Ransack Methods (if existing)
def self.ransackable_attributes(auth_object = nil) ["email", "name", "created_at"] end def self.ransackable_associations(auth_object = nil) ["posts", "comments"] end
2.7 Soft Delete Detection
# Paranoia gem: acts_as_paranoid # Discard gem: include Discard::Model
Store soft_delete info: soft_delete = "paranoia" | "discard" | nil
Phase 3: Interactive Questions
Use the AskUserQuestion tool with these questions:
Question 1: Namespace
Build the question dynamically based on detected existing admins:
If existing admin dashboards were detected:
First, inform the user what was found:
Detected existing admin dashboard(s): - Rails Admin at /admin - Custom admin at /backend
Then ask with only available (non-conflicting) options:
{
"question": "Which namespace should the new admin panel use?",
"header": "Namespace",
"options": [
// Only include namespaces NOT in taken_namespaces
// First available option from this priority list gets "(Recommended)":
// 1. "admin" (if available)
// 2. "backend" (if available)
// 3. "dashboard" (if available)
// 4. "management" (if available)
// 5. "panel" (if available)
// Example if "admin" and "backend" are taken:
{"label": "dashboard (Recommended)", "description": "Use /dashboard path"},
{"label": "management", "description": "Use /management path"},
{"label": "panel", "description": "Use /panel path"}
],
"multiSelect": false
}
If no existing admin dashboards detected:
{
"question": "What namespace should the admin panel use?",
"header": "Namespace",
"options": [
{"label": "admin (Recommended)", "description": "Standard /admin path"},
{"label": "backend", "description": "Alternative /backend path"},
{"label": "dashboard", "description": "Use /dashboard path"},
{"label": "management", "description": "Use /management path"}
],
"multiSelect": false
}
Available namespace pool (in priority order):
admin, backend, dashboard, management, panel, console, staff
If user selects "Other", use their custom namespace.
Question 2: Models to Exclude
Before asking, analyze models and categorize them:
# Auto-detect model categories:
# 1. Technical models (always exclude)
technical_models = [
# Abstract/base classes
"ApplicationRecord",
# Models matching patterns: *Record, *Base, Abstract*
]
# 2. Join table models (likely exclude)
# Models with only foreign keys + timestamps, used for has_many :through
join_models = models.select do |m|
columns = m.column_names - %w[id created_at updated_at]
columns.all? { |c| c.ends_with?('_id') }
end
# 3. Empty tables (likely exclude)
# Models with 0 records - often internal registries, configs
empty_models = models.select { |m| m.count == 0 }
# 4. Nested/child models (suggest exclude, manage via parent)
# Naming patterns: Parent* (CandidateEmailAddress, UserPhone, etc.)
# Models that only belong_to one parent and have no independent meaning
nested_models = models.select do |m|
# Has belongs_to and name starts with parent model name
# e.g., CandidatePhone belongs_to :candidate
end
# 5. Primary models (include by default)
primary_models = models - technical_models - join_models - empty_models - nested_models
Build dynamic options based on detection:
{
"question": "Which models should be EXCLUDED from the admin panel?",
"header": "Exclude",
"options": [
{
"label": "Smart exclude (Recommended)",
"description": "Include {N} primary models, exclude {M} technical/join/empty models"
},
{
"label": "Include all {total} models",
"description": "Generate admin for every model including join tables"
},
{
"label": "Select specific models",
"description": "I'll review and choose which to exclude"
}
],
"multiSelect": false
}
If "Smart exclude" selected, show summary:
Including {N} primary models:
User, Post, Comment, Order, Product, ...
Excluding {M} models:
Technical: ApplicationRecord
Join tables: PostTag, UserRole
Empty tables: LiquidTemplate, FeatureFlag
Nested (manage via parent): CandidateEmail, CandidatePhone, UserAddress
You can manage nested models through their parent's edit form with nested attributes.
If "Select specific models", show multiSelect with categories:
{
"question": "Select models to EXCLUDE from admin:",
"header": "Exclude",
"options": [
// Group by category, pre-select recommended exclusions
{"label": "ApplicationRecord", "description": "technical ✓"},
{"label": "PostTag", "description": "join table ✓"},
{"label": "LiquidTemplate", "description": "empty (0 records) ✓"},
{"label": "CandidatePhone", "description": "nested → Candidate ✓"},
{"label": "User", "description": "primary (1,234 records)"},
{"label": "Post", "description": "primary (567 records)"},
// ...
],
"multiSelect": true
}
Question 3: Model Configuration Mode
{
"question": "How do you want to configure fields and views for each model?",
"header": "Config",
"options": [
{"label": "Smart defaults (Recommended)", "description": "Auto-detect fields, hide sensitive data, full CRUD for all"},
{"label": "Configure each model", "description": "I'll specify fields and views for each model individually"}
],
"multiSelect": false
}
If "Smart defaults":
- •All included models get full CRUD (index, show, new, edit, delete)
- •Show all fields except sensitive ones (passwords, tokens, secrets)
- •Auto-generate filters for searchable fields
- •Skip to Question 4 (Authentication)
If "Configure each model": For each included model, ask the following questions:
3a. Model View Type
{
"question": "What views should {Model} have?",
"header": "{Model}",
"options": [
{"label": "Full CRUD", "description": "Index, show, new, edit, delete"},
{"label": "Read-only", "description": "Index and show only, no editing"},
{"label": "Skip", "description": "Don't generate admin for this model"}
],
"multiSelect": false
}
3b. Index/Table Columns
{
"question": "Which fields should appear in the {Model} table (index view)?",
"header": "Table",
"options": [
// List all non-sensitive fields, pre-select recommended ones
// Pre-select: id, name/title (if exists), key identifiers, status, created_at
{"label": "id", "description": "bigint"},
{"label": "name", "description": "string ✓"},
{"label": "email", "description": "string ✓"},
{"label": "status", "description": "enum ✓"},
{"label": "created_at", "description": "datetime ✓"},
{"label": "bio", "description": "text"},
// ... (exclude sensitive fields entirely)
],
"multiSelect": true
}
3c. Show Page Fields
{
"question": "Which fields should appear on the {Model} show page?",
"header": "Show",
"options": [
// List all non-sensitive fields, pre-select all by default
{"label": "id", "description": "bigint ✓"},
{"label": "name", "description": "string ✓"},
{"label": "email", "description": "string ✓"},
{"label": "bio", "description": "text ✓"},
{"label": "created_at", "description": "datetime ✓"},
{"label": "updated_at", "description": "datetime ✓"},
// ...
],
"multiSelect": true
}
3d. Form Fields (only if Full CRUD selected)
{
"question": "Which fields should be editable in the {Model} form?",
"header": "Form",
"options": [
// List editable fields (exclude id, timestamps, computed fields)
// Pre-select all editable fields
{"label": "name", "description": "string ✓"},
{"label": "email", "description": "string ✓"},
{"label": "bio", "description": "text ✓"},
{"label": "status", "description": "enum ✓"},
{"label": "role", "description": "enum ✓"},
// ...
],
"multiSelect": true
}
Sensitive fields (always excluded from options):
SENSITIVE_FIELDS = %w[ encrypted_password password_digest password reset_password_token confirmation_token unlock_token api_token access_token refresh_token auth_token secret_token otp_secret encrypted_otp_secret secret_key api_key private_key ]
Note: If existing admin panel was detected (Section 1.7), show detected custom features and ask if they should be replicated.
Question 4: Authentication
{
"question": "How should admin authentication work?",
"header": "Auth",
"options": [
{"label": "Devise AdminUser (Recommended)", "description": "Create separate AdminUser model with Devise"},
{"label": "Existing User model", "description": "Use current User model with admin role/flag"},
{"label": "Skip authentication", "description": "No auth - secure with other means"},
{"label": "HTTP Basic Auth", "description": "Simple username/password protection"}
],
"multiSelect": false
}
Question 5: Internationalization
{
"question": "Do you want internationalization (i18n) support?",
"header": "i18n",
"options": [
{"label": "No i18n", "description": "English-only, hardcoded strings"},
{"label": "Yes - English", "description": "i18n with English locale file"},
{"label": "Yes - Russian", "description": "i18n with Russian locale file"},
{"label": "Yes - Other language", "description": "I'll specify the language"}
],
"multiSelect": false
}
Question 6: Tests
{
"question": "Should tests be generated for the admin controllers?",
"header": "Tests",
"options": [
{"label": "Yes (Recommended)", "description": "Generate controller tests"},
{"label": "No", "description": "Skip test generation"}
],
"multiSelect": false
}
Question 7: Export
{
"question": "What export functionality do you need?",
"header": "Export",
"options": [
{"label": "CSV with field selection (Recommended)", "description": "Export to CSV, admin chooses which fields"},
{"label": "CSV + Excel with field selection", "description": "Both formats, admin chooses fields"},
{"label": "Simple CSV (all fields)", "description": "Quick export without field picker"},
{"label": "No export", "description": "Skip export functionality"}
],
"multiSelect": false
}
Phase 4: Generation
Generate files in this specific order. Use templates from templates/ directory.
4.1 Add Gems (if needed)
If pagination gem not found, add to Gemfile:
gem 'pagy', '~> 6.0' gem 'ransack', '~> 4.0'
If Excel export requested:
gem 'caxlsx', '~> 4.0' gem 'caxlsx_rails', '~> 0.6'
4.2 Create Concerns
Create app/controllers/concerns/{namespace}/:
date_range_filterable.rb - See templates/concerns/date_range_filterable.rb
exportable.rb - Advanced export concern with field selection:
# app/controllers/concerns/{namespace}/exportable.rb
module {Namespace}
module Exportable
extend ActiveSupport::Concern
def export
@model_class = controller_name.classify.constantize
@exportable_fields = exportable_fields_for(@model_class)
if request.get?
# Show export modal/form with field selection
render "#{namespace}/shared/export_modal", locals: {
fields: @exportable_fields,
record_count: filtered_scope.count,
selected_ids: params[:ids]
}
else
# Perform export with selected fields
selected_fields = params[:export_fields] || @exportable_fields.keys
records = export_scope(params[:ids])
respond_to do |format|
format.csv { send_csv(records, selected_fields) }
format.xlsx { send_xlsx(records, selected_fields) }
end
end
end
private
def exportable_fields_for(model_class)
# Returns hash of field_name => human_label
model_class.column_names.each_with_object({}) do |col, hash|
next if col.in?(%w[encrypted_password reset_password_token])
hash[col] = col.humanize
end
end
def export_scope(selected_ids = nil)
scope = filtered_scope # Uses current ransack filters
scope = scope.where(id: selected_ids) if selected_ids.present?
scope
end
def filtered_scope
# Reuse ransack query from index
model_class = controller_name.classify.constantize
model_class.ransack(params[:q]).result
end
def send_csv(records, fields)
csv_data = CSV.generate(headers: true) do |csv|
csv << fields.map { |f| f.humanize }
records.find_each do |record|
csv << fields.map { |f| format_field(record, f) }
end
end
send_data csv_data, filename: "\#{controller_name}-\#{Date.current}.csv"
end
def format_field(record, field)
value = record.send(field)
case value
when Time, DateTime then value.strftime("%Y-%m-%d %H:%M")
when Date then value.strftime("%Y-%m-%d")
when true then "Yes"
when false then "No"
else value.to_s
end
end
end
end
bulk_actions.rb - See templates/features/bulk_actions.rb
4.3 Create Base Controller
Use templates/controllers/base_controller.rb as template.
Create app/controllers/{namespace}/base_controller.rb:
module {Namespace}
class BaseController < ApplicationController
include Pagy::Backend
include {Namespace}::DateRangeFilterable
include {Namespace}::Exportable
include {Namespace}::BulkActions
before_action :authenticate_{auth_method}! # Based on auth choice
layout "{namespace}"
private
def pagy_get_vars(collection, vars)
vars[:items] ||= 25
vars[:count] ||= collection.count(:all)
vars
end
end
end
4.4 Create Layout and Shared Partials
Create app/views/layouts/{namespace}.html.erb using templates/shared/layout.html.erb.
Create app/views/{namespace}/shared/ partials:
- •
_sidebar.html.erb- Navigation with all resources - •
_flash.html.erb- Flash message display - •
_pagination.html.erb- Pagination controls - •
_table_header.html.erb- Sortable column headers - •
_bulk_actions.html.erb- Bulk action dropdown - •
_export_modal.html.erb- Export dialog with field selection (if export enabled)
Export Modal (_export_modal.html.erb):
<%# Export modal with field selection %>
<div id="export-modal" class="{CSS: modal}" data-controller="export-modal">
<div class="{CSS: modal-content}">
<div class="{CSS: modal-header}">
<h3>Export <%= controller_name.humanize %></h3>
<button type="button" data-action="export-modal#close">×</button>
</div>
<%= form_tag export_path(format: :csv), method: :post, data: { export_modal_target: "form" } do %>
<%# Pass current filters %>
<% params[:q]&.each do |key, value| %>
<%= hidden_field_tag "q[#{key}]", value %>
<% end %>
<%# Pass selected IDs if bulk export %>
<% if local_assigns[:selected_ids].present? %>
<% selected_ids.each do |id| %>
<%= hidden_field_tag "ids[]", id %>
<% end %>
<% end %>
<div class="{CSS: modal-body}">
<p class="{CSS: text-muted}">
Exporting <strong><%= record_count %></strong> records
<% if selected_ids.present? %>
(<%= selected_ids.size %> selected)
<% else %>
(filtered)
<% end %>
</p>
<div class="{CSS: form-group}">
<label class="{CSS: label}">Select fields to export:</label>
<div class="{CSS: checkbox-group}">
<label class="{CSS: checkbox}">
<%= check_box_tag "select_all", "1", true, data: { action: "export-modal#toggleAll" } %>
<strong>Select All</strong>
</label>
</div>
<div class="{CSS: checkbox-grid}" data-export-modal-target="fields">
<% fields.each do |field_name, field_label| %>
<label class="{CSS: checkbox}">
<%= check_box_tag "export_fields[]", field_name, true, data: { export_modal_target: "field" } %>
<%= field_label %>
</label>
<% end %>
</div>
</div>
</div>
<div class="{CSS: modal-footer}">
<button type="button" class="{CSS: btn-secondary}" data-action="export-modal#close">
Cancel
</button>
<button type="submit" name="format" value="csv" class="{CSS: btn-primary}">
Export CSV
</button>
<%# If Excel enabled %>
<button type="submit" name="format" value="xlsx" class="{CSS: btn-primary}">
Export Excel
</button>
</div>
<% end %>
</div>
</div>
Export Modal Stimulus Controller (if has_stimulus):
// app/javascript/controllers/{namespace}/export_modal_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form", "fields", "field"]
open(event) {
event.preventDefault()
// If triggered from bulk actions, collect selected IDs
const selectedIds = this.getSelectedIds()
if (selectedIds.length > 0) {
this.addSelectedIds(selectedIds)
}
this.element.classList.remove("hidden")
}
close() {
this.element.classList.add("hidden")
}
toggleAll(event) {
const checked = event.target.checked
this.fieldTargets.forEach(field => field.checked = checked)
}
getSelectedIds() {
// Get IDs from bulk select checkboxes
const checkboxes = document.querySelectorAll('[data-bulk-select-target="checkbox"]:checked')
return Array.from(checkboxes).map(cb => cb.value)
}
addSelectedIds(ids) {
// Remove existing hidden ID fields
this.formTarget.querySelectorAll('input[name="ids[]"]').forEach(el => el.remove())
// Add new ones
ids.forEach(id => {
const input = document.createElement('input')
input.type = 'hidden'
input.name = 'ids[]'
input.value = id
this.formTarget.appendChild(input)
})
}
}
IMPORTANT: Use CSS classes from css/{css_framework}.md for all styling.
4.5 Create Dashboard
Create app/controllers/{namespace}/dashboard_controller.rb:
module {Namespace}
class DashboardController < BaseController
def index
@stats = {
# For each model:
users: User.count,
posts: Post.count,
# etc.
}
@recent_activity = [
# Last 10 records from each model with timestamps
]
end
end
end
Create app/views/{namespace}/dashboard/index.html.erb with:
- •Stat cards for each model (count, recent count)
- •Recent activity feed
- •Quick links to each resource
4.6 Create Resource Controllers
For each included model, create app/controllers/{namespace}/{model_plural}_controller.rb:
Use templates/controllers/resource_controller.rb as base.
Based on CRUD configuration (Question 3):
If model has full CRUD:
- •
indexwith ransack filtering and pagy pagination - •
showwith association preloading - •
new/createwith strong parameters - •
edit/updatewith strong parameters - •
destroy(or soft delete if detected) - •
exportaction for CSV/Excel - •
bulk_destroyaction - •
restoreaction (if soft delete)
If model is read-only:
- •
indexwith ransack filtering and pagy pagination - •
showwith association preloading - •
exportaction for CSV/Excel - •NO new/create/edit/update/destroy actions
If existing admin features detected (Section 1.7):
Replicate custom functionality from existing admin:
# Example: If ActiveAdmin had custom action member_action :impersonate, method: :post do # Replicate as: end # Becomes in new controller: def impersonate @user = User.find(params[:id]) # ... implementation end
Include detected features:
- •Custom actions → add as controller methods
- •Custom filters → add to ransack configuration
- •Nested forms → include in strong parameters and form
- •Custom show sections → add to show view
- •Bulk actions → add to bulk_actions concern
4.7 Create Resource Views
For each included model, create views in app/views/{namespace}/{model_plural}/:
Based on CRUD configuration (Question 3):
All models (read-only and CRUD):
index.html.erb - Use templates/views/index.html.erb:
- •Filter form with ransack
- •Data table with sortable columns
- •Pagination
- •If CRUD: "New" button, bulk action checkboxes, edit/delete links
- •If read-only: only "View" links, no bulk actions
Export buttons (if export enabled):
<div class="{CSS: btn-group}">
<%# Export filtered records %>
<%= link_to "Export", "#",
class: "{CSS: btn-secondary}",
data: { action: "export-modal#open" } %>
<%# Export selected records (shown when records selected) %>
<span data-bulk-select-target="bulkActions" class="hidden">
<%= link_to "Export Selected", "#",
class: "{CSS: btn-secondary}",
data: { action: "export-modal#open" } %>
</span>
</div>
<%# Include export modal %>
<%= render "{namespace}/shared/export_modal",
fields: exportable_fields_for(@model_class),
record_count: @query.result.count,
selected_ids: nil %>
show.html.erb - Use templates/views/show.html.erb:
- •Field display based on type
- •Associated records lists
- •Pretty JSON for jsonb fields
- •Image previews for attachments
- •If CRUD: Edit, Delete, Back buttons
- •If read-only: only Back button
_table.html.erb - Use templates/views/_table.html.erb
_filters.html.erb - Use templates/views/_filters.html.erb
Only for models with full CRUD:
new.html.erb - Use templates/views/new.html.erb
edit.html.erb - Use templates/views/edit.html.erb
_form.html.erb - Use templates/views/_form.html.erb:
- •Form fields based on column types (see field-mappings.md)
- •Association selects
- •Enum dropdowns
- •File upload fields with previews
- •JSON textarea for jsonb
If existing admin features detected:
Replicate custom view sections from existing admin:
- •Custom show page sections → add to show.html.erb
- •Custom index columns → add to _table.html.erb
- •Custom form fields → add to _form.html.erb
- •Nested resource forms → add accepts_nested_attributes and fields_for
4.8 Add Ransackable Methods to Models
For each model without existing ransackable methods, add:
# In app/models/{model}.rb
def self.ransackable_attributes(auth_object = nil)
# List of searchable column names
["name", "email", "status", "created_at"]
end
def self.ransackable_associations(auth_object = nil)
# List of searchable association names
["author", "comments"]
end
4.9 Create Routes
Add to config/routes.rb:
Based on CRUD configuration (Question 3):
namespace :{namespace} do
root to: "dashboard#index"
# For models with FULL CRUD:
resources :users do
collection do
get :export # Show export modal with field selection
post :export # Perform export with selected fields
delete :bulk_destroy
end
member do
patch :restore # Only if soft delete
# Custom actions from existing admin:
post :impersonate # If detected
post :ban # If detected
end
end
# For READ-ONLY models:
resources :audit_logs, only: [:index, :show] do
collection do
get :export
post :export
end
end
# Repeat for all models...
end
Export route explanation:
- •
GET /admin/users/export- Opens modal (if JS disabled, shows export page) - •
POST /admin/users/export- Performs export with selected fields, respects current filters - •
POST /admin/users/export?ids[]=1&ids[]=2- Exports only selected records
4.10 Create Stimulus Controllers (if has_stimulus)
Create app/javascript/controllers/{namespace}/:
bulk_select_controller.js:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["checkbox", "selectAll", "bulkActions", "selectedCount"]
connect() {
this.updateUI()
}
toggleAll() {
const checked = this.selectAllTarget.checked
this.checkboxTargets.forEach(cb => cb.checked = checked)
this.updateUI()
}
toggle() {
this.updateUI()
}
updateUI() {
const checked = this.checkboxTargets.filter(cb => cb.checked)
const count = checked.length
this.bulkActionsTarget.classList.toggle("hidden", count === 0)
this.selectedCountTarget.textContent = count
this.selectAllTarget.checked = count === this.checkboxTargets.length
this.selectAllTarget.indeterminate = count > 0 && count < this.checkboxTargets.length
}
getSelectedIds() {
return this.checkboxTargets
.filter(cb => cb.checked)
.map(cb => cb.value)
}
}
confirm_controller.js:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { message: String }
confirm(event) {
if (!window.confirm(this.messageValue || "Are you sure?")) {
event.preventDefault()
}
}
}
4.11 Create Tests (if requested)
Use templates from templates/specs/:
RSpec - Create spec/controllers/{namespace}/ or spec/requests/{namespace}/
Minitest - Create test/controllers/{namespace}/
4.12 Create i18n Files (if requested)
Create config/locales/{namespace}.{locale}.yml:
{locale}:
{namespace}:
shared:
actions: "Actions"
edit: "Edit"
delete: "Delete"
back: "Back"
save: "Save"
cancel: "Cancel"
search: "Search"
filter: "Filter"
export: "Export"
export_csv: "Export CSV"
export_excel: "Export Excel"
confirm_delete: "Are you sure you want to delete this record?"
bulk_delete: "Delete selected"
no_records: "No records found"
# ... more keys
dashboard:
title: "Dashboard"
total_records: "Total records"
recent_activity: "Recent activity"
# Per-model translations:
users:
title: "Users"
new: "New User"
edit: "Edit User"
# field names...
Phase 5: Verification
After generation, output these instructions with SPECIFIC details based on user choices:
Output Template
## ✅ Admin Panel Generated Successfully!
### 🔗 Admin Panel URL
**URL:** http://localhost:3000/{namespace}
(Replace `localhost:3000` with your actual host if different)
---
### 🔐 Authentication
{IF auth_choice == "Devise AdminUser"}
**Method:** Devise with AdminUser model
1. Run migrations:
```bash
rails db:migrate
- •
Create your first admin user:
bashrails console
rubyAdminUser.create!(email: 'admin@example.com', password: 'password123')
- •
Login at: http://localhost:3000/{namespace}/login
- •Email: admin@example.com
- •Password: password123
{ELSE IF auth_choice == "Existing User model"} Method: Using existing User model with admin check
Make sure your User model has an admin? method or admin boolean field.
Users with admin? == true can access the admin panel.
Login with any admin user credentials at: http://localhost:3000/{namespace}
{ELSE IF auth_choice == "HTTP Basic Auth"} Method: HTTP Basic Authentication
Credentials are set in app/controllers/{namespace}/base_controller.rb:
- •Username: admin
- •Password: (check the controller file)
You can change credentials in the authenticate method.
{ELSE IF auth_choice == "Skip authentication"} ⚠️ WARNING: No authentication configured!
Your admin panel is currently publicly accessible. This is fine for development, but NEVER deploy to production without authentication.
Recommended: Add Devise authentication:
- •
Add to Gemfile:
rubygem 'devise'
- •
Run:
bashbundle install rails generate devise:install rails generate devise AdminUser rails db:migrate
- •
Create admin user:
rubyAdminUser.create!(email: 'admin@example.com', password: 'password123')
- •
Add to
app/controllers/{namespace}/base_controller.rb:rubybefore_action :authenticate_admin_user!
{END IF}
📁 Files Created
- •
app/controllers/{namespace}/- Admin controllers - •
app/views/{namespace}/- Admin views - •
app/views/layouts/{namespace}.html.erb- Admin layout - •
config/routes.rb- Updated with admin routes {if tests}-{spec|test}/controllers/{namespace}/- Controller tests{/if} {if i18n}-config/locales/{namespace}.{locale}.yml- Translations{/if}
🚀 Quick Start
- •
Install dependencies:
bashbundle install
- •
Start server:
bashrails server
- •
Open in browser:
codehttp://localhost:3000/{namespace}
🎨 Customization
- •Navigation: Edit
app/views/{namespace}/shared/_sidebar.html.erb - •Global settings: Modify
app/controllers/{namespace}/base_controller.rb - •Styles: Adjust the layout file for your design preferences
--- ## CSS Framework Reference When generating views, use the appropriate CSS classes based on detected framework. ### Tailwind (Default) See `css/tailwind.md` for class mappings. ### Bootstrap See `css/bootstrap.md` for class mappings. ### Bulma See `css/bulma.md` for class mappings. --- ## Field Type Mappings See `reference/field-mappings.md` for complete mapping of: - Database column types → Form input types - Database column types → Table display formats - Special handling for JSON, attachments, enums --- ## Patterns Reference See `reference/patterns.md` for: - Ransack query patterns - Pagy/Kaminari/WillPaginate integration - Soft delete patterns - Export patterns - Bulk action patterns