From 1fd37b9a513a912add8a810f03a4e7feed1ceea3 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 1 Jun 2026 19:34:39 -0500 Subject: [PATCH] feat(channel-form): reorganize channel edit form into three semantic columns for improved layout and usability --- CHANGELOG.md | 1 + frontend/src/components/forms/Channel.jsx | 278 +++++++++++----------- 2 files changed, 144 insertions(+), 135 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bad7b61..0a195795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Channel edit form reorganized into three semantic columns.** Fields are now grouped as Identity (name, number, group, logo), Guide Data (TVG-ID, Gracenote StationId, EPG picker, current program preview), and Behavior/Access (stream profile, user level, mature content, hidden). The EPG section having its own column gives the `ProgramPreview` card enough width to truncate long titles correctly rather than expanding the column. - **IP lookup result delivered via WebSocket push.** When the background lookup completes, an `ip_lookup_complete` event is pushed to all connected clients so the sidebar IP field populates without polling. A `Skeleton` placeholder is shown while the lookup is in progress. - **`get_host_and_port` and `build_absolute_uri_with_port` moved from `apps/output/views.py` to `core/utils.py`.** Both helpers have no dependencies on anything in `apps/output` and are now used in `apps/channels/serializers.py` as well. Moving them to `core/utils` eliminates the need for a local import inside `LogoSerializer` and makes them available to the rest of the codebase without circular-import risk. `LogoSerializer.get_cache_url()` was also updated to use `build_absolute_uri_with_port` instead of `request.build_absolute_uri()`, so logo cache URLs now correctly include non-standard ports (fixing port-stripping for logo URLs behind reverse proxies, matching the existing fix applied to M3U and EPG URLs). - **`debian_install.sh` switched from Gunicorn to uWSGI with gevent workers.** The Debian/LXC bare-metal installer now deploys Dispatcharr under the same uWSGI + gevent stack used by the Docker image, eliminating a class of compatibility differences between the two deployment paths. The installer writes a `uwsgi-debian.ini` next to the app and manages uWSGI via a systemd service. The nginx site config now uses `uwsgi_pass` + `include uwsgi_params` instead of `proxy_pass`, which correctly populates `SERVER_PORT` in the WSGI environ so M3U playlist URLs include the configured port number (fixing the port-stripping bug from #1267 for bare-metal installs). Python 3.13 is now provisioned through uv's managed runtime so the install works on Debian 12 and Ubuntu 24.04 LTS regardless of the system Python version. diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index 9f7eb25c..c904641c 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -528,7 +528,8 @@ const ChannelForm = ({ channel: channelProp = null, isOpen, onClose }) => { )} - + {/* Col 1: Identity - Channel Name, Number, Group, Logo */} + { style={{ flex: 1 }} /> + + setValue( + 'channel_number', + getProviderFormValue(channel, 'channel_number'), + { shouldDirty: true } + ) + } + /> + } + value={watch('channel_number')} + onChange={(value) => setValue('channel_number', value)} + error={errors.channel_number?.message} + size="xs" + step={0.1} + precision={1} + /> + { - { - return { - label: USER_LEVEL_LABELS[value], - value: `${value}`, - }; - })} - value={watch('user_level')} - onChange={(value) => { - setValue('user_level', value); - }} - error={errors.user_level?.message} - /> - - - - - { > Upload or Create Logo - - - - setValue('is_adult', event.currentTarget.checked) - } - size="md" - /> - - - - - - setValue( - 'hidden_from_output', - event.currentTarget.checked - ) - } - size="md" - /> - - - {channel?.auto_created && hasAnyOverride && ( - - - - )} - - - setValue( - 'channel_number', - getProviderFormValue(channel, 'channel_number'), - { shouldDirty: true } - ) - } - /> - } - value={watch('channel_number')} - onChange={(value) => setValue('channel_number', value)} - error={errors.channel_number?.message} - size="xs" - step={0.1} // Add step prop to allow decimal inputs - precision={1} // Specify decimal precision - /> - + {/* Col 2: Guide Data - TVG-ID, Gracenote StationId, EPG, Program Preview */} + { {(isLoadingProgram || hasFetchedProgram || currentProgram) && ( - + { )} + + + + {/* Col 3: Behavior/Access - Stream Profile, User Level, Mature Content, Hidden */} + + { + return { + label: USER_LEVEL_LABELS[value], + value: `${value}`, + }; + })} + value={watch('user_level')} + onChange={(value) => { + setValue('user_level', value); + }} + error={errors.user_level?.message} + /> + + + + + setValue('is_adult', event.currentTarget.checked) + } + size="md" + /> + + + + + + setValue( + 'hidden_from_output', + event.currentTarget.checked + ) + } + size="md" + /> + + + {channel?.auto_created && hasAnyOverride && ( + + + + )} +