From 2dc56a0e64652f75c2722113d8f0d202f1003705 Mon Sep 17 00:00:00 2001 From: Andrew Ohnstad Date: Wed, 17 Dec 2025 20:44:39 -0500 Subject: [PATCH 1/2] Update documentation and UI polish for zone architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documentation Updates: - docs/MODELS.md: Complete rewrite with new zone architecture - Zone is now standalone, owned by user - Added ZoneSystem, ZoneSystemTalkGroup, CodeplugZone documentation - Added ChannelGenerator service documentation - Updated relationships and business rules - CLAUDE.md: Update for zone workflows - Updated Core Data Models section - Added zone workflow and channel generation logic - Added ChannelGenerator to service object examples - Updated last updated date - README.md: Add zone workflow documentation - Updated data model section with zone architecture overview - Added "Creating Zones" user workflow section - Added "Regenerating Channels" workflow - Updated seed data description - docs/ZONE_MIGRATION_GUIDE.md: New migration guide for developers - Documents before/after architecture - Lists database and model changes - Provides upgrade steps and common issues - References all related issues UI Polish: - Zone form: Added helpful intro explaining what zones are - Zone form: Improved public checkbox help text - Codeplug show: Added subtitle explaining zones are templates Closes #106 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 59 ++++- README.md | 50 +++- app/views/codeplugs/show.html.erb | 3 +- app/views/zones/_form.html.erb | 13 +- docs/MODELS.md | 372 ++++++++++++++++++------------ docs/ZONE_MIGRATION_GUIDE.md | 360 +++++++++++++++++++++++++++++ 6 files changed, 692 insertions(+), 165 deletions(-) create mode 100644 docs/ZONE_MIGRATION_GUIDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 82146fe..9ccac73 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,8 +80,9 @@ If ANY test fails (even outside your changes), you MUST fix it before creating t See `docs/MODELS.md` for complete specifications. Key models: ### User & Ownership -- `User` - Rails 8 authentication, owns codeplugs +- `User` - Rails 8 authentication, owns codeplugs and zones - `Codeplug` - User's complete radio configuration (can be public/private) +- `Zone` - Standalone template owned by user (can be public/private) ### Radio Hardware (Shared/Universal) - `Manufacturer` - Radio manufacturers (Motorola, Baofeng, etc.) @@ -94,19 +95,24 @@ See `docs/MODELS.md` for complete specifications. Key models: - `Network` - Talkgroup organization (Brandmeister, DMRVA, etc.) - `TalkGroup` - Digital radio talkgroup -### Join Tables with Metadata -- `SystemNetwork` - Systems can be on multiple networks -- `SystemTalkGroup` - Talkgroup + timeslot per system -- `ChannelZone` - Channel position within zone +### Zone Architecture (Template-Based) +- `Zone` - Standalone template defining systems/talkgroups (owned by user, public/private) +- `ZoneSystem` - Systems in a zone (with position) +- `ZoneSystemTalkGroup` - Talkgroups for digital systems in a zone +- `CodeplugZone` - Links zones to codeplugs (with position) -### User's Configuration -- `Zone` - Logical grouping of channels (unlimited size in app) -- `Channel` - User's configuration to access a System (references System + adds settings) +### Channel Management +- `Channel` - Generated from zones or manually created (has `source_zone_id` for tracking) +- `ChannelZone` - Channel position within zone +- `SystemTalkGroup` - Talkgroup + timeslot per system ### Key Relationships +- Zone → ZoneSystem → ZoneSystemTalkGroup (defines what to generate) +- Codeplug → CodeplugZone → Zone (links zones to codeplugs) - Channel → System (pulls in frequencies, tones) - Channel → SystemTalkGroup (for digital: talkgroup + timeslot) -- Channel ↔ Zone (many-to-many with position) +- Channel → source_zone (tracks which zone generated this channel) +- Channel ↔ Zone via ChannelZone (many-to-many with position) - System → ModeDetail (polymorphic: analog/DMR/P25/etc.) --- @@ -117,7 +123,8 @@ See `docs/MODELS.md` for complete specifications. Key models: - Complex multi-step operations - CSV export/import logic - Orchestrating multiple models -- Example: `CodeplugExporter`, `CsvImporter` +- Channel generation from zones +- Example: `ChannelGenerator`, `CodeplugExporter`, `CsvImporter` ### When to Use Form Objects - Forms spanning multiple models @@ -216,6 +223,35 @@ end ## Important Business Logic +### Zone Workflow (Template-Based Architecture) +Zones are standalone templates that define what channels should be generated: + +1. **Create Zone**: User creates a zone (private by default, can be made public) +2. **Add Systems**: User adds systems to the zone via ZoneSystem +3. **Add Talkgroups**: For digital systems, user adds talkgroups via ZoneSystemTalkGroup +4. **Add to Codeplug**: User adds zones to codeplug via CodeplugZone +5. **Generate Channels**: User clicks "Generate Channels" to create channels from zones +6. **Customize**: User can edit generated channels (changes persist until regeneration) + +### Channel Generation Logic +The `ChannelGenerator` service creates channels from zones: +- **Analog systems**: Creates one channel per system +- **Digital systems**: Creates one channel per ZoneSystemTalkGroup +- Sets `source_zone_id` to track which zone generated the channel +- Creates `ChannelZone` records with sequential positions +- With `regenerate: true`: Destroys existing channels first + +```ruby +generator = ChannelGenerator.new(codeplug) +result = generator.generate_channels(regenerate: false) +# => { channels_created: 5, zones_processed: 2, skipped: false } +``` + +### Public vs Private Zones +- Private zones: Only owner can view/edit/use +- Public zones: Any user can view and add to their codeplugs +- Use `Zone.available_to_user(user)` scope to get zones a user can see + ### Polymorphic Mode Details Systems have different attributes based on mode: - **DMR**: color_code (0-15) @@ -229,6 +265,7 @@ Use polymorphic association: `System belongs_to :mode_detail, polymorphic: true` - Channel references a **System** (gets frequencies, tones) - For digital systems, Channel references **SystemTalkGroup** (includes timeslot) - Channel adds user preferences (power, bandwidth override, tone_mode) +- Channel has `source_zone_id` if it was generated from a zone ### Tone Handling - System has `tx_tone_value` and `rx_tone_value` (CTCSS/DCS codes) @@ -572,4 +609,4 @@ When helping with this project: --- -**Last Updated**: 2025-11-01 +**Last Updated**: 2025-12-17 diff --git a/README.md b/README.md index c7b4baa..2e2611a 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,22 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for detailed architectural deci The application manages several key entities: - **Codeplugs**: User's complete radio configuration -- **Channels**: Individual channel configurations referencing systems -- **Zones**: Logical groupings of channels (unlimited in app, split on export) -- **Systems**: Repeater/simplex frequencies with technical specs +- **Zones**: Standalone templates defining systems and talkgroups (public/private) +- **Channels**: Generated from zones or manually created +- **Systems**: Repeater/simplex frequencies with technical specs (shared resource) - **TalkGroups**: Digital radio talkgroup definitions organized by networks - **Radio Models**: Radio hardware specifications and capabilities - **Codeplug Layouts**: CSV export format definitions for specific radios +### Zone Architecture + +Zones use a template-based approach: +- Zones are **standalone entities** owned by users (not embedded in codeplugs) +- Zones can be **public** (shareable) or **private** +- Zones define which **systems** and **talkgroups** to include +- Channels are **generated** from zones when the user is ready +- Generated channels can be **customized** (changes persist until regeneration) + See [docs/MODELS.md](docs/MODELS.md) for complete data model documentation. ## Requirements @@ -87,6 +96,11 @@ The seed data creates: - 1 development user account - 10 radio manufacturers - 10 realistic radio models with specifications +- Sample networks (Brandmeister, TGIF, P25 Network) +- Sample systems (analog and DMR repeaters) +- Sample talkgroups +- Sample zones with systems and talkgroups +- Sample codeplug with generated channels ### 5. Start Development Server @@ -253,15 +267,33 @@ rails db:rollback ## User Workflows -### Creating a Codeplug +### Creating Zones (Templates) 1. User registers/logs in -2. Creates a new Codeplug -3. Adds Systems (repeaters) with location and technical specs -4. Creates Channels referencing Systems -5. Organizes Channels into Zones +2. Navigates to "Zones" in the navigation menu +3. Creates a new Zone (name, optional long/short names) +4. Adds Systems to the zone (analog repeaters, DMR repeaters, etc.) +5. For digital systems, adds Talkgroups (with timeslot info) +6. Optionally marks the zone as "Public" to share with other users + +### Creating a Codeplug + +1. User creates a new Codeplug +2. Adds Zones to the codeplug (own zones or public zones from other users) +3. Reorders zones as desired using drag-and-drop +4. Clicks "Generate Channels" to create channels from the zone templates +5. Optionally customizes generated channels (name, power level, etc.) 6. Exports Codeplug for specific Radio Model +### Regenerating Channels + +If you modify zones (add systems/talkgroups) after generating channels: +1. Navigate to the codeplug +2. Click "Regenerate Channels" +3. Confirm the regeneration (existing channels will be replaced) +4. New channels are generated from the updated zone templates +5. Any previous customizations will be lost + ### Exporting to Radio Format 1. User selects Codeplug to export @@ -430,4 +462,4 @@ Built with: **Version**: 0.1.0 (Initial Development) -**Last Updated**: 2025-11-01 +**Last Updated**: 2025-12-17 diff --git a/app/views/codeplugs/show.html.erb b/app/views/codeplugs/show.html.erb index f135423..5858604 100644 --- a/app/views/codeplugs/show.html.erb +++ b/app/views/codeplugs/show.html.erb @@ -42,7 +42,8 @@
-
Standalone Zones
+
Zones
+ Templates that define what channels to generate
<%# Add zone form %> diff --git a/app/views/zones/_form.html.erb b/app/views/zones/_form.html.erb index c127cd0..698b28f 100644 --- a/app/views/zones/_form.html.erb +++ b/app/views/zones/_form.html.erb @@ -1,4 +1,12 @@ <%= form_with model: zone do |f| %> +
+ What is a Zone? +

+ Zones are templates that define which systems (repeaters) and talkgroups you want on your radio. + After creating a zone, you'll add systems to it, then add the zone to your codeplug to generate channels. +

+
+ <% if zone.errors.any? %>
<%= pluralize(zone.errors.count, "error") %> prohibited this zone from being saved:
@@ -32,7 +40,10 @@
<%= f.check_box :public, class: "form-check-input" %> <%= f.label :public, "Make this zone public", class: "form-check-label" %> -
Public zones can be viewed by all users. Private zones are only visible to you.
+
+ Public zones can be viewed and added to codeplugs by any user. + This is useful for sharing commonly used repeater groups with the community. +
diff --git a/docs/MODELS.md b/docs/MODELS.md index 1c8c020..9fa3be9 100644 --- a/docs/MODELS.md +++ b/docs/MODELS.md @@ -6,10 +6,10 @@ This document defines all data models for the Codeplug Application. The app foll ## Core Concepts -- **System**: A radio repeater or simplex frequency with technical specifications -- **Channel**: A user's configuration to access a System (references System + adds user preferences) -- **Zone**: A logical grouping of Channels (unlimited size in app, split on export if needed) -- **Codeplug**: A user's complete radio programming (contains Zones and Channels) +- **System**: A radio repeater or simplex frequency with technical specifications (shared resource) +- **Zone**: A standalone template defining which systems and talkgroups to include (owned by user, can be public/private) +- **Channel**: A user's configuration to access a System, generated from zones or created manually +- **Codeplug**: A user's complete radio programming configuration (contains generated channels, references zones) - **RadioModel**: A specific make/model of radio with capabilities and limits - **CodeplugLayout**: The CSV export format for a specific RadioModel - **TalkGroup**: Digital radio talkgroup (DMR, P25, etc.) @@ -17,6 +17,31 @@ This document defines all data models for the Codeplug Application. The app foll --- +## Zone Architecture Overview + +The zone architecture follows a template-based approach: + +``` +Zone (template, owned by user) +├── ZoneSystem (systems in this zone) +│ └── ZoneSystemTalkGroup (talkgroups for digital systems) +└── Linked to Codeplugs via CodeplugZone + +Codeplug (user's radio configuration) +├── CodeplugZone (references to zones, ordered) +└── Channels (generated from zones or manual) + └── ChannelZone (channel position within zones) +``` + +**Workflow:** +1. User creates standalone Zones with Systems and Talkgroups +2. User adds Zones to a Codeplug via CodeplugZone +3. User clicks "Generate Channels" to create Channels from the zones +4. Channels are created with `source_zone_id` tracking their origin +5. User can customize generated channels (changes persist until regeneration) + +--- + ## Models ### User @@ -27,12 +52,12 @@ Standard Rails 8 authentication model with user preferences. - `password_digest` (string, required) - `name` (string) - `callsign` (string) - ham radio callsign -- User preference columns (TBD as needed): - - `default_power_level` (string) - - `measurement_preference` (string) - display format preferences +- `default_power_level` (string) +- `measurement_preference` (string) - display format preferences **Associations:** - `has_many :codeplugs` +- `has_many :zones` - standalone zones owned by user **Validations:** - Email presence and uniqueness @@ -45,9 +70,12 @@ Radio manufacturers (Motorola, Baofeng, Kenwood, etc.) **Attributes:** - `name` (string, required, unique) +- `system_record` (boolean) - true for system-provided records +- `user_id` (integer, foreign key, nullable) - creator for user-defined records **Associations:** - `has_many :radio_models` +- `belongs_to :user, optional: true` **Validations:** - Name presence and uniqueness @@ -67,42 +95,30 @@ Specific radio make/model with capabilities and constraints. - `short_channel_name_length` (integer) - `long_zone_name_length` (integer) - `short_zone_name_length` (integer) -- `frequency_ranges` (text/json) - array of hashes: `[{band: "2m", min: 144.0, max: 148.0}, {band: "70cm", min: 420.0, max: 450.0}]` +- `frequency_ranges` (text/json) - array of hashes +- `system_record` (boolean) - true for system-provided records +- `user_id` (integer, foreign key, nullable) - creator for user-defined records **Associations:** - `belongs_to :manufacturer` +- `belongs_to :user, optional: true` - `has_many :codeplug_layouts` **Validations:** - Manufacturer presence - Name presence - At least one supported mode -- Positive integers for zone/channel limits and name lengths - -**Notes:** -- Frequency ranges stored as serialized array/JSON for flexibility -- Some radios are "single zone" (treat as 1 zone with X channels) --- ### CodeplugLayout -Defines CSV export format for a specific RadioModel. Stores field mappings so users can customize export formats. +Defines CSV export format for a specific RadioModel. **Attributes:** - `radio_model_id` (integer, foreign key, required) - `name` (string, required) - e.g., "Chirp CSV Format", "CPS Standard" - `user_id` (integer, foreign key, nullable) - creator, null for system defaults -- `layout_definition` (text/json) - field mapping configuration: - ```json - { - "columns": [ - {"header": "Channel Name", "maps_to": "long_name"}, - {"header": "RX Freq", "maps_to": "system.rx_frequency"}, - {"header": "TX Freq", "maps_to": "system.tx_frequency"}, - {"header": "Power", "maps_to": "power_level"} - ] - } - ``` +- `layout_definition` (text/json) - field mapping configuration **Associations:** - `belongs_to :radio_model` @@ -113,10 +129,6 @@ Defines CSV export format for a specific RadioModel. Stores field mappings so us - Name presence - Valid JSON structure for layout_definition -**Notes:** -- Users can create custom layouts via field picker interface -- System-provided layouts have `user_id: null` - --- ### Network @@ -126,20 +138,16 @@ Organization/network that operates TalkGroups (e.g., Brandmeister, DMRVA). - `name` (string, required, unique) - `description` (text) - `website` (string) -- `network_type` (string) - e.g., "DMR", "P25", "NXDN" +- `network_type` (string) - e.g., "Digital-DMR", "Digital-P25", "Digital-NXDN" **Associations:** -- `has_many :talkgroups` +- `has_many :talk_groups` - `has_many :system_networks` - `has_many :systems, through: :system_networks` **Validations:** - Name presence and uniqueness -**Notes:** -- Users can create new networks -- Network may be specific to a digital mode or support multiple - --- ### TalkGroup @@ -153,8 +161,8 @@ Digital radio talkgroup identifier (DMR, P25, etc.). **Associations:** - `belongs_to :network` -- `has_many :system_talkgroups` -- `has_many :systems, through: :system_talkgroups` +- `has_many :system_talk_groups` +- `has_many :systems, through: :system_talk_groups` **Validations:** - Network presence @@ -175,16 +183,13 @@ A radio repeater or simplex frequency with technical specifications. Shared acro - `mode` (string, required, enum) - "analog", "dmr", "p25", "nxdn", etc. - `tx_frequency` (decimal, required) - repeater transmit (radio receive) - `rx_frequency` (decimal, required) - repeater receive (radio transmit) -- `bandwidth` (string) - default bandwidth (e.g., "25kHz", "12.5kHz") +- `bandwidth` (string) - default bandwidth - `supports_tx_tone` (boolean, default: false) - `supports_rx_tone` (boolean, default: false) -- `tx_tone_value` (string, nullable) - e.g., "127.3", "065" +- `tx_tone_value` (string, nullable) - `rx_tone_value` (string, nullable) -- `city` (string) -- `state` (string) -- `county` (string) -- `latitude` (decimal) -- `longitude` (decimal) +- `city`, `state`, `county` (strings) +- `latitude`, `longitude` (decimals) - `mode_detail_id` (integer, foreign key, polymorphic) - `mode_detail_type` (string, polymorphic) @@ -192,20 +197,16 @@ A radio repeater or simplex frequency with technical specifications. Shared acro - `belongs_to :mode_detail, polymorphic: true` - `has_many :system_networks` - `has_many :networks, through: :system_networks` -- `has_many :system_talkgroups` -- `has_many :talkgroups, through: :system_talkgroups` +- `has_many :system_talk_groups` +- `has_many :talk_groups, through: :system_talk_groups` - `has_many :channels` +- `has_many :zone_systems` +- `has_many :zones, through: :zone_systems` **Validations:** - Name, mode, tx_frequency, rx_frequency presence - Valid mode from enum - Frequencies within valid ranges -- Tone values from valid CTCSS/DCS list if present - -**Notes:** -- Tone values stored as strings: "127.3" or "065" -- Mode-specific attributes stored in polymorphic mode_detail -- Location data optional but recommended --- @@ -216,23 +217,13 @@ Base for mode-specific system attributes. **Attributes:** - `color_code` (integer, required, 0-15) -**Validations:** -- Color code between 0 and 15 - #### P25ModeDetail **Attributes:** -- `nac` (string, required) - Network Access Code, e.g., "293" - -**Validations:** -- NAC presence and format +- `nac` (string, required) - Network Access Code #### AnalogModeDetail **Attributes:** -- (May not need any additional attributes, or could store analog-specific settings) - -**Notes:** -- Additional mode detail models (NxdnModeDetail, etc.) added as needed -- Each mode detail model has specific validations for its attributes +- (No additional attributes needed) --- @@ -248,37 +239,29 @@ Associates Systems with Networks (many-to-many). - `belongs_to :network` **Validations:** -- System and network presence - Unique combination of system_id and network_id -**Notes:** -- Most systems connected to one network, but some support multiple -- Used to filter available talkgroups when building channels - --- ### SystemTalkGroup (Join Table) -Associates TalkGroups with Systems, including mode-specific attributes like timeslot. +Associates TalkGroups with Systems, including timeslot for DMR. **Attributes:** - `system_id` (integer, foreign key, required) -- `talkgroup_id` (integer, foreign key, required) -- `timeslot` (integer, nullable) - DMR timeslot (1 or 2), null for non-DMR +- `talk_group_id` (integer, foreign key, required) +- `timeslot` (integer, nullable) - DMR timeslot (1 or 2) **Associations:** - `belongs_to :system` -- `belongs_to :talkgroup` +- `belongs_to :talk_group` - `has_many :channels` +- `has_many :zone_system_talk_groups` **Validations:** -- System and talkgroup presence -- Unique combination of system, talkgroup, and timeslot +- System and talk_group presence +- Unique combination of system, talk_group, and timeslot - Timeslot 1 or 2 if present (DMR only) -**Notes:** -- Same talkgroup can be on different timeslots on different systems -- Channels reference SystemTalkGroup (not just TalkGroup) to capture timeslot - --- ### Codeplug @@ -288,87 +271,181 @@ User's complete radio programming configuration. - `user_id` (integer, foreign key, required) - `name` (string, required) - `description` (text) -- `public` (boolean, default: false) - whether other users can view/clone +- `public` (boolean, default: false) **Associations:** - `belongs_to :user` -- `has_many :zones, dependent: :destroy` - `has_many :channels, dependent: :destroy` +- `has_many :codeplug_zones, dependent: :destroy` +- `has_many :zones, through: :codeplug_zones` **Validations:** - User presence - Name presence **Notes:** -- A codeplug is the "meta" configuration, independent of specific radios +- Zones are linked via CodeplugZone (many-to-many) +- Channels are generated from zones or created manually - Can be exported to multiple radio formats --- ### Zone -Logical grouping of channels within a codeplug. No size limits in app (handled on export). +Standalone template defining which systems and talkgroups to include. Owned by a user and can be public or private. **Attributes:** -- `codeplug_id` (integer, foreign key, required) +- `user_id` (integer, foreign key, required) - `name` (string, required) - `long_name` (string) - for radios supporting long zone names - `short_name` (string) - for radios requiring short zone names +- `public` (boolean, default: false) - whether other users can view/use **Associations:** -- `belongs_to :codeplug` +- `belongs_to :user` +- `has_many :zone_systems, dependent: :destroy` +- `has_many :systems, through: :zone_systems` +- `has_many :codeplug_zones, dependent: :destroy` +- `has_many :codeplugs, through: :codeplug_zones` - `has_many :channel_zones, dependent: :destroy` - `has_many :channels, through: :channel_zones` +**Scopes:** +- `publicly_visible` - zones marked as public +- `owned_by(user)` - zones owned by specific user +- `available_to_user(user)` - public zones OR owned by user + +**Methods:** +- `editable_by?(user)` - true if user owns the zone +- `viewable_by?(user)` - true if public OR owned by user + **Validations:** -- Codeplug presence +- User presence - Name presence **Notes:** -- Zones have unlimited channels in app -- On export, if zone exceeds radio's max_channels_per_zone, prompt user for split strategy -- Radios without zone concept treated as "single zone" with X channels +- Zones are templates, not containers +- Public zones can be added to any user's codeplug +- Systems and talkgroups define what channels will be generated + +--- + +### ZoneSystem (Join Table) +Associates Systems with Zones, with position tracking. + +**Attributes:** +- `zone_id` (integer, foreign key, required) +- `system_id` (integer, foreign key, required) +- `position` (integer, required) - order within zone + +**Associations:** +- `belongs_to :zone` +- `belongs_to :system` +- `has_many :zone_system_talkgroups, dependent: :destroy` +- `has_many :system_talkgroups, through: :zone_system_talkgroups` + +**Validations:** +- Zone and system presence +- Position > 0 +- Unique system within zone +- Unique position within zone + +**Notes:** +- Position determines system order when generating channels +- For digital systems, talkgroups are added via ZoneSystemTalkGroup + +--- + +### ZoneSystemTalkGroup (Join Table) +Associates SystemTalkGroups with ZoneSystems for digital modes. + +**Attributes:** +- `zone_system_id` (integer, foreign key, required) +- `system_talk_group_id` (integer, foreign key, required) + +**Associations:** +- `belongs_to :zone_system` +- `belongs_to :system_talk_group` + +**Validations:** +- ZoneSystem and SystemTalkGroup presence +- Unique combination +- SystemTalkGroup must belong to the same system as the ZoneSystem + +**Notes:** +- Only used for digital systems (DMR, P25, NXDN) +- Each ZoneSystemTalkGroup results in one generated channel + +--- + +### CodeplugZone (Join Table) +Associates Zones with Codeplugs, with position tracking. + +**Attributes:** +- `codeplug_id` (integer, foreign key, required) +- `zone_id` (integer, foreign key, required) +- `position` (integer, required) - order within codeplug + +**Associations:** +- `belongs_to :codeplug` +- `belongs_to :zone` + +**Validations:** +- Codeplug and zone presence +- Position > 0 +- Unique zone within codeplug +- Unique position within codeplug + +**Default Scope:** +- Ordered by position ascending + +**Notes:** +- Position determines zone order when generating channels +- Same zone can be in multiple codeplugs --- ### Channel -User's configuration to access a System. References System and adds user/radio-specific settings. +User's configuration to access a System. Can be generated from zones or created manually. **Attributes:** - `codeplug_id` (integer, foreign key, required) - `system_id` (integer, foreign key, required) -- `system_talkgroup_id` (integer, foreign key, nullable) - only for digital modes +- `system_talk_group_id` (integer, foreign key, nullable) - only for digital modes +- `source_zone_id` (integer, foreign key, nullable) - zone this channel was generated from - `name` (string, required) - `long_name` (string) - `short_name` (string) -- `power_level` (string) - e.g., "High", "Low", "Medium" -- `bandwidth` (string, nullable) - overrides system default if set +- `power_level` (string) +- `bandwidth` (string, nullable) - `tone_mode` (string, enum) - "none", "tx_only", "rx_only", "tx_rx" -- `transmit_permission` (string, enum) - values TBD, includes "forbid_tx" option +- `transmit_permission` (string, enum) - "allow", "forbid_tx" **Associations:** - `belongs_to :codeplug` - `belongs_to :system` -- `belongs_to :system_talkgroup, optional: true` +- `belongs_to :system_talk_group, optional: true` +- `belongs_to :source_zone, class_name: "Zone", optional: true` - `has_many :channel_zones, dependent: :destroy` - `has_many :zones, through: :channel_zones` +**Methods:** +- `generated?` - true if source_zone_id is present + **Validations:** - Codeplug and system presence - Name presence - Valid tone_mode from enum - Valid transmit_permission from enum -- system_talkgroup required if system mode is digital (business logic) -- tone_mode must respect system's supports_tx_tone and supports_rx_tone **Notes:** -- Channel inherits system data (frequencies, tones) and adds user preferences -- A channel can appear in multiple zones at different positions -- For digital systems, must reference a SystemTalkGroup to capture talkgroup + timeslot +- `source_zone_id` tracks which zone the channel was generated from +- Generated channels can be customized; changes persist until regeneration +- For digital systems, must reference a SystemTalkGroup --- ### ChannelZone (Join Table) -Associates Channels with Zones, with position/sequence tracking. +Associates Channels with Zones, with position tracking. **Attributes:** - `channel_id` (integer, foreign key, required) @@ -381,13 +458,12 @@ Associates Channels with Zones, with position/sequence tracking. **Validations:** - Channel and zone presence -- Position is positive integer -- Unique position within a zone +- Position > 0 +- Unique position within zone **Notes:** - Position determines channel order within zone - Same channel can be at different positions in different zones -- Users can reorder channels within zones --- @@ -398,7 +474,6 @@ Associates Channels with Zones, with position/sequence tracking. - `dmr` - `p25` - `nxdn` -- Additional modes added as needed ### Tone Mode - `none` - no tones @@ -407,32 +482,50 @@ Associates Channels with Zones, with position/sequence tracking. - `tx_rx` - both transmit and receive tones ### Transmit Permission -- TBD - will include options like: - - `always` - always allow transmit - - `forbid_tx` - receive only - - Additional options as needed +- `allow` - transmit allowed +- `forbid_tx` - receive only --- -## CTCSS/DCS Tone Values - -Tones stored as strings in database. Valid values maintained in application config/constant. +## Services -**CTCSS (Hz):** -- "67.0", "71.9", "74.4", "77.0", "79.7", "82.5", "85.4", "88.5", "91.5", "94.8", "97.4", "100.0", "103.5", "107.2", "110.9", "114.8", "118.8", "123.0", "127.3", "131.8", "136.5", "141.3", "146.2", "151.4", "156.7", "162.2", "167.9", "173.8", "179.9", "186.2", "192.8", "203.5", "210.7", "218.1", "225.7", "233.6", "241.8", "250.3" +### ChannelGenerator +Service class that generates channels from zones for a codeplug. -**DCS (Codes):** -- "023", "025", "026", "031", "032", "036", "043", "047", "051", "053", "054", "065", "071", "072", "073", "074", "114", "115", "116", "122", "125", "131", "132", "134", "143", "145", "152", "155", "156", "162", "165", "172", "174", "205", "212", "223", "225", "226", "243", "244", "245", "246", "251", "252", "255", "261", "263", "265", "266", "271", "274", "306", "311", "315", "325", "331", "332", "343", "346", "351", "356", "364", "365", "371", "411", "412", "413", "423", "431", "432", "445", "446", "452", "454", "455", "462", "464", "465", "466", "503", "506", "516", "523", "526", "532", "546", "565", "606", "612", "624", "627", "631", "632", "654", "662", "664", "703", "712", "723", "731", "732", "734", "743", "754" +**Usage:** +```ruby +generator = ChannelGenerator.new(codeplug) +result = generator.generate_channels(regenerate: false) +# => { channels_created: 5, channel_zones_created: 5, zones_processed: 2, skipped: false } +``` -**Display Format:** -- CTCSS displayed with "Hz" suffix in UI: "127.3 Hz" -- DCS displayed as-is: "065" -- Export format varies by radio (some want "Hz", some don't) +**Behavior:** +- For analog systems: creates one channel per system +- For digital systems: creates one channel per ZoneSystemTalkGroup +- Sets `source_zone_id` on generated channels +- Creates ChannelZone records with correct positions +- With `regenerate: true`: destroys existing channels first +- Without `regenerate`: skips if channels already exist --- ## Business Rules & Constraints +### Zone Architecture +1. Zones are standalone entities owned by users +2. Zones can be public (viewable/usable by all) or private +3. Zones define systems and talkgroups, not channels directly +4. Zones are linked to codeplugs via CodeplugZone +5. Channels are generated from zones using ChannelGenerator + +### Channel Generation Logic +1. Process zones in CodeplugZone position order +2. Within each zone, process systems in ZoneSystem position order +3. For analog systems: create one channel per system +4. For digital systems: create one channel per ZoneSystemTalkGroup +5. Set source_zone_id to track origin +6. Create ChannelZone with sequential positions + ### Channel/System/Talkgroup Logic 1. If system mode is digital, channel must reference a system_talkgroup 2. If system mode is analog, channel should not reference a system_talkgroup @@ -445,27 +538,17 @@ Tones stored as strings in database. Valid values maintained in application conf - If zone exceeds limit, prompt user for split strategy - Generate physical zones on-the-fly during export -### System/Network/Talkgroup Filtering -1. When user selects a System for a Channel, only show TalkGroups from Networks the System is connected to -2. Show SystemTalkGroup options (includes timeslot) rather than raw TalkGroups +--- -### Simplex Systems -- For simplex operation (no repeater), create a System where tx_frequency == rx_frequency -- Could have a "simplex" mode or use "analog"/"dmr" mode with matching frequencies +## CTCSS/DCS Tone Values ---- +Tones stored as strings in database. -## Future Considerations +**CTCSS (Hz):** +- "67.0", "71.9", "74.4", "77.0", "79.7", "82.5", "85.4", "88.5", "91.5", "94.8", "97.4", "100.0", "103.5", "107.2", "110.9", "114.8", "118.8", "123.0", "127.3", "131.8", "136.5", "141.3", "146.2", "151.4", "156.7", "162.2", "167.9", "173.8", "179.9", "186.2", "192.8", "203.5", "210.7", "218.1", "225.7", "233.6", "241.8", "250.3" -Features to add in later iterations: -- Import codeplug from CSV (with auto-detect of format) -- Export history/audit log -- Codeplug versioning -- Shared/community codeplugs -- Frequency allocation validation (regulatory compliance) -- Simplex frequency library (common calling frequencies, etc.) -- Channel templates -- Bulk operations (clone channels, mass edit, etc.) +**DCS (Codes):** +- "023", "025", "026", "031", "032", "036", "043", "047", "051", "053", "054", "065", "071", "072", "073", "074", etc. --- @@ -473,12 +556,15 @@ Features to add in later iterations: Recommended indexes for performance: - `users.email` (unique) -- `radio_models.manufacturer_id` -- `systems.mode` -- `systems.latitude, systems.longitude` (for geographic queries) -- `talkgroups.network_id` -- `system_talkgroups.system_id, system_talkgroups.talkgroup_id` +- `zones.user_id` +- `zones.public` +- `zone_systems.zone_id, zone_systems.position` (unique) +- `zone_systems.zone_id, zone_systems.system_id` (unique) +- `zone_system_talk_groups.zone_system_id, zone_system_talk_groups.system_talk_group_id` (unique) +- `codeplug_zones.codeplug_id, codeplug_zones.position` (unique) +- `codeplug_zones.codeplug_id, codeplug_zones.zone_id` (unique) - `channels.codeplug_id` - `channels.system_id` -- `channel_zones.zone_id, channel_zones.position` +- `channels.source_zone_id` +- `channel_zones.zone_id, channel_zones.position` (unique) - Foreign key indexes on all join tables diff --git a/docs/ZONE_MIGRATION_GUIDE.md b/docs/ZONE_MIGRATION_GUIDE.md new file mode 100644 index 0000000..33dd5eb --- /dev/null +++ b/docs/ZONE_MIGRATION_GUIDE.md @@ -0,0 +1,360 @@ +# Zone Architecture Migration Guide + +This guide is for developers who need to understand the zone architecture changes implemented in Epic #91. + +## Overview + +The zone architecture was refactored from a direct relationship (zones embedded in codeplugs) to a template-based approach (zones as standalone, reusable entities). + +## Architecture Changes + +### Before (Old Architecture) + +``` +Codeplug +└── Zone (belongs_to :codeplug) + └── ChannelZone → Channel + +Zone.codeplug_id was required +Zones were created inside codeplugs +Zones could not be shared between codeplugs +``` + +### After (New Architecture) + +``` +Zone (standalone, owned by user) +├── ZoneSystem → System +│ └── ZoneSystemTalkGroup → SystemTalkGroup +└── CodeplugZone → Codeplug + +Codeplug +├── CodeplugZone → Zone +└── Channel (generated from zones) + ├── source_zone_id (tracks origin) + └── ChannelZone (position in zone) +``` + +## Database Changes + +### New Tables + +1. **zone_systems** + - Links zones to systems + - Has `position` for ordering + - Unique constraint on `zone_id, system_id` + +2. **zone_system_talk_groups** + - Links zone_systems to system_talk_groups + - For digital systems (DMR, P25, NXDN) + - Unique constraint on `zone_system_id, system_talk_group_id` + +3. **codeplug_zones** + - Links codeplugs to zones + - Has `position` for ordering + - Unique constraint on `codeplug_id, zone_id` + +### Modified Tables + +1. **zones** + - Removed: `codeplug_id` column + - Added: `user_id` (required, owner of the zone) + - Added: `public` boolean (default false) + +2. **channels** + - Added: `source_zone_id` (tracks which zone generated the channel) + +## Model Changes + +### Zone Model + +```ruby +# OLD +class Zone < ApplicationRecord + belongs_to :codeplug + has_many :channel_zones + has_many :channels, through: :channel_zones +end + +# NEW +class Zone < ApplicationRecord + belongs_to :user + has_many :zone_systems, dependent: :destroy + has_many :systems, through: :zone_systems + has_many :codeplug_zones, dependent: :destroy + has_many :codeplugs, through: :codeplug_zones + has_many :channel_zones, dependent: :destroy + has_many :channels, through: :channel_zones + + scope :publicly_visible, -> { where(public: true) } + scope :available_to_user, ->(user) { where(public: true).or(where(user: user)) } + + def editable_by?(user) + self.user == user + end + + def viewable_by?(user) + public? || self.user == user + end +end +``` + +### Codeplug Model + +```ruby +# OLD +class Codeplug < ApplicationRecord + has_many :zones, dependent: :destroy +end + +# NEW +class Codeplug < ApplicationRecord + has_many :codeplug_zones, dependent: :destroy + has_many :zones, through: :codeplug_zones +end +``` + +### Channel Model + +```ruby +# NEW addition +class Channel < ApplicationRecord + belongs_to :source_zone, class_name: "Zone", optional: true + + def generated? + source_zone_id.present? + end +end +``` + +## New Models + +### ZoneSystem + +```ruby +class ZoneSystem < ApplicationRecord + belongs_to :zone + belongs_to :system + has_many :zone_system_talkgroups, dependent: :destroy + + validates :position, presence: true, numericality: { greater_than: 0 } + validates :system_id, uniqueness: { scope: :zone_id } + validates :position, uniqueness: { scope: :zone_id } +end +``` + +### ZoneSystemTalkGroup + +```ruby +class ZoneSystemTalkGroup < ApplicationRecord + belongs_to :zone_system + belongs_to :system_talk_group + + validates :system_talk_group_id, uniqueness: { scope: :zone_system_id } + validate :system_talk_group_must_belong_to_zone_system_system +end +``` + +### CodeplugZone + +```ruby +class CodeplugZone < ApplicationRecord + belongs_to :codeplug + belongs_to :zone + + validates :position, presence: true, numericality: { greater_than: 0 } + validates :zone_id, uniqueness: { scope: :codeplug_id } + validates :position, uniqueness: { scope: :codeplug_id } + + default_scope { order(position: :asc) } +end +``` + +## Service Classes + +### ChannelGenerator + +New service for generating channels from zones: + +```ruby +generator = ChannelGenerator.new(codeplug) +result = generator.generate_channels(regenerate: false) +# => { channels_created: 5, channel_zones_created: 5, zones_processed: 2, skipped: false } +``` + +**Behavior:** +- Processes zones in CodeplugZone position order +- For analog systems: creates one channel per system +- For digital systems: creates one channel per ZoneSystemTalkGroup +- Sets `source_zone_id` on generated channels +- With `regenerate: true`: destroys existing channels first + +## Route Changes + +### Removed Routes + +```ruby +# OLD - nested zones under codeplugs +resources :codeplugs do + resources :zones do + resources :channel_zones + end +end +``` + +### New Routes + +```ruby +# Standalone zones +resources :zones do + resources :zone_systems, only: [:create, :destroy] + member do + patch :update_positions + end +end + +# Zone-system talkgroups +resources :zone_systems do + resources :zone_system_talkgroups, only: [:create, :destroy] +end + +# Codeplug zones (linking zones to codeplugs) +resources :codeplugs do + resources :codeplug_zones, only: [:create, :destroy] do + collection do + patch :update_positions + end + end + member do + post :generate_channels + end +end +``` + +## Data Migration + +A rake task is provided for migrating existing data: + +```bash +# Analyze existing data +rails zones:analyze + +# Perform migration +rails zones:migrate + +# Verify migration +rails zones:verify + +# Rollback if needed +rails zones:rollback + +# Clear legacy codeplug_id after verification +rails zones:clear_legacy_codeplug_ids +``` + +## Testing Changes + +### Factory Updates + +```ruby +# OLD +factory :zone do + association :codeplug +end + +# NEW +factory :zone do + association :user + public { false } +end +``` + +### Test Updates + +Replace direct zone-codeplug creation with CodeplugZone: + +```ruby +# OLD +zone = create(:zone, codeplug: codeplug) + +# NEW +zone = create(:zone, user: user) +create(:codeplug_zone, codeplug: codeplug, zone: zone, position: 1) +``` + +## Breaking Changes + +1. **Zone no longer has `codeplug_id`** + - Zones are standalone entities + - Use `CodeplugZone` to link zones to codeplugs + +2. **Zone requires `user_id`** + - All zones must have an owner + - Owner controls edit permissions + +3. **Nested zone routes removed** + - `/codeplugs/:id/zones` routes no longer exist + - Use `/zones` for zone management + - Use `/codeplugs/:id/codeplug_zones` for adding zones to codeplugs + +4. **Channel generation is explicit** + - Channels are not auto-created + - User must click "Generate Channels" + - Regeneration destroys existing channels + +## Upgrade Steps + +1. Run database migrations +2. Run `rails zones:migrate` to migrate existing data +3. Run `rails zones:verify` to check data integrity +4. Update any custom code that references `zone.codeplug` +5. Update tests to use new factory patterns +6. Run `rails zones:clear_legacy_codeplug_ids` after verification + +## Common Issues + +### "undefined method 'codeplug=' for Zone" + +Code is using the old direct relationship. Update to use CodeplugZone: + +```ruby +# OLD +zone.codeplug = codeplug + +# NEW +CodeplugZone.create!(codeplug: codeplug, zone: zone, position: 1) +``` + +### "Zone must have user" + +Zones now require an owner. Ensure user is set: + +```ruby +zone = Zone.create!(user: current_user, name: "My Zone") +``` + +### Tests failing with "codeplug_id doesn't exist" + +Update test factories and setup to use new architecture: + +```ruby +# In test setup +user = create(:user) +codeplug = create(:codeplug, user: user) +zone = create(:zone, user: user) +create(:codeplug_zone, codeplug: codeplug, zone: zone, position: 1) +``` + +## Related Issues + +- Epic: #91 - Zone Architecture Refactor +- Issue #97: Add systems to zones +- Issue #98: Add talkgroup selection for digital systems +- Issue #99: Add zones to codeplugs +- Issue #100: Implement zone reordering +- Issue #101: Channel generation service +- Issue #102: Generate channels button +- Issue #103: Channel customization after generation +- Issue #104: Zone data migration +- Issue #105: Remove old zone relationships +- Issue #106: Documentation and UI polish From 9167aae5a3d19df88b13779760fb538ecc60ff53 Mon Sep 17 00:00:00 2001 From: Andrew Ohnstad Date: Wed, 17 Dec 2025 21:12:09 -0500 Subject: [PATCH 2/2] Fix test to match updated zones section header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed 'Standalone Zones' to check for the new subtitle text 'Templates that define what channels to generate'. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- test/system/codeplugs_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/system/codeplugs_test.rb b/test/system/codeplugs_test.rb index 036104a..ee244f4 100644 --- a/test/system/codeplugs_test.rb +++ b/test/system/codeplugs_test.rb @@ -223,12 +223,12 @@ class CodeplugsTest < ApplicationSystemTestCase assert_text "Channels - Test Codeplug" end - # Standalone zones in codeplug tests - test "codeplug show page displays standalone zones section" do + # Zones in codeplug tests + test "codeplug show page displays zones section" do user = create(:user, email: "test@example.com", password: "password123") codeplug = create(:codeplug, user: user, name: "Test Codeplug") - # Create standalone zones and add to codeplug + # Create zones and add to codeplug zone1 = create(:zone, user: user, name: "Zone 1", public: false) zone2 = create(:zone, user: user, name: "Zone 2", public: false) create(:codeplug_zone, codeplug: codeplug, zone: zone1, position: 1) @@ -239,7 +239,7 @@ class CodeplugsTest < ApplicationSystemTestCase fill_in "Password", with: "password123" click_button "Log In" - assert_text "Standalone Zones" + assert_text "Templates that define what channels to generate" assert_text "Zone 1" assert_text "Zone 2" assert_text "2 zones"