diff --git a/CHANGELOG.md b/CHANGELOG.md index fd83d37a..6335368d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Per-field channel overrides for auto-synced channels.** A new `ChannelOverride` table (one-to-one with `Channel`) holds user-specified values for `name`, `channel_number`, `channel_group`, `logo`, `tvg_id`, `tvc_guide_stationid`, `epg_data`, and `stream_profile`. Sync writes only to `Channel.*` columns; the override row takes precedence at read time via a new `with_effective_values()` queryset helper that coalesces both sources at the SQL layer. Every output surface that consumes channel data reads through this helper so overrides surface consistently: HDHR (`/hdhr/lineup.json` and per-profile variants, `/hdhr/discover.json`), M3U (`/output/m3u`, per-profile variants), EPG (`/output/epg`, per-profile variants), XC API (`get_live_streams`, `xmltv.php`), the channels list/detail endpoints, and the TV Guide summary endpoint (`GET /api/channels/channels/summary/`). Channel API responses now include both the raw `name`/`channel_number`/etc. fields AND new `effective_*` annotations plus the `override` object, so frontend consumers can show both "what the user picked" and "what the provider sent" simultaneously. - **Per-field reset-to-provider icon.** FK pickers (channel group, logo, EPG, stream profile) and scalar inputs (name, channel_number, tvg_id, tvc_guide_stationid) display a small reset icon ("Provider: " subtext + Undo2 button) when the field has an active override on an auto-synced channel. Clicking it sets the form value back to the provider value; the override for that field is cleared on save while other overrides remain intact. - **Auto-created channel source attribution.** The channel edit form shows an "Auto-created from: / " label at the top of auto-synced channels so the user can see which M3U account and which stream produced the row. - - **Hide-from-output flag (`user_hidden`).** A user-toggleable boolean that excludes a channel from HDHR, M3U, EPG, and XC output without deleting it. Hidden channels are preserved across auto-sync refreshes and excluded from auto-cleanup. The channels table shows an EyeOff icon on hidden rows. + - **Hide-from-output flag (`hidden_from_output`).** A user-toggleable boolean that excludes a channel from HDHR, M3U, EPG, and XC output without deleting it. Hidden channels are preserved across auto-sync refreshes and excluded from auto-cleanup. The channels table shows an EyeOff icon on hidden rows. - **Channels table visibility filter and inline indicators.** The table header has a new visibility selector ("Active Only" / "Hidden Only" / "Show All") plus a "Has Overrides" filter that narrows to channels with an active override row. Each row's name cell renders a yellow Pencil icon when overrides are active (tooltip lists the overridden field names) and an EyeOff icon when the channel is hidden. - - **Bulk hide / unhide in the bulk edit modal.** Selecting any number of channels and toggling user_hidden in bulk edit applies the change in a single PATCH; auto-routes through override-aware bulk endpoint so it works equivalently for auto-synced and manual channels. + - **Bulk hide / unhide in the bulk edit modal.** Selecting any number of channels and toggling hidden_from_output in bulk edit applies the change in a single PATCH; auto-routes through override-aware bulk endpoint so it works equivalently for auto-synced and manual channels. - **Auto-sync channel ranges per group.** `ChannelGroupM3UAccount` accepts `auto_sync_channel_start` (lower bound, inclusive) and `auto_sync_channel_end` (upper bound, inclusive; nullable for unbounded fill). Channels created by auto-sync are assigned numbers within the configured range; streams that do not fit are surfaced in the failure detail modal with a typed "RANGE_EXHAUSTED" reason. - **Per-group inline configuration in the M3U Live group filter.** Each group row now exposes a Sync toggle, a Numbering Mode selector (Provider / Next Available / Fixed), and Start/End channel-number inputs alongside the existing fields. The numbering modes control how each stream's channel number is chosen during sync: Provider tries the M3U-supplied number first then falls back to next-available; Next Available always picks the lowest free number from 1 upward; Fixed picks sequentially from the configured Start. - **Per-group "Configure" modal.** A gear-icon button on each group row opens a `GroupConfigureModal` containing the full set of advanced options. Fields surfaced in the modal: Channel Sort Order (provider order / name / tvg_id / updated_at, with reverse toggle), Compact Numbering toggle and "Re-pack now" button, Group Override (re-route this group's auto-created channels into a different `ChannelGroup` so multiple provider groups merge into one logical group), Name Regex Find/Replace pattern, Exclude Regex pattern, Force Dummy EPG, and a live regex preview pane. @@ -102,7 +102,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **`Channel.channel_number` is now nullable.** Auto-sync may produce channels without an assigned number (for example, a sync run where the configured range was exhausted before this stream could be slotted, or a hidden channel in compact mode whose slot was released for visible channels). Existing channels are unchanged. HDHR / M3U / EPG / XC output endpoints filter out channels with `null` channel_number, so a number-less row is invisible to clients but visible in the channels page where the user can assign one. The 0037 auto-sync overhaul migration includes a reverse-direction backfill on the `channel_number` AlterField step so a rollback that re-imposes NOT NULL still succeeds even if NULLs are present. -- **M3U account delete always cascades auto-created channels.** The delete dialog is a single confirmation showing the count of affected channels (fetched from the new auto-created-channels-count endpoint). Auto-created channels are removed regardless of `user_hidden` or override state: override rows cascade via the one-to-one FK; hidden status does not preserve the channel because there is no provider left to populate it on the next sync. Manual channels are unaffected; their non-provider streams remain intact, and only streams owned by the deleted account are removed. The destroy response now returns `{"deleted_channels": N}` (HTTP 200) instead of an empty 204 so the confirmation toast can show the actual count. +- **M3U account delete always cascades auto-created channels.** The delete dialog is a single confirmation showing the count of affected channels (fetched from the new auto-created-channels-count endpoint). Auto-created channels are removed regardless of `hidden_from_output` or override state: override rows cascade via the one-to-one FK; hidden status does not preserve the channel because there is no provider left to populate it on the next sync. Manual channels are unaffected; their non-provider streams remain intact, and only streams owned by the deleted account are removed. The destroy response now returns `{"deleted_channels": N}` (HTTP 200) instead of an empty 204 so the confirmation toast can show the actual count. - **Bulk channel edit auto-routes auto-created channels through the override path.** When a bulk edit includes auto-synced channels, the changed fields are written to each channel's `ChannelOverride` row instead of the raw `Channel.*` columns, so the edit survives the next sync. Manual channels in the same selection write directly to `Channel.*`. A "Clear all overrides" affordance pre-clears overrides on the selection before applying new edits in the same submit (clear runs before the routing PATCH so a same-submit clear cannot wipe just-written overrides). - **Inline edits on the channels table route through the override row for auto-synced channels.** Editing a cell directly in the table (number, name, group, EPG, logo) now writes to `ChannelOverride.` for auto-synced rows so the edit survives the next refresh; manual rows continue to write directly. The save mechanic and validation are unchanged from the user's perspective. - **Channel-number duplicates are explicitly allowed.** Sync's `used_numbers` seed still avoids assigning the same number to two newly-created auto channels in the same run, so accidental duplication during sync remains impossible. Manual or override-driven duplication is permitted; downstream client behavior on duplicates varies by client. diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index cd21c7dd..862fec21 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -859,9 +859,9 @@ class ChannelViewSet(viewsets.ModelViewSet): # the same hidden semantic as downstream clients. if self.action in ("list", "get_ids", "summary"): if visibility_filter == "hidden": - q_filters &= Q(user_hidden=True) + q_filters &= Q(hidden_from_output=True) elif visibility_filter != "all": - q_filters &= Q(user_hidden=False) + q_filters &= Q(hidden_from_output=False) if self.request.user.user_level < 10: filters["user_level__lte"] = self.request.user.user_level @@ -1024,14 +1024,14 @@ class ChannelViewSet(viewsets.ModelViewSet): unhide_transition_ids = [ channel.id for channel, validated_data in validated_updates - if validated_data.get("user_hidden") is False - and channel.user_hidden is True + if validated_data.get("hidden_from_output") is False + and channel.hidden_from_output is True ] hide_transition_candidates = [ channel for channel, validated_data in validated_updates - if validated_data.get("user_hidden") is True - and channel.user_hidden is False + if validated_data.get("hidden_from_output") is True + and channel.hidden_from_output is False and channel.channel_number is not None and channel.auto_created and channel.auto_created_by_id diff --git a/apps/channels/compact_numbering.py b/apps/channels/compact_numbering.py index d1eae6c2..f3d7183c 100644 --- a/apps/channels/compact_numbering.py +++ b/apps/channels/compact_numbering.py @@ -123,7 +123,7 @@ def assign_compact_numbers_for_channels(channel_ids): id__in=channel_ids, auto_created=True, auto_created_by__isnull=False, - user_hidden=False, + hidden_from_output=False, channel_number__isnull=True, ).select_related("override", "channel_group", "auto_created_by") ) @@ -279,7 +279,7 @@ def _repack_inner(group_relation): for ch in channels: if _channel_has_number_override(ch): pinned.append(ch) - elif ch.user_hidden: + elif ch.hidden_from_output: hidden.append(ch) else: visible.append(ch) diff --git a/apps/channels/migrations/0037_auto_sync_overhaul.py b/apps/channels/migrations/0037_auto_sync_overhaul.py index 17dd601f..d93a1789 100644 --- a/apps/channels/migrations/0037_auto_sync_overhaul.py +++ b/apps/channels/migrations/0037_auto_sync_overhaul.py @@ -4,7 +4,7 @@ flag, configurable auto-sync number range, and nullable channel_number for compact-numbering slot release. Bundled operations (in forward order; reversed on rollback): - 1. AddField Channel.user_hidden + 1. AddField Channel.hidden_from_output 2. CreateModel ChannelOverride (one-to-one Channel) 3. AddField ChannelGroupM3UAccount.auto_sync_channel_end 4. RunPython backfill_auto_created_by_null (orphan re-attribution / demotion) @@ -113,7 +113,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='channel', - name='user_hidden', + name='hidden_from_output', field=models.BooleanField( db_index=True, default=False, diff --git a/apps/channels/models.py b/apps/channels/models.py index d8ad1d0d..7bf53b73 100644 --- a/apps/channels/models.py +++ b/apps/channels/models.py @@ -378,7 +378,7 @@ class Channel(models.Model): # Auto-sync still recognizes them so they are not recreated when their # underlying provider stream persists; this is an output-layer concern, not # a sync-time flag. - user_hidden = models.BooleanField( + hidden_from_output = models.BooleanField( default=False, db_index=True, help_text="Exclude this channel from downstream client output (HDHR, M3U, EPG, XC). Auto-sync still updates provider metadata." diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index 14c83201..b6dc4cc0 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -441,7 +441,7 @@ class ChannelSerializer(serializers.ModelSerializer): "logo_id", "user_level", "is_adult", - "user_hidden", + "hidden_from_output", "auto_created", "auto_created_by", "auto_created_by_name", diff --git a/apps/channels/signals.py b/apps/channels/signals.py index 4033204a..2eecbebf 100644 --- a/apps/channels/signals.py +++ b/apps/channels/signals.py @@ -109,13 +109,13 @@ def assign_compact_number_on_unhide(sender, instance, created, **kwargs): """ if created: return - # Skip the signal when update_fields proves user_hidden was not + # Skip the signal when update_fields proves hidden_from_output was not # touched. Sync sets update_fields on every save, so this keeps the # signal off the sync hot path. update_fields = kwargs.get("update_fields") - if update_fields is not None and "user_hidden" not in update_fields: + if update_fields is not None and "hidden_from_output" not in update_fields: return - if instance.user_hidden: + if instance.hidden_from_output: return if instance.channel_number is not None: return @@ -161,9 +161,9 @@ def release_compact_number_on_hide(sender, instance, created, **kwargs): if created: return update_fields = kwargs.get("update_fields") - if update_fields is not None and "user_hidden" not in update_fields: + if update_fields is not None and "hidden_from_output" not in update_fields: return - if not instance.user_hidden: + if not instance.hidden_from_output: return if instance.channel_number is None: return diff --git a/apps/hdhr/api_views.py b/apps/hdhr/api_views.py index 1645a7cf..41c1a3d9 100644 --- a/apps/hdhr/api_views.py +++ b/apps/hdhr/api_views.py @@ -139,7 +139,7 @@ class LineupAPIView(APIView): channels = ( with_effective_values(base_qs) - .exclude(user_hidden=True) + .exclude(hidden_from_output=True) .order_by("effective_channel_number") ) diff --git a/apps/hdhr/views.py b/apps/hdhr/views.py index 5ece59d2..6777bdbf 100644 --- a/apps/hdhr/views.py +++ b/apps/hdhr/views.py @@ -89,7 +89,7 @@ class LineupAPIView(APIView): channels = ( with_effective_values(Channel.objects.all()) - .exclude(user_hidden=True) + .exclude(hidden_from_output=True) .order_by("effective_channel_number") ) lineup = [] diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index f0b98f1d..e524a5c6 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -1849,7 +1849,7 @@ def sync_auto_channels(account_id, scan_start_time=None): Channel.objects.exclude( auto_created=True, auto_created_by=account, - user_hidden=False, + hidden_from_output=False, ).values_list("channel_number", flat=True) ) # Override pins are global reservations: effective_channel_number @@ -2158,7 +2158,7 @@ def sync_auto_channels(account_id, scan_start_time=None): channels_to_delete = [ ch for ch in existing_channel_map.values() - if not ch.user_hidden + if not ch.hidden_from_output ] if channels_to_delete: deleted_count = len(channels_to_delete) @@ -2275,7 +2275,7 @@ def sync_auto_channels(account_id, scan_start_time=None): if end_number is not None: overflow_delete_ids = [] for stream_id, ch in list(existing_channel_map.items()): - if ch.user_hidden: + if ch.hidden_from_output: continue num = ch.channel_number if num is None: @@ -2654,7 +2654,7 @@ def sync_auto_channels(account_id, scan_start_time=None): channels_to_delete = [] for ch_id, pairs in channel_streams_in_group.items(): channel = pairs[0][1] - if channel.user_hidden: + if channel.hidden_from_output: continue stream_ids = {sid for sid, _ in pairs} if not (stream_ids & processed_stream_ids): @@ -2713,7 +2713,7 @@ def sync_auto_channels(account_id, scan_start_time=None): orphaned_channels = Channel.objects.filter( auto_created=True, auto_created_by=account, - user_hidden=False, + hidden_from_output=False, ).exclude( id__in=ChannelStream.objects.filter( stream__m3u_account=account, diff --git a/apps/m3u/tests/test_sync_compound.py b/apps/m3u/tests/test_sync_compound.py index b82c6c9f..5f7b4742 100644 --- a/apps/m3u/tests/test_sync_compound.py +++ b/apps/m3u/tests/test_sync_compound.py @@ -103,7 +103,7 @@ class CompoundFixtureSyncTests(TestCase): channel_group=self.group, auto_created=True, auto_created_by=self.account, - user_hidden=True, + hidden_from_output=True, ) ChannelStream.objects.create( channel=self.hidden_channel, stream=self.hidden_stream, order=0 @@ -172,7 +172,7 @@ class CompoundFixtureSyncTests(TestCase): # Hidden channel survives and remains hidden. self.hidden_channel.refresh_from_db() - self.assertTrue(self.hidden_channel.user_hidden) + self.assertTrue(self.hidden_channel.hidden_from_output) # Overridden channel: the override row is intact, not cleared by # sync. The pinned channel_number persists. diff --git a/apps/m3u/tests/test_sync_correctness.py b/apps/m3u/tests/test_sync_correctness.py index b1c84563..4bc56da9 100644 --- a/apps/m3u/tests/test_sync_correctness.py +++ b/apps/m3u/tests/test_sync_correctness.py @@ -968,7 +968,7 @@ class ChannelOverrideClearResponseTests(TestCase): class HiddenChannelPreservationTests(TestCase): """ - Hidden channels (`user_hidden=True`) must be preserved across every + Hidden channels (`hidden_from_output=True`) must be preserved across every sync cleanup path so users can rely on the hide flag as "keep this channel even if its source stream temporarily disappears". Common case: event / PPV / seasonal channels whose provider stream comes @@ -1003,13 +1003,13 @@ class HiddenChannelPreservationTests(TestCase): channel_group=group, auto_created=True, auto_created_by=account, - user_hidden=True, + hidden_from_output=True, ) ChannelStream.objects.create(channel=ch, stream=stream, order=0) # Sync's scan_start_time is now (stream's last_seen is older, # so the stream is excluded from current_streams - simulating - # the provider dropping it). Without the user_hidden guard, the + # the provider dropping it). Without the hidden_from_output guard, the # channel would be cleaned up here. result = _sync(account) @@ -1047,7 +1047,7 @@ class HiddenChannelPreservationTests(TestCase): channel_group=group, auto_created=True, auto_created_by=account, - user_hidden=False, + hidden_from_output=False, ) ChannelStream.objects.create(channel=ch, stream=stream, order=0) @@ -1105,7 +1105,7 @@ class CompactNumberingTests(TestCase): ) Channel.objects.filter( id__in=[c.id for c in hidden_targets] - ).update(user_hidden=True) + ).update(hidden_from_output=True) # Re-run sync; compact pack runs at the end. _sync(account) @@ -1114,7 +1114,7 @@ class CompactNumberingTests(TestCase): Channel.objects.filter( auto_created=True, auto_created_by=account, - user_hidden=False, + hidden_from_output=False, ).values_list("channel_number", flat=True) ) self.assertEqual(nums, [100, 101, 102]) @@ -1123,7 +1123,7 @@ class CompactNumberingTests(TestCase): Channel.objects.filter( auto_created=True, auto_created_by=account, - user_hidden=True, + hidden_from_output=True, ).values_list("channel_number", flat=True) ) self.assertTrue( @@ -1184,10 +1184,10 @@ class CompactNumberingTests(TestCase): channel_group=group, auto_created=True, auto_created_by=account, - user_hidden=True, + hidden_from_output=True, ) - ch.user_hidden = False + ch.hidden_from_output = False ch.save() ch.refresh_from_db() @@ -1293,7 +1293,7 @@ class CompactNumberingTests(TestCase): channel_group=group, auto_created=True, auto_created_by=account, - user_hidden=False, + hidden_from_output=False, ) # Make sure the lock starts free. @@ -1526,7 +1526,7 @@ class AutoCleanupToggleTests(TestCase): channel_group=group, auto_created=True, auto_created_by=account, - user_hidden=True, + hidden_from_output=True, ) result = _sync(account) @@ -1536,7 +1536,7 @@ class AutoCleanupToggleTests(TestCase): Channel.objects.filter( auto_created=True, auto_created_by=account, - user_hidden=True, + hidden_from_output=True, ).count(), 1, "Hidden channels must survive cleanup regardless of the toggle", @@ -1698,7 +1698,7 @@ class HiddenChannelNumberCollisionTests(TestCase): rel_b.save() # A hidden auto-created channel pinned at #100 in group_a. - # Its source stream is gone (last_seen old), but user_hidden=True + # Its source stream is gone (last_seen old), but hidden_from_output=True # protects the channel from cleanup. old_seen = timezone.now() - timedelta(days=1) Stream.objects.create( @@ -1715,7 +1715,7 @@ class HiddenChannelNumberCollisionTests(TestCase): channel_group=group_a, auto_created=True, auto_created_by=account, - user_hidden=True, + hidden_from_output=True, ) # A NEW stream in group_b, no channel yet. Sync should create a diff --git a/apps/output/views.py b/apps/output/views.py index c1a98a7c..74133dbe 100644 --- a/apps/output/views.py +++ b/apps/output/views.py @@ -169,12 +169,12 @@ def generate_m3u(request, profile_name=None, user=None): base_qs = Channel.objects.select_related('channel_group', 'logo') # Resolve effective (override | provider) values at SQL level so ordering, - # naming, and logo resolution honor user overrides. `exclude(user_hidden=True)` + # naming, and logo resolution honor user overrides. `exclude(hidden_from_output=True)` # is the consumer-facing hide guarantee. from apps.channels.managers import with_effective_values channels = ( with_effective_values(base_qs, select_related_fks=True) - .exclude(user_hidden=True) + .exclude(hidden_from_output=True) .order_by("effective_channel_number") ) @@ -1372,7 +1372,7 @@ def generate_epg(request, profile_name=None, user=None): from apps.channels.managers import with_effective_values channels = ( with_effective_values(base_qs, select_related_fks=True) - .exclude(user_hidden=True) + .exclude(hidden_from_output=True) .order_by("effective_channel_number") ) @@ -2133,12 +2133,12 @@ def xc_get_live_categories(user): response = [] # Rank categories by the minimum EFFECTIVE channel number across their - # visible (not user_hidden) channels so overridden numbers drive the + # visible (not hidden_from_output) channels so overridden numbers drive the # ordering, not the underlying provider values. effective_min = Min( Coalesce("channels__override__channel_number", "channels__channel_number") ) - hidden_exclusion = {"channels__user_hidden": False} + hidden_exclusion = {"channels__hidden_from_output": False} if user.user_level < 10: user_profile_count = user.channel_profiles.count() @@ -2220,7 +2220,7 @@ def xc_get_live_streams(request, user, category_id=None): channels = ( with_effective_values(base_qs, select_related_fks=True) - .exclude(user_hidden=True) + .exclude(hidden_from_output=True) .order_by("effective_channel_number") ) @@ -2305,7 +2305,7 @@ def xc_get_epg(request, user, short=False): # resolution path so a single channel lookup honors the same visibility # rules as xc_get_live_streams. def _annotate(qs): - return with_effective_values(qs, select_related_fks=True).exclude(user_hidden=True) + return with_effective_values(qs, select_related_fks=True).exclude(hidden_from_output=True) if user.user_level < 10: user_profile_count = user.channel_profiles.count() @@ -2354,7 +2354,7 @@ def xc_get_epg(request, user, short=False): with_effective_values( Channel.objects.filter(channel_group=effective_group) if effective_group else Channel.objects.none() ) - .exclude(user_hidden=True) + .exclude(hidden_from_output=True) .order_by("effective_channel_number") ) diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index 1a62642c..c8451056 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -927,9 +927,9 @@ const ChannelForm = ({ channel: channelProp = null, isOpen, onClose }) => { - setValue('user_hidden', event.currentTarget.checked) + setValue('hidden_from_output', event.currentTarget.checked) } size="md" /> diff --git a/frontend/src/components/forms/ChannelBatch.jsx b/frontend/src/components/forms/ChannelBatch.jsx index 395ad634..fb9a80c0 100644 --- a/frontend/src/components/forms/ChannelBatch.jsx +++ b/frontend/src/components/forms/ChannelBatch.jsx @@ -124,7 +124,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { stream_profile_id: '-1', user_level: '-1', is_adult: '-1', - user_hidden: '-1', + hidden_from_output: '-1', clear_overrides: '-1', }, }); @@ -166,9 +166,9 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { getMatureContentChange(values.is_adult), getEpgChange(selectedDummyEpgId, epgs), ]; - if (values.user_hidden && values.user_hidden !== '-1') { + if (values.hidden_from_output && values.hidden_from_output !== '-1') { lines.push( - `• Hide from Clients: ${values.user_hidden === 'true' ? 'Yes' : 'No'}` + `• Hide from Clients: ${values.hidden_from_output === 'true' ? 'Yes' : 'No'}` ); } if (values.clear_overrides === 'clear') { @@ -877,8 +877,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { size="xs" label="Hide from Clients" description="Hidden channels are excluded from HDHR, M3U, EPG, and XC output." - {...form.getInputProps('user_hidden')} - key={form.key('user_hidden')} + {...form.getInputProps('hidden_from_output')} + key={form.key('hidden_from_output')} data={[ { value: '-1', label: '(no change)' }, { value: 'true', label: 'Hide' }, diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 824a6ae8..b0b08be0 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -967,7 +967,7 @@ const ChannelsTable = ({ onReady }) => { )} - {row.user_hidden && ( + {row.hidden_from_output && ( { logo_id: logoId ? `${logoId}` : '', user_level: `${channel?.user_level ?? '0'}`, is_adult: channel?.is_adult ?? false, - user_hidden: channel?.user_hidden ?? false, + hidden_from_output: channel?.hidden_from_output ?? false, }; }; @@ -235,14 +235,14 @@ export const handleEpgUpdate = async ( ) => { // Auto-synced channels route identity edits into the override row. Sync // keeps writing provider values to Channel.* unmodified, so the override - // is what actually persists user changes across refreshes. `user_hidden` + // is what actually persists user changes across refreshes. `hidden_from_output` // stays as a direct Channel field even for auto-created channels because // it is a status flag, not a value replacement. if (channel.auto_created) { const overridePayload = buildOverridePayload(channel, formattedValues); const payload = { id: channel.id, - user_hidden: formattedValues.user_hidden, + hidden_from_output: formattedValues.hidden_from_output, }; if (overridePayload !== undefined) { payload.override = overridePayload; diff --git a/frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js b/frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js index 968450ad..00e4f907 100644 --- a/frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js +++ b/frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js @@ -73,20 +73,20 @@ describe('updateChannelsWithOverrideRouting (manual finding #4)', () => { ]); }); - it('always-raw fields (user_hidden, user_level, is_adult) bypass override routing', async () => { - // user_hidden is a status flag, not a value override - it goes to - // Channel.user_hidden directly even on auto-created channels. + it('always-raw fields (hidden_from_output, user_level, is_adult) bypass override routing', async () => { + // hidden_from_output is a status flag, not a value override - it goes to + // Channel.hidden_from_output directly even on auto-created channels. const channelsById = { 1: { id: 1, auto_created: true }, }; await updateChannelsWithOverrideRouting( [1], - { user_hidden: true, name: 'Renamed' }, + { hidden_from_output: true, name: 'Renamed' }, channelsById, ); const body = API.bulkUpdateChannels.mock.calls[0][0]; expect(body).toEqual([ - { id: 1, user_hidden: true, override: { name: 'Renamed' } }, + { id: 1, hidden_from_output: true, override: { name: 'Renamed' } }, ]); }); diff --git a/frontend/src/utils/forms/__tests__/ChannelUtils.test.js b/frontend/src/utils/forms/__tests__/ChannelUtils.test.js index aa4280d3..73c550ee 100644 --- a/frontend/src/utils/forms/__tests__/ChannelUtils.test.js +++ b/frontend/src/utils/forms/__tests__/ChannelUtils.test.js @@ -140,7 +140,7 @@ describe('ChannelUtils', () => { logo_id: '10', user_level: '1', is_adult: false, - user_hidden: false, + hidden_from_output: false, }); }); @@ -237,7 +237,7 @@ describe('ChannelUtils', () => { logo_id: '', user_level: '0', is_adult: false, - user_hidden: false, + hidden_from_output: false, }); }); });