The Codeplug Application is a web-based tool for managing two-way radio programming data. It serves as a "Rosetta Stone" for radio codeplugs - users create a single canonical codeplug in the app that can be exported to dozens of different CSV formats for various radio makes and models.
User Creates Meta Codeplug (app format)
↓
Contains: Channels, Zones, Systems, TalkGroups
↓
Export to Specific Radio Format
↓
Generate CSV for: Motorola CPS, Baofeng CHIRP, Kenwood KPG, etc.
Key Principle: Separate the data (what channels/frequencies you want) from the format (how each radio expects to receive that data).
- Data Layer: Models store canonical radio programming data
- Logic Layer: Services handle business logic and validations
- Export Layer: Formatters/exporters transform data into radio-specific CSV formats
- Presentation Layer: Hotwire/Turbo for dynamic UI
- Ruby on Rails 8 - Latest stable version
- Ruby 3.x - Version specified in
.ruby-version - PostgreSQL - Primary database (version 12 or higher recommended)
- Solid Queue - Background jobs (Rails 8 default)
- Solid Cache - Caching (Rails 8 default)
- Hotwire (Turbo + Stimulus) - Modern Rails frontend framework
- Bootstrap 5 - CSS framework for UI components
- esbuild - JavaScript bundling (fast, modern)
- Importmap - JavaScript module management
- Minitest - Rails default testing framework
- Capybara - System/integration testing
- Selenium/Browser Driver - For system tests
- RuboCop - Ruby linter and style enforcer (run with -a to auto-fix)
- Brakeman - Security vulnerability scanner (already in bin/)
- Kamal - Modern deployment tool
- Docker - Containerization
Strict TDD Workflow:
1. Write failing test
2. Run test (verify it fails)
3. Write minimum code to pass
4. Run test (verify it passes)
5. Refactor
6. Repeat
Rules:
- NO code without tests
- NO PRs with failing tests
- ALL tests must pass before PR creation
- Follow Red-Green-Refactor cycle
- Convention over Configuration - Follow Rails conventions
- Fat Models, Skinny Controllers - Business logic in models/services, not controllers
- RESTful Routes - Use standard REST actions where possible
- DRY (Don't Repeat Yourself) - Extract common patterns
- KISS (Keep It Simple, Stupid) - Simple solutions over clever ones
Use for complex business logic that doesn't fit cleanly in a model:
# app/services/codeplug_exporter.rb
class CodeplugExporter
def initialize(codeplug, radio_model)
@codeplug = codeplug
@radio_model = radio_model
end
def export
# Complex export logic here
end
endWhen to use:
- Multi-step operations
- Orchestrating multiple models
- External API interactions
- File generation (CSV exports)
Use for complex forms that span multiple models:
# app/forms/channel_builder_form.rb
class ChannelBuilderForm
include ActiveModel::Model
# Handle channel + system_talkgroup selection
endWhen to use:
- Forms that create/update multiple models
- Complex validation logic
- Non-RESTful form flows
Use for shared behavior across models:
# app/models/concerns/nameable.rb
module Nameable
extend ActiveSupport::Concern
included do
validates :name, presence: true
end
endWhen to use:
- Shared validations
- Common query scopes
- Reusable model behavior
Use for complex database queries:
# app/queries/available_talkgroups_query.rb
class AvailableTalkgroupsQuery
def initialize(system)
@system = system
end
def call
# Complex query to find available talkgroups
end
endWhen to use:
- Complex joins
- Filtering logic
- Reusable queries across controllers
- Test validations
- Test associations
- Test scopes
- Test instance methods
- Test class methods
# test/models/channel_test.rb
class ChannelTest < ActiveSupport::TestCase
test "should not save channel without name" do
channel = Channel.new
assert_not channel.save
end
test "should belong to codeplug" do
channel = channels(:one)
assert_respond_to channel, :codeplug
end
endFocus on:
- HTTP responses
- Redirects
- Flash messages
- Authorization
- Parameter handling
# test/controllers/channels_controller_test.rb
class ChannelsControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
get channels_url
assert_response :success
end
test "should create channel" do
assert_difference('Channel.count') do
post channels_url, params: { channel: { name: "Test" } }
end
assert_redirected_to channel_url(Channel.last)
end
endEnd-to-end user workflows using Capybara:
# test/system/channel_creation_test.rb
class ChannelCreationTest < ApplicationSystemTestCase
test "user creates a new channel" do
visit new_channel_path
fill_in "Name", with: "Local Repeater"
select "W4ABC Repeater", from: "System"
click_on "Create Channel"
assert_text "Channel was successfully created"
end
endSystem test focus:
- Complete user workflows
- JavaScript interactions
- Form submissions
- Navigation flows
- Test complex business logic
- Test formatters/exporters
- Test helper methods
No Active Storage needed - CSVs are transient:
Import Flow:
# Controller receives uploaded file
def import
csv_file = params[:file]
result = CsvImporter.new(csv_file).import
# File is processed and discarded
endExport Flow:
# Generate CSV on-the-fly
def export
csv_data = CodeplugExporter.new(@codeplug, @radio_model).to_csv
send_data csv_data, filename: "codeplug.csv", type: "text/csv"
endKey Points:
- Files never saved to disk/storage
- Processed in memory
- Streamed to user on export
- No cleanup needed
Pre-PR Requirements:
- All tests must pass (unit, integration, system)
- RuboCop violations must be resolved (run rubocop -a to auto-fix)
- Brakeman security scan must pass
Automated Checks:
# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- Run database setup
- Run rails test (all tests)
- Run rubocop -a
- Run brakemanRules:
- Breaking tests MUST be fixed before PR, even if outside commit scope
- No exceptions to passing tests requirement
- CI must be green before merge
Before creating PR:
# Run full test suite
rails test
rails test:system
# Auto-fix and check code style
rubocop -a
# Check security
brakeman
# All must pass before proceedingPre-commit Hook (Optional but Recommended):
#!/bin/bash
# .git/hooks/pre-commit
rails test && rubocop -aUser Input (Form)
↓
Controller validates params
↓
Service Object (ChannelBuilder)
↓
Creates Channel + Associates SystemTalkGroup
↓
Creates ChannelZone with position
↓
Validates against System capabilities
↓
Saves to database
↓
Turbo stream updates UI
User selects RadioModel + Codeplug
↓
Controller calls CodeplugExporter service
↓
Service fetches CodeplugLayout for RadioModel
↓
Iterates through Zones (respecting max_channels_per_zone)
↓
For each Channel, pulls System data
↓
Maps to CSV columns per layout_definition
↓
Generates CSV string
↓
Streams to user as download
Zone has 30 channels
↓
Target RadioModel.max_channels_per_zone = 16
↓
Service detects overflow
↓
Prompts user: "Split 30 channels across 2 zones?"
↓
User chooses split strategy (16/14 or 15/15)
↓
Generates 2 zones in CSV output
↓
Maintains channel order via position
Used for mode-specific system attributes:
class System < ApplicationRecord
belongs_to :mode_detail, polymorphic: true
end
class DmrModeDetail < ApplicationRecord
has_one :system, as: :mode_detail
endWhy: Keeps mode-specific attributes in separate tables, clean schema, easy to extend with new modes.
Used when relationship needs metadata:
class ChannelZone < ApplicationRecord
belongs_to :channel
belongs_to :zone
# position attribute stores channel order
endWhy: Many-to-many with additional data (position, timeslot, etc.)
- Uses PostgreSQL for robust performance and features
- JSONB columns can be used for flexible data structures where appropriate
- Array columns available for storing lists
- Full Rails Active Record support
- Rails 8 built-in authentication
- Session-based (cookies)
- Password hashing via bcrypt
- User owns their Codeplugs
- Systems/Networks/TalkGroups are shared (public read)
- Codeplugs have public/private flag
- Strong parameters in controllers
- Model validations
- CSV parsing with error handling
- Prevent SQL injection (use ActiveRecord)
- Prevent XSS (Rails auto-escaping)
- Validate CSV format
- Limit file size
- Parse safely (CSV library)
- No file execution
See MODELS.md for comprehensive index list.
Key indexes:
- Foreign keys
- User lookup (email)
- Geographic queries (lat/lng)
- Join table combinations
- Fragment caching for expensive views
- Solid Cache (Rails 8 default)
- Cache system/network data (rarely changes)
- Use
includesfor associations - Bullet gem in development (optional)
- Monitor query counts in logs
- CSV generation for large codeplugs
- Bulk operations
- Email notifications
- Use Solid Queue (Rails 8 default)
Turbo Drive:
- Automatic page navigation
- Maintains speed of SPA
Turbo Frames:
- Partial page updates
- Independent frame navigation
<turbo-frame id="channel_form">
<%= render "form" %>
</turbo-frame>Turbo Streams:
- Real-time updates
- CRUD operations without full page reload
# controller
respond_to do |format|
format.turbo_stream
format.html
endMinimal JavaScript:
- Form enhancements
- Dynamic field showing/hiding
- CSV field picker interface
- Drag-and-drop for channel reordering
// app/javascript/controllers/channel_reorder_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
// Initialize drag-and-drop
}
}Components:
- Forms (form-control, form-select)
- Buttons (btn, btn-primary)
- Cards (card, card-body)
- Navigation (navbar, nav)
- Modals for confirmations
- Toasts for flash messages
Customization:
- Override Bootstrap variables if needed
- Keep custom CSS minimal
- Use Bootstrap utilities
Infrastructure:
- Docker containers
- Zero-downtime deploys
- Health checks
- Automatic rollback
Configuration:
config/deploy.yml- Environment variables
- Database configuration
- Asset compilation
Process:
kamal setup # First-time setup
kamal deploy # Deploy updates
kamal app logs # View logsWhen needed, add:
- JSON API endpoints
- Token authentication
- API versioning
- Rate limiting
If needed:
- Action Cable for live updates
- Turbo Streams over WebSocket
- Shared codeplug editing
- Responsive design (Bootstrap handles this)
- Progressive Web App (PWA) capabilities
- Touch-friendly interfaces
- Background job for large exports
- Export queue management
- Email delivery of completed exports
- Export templates/favorites
- Singular, PascalCase:
RadioModel,SystemTalkGroup
- Plural, PascalCase:
ChannelsController,CodeplugsController
- Noun or Verb, descriptive:
CodeplugExporter,CsvImporter
- Match file being tested +
_test.rb:channel_test.rb,channels_controller_test.rb
- Plural, snake_case:
radio_models,system_talkgroups
- RESTful where possible
- Plural resource names
resources :codeplugs do
member do
get :export
end
end- Explain WHY, not WHAT
- Document complex algorithms
- Note edge cases
- Reference external docs (radio specs)
- Project overview
- Setup instructions
- Testing instructions
- Deployment guide
- Semantic versioning
- Feature additions
- Bug fixes
- Breaking changes
- Clear, actionable error messages
- Field-level validation errors
- Form inline errors (Bootstrap styling)
- Log to Rails logger
- Notify admins (future: error tracking service)
- Graceful degradation
- Helpful error pages (400, 404, 500)
- Validate format before processing
- Report line-by-line errors
- Allow partial imports with warnings
- Provide example/template files
- Semantic HTML
- ARIA labels where needed
- Keyboard navigation
- Color contrast (Bootstrap defaults)
- Form labels and instructions
- Manual keyboard testing
- Screen reader testing (when possible)
- Automated accessibility checks (future)
Not a priority now, but architecture supports it:
- Use I18n for all user-facing text
- Extract strings to locale files
- Future: Support multiple languages
- Store user locale preference
- Rails logger (verbose)
- Detailed error traces
- Query logging
- Rails logger (info level)
- Exception tracking (future: Sentry, Rollbar)
- Performance monitoring (future: Scout, Skylight)
- Uptime monitoring
- Feature branches from main
- PR required for merge
- Meaningful commit messages
- Squash merge for clean history
Add channel reordering feature
- Add drag-and-drop with Stimulus
- Update position on drop
- Add system tests for reordering
Closes #123
feature/channel-reorderingfix/tone-validation-bugrefactor/codeplug-exporter
- Ruby (version in
.ruby-version) - Node.js (version in
.node-version) - MariaDB/MySQL
- Git
- Docker (for Kamal deployment)
- VS Code with Ruby LSP
- TablePlus or similar DB GUI
- Postman (for future API testing)
bin/setup # Initial setup
bin/rails db:setup # Database setup
bin/dev # Start development servers
bin/rails test # Run tests- Tests present and passing
- Follows Rails conventions
- No RuboCop violations (after running rubocop -a)
- Clear, readable code
- Proper error handling
- Security considerations
- Performance implications
- What changed and why
- Testing performed
- Screenshots (for UI changes)
- Migration details (if applicable)
- Breaking changes noted
This architecture is designed to be:
- Scalable: Easy to add new radio models and modes
- Maintainable: Clear patterns and conventions
- Testable: TDD from the start
- Performant: Efficient queries and caching
- Secure: Rails defaults + validation
- User-Friendly: Modern UI with Hotwire
The architecture will evolve as the application grows, but these foundational principles guide all development decisions.