Problem
Per #36 and #46, at export time we may expand a single logical channel into multiple single-mode channels, or into multiple channel×talkgroup pair channels.
The same denormalise-at-export pattern should extend to talk groups and RX group lists.
On OpenGD77 (and similar CPS layouts), repeaters carry different talk groups on different time slots (TS1 vs TS2). To support promiscuous RX correctly, operators today often:
- Create two copies of each talk group — one per time slot (e.g.
Scotland TS1, Scotland TS2).
- Build a per-repeater RX Group List and assign the appropriate TG×TS variant to that list (which TGs appear varies by repeater; which slot a given TG uses also varies by repeater).
This is tedious, error-prone, and duplicates DMR IDs across wire names that differ only by slot.
Intended outcome
Model one logical talk group internally. Carry per-member time slot on RxGroupList membership. Expand into the required TS1/TS2 wire contacts at the export boundary, and wire each RGL member to the correct expanded copy.
Model changes
- Remove
TalkGroup.timeslotOverride from the internal model.
- Add an optional time slot on each
RxGroupList membership entry (alongside the existing EntityRef to a talk group or private contact). Semantics: which TS variant this member should resolve to on export (1, 2, or unset → export decides / both variants emitted only when needed).
- Logical talk groups keep a single
name, number (DMR ID), and optional abbreviation — no slot baked into the entity.
Contact.timeslotOverride is unchanged in this issue — private contacts in RGLs are a separate concern.
Export (required)
When the target format requires separate wire contacts per time slot (OpenGD77 Contacts.csv + TG_Lists.csv):
- For each logical talk group referenced by any RGL member (with a time slot set), emit the required expanded wire copies — typically one per distinct slot in use, with
T1 or T2 appended to the contact name (e.g. Scotland → Scotland T1, Scotland T2).
- Set the vendor
TS Override column on each expanded copy from the membership slot (not from a field on the logical TG).
- Resolve each
RxGroupList.memberRefs entry to the matching expanded wire name for that member's time slot when serialising TG_Lists.csv.
- Naming must be deterministic, collision-resistant, and respect profile display limits (same design constraints as #36 / #46).
Expansion is an export-boundary concern — internal CRUD, validation, and the vendor-neutral model must not assume OpenGD77 slot suffixes or TS Override wire values.
Import merge / de-dupe (best-effort, NOT a blocker for export)
Flat CPS exports arrive with separate wire contacts per slot. Import must include an automatic collapse path at the format boundary (alongside existing importMerge.ts talk-group merge):
- Detect wire contact pairs (or larger families) that share the same DMR ID (
number) and a compatible name stem after stripping T1/T2, TS1/TS2, or timeslotOverride-derived suffixes.
- Collapse to one logical talk group (survivor name = stem; preserve
abbreviation when unambiguous).
- Rewire RGL membership: each
ContactN cell that referenced an expanded wire name becomes one memberRef with the inferred per-member time slot.
- Rewire channel
contactRef and any other TG references from absorbed ids → survivor id.
- Ambiguous groups (same ID, incompatible stems, or slot conflicts) stay as separate talk groups — no regression.
Explicitly not required to ship export-side support. Mis-grouping is acceptable.
Manual merge (post-hoc repair)
Import heuristics will miss cases. Operators need a Find merge candidates flow on the Talk Groups list — same pattern as channel merge candidates (#116):
- Scan talk groups for groups that are TS-slot duplicates of the same logical TG (same
number, compatible name stem, differing only by T1/T2/TS1/TS2 suffix or timeslotOverride).
- Preview merge result: survivor name, absorbed members, RGL membership rewiring (member refs + inferred slots), channel
contactRef impact, validation warnings.
- Operator confirms/applies selected merges (batch or one-by-one).
- Entry point: Talk Groups list section nav — Find merge candidates (mirror
ChannelsListSectionNav + ChannelMergeCandidatesModal).
Implementation likely splits into lib (talkGroupMergeCandidates.ts or extend channelExpansion/), mutations (mergeTalkGroupsIntoOne), store wiring, and modal UI — follow #116 slice structure and progress docs.
Pattern
Mirrors #36 (channel×TG expansion) and #46 (channel×mode expansion):
| Concept |
#36 |
#46 |
This issue |
| Logical entity |
One channel, many TGs (via RGL) |
One channel, many modes |
One talk group, many time slots (via RGL membership) |
| Denormalise on export |
channel × TG rows |
channel × mode rows |
talk group × TS wire contacts |
| Slot/context lives on |
rxGroupListId + RGL members |
modeProfiles |
RGL member time slot |
| Import collapse |
Best-effort at boundary |
Best-effort at boundary |
Best-effort at boundary + manual repair |
| Manual repair UI |
#116 (channels) |
#116 (channels) |
Talk Groups list merge candidates (this issue) |
Orthogonal to multi-talkgroup expansion (DM32-style channel×TG row fan-out). This issue is primarily for formats with native RGL support (OpenGD77) where the wire still requires separate contact names per slot.
Affected
- Internal data model (
src/models/codeplug.ts) — RGL membership shape; remove TalkGroup.timeslotOverride; schema version bump + migration.
- Export layer — OpenGD77
serialise.ts (Contacts + TG_Lists); expansion pass alongside existing channel expansion in src/lib/channelExpansion/.
- Import layer — OpenGD77
parse.ts + importMerge.ts TS-suffix collapse and RGL member slot reconstruction.
- Manual merge — detection/preview/apply lib; mutations;
TalkGroupsList section nav + merge modal (pattern from #116).
- CRUD — Talk group edit (remove timeslot override field); RGL member picker (per-member time slot).
- Docs —
docs/features/data-model/, docs/reference/opengd77/contacts.md, docs/features/crud/ progress logs for merge candidates.
Notes / dependencies
- Builds on #36 and #46 (export expansion infrastructure).
- UI pattern from #116 (channel merge candidates — detection, preview, apply, zone/RGL rewiring).
- OpenGD77-first motivating case; other formats with
TS Override or per-slot contact names may share the same expansion hook.
- Existing codeplugs with
timeslotOverride on talk groups need a one-time migration (slot → nearest RGL membership, or warn and drop).
Out of scope
- Changing radio firmware behaviour.
- Guaranteed loss-free import re-normalisation.
Contact.timeslotOverride redesign (private contacts).
Workflow note
Likely multi-commit work: branch from origin/main, atomic conventional commits per logical change, PR linking Closes #. Pair with docs/features / progress-tracking skills.
Problem
Per #36 and #46, at export time we may expand a single logical channel into multiple single-mode channels, or into multiple channel×talkgroup pair channels.
The same denormalise-at-export pattern should extend to talk groups and RX group lists.
On OpenGD77 (and similar CPS layouts), repeaters carry different talk groups on different time slots (TS1 vs TS2). To support promiscuous RX correctly, operators today often:
Scotland TS1,Scotland TS2).This is tedious, error-prone, and duplicates DMR IDs across wire names that differ only by slot.
Intended outcome
Model one logical talk group internally. Carry per-member time slot on
RxGroupListmembership. Expand into the required TS1/TS2 wire contacts at the export boundary, and wire each RGL member to the correct expanded copy.Model changes
TalkGroup.timeslotOverridefrom the internal model.RxGroupListmembership entry (alongside the existingEntityRefto a talk group or private contact). Semantics: which TS variant this member should resolve to on export (1,2, or unset → export decides / both variants emitted only when needed).name,number(DMR ID), and optionalabbreviation— no slot baked into the entity.Export (required)
When the target format requires separate wire contacts per time slot (OpenGD77
Contacts.csv+TG_Lists.csv):T1orT2appended to the contact name (e.g.Scotland→Scotland T1,Scotland T2).TS Overridecolumn on each expanded copy from the membership slot (not from a field on the logical TG).RxGroupList.memberRefsentry to the matching expanded wire name for that member's time slot when serialisingTG_Lists.csv.Expansion is an export-boundary concern — internal CRUD, validation, and the vendor-neutral model must not assume OpenGD77 slot suffixes or
TS Overridewire values.Import merge / de-dupe (best-effort, NOT a blocker for export)
Flat CPS exports arrive with separate wire contacts per slot. Import must include an automatic collapse path at the format boundary (alongside existing
importMerge.tstalk-group merge):number) and a compatible name stem after strippingT1/T2,TS1/TS2, ortimeslotOverride-derived suffixes.abbreviationwhen unambiguous).ContactNcell that referenced an expanded wire name becomes onememberRefwith the inferred per-member time slot.contactRefand any other TG references from absorbed ids → survivor id.Explicitly not required to ship export-side support. Mis-grouping is acceptable.
Manual merge (post-hoc repair)
Import heuristics will miss cases. Operators need a Find merge candidates flow on the Talk Groups list — same pattern as channel merge candidates (#116):
number, compatible name stem, differing only byT1/T2/TS1/TS2suffix ortimeslotOverride).contactRefimpact, validation warnings.ChannelsListSectionNav+ChannelMergeCandidatesModal).Implementation likely splits into lib (
talkGroupMergeCandidates.tsor extendchannelExpansion/), mutations (mergeTalkGroupsIntoOne), store wiring, and modal UI — follow #116 slice structure and progress docs.Pattern
Mirrors #36 (channel×TG expansion) and #46 (channel×mode expansion):
rxGroupListId+ RGL membersmodeProfilesOrthogonal to multi-talkgroup expansion (DM32-style channel×TG row fan-out). This issue is primarily for formats with native RGL support (OpenGD77) where the wire still requires separate contact names per slot.
Affected
src/models/codeplug.ts) — RGL membership shape; removeTalkGroup.timeslotOverride; schema version bump + migration.serialise.ts(Contacts + TG_Lists); expansion pass alongside existing channel expansion insrc/lib/channelExpansion/.parse.ts+importMerge.tsTS-suffix collapse and RGL member slot reconstruction.TalkGroupsListsection nav + merge modal (pattern from #116).docs/features/data-model/,docs/reference/opengd77/contacts.md,docs/features/crud/progress logs for merge candidates.Notes / dependencies
TS Overrideor per-slot contact names may share the same expansion hook.timeslotOverrideon talk groups need a one-time migration (slot → nearest RGL membership, or warn and drop).Out of scope
Contact.timeslotOverrideredesign (private contacts).Workflow note
Likely multi-commit work: branch from
origin/main, atomic conventional commits per logical change, PR linkingCloses #. Pair withdocs/features/ progress-tracking skills.