Skip to content

feat: detect and auto-merge existing channels #116

Description

@pskillen

Problem

Import best-effort merge (#46) collapses paired Analogue/Digital CPS rows at import time. Operators can still end up with split logical channels inside an active project when:

  • They created FM and DMR rows manually in CRUD before multi-mode existed
  • Import heuristics did not match (name stem, location, or suffix ambiguity)
  • They imported from a flat CPS twice or merged projects without collapse
  • Future multi-talkgroup work (#36) will have the same problem for repeater × talk-group pairs

Today there is no way to discover these duplicates or merge them into one logical multiMode or multi-TG channel after the fact.

Intended outcome

A Detect merge candidates action on the channels list page that scans the active codeplug, proposes groups of channels that likely represent one logical site, and lets the operator review and apply merges.

Candidate detection (vendor-neutral)

Group channels into merge candidates when they share strong identity signals and differ only in dimensions we intend to collapse:

Signal Use
RX / TX frequency Match (Hz equality, or within a small tolerance if documented)
Name Fuzzy match — normalised stem, strip known export suffixes (-F / -D), Levenshtein or similar; case-sensitive awareness per CPS quirks at display only
Location Match when both set (lat/lon); optional tie-breaker
Callsign Optional secondary signal when populated

Do not use as primary match criteria:

A candidate group must contain ≥2 channels and must be mergeable along one axis:

  1. Multi-mode merge (#46) — same site, different modes → one multiMode: true channel with modeProfiles
  2. Multi-talkgroup merge (#36) — same site, same mode, different TX contacts/TGs → one logical channel with multiple talk groups (when feat: multi-talkgroup channels (denormalise one channel into repeater × talkgroup pairs on export) #36 ships)

If a group is ambiguous (could be either axis, or conflicting shared fields), surface as review-only or skip — do not auto-merge silently.

UI (channels list)

  • Button on list toolbar / section nav (e.g. Find merge candidates)
  • Opens a review panel or modal (full page optional later; preview page patterns in #113 are a useful reference)
  • For each candidate group:
    • Show channel names, modes, frequencies, location, contact/TG summary
    • Proposed merge type: multi-mode or multi-talkgroup (when supported)
    • Proposed result name (operator-editable?)
    • Checkboxes to include/exclude groups
  • Apply runs merge mutations; Cancel leaves codeplug unchanged
  • Report: merged count, skipped ambiguous groups, zone member rewiring notes

Merge behaviour

  • Reuse / extend src/lib/channelExpansion/ helpers where possible (mergeImportChannelsBestEffort logic is a starting point — extract vendor-neutral candidate grouping and merge functions)
  • Zones: replace member ids for merged-away channels with the single logical channel id (dedupe)
  • Validation: merged channel must pass validateChannel
  • Undo: out of scope v1 — operator restores from export/backup

Detection must not run automatically

Operator-initiated only (button). No silent background merge on load.

Affected

  • src/routes/channels/list.tsx — entry point
  • New lib module e.g. src/lib/channelMergeCandidates.ts — grouping heuristics + merge apply
  • src/lib/codeplugMutations.tsmergeChannelsIntoOne (or similar)
  • src/lib/validation/channel.ts — ensure multi-mode / multi-TG rules cover merged result
  • Tests: unit (grouping fixtures), integration (zones rewired)
  • Docs: docs/features/crud/README.md, optional docs/features/data-model/ note

Notes / dependencies

  • Builds on #46 (multi-mode model + expansion) — multi-mode merge path shippable first
  • Pairs with #36 — add multi-TG candidate detection when multi-talkgroup model ships
  • Related #113 — similar review-before-apply UX for import decisions
  • Vendor boundaries: detection and merge are internal model only; no OpenGD77 naming rules in the UI copy
  • No wire stash: merge must populate model fields (modeProfiles, refs, opengd77Extras per profile if needed) — not provenance replay (#46 round-trip rules)

Out of scope

Workflow note

Branch from origin/main, atomic conventional commits (detection lib → apply mutation → list UI → tests/docs), PR linking Closes #N. Pair with docs/features / progress-tracking skills.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestquality-of-lifefeature to improve the QOL of the person making a codeplug

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions