---
key: graphics.channel-level-attachment
title: 2026-07-22 — Channel-level graphics-element attachment + seeded On Now/Next text element (#74)
status: active
since: '2026-07-22'
supersedes: none
superseded-by: none
rule: A channel can attach `GraphicsElement`s directly via a new `ChannelGraphicsElement` join table (a base layer under deco/playout-item elements), and a built-in text element (`on-now-next.yml`) is seeded once per database so the On Now/Next overlay works out of the box.
signals: 'ChannelGraphicsElement, Channel graphics attachment, GraphicsElementSelector base layer, on-now-next seeded element, GraphicsElementDefaults.OnNowNextFileName, builtIn discriminator · paths: `ErsatzTV.Core/Domain/ChannelGraphicsElement.cs`, `ErsatzTV.Core/FFmpeg/GraphicsElementSelector.cs`, `ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementSeeder.cs`, `ConfigElementKey.GraphicsOnNowNextSeeded`, `GraphicsElementResponseModel.BuiltIn` · issues: #74'
---

#74 asked for a transient "On Now / Next" text bug burned onto the transcoded stream at each
program transition. The rendering and EPG-template-data infrastructure already existed (upstream
graphics engine + our #502/#511 remote-image/graphics-engine work); the gap was that graphics
elements had **no channel-level attachment** — only `PlayoutItem`/`ProgramScheduleItem`/`BlockItem`/
`Deco` joins existed — and there was **no seeded/built-in graphics element**, unlike watermarks.

**Decision: add a 5th join table, `ChannelGraphicsElement`, rather than reuse the watermark FK.**
Watermarks and graphics elements are separate parallel systems; a channel already has exactly one
`WatermarkId`, already spent on the #67 logo bug, and multi-line EPG text is a poor fit for the
single-image watermark model. `ChannelGraphicsElement` is structurally identical to the existing
four joins (composite key `{ChannelId, GraphicsElementId}`), added via a dual-provider migration
(`scripts/add-migration.sh Add_ChannelGraphicsElement`).

- `GraphicsElementSelector.SelectGraphicsElements` appends channel-level elements at the **final
  fall-through**, alongside `playoutItem.PlayoutItemGraphicsElements` — a **base layer**. A deco in
  `Merge` mode composes with it; a deco in `Override`/`Disable` mode returns earlier and so
  suppresses it (decos are allowed to override channel defaults, a deliberate rule). One more
  suppression path: on a **filler** item, a deco whose graphics-elements section is not set to run
  during filler (`UseGraphicsElementsDuringFiller` false) clears the result and returns for `Merge`
  and `Override` alike, so the channel base layer is dropped there too.
  `HttpLiveStreamingDirect` continues to return empty (ErsatzTV isn't transcoding, so there is no
  frame pipeline to draw into).
- **Seeded built-in element**, mirroring the `iptv.logo-drives-bug-preset` (#67) pattern:
  `GraphicsElementSeeder.SeedOnNowNext` writes `on-now-next.yml` into the graphics-elements
  templates folder (only if the file is absent — operator edits are never clobbered) and ensures a
  `GraphicsElement` row exists for it, guarded by the `graphics.on_now_next_seeded` `ConfigElement`
  marker so it runs once per database, not once per file-absence (the same reasoning as #67: the
  seeder runs at every startup, so a name/file-only guard would resurrect a deliberately deleted
  preset).
- The API needed a way for the SPA to find the built-in element without a fragile name-match — the
  direct #67 lesson (`WatermarkResponseModel.imageSource`). `GraphicsElementResponseModel` gained a
  server-derived `BuiltIn` bool, computed by comparing the row's `Path` filename to
  `GraphicsElementDefaults.OnNowNextFileName` rather than trusting the element's editable `Name`.
- The channel editor's Branding-tab "Show On Now / Next overlay" switch follows the exact pattern
  of the existing logo-bug toggle: on adds the built-in element's id to `graphicsElementIds`, off
  removes it; disabled (with an explanatory caption) when the channel is HLS-Direct.

**Accepted trade-off:** all channels that enable the toggle share one seeded element's geometry/
content; per-channel customization means editing the shared YAML or attaching a different element
(the join is general, not restricted to the seeded one).
