---
key: spa.channel-editor-create-logo
title: '2026-07-11 — Channel editor: bare-create entry point + external-logo mutual exclusion (#212)'
status: active
since: '2026-07-11'
supersedes: none
superseded-by: none
rule: Bare-channel create is a "New blank channel" action on the channels list (reusing Blazor's add-mode defaults) that navigates into the full editor, and an external logo URL always wins over an uploaded logo, matching `ChannelEditViewModel` precedence.
signals: '`ChannelEditor.razor` add-mode defaults, `ArtworkContentTypeModel.isExternalUrl`, `EMPTY_LOGO` · paths: `web/src/App.tsx`, `ChannelEditScreen.tsx` · issues: #212'
mechanics: '`ChannelEditScreen.tsx`'
---

Two Blazor-parity decisions closing the channel-editor gaps (`ChannelEditor.razor` +
`ChannelEditViewModel`):

1. **Bare-channel create lives on the channels list, not a form-first route.** Blazor's
   `/channels` (no `Id`) is a full add form; the SPA instead adds a "New blank channel" action next
   to "Add Channel" on `ChannelsScreen` (`web/src/App.tsx`) that POSTs `CreateChannelRequest` with
   Blazor's computed add-mode defaults (`(max existing int-parsed channel number) + 1`, `name: "New
   Channel"`, `group: "ErsatzTV"`, `ffmpegProfileId` = `GET /api/settings/ffmpeg`'s
   `defaultFFmpegProfileId`, `streamingMode: "TransportStreamHybrid"`, `isEnabled`/`showInEpg: true`,
   every other field at its C# `default(T)` — see `ChannelEditor.razor`'s `else` branch for the
   source of truth) directly, then navigates to `/app/edit-channel/{id}` for the rest of the fields.
   This is a deliberate equivalent, not a parity gap: it reuses the existing full editor instead of
   duplicating its ~20 fields into a second form. "Add Channel" (`/app/new-channel`, the
   library-to-lineup `ChannelBuilder` flow) is unrelated and untouched.
2. **External logo URL wins over an uploaded logo, mirroring
   `ChannelEditViewModel.ToUpdate/ToCreate`.** The channel's logo is `ArtworkContentTypeModel {
   path, contentType, isExternalUrl }`; on hydration, `isExternalUrl: true` populates a separate
   "External logo URL" field and the uploaded-logo draft state is treated as empty (`EMPTY_LOGO =
   { path: '', contentType: '' }` in `ChannelEditScreen.tsx`) so the two never disagree. On submit,
   a non-blank URL always wins: `logo: { path: url, contentType: '' }`, exactly matching
   `ExternalLogoUrl`'s precedence in the C# view model. Uploading a file clears the URL field (the
   last-set field wins, Blazor parity via `UploadLogo`'s `_model.ExternalLogoUrl = null`). The URL
   is validated as http(s) client-side before save is enabled (Blazor has no equivalent validation;
   added because the field is free text with no server-side format check surfaced to the SPA).

Also landed with #212: `preferredAudioLanguageCode`/`preferredSubtitleLanguageCode`,
`musicVideoCreditsTemplate`, and `streamSelector` moved from free-text `Input`s to `Select`s fed by
`GET /api/languages` / `/api/channels/music-video-credits-templates` /
`/api/channels/stream-selectors`. Each keeps the channel's currently-stored value selectable even if
it's absent from the reference list (`optionsKeepingCurrent` in `ChannelEditScreen.tsx`) so loading
an existing channel never silently changes the value out from under an unmanaged language code or a
template/selector file removed from disk since save.
