Part of #200. Depends on the Phase 3 write-back commands.
New src/components/DispatcharrSyncDialog.tsx
Opened from a Toolbar "Sync to Dispatcharr" button shown only when isDispatcharrSource(source_identity) and scan results exist. This is a network mutation, so it gets its own affordance — ExportMenu stays file-export-only.
- Channel-grouped list built from
groupResultsByDispatcharrChannel(scan results); each Dispatcharr channel expands to its ordered streams with status badge + resolution/bitrate/latency from the scan.
- Per stream: "remove from channel" checkbox (pre-checked for Dead, unchecked for Alive/untested), "Promote to primary" button, up/down reorder buttons (drag handle can come later).
- Global toggles: "Push probe stats to Dispatcharr" (default on, with count), "Also hard-delete removed streams from Dispatcharr entirely" (default off — otherwise they only leave the channel).
- "Auto-fix" shortcut: per channel, compute
ordered = alive sorted by height desc, then original order followed by untested streams in original order — previewed before Apply.
- Apply flow: re-fetch each affected channel's streams and diff against expected IDs (warn on drift instead of blind-PATCHing) → push stats once → per-changed-channel
set_channel_streams → optional hard deletes → render per-item success/failure inline (partial failure must not abort the batch) → offer "Reload from Dispatcharr" (re-invokes the open command) instead of mutating the local playlist.
State is local component state; scan results come from the existing scan slice selectors. No new zustand slice.
Verification
End-to-end against a real Dispatcharr instance: load via the API source, scan, open the dialog, push stats + promote a stream + drop a dead one, then confirm in Dispatcharr's UI/API that stream_stats and the channel's stream order changed.
Part of #200. Depends on the Phase 3 write-back commands.
New
src/components/DispatcharrSyncDialog.tsxOpened from a Toolbar "Sync to Dispatcharr" button shown only when
isDispatcharrSource(source_identity)and scan results exist. This is a network mutation, so it gets its own affordance — ExportMenu stays file-export-only.groupResultsByDispatcharrChannel(scan results); each Dispatcharr channel expands to its ordered streams with status badge + resolution/bitrate/latency from the scan.ordered = alive sorted by height desc, then original orderfollowed by untested streams in original order — previewed before Apply.set_channel_streams→ optional hard deletes → render per-item success/failure inline (partial failure must not abort the batch) → offer "Reload from Dispatcharr" (re-invokes the open command) instead of mutating the local playlist.State is local component state; scan results come from the existing scan slice selectors. No new zustand slice.
Verification
End-to-end against a real Dispatcharr instance: load via the API source, scan, open the dialog, push stats + promote a stream + drop a dead one, then confirm in Dispatcharr's UI/API that
stream_statsand the channel's stream order changed.