Added
- Clear Emby Logos — flush Emby's cached channel logos so it re-fetches fresh ones (GH #475, bd-v9tp7, build 0009). Emby caches channel logos and keeps serving a stale image even after the logo changes upstream in Dispatcharr; the operator had no way to force a refresh. Replicates Channel Identifiarr's "clear logos" feature, reusing ECM's existing Emby connection — the saved
emby_base_url+emby_api_keywithX-Emby-Tokenauth — so no new credentials and no username/password login (a static Emby API key authenticatesGET /LiveTv/Channelsenumeration and the per-channelDELETE /Items/{id}/Images/{type}calls just as well; deleting the cached image makes Emby re-download the logo from its source on next channel access). Backend: newEmbyClient.get_livetv_channels()(the auth gate) +delete_item_image(id, type)with aPrimary/LogoLight/LogoLightColorimage-type whitelist; new admin-gatedPOST /api/emby/clear-logosreturns 202 + {job_id} and runs a supervised background task (the full-lineup sweep — hundreds of channels × up to 3 types — would otherwise exceed the 30s request-timeout middleware, same 202+poll pattern as bulk-commit, bd-ggxks), with aGET /api/emby/clear-logos/{job_id}poll endpoint. The worker clears all Live TV channels (accepts an optionalchannel_idsfilter for a future per-group UI), is per-channel resilient (one channel lacking a logo type never aborts the run), and validates Emby is configured (400 otherwise). Progress via the notification system, like stream probe: the job emits atask_clear_emby_logosprogress notification (create → rate-limited updates → finalize success/warning) that the NotificationCenter renders live — a progress bar, deleted/skipped/error counts, and the current channel name — driven by a newclearingactive status. Frontend: a "Clear Cached Logos" block in Settings → Integrations → Emby with per-type checkboxes (default all three) and a fire-and-forget button gated on a saved Emby key. MCP: newclear_emby_logostool (202+poll, same whitelist). (GH #475, bd-v9tp7, build 0009) - Global date-format preference under Settings → Appearance (bd-8j47e, build 0007). ECM now has a single instance-wide "Date Format" control (Automatic / Month-Day-Year / Day-Month-Year / Year-Month-Day ISO) that drives every date and time shown in the UI. Previously date rendering was inconsistent:
frontend/src/utils/formatting.tsand ~14 components each formatted dates independently — some hardcodeden-US(always m/d/y), others passed no locale and silently deferred to each viewer's browser locale. That split is exactly why one operator saw06/04while another saw04/06on the same screen. Fix: a singlesetDateFormatLocale/getDateLocaleresolver informatting.tsmaps the preference to a locale (auto→browser,mdy→en-US,dmy→en-GB,iso→en-CA), and every date call site — the shared formatters plus all directtoLocaleString/toLocaleDateString/Intl.DateTimeFormatcallers — routes through it. The setting is applied on app init and updates live (no reload) when changed. Backend persistsdate_formatinDispatcharrSettingsalongsidetheme. Scope: settings are instance-wide, so this is one shared format for all users; the default isauto, which preserves the prior per-browser behavior, so no display changes until an operator explicitly pins a format. Time-of-day 12h/24h rendering is unchanged. (bd-8j47e, build 0007)
Changed
{normalized_name}now applies the rule's normalization groups in every auto-creation action, not just Create Channel (GH #466, bd-6gvt8, build 0006). The template variable is documented as "Name after normalization rules", but it only behaved that way in Create Channel — which re-normalizes its expanded name in a separate step. Everywhere else (Assign TVG-ID, Set Variable, Assign Logo, …){normalized_name}resolved to the raw stream name, becauseStreamContext.normalized_nameis never populated during a run and the variable fell back to the stream name. So with a "Strip country prefix" group,{normalized_name}producedCHANNELin Create Channel butUS | CHANNELin Assign TVG-ID. Fix:{normalized_name}is now resolved once at the per-action template-context chokepoint (_build_template_context) using the firing rule'snormalization_group_ids, so it means the same thing in every action. Scope of change: only rules that actually have normalization groups are affected — a rule with no groups still yields the raw stream name (unchanged), and Create Channel output is unchanged (its post-expansion normalization is idempotent on the already-normalized value). (GH #466, bd-6gvt8, build 0006)
Fixed
- Print Channel Guide rendered nothing but greyed-out phantom rows for groups numbered with float/decimal channel numbers (e.g. OTA 2.1, 5.1) (bd-szsb2, build 0008). With "Show empty slots" on (the default),
generatePrintHtmlwalked the group's range by integer steps and looked each slot up in a map keyed by the channel's exact number. For a float-numbered group no integer ever matched a float key, so every real channel was skipped (no data) and every integer in the range printed as a greyed—placeholder — the operator saw "2–38, all empty, plus channels that don't exist." A secondary bug parsed the per-group From/To bounds withparseInt, truncating e.g.38.5 → 38and dropping the highest float channel from the range. Fix: a group containing any non-integer channel number now skips integer gap-fill entirely and renders only its real channels (the integer gaps between 2.1 and 5.1 are unused major numbers, not "missing channels" — PO decision); pure-integer groups keep the existing empty-slot fill unchanged. The four From/To parse sites switchparseInt→parseFloatso decimal bounds and the top float channel are respected. Adds 3 float-specific tests (render-not-skipped, no-placeholders-for-float-group, no-parseInt-truncation regression). (bd-szsb2, build 0008) - Scheduled tasks sent a "starting" external alert (Telegram/Discord/email) on every run even when only warning/error alerts were enabled (GH #462, bd-on4sr, build 0005). A task's start/progress notification (
task_scheduler._create_progress_notification) is an "info"-level " starting..." message, buttask_engine._execute_taskwired its external-alert dispatch straight from the mastersend_alertstoggle, never consulting the per-taskalert_on_infoflag. So a task configured with only warning + error alerts (alert_on_info=False, the column default) still pushed a "starting" alert to every configured channel on every run — the reporter saw it via Telegram for the auto-creation schedule. The completion alert was already gated per-level (check_and_run_taskshonoursalert_on_success/_warning/_error/_info); only the start notification leaked. Fix:_execute_tasknow passessend_alerts AND alert_on_infoas the progress-notification alert flag, so the "starting" message dispatches externally only when info alerts are explicitly enabled. The in-app NotificationCenter toast is unchanged — it stays gated byshow_notifications, independent of the external-alert level. (GH #462, bd-on4sr, build 0005) - Deleting a normalization rule group left a dangling reference in auto-creation rules that then refused to save (GH #465, bd-miut3, build 0004). An auto-creation rule stores the normalization rule groups it applies as
normalization_group_ids(the "Normalization Groups" selector in the rule editor — the groups that strip quality tags).DELETE /api/normalization/groups/{id}removed the group and its rules but never stripped that id from any rule'snormalization_group_ids, so the rule kept a reference to a group that no longer existed. On the next edit the rule editor reloaded the full id list but couldn't render a checkbox for the missing group, and the write-time validator (_validate_normalization_group_ids, bd-i75ax) then rejected every save with422 — normalization_group_ids do not exist; the only workaround was "Clear all + re-select". Impact was save-block only — at run timenormalize(group_ids=…)filters by membership, so a missing id was silently ignored and auto-creation still ran correctly. Fix: deleting a normalization group now cascade-strips its id from every auto-creation rule'snormalization_group_idsin the same transaction (_strip_normalization_group_ref), and a startup heal (_heal_orphaned_normalization_group_refs, in_run_migrations) repairs rules orphaned by deletions that happened before this fix shipped — so already-broken rules become saveable again without operator action. (GH #465, bd-miut3, build 0004) - Scheduled tasks showed "Next Run: Never" in Settings → Scheduled Tasks despite having an active schedule (GH #468, bd-a80u2, build 0003). The "Next Run" the UI renders came from the registry's in-memory
instance._next_run, which is loaded from the DB only once at startup (sync_from_database). Every path that maintains schedules — the schedule create/update/delete endpoints (_update_task_next_run) andtask_engine.check_and_run_tasksafter a run — updates the database (scheduled_tasks.next_run_at+task_schedules.next_run_at) but never refreshes the in-memory instance. Tasks whose default schedule isMANUAL(Stream Probe, Re-probe Failed Streams, EPG/M3U refresh, etc.) start withinstance._next_run = None, so adding a schedule through the multi-schedule UI left the in-memory value atNoneand the UI showed "Never" until the next container restart. Compounding this,_save_task_to_dbwrote the stale in-memoryNoneback over the correctly-computedscheduled_tasks.next_run_at, so any latersync_to_database(e.g. editing alert settings) clobbered the DB value and made "Never" persist across restarts. Impact was display-only — the task engine fires off the childtask_schedules.next_run_atrows, which were always correct, so the tasks did run on schedule. Fix:GET /api/tasksandGET /api/tasks/{id}now source "Next Run" from the earliest enabledtask_schedules.next_run_at(the real firing source) instead of the stale in-memory value, falling back to the in-memory value only for tasks with no schedule rows; and_save_task_to_dbno longer overwritesnext_run_atfor tasks that havetask_schedulesrows (those are DB-managed). (GH #468, bd-a80u2, build 0003) - Bulk channel creation re-paginated the entire Dispatcharr logo catalog once per channel, making large batches O(channels × catalog-pages) and exhausting the request budget (bd-raehx, build 0002). Every
createChannelop carrying alogoUrlbut nologoIdcalledfind_logo_by_url(), which paginates the whole logo catalog (500/page) on every call with no caching. Two real debug bundles confirm the cost: a 441-op SiriusXM batch issued 2,391 logo GETs (~5-page catalog, finished server-side in ~6 min after a 504), and a 113-op batch against a larger ~25-page catalog issued 859 logo GETs and reached only ~channel 22 before the 30s504. This is why batches "created channels but didn't assign streams past ~15", why an op reported failed while the write still happened (the handler keeps running after the client's 504), and why adding streams manually afterward worked. Fix:_run_bulk_commitnow builds a per-run{url → logo}index once, lazily (only the first time an op actually needs a logo lookup, sovalidateOnlyand logo-free batches pay nothing), and every subsequent op reuses it; logos created mid-batch are inserted into the index so a later op sharing the samelogoUrlreuses them instead of creating a duplicate (also fixes a latent duplicate-logo bug). Catalog fetches drop from ~859/2,391 to ~25/5 — one pagination per batch. The per-channelfind_logo_by_urlfirst-match semantics and the "logo failure still creates the channel without a logo" fallthrough are preserved. (bd-raehx, build 0002) - Bulk-commit partial outcomes are now flagged explicitly so the client can distinguish "some applied, some failed" from a total failure (bd-5xciq, build 0002). The
BulkCommitResponseenvelope gains apartialboolean (operationsApplied > 0 AND operationsFailed > 0), present on every return path. This makes a partially-applied batch reconcilable viatempIdMapinstead of reading as a flat failure that prompts a duplicate-creating retry. The frontendBulkCommitResponsetype carries the field; existinguseEditModerendering already derives partial state from the applied/failed counts, so behavior is unchanged and the flag is exposed for future use. (bd-5xciq, build 0002) POST /api/channels/bulk-commitreturned 504 mid-flight on large batches while the handler kept running, producing duplicates on retry (bd-ggxks, build 0001). The endpoint was synchronous and pinned behind the 30sECM_REQUEST_TIMEOUT_SECONDSmiddleware. A 441-op SiriusXM batch (~6 min of sequential Dispatcharr POSTs) caused the middleware to fire504 Gateway Timeoutto the operator while the handler kept running in the background; the operator retried, each retry committed another ~30 partial channels, and Dispatcharr accumulated duplicates. Fix: bulk-commit now follows the bd-cns7j / bd-enfsy 202+poll pattern. Non-validateOnlyPOST /api/channels/bulk-commitreturns202 + {job_id, status: "running"}immediately and dispatches a supervised background task; clients pollGET /api/channels/bulk-commit/{job_id}until the status is terminal (completedwith the fullBulkCommitResponseunderresult, orfailedwith anerrormessage).validateOnlystays synchronous (200 with the response body inline) because pre-commit validation is fast and the frontend uses it for instant feedback. FrontendbulkCommit()and the MCPbulk_commit_channels/build_channel_lineuptools drive the new POST→poll loop transparently — their public shapes are unchanged so all threeuseEditModecall sites and existing MCP consumers keep working. Job state is in-memory with a 30-min TTL prune on every new POST; completed jobs are evicted on first read so RAM stays bounded; failed jobs persist until the TTL so operators can re-poll the error message. (bd-ggxks, build 0001)