prismcast 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/dist/app.d.ts +1 -1
  2. package/dist/app.js +27 -9
  3. package/dist/app.js.map +1 -1
  4. package/dist/browser/cdp.js +7 -7
  5. package/dist/browser/cdp.js.map +1 -1
  6. package/dist/browser/channelSelection.d.ts +41 -11
  7. package/dist/browser/channelSelection.js +75 -24
  8. package/dist/browser/channelSelection.js.map +1 -1
  9. package/dist/browser/display.d.ts +1 -1
  10. package/dist/browser/index.d.ts +8 -8
  11. package/dist/browser/index.js +42 -43
  12. package/dist/browser/index.js.map +1 -1
  13. package/dist/browser/login.d.ts +2 -2
  14. package/dist/browser/login.js +1 -1
  15. package/dist/browser/manifestInterceptor.d.ts +77 -0
  16. package/dist/browser/manifestInterceptor.js +323 -0
  17. package/dist/browser/manifestInterceptor.js.map +1 -0
  18. package/dist/browser/precaching.js +4 -4
  19. package/dist/browser/tuning/comcastPolymer.d.ts +1 -1
  20. package/dist/browser/tuning/cox.d.ts +1 -1
  21. package/dist/browser/tuning/directv.d.ts +1 -1
  22. package/dist/browser/tuning/directv.js +61 -62
  23. package/dist/browser/tuning/directv.js.map +1 -1
  24. package/dist/browser/tuning/fox.d.ts +1 -1
  25. package/dist/browser/tuning/fox.js +171 -14
  26. package/dist/browser/tuning/fox.js.map +1 -1
  27. package/dist/browser/tuning/hbo.d.ts +1 -1
  28. package/dist/browser/tuning/hbo.js +6 -6
  29. package/dist/browser/tuning/hbo.js.map +1 -1
  30. package/dist/browser/tuning/hulu.d.ts +1 -1
  31. package/dist/browser/tuning/hulu.js +117 -110
  32. package/dist/browser/tuning/hulu.js.map +1 -1
  33. package/dist/browser/tuning/shared.d.ts +3 -3
  34. package/dist/browser/tuning/shared.js +5 -5
  35. package/dist/browser/tuning/sling.d.ts +1 -1
  36. package/dist/browser/tuning/sling.js +28 -28
  37. package/dist/browser/tuning/sling.js.map +1 -1
  38. package/dist/browser/tuning/spectrum.d.ts +1 -1
  39. package/dist/browser/tuning/spectrum.js +21 -22
  40. package/dist/browser/tuning/spectrum.js.map +1 -1
  41. package/dist/browser/tuning/thumbnailRow.d.ts +1 -1
  42. package/dist/browser/tuning/thumbnailRow.js +1 -1
  43. package/dist/browser/tuning/tileClick.d.ts +1 -1
  44. package/dist/browser/tuning/tileClick.js +5 -5
  45. package/dist/browser/tuning/xfinity.d.ts +1 -1
  46. package/dist/browser/tuning/youtubeTv.d.ts +1 -1
  47. package/dist/browser/tuning/youtubeTv.js +17 -18
  48. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  49. package/dist/browser/video.d.ts +20 -6
  50. package/dist/browser/video.js +30 -38
  51. package/dist/browser/video.js.map +1 -1
  52. package/dist/channels/index.d.ts +1 -2
  53. package/dist/channels/index.js +73 -40
  54. package/dist/channels/index.js.map +1 -1
  55. package/dist/config/channelForm.d.ts +58 -0
  56. package/dist/config/channelForm.js +129 -0
  57. package/dist/config/channelForm.js.map +1 -0
  58. package/dist/config/consistencyProbe.d.ts +7 -0
  59. package/dist/config/consistencyProbe.js +143 -0
  60. package/dist/config/consistencyProbe.js.map +1 -0
  61. package/dist/config/health.d.ts +4 -3
  62. package/dist/config/health.js +75 -62
  63. package/dist/config/health.js.map +1 -1
  64. package/dist/config/index.d.ts +1 -1
  65. package/dist/config/index.js +19 -12
  66. package/dist/config/index.js.map +1 -1
  67. package/dist/config/paths.d.ts +9 -3
  68. package/dist/config/paths.js +12 -4
  69. package/dist/config/paths.js.map +1 -1
  70. package/dist/config/persistence.context.d.ts +8 -0
  71. package/dist/config/persistence.context.js +30 -0
  72. package/dist/config/persistence.context.js.map +1 -0
  73. package/dist/config/persistence.d.ts +110 -12
  74. package/dist/config/persistence.js +351 -32
  75. package/dist/config/persistence.js.map +1 -1
  76. package/dist/config/presets.d.ts +14 -4
  77. package/dist/config/presets.js +84 -75
  78. package/dist/config/presets.js.map +1 -1
  79. package/dist/config/profiles.d.ts +2 -2
  80. package/dist/config/profiles.js +7 -9
  81. package/dist/config/profiles.js.map +1 -1
  82. package/dist/config/servicePacks.d.ts +2 -2
  83. package/dist/config/servicePacks.js +28 -28
  84. package/dist/config/servicePacks.js.map +1 -1
  85. package/dist/config/services.d.ts +62 -36
  86. package/dist/config/services.js +150 -136
  87. package/dist/config/services.js.map +1 -1
  88. package/dist/config/sites.d.ts +3 -3
  89. package/dist/config/sites.js +16 -16
  90. package/dist/config/sites.js.map +1 -1
  91. package/dist/config/userChannels.d.ts +500 -41
  92. package/dist/config/userChannels.js +1201 -312
  93. package/dist/config/userChannels.js.map +1 -1
  94. package/dist/config/userConfig.d.ts +103 -10
  95. package/dist/config/userConfig.js +277 -117
  96. package/dist/config/userConfig.js.map +1 -1
  97. package/dist/config/userProfiles.d.ts +4 -2
  98. package/dist/config/userProfiles.js +72 -26
  99. package/dist/config/userProfiles.js.map +1 -1
  100. package/dist/hdhr/channelMap.js +8 -11
  101. package/dist/hdhr/channelMap.js.map +1 -1
  102. package/dist/hdhr/deviceId.js +10 -9
  103. package/dist/hdhr/deviceId.js.map +1 -1
  104. package/dist/hdhr/discover.js +4 -5
  105. package/dist/hdhr/discover.js.map +1 -1
  106. package/dist/hdhr/index.js +6 -10
  107. package/dist/hdhr/index.js.map +1 -1
  108. package/dist/index.js +12 -12
  109. package/dist/index.js.map +1 -1
  110. package/dist/native/codecInference.d.ts +44 -0
  111. package/dist/native/codecInference.js +327 -0
  112. package/dist/native/codecInference.js.map +1 -0
  113. package/dist/native/decrypt.d.ts +1 -1
  114. package/dist/native/index.d.ts +5 -5
  115. package/dist/native/index.js +42 -64
  116. package/dist/native/index.js.map +1 -1
  117. package/dist/native/probe.d.ts +27 -9
  118. package/dist/native/probe.js +158 -67
  119. package/dist/native/probe.js.map +1 -1
  120. package/dist/native/proxy.d.ts +4 -2
  121. package/dist/native/proxy.js +52 -49
  122. package/dist/native/proxy.js.map +1 -1
  123. package/dist/native/tokenExpiry.d.ts +22 -0
  124. package/dist/native/tokenExpiry.js +153 -0
  125. package/dist/native/tokenExpiry.js.map +1 -0
  126. package/dist/routes/auth.js +6 -10
  127. package/dist/routes/auth.js.map +1 -1
  128. package/dist/routes/components.js +2 -2
  129. package/dist/routes/components.js.map +1 -1
  130. package/dist/routes/config/channels/endpoints/browse.d.ts +6 -0
  131. package/dist/routes/config/channels/endpoints/browse.js +195 -0
  132. package/dist/routes/config/channels/endpoints/browse.js.map +1 -0
  133. package/dist/routes/config/channels/endpoints/bulk.d.ts +6 -0
  134. package/dist/routes/config/channels/endpoints/bulk.js +128 -0
  135. package/dist/routes/config/channels/endpoints/bulk.js.map +1 -0
  136. package/dist/routes/config/channels/endpoints/crud.d.ts +6 -0
  137. package/dist/routes/config/channels/endpoints/crud.js +504 -0
  138. package/dist/routes/config/channels/endpoints/crud.js.map +1 -0
  139. package/dist/routes/config/channels/endpoints/importExport.d.ts +6 -0
  140. package/dist/routes/config/channels/endpoints/importExport.js +131 -0
  141. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -0
  142. package/dist/routes/config/channels/endpoints/predefined.d.ts +6 -0
  143. package/dist/routes/config/channels/endpoints/predefined.js +78 -0
  144. package/dist/routes/config/channels/endpoints/predefined.js.map +1 -0
  145. package/dist/routes/config/channels/endpoints/prefs.d.ts +6 -0
  146. package/dist/routes/config/channels/endpoints/prefs.js +58 -0
  147. package/dist/routes/config/channels/endpoints/prefs.js.map +1 -0
  148. package/dist/routes/config/channels/endpoints/service.d.ts +6 -0
  149. package/dist/routes/config/channels/endpoints/service.js +149 -0
  150. package/dist/routes/config/channels/endpoints/service.js.map +1 -0
  151. package/dist/routes/config/channels/endpoints/tags.d.ts +6 -0
  152. package/dist/routes/config/channels/endpoints/tags.js +185 -0
  153. package/dist/routes/config/channels/endpoints/tags.js.map +1 -0
  154. package/dist/routes/config/channels/http/handler.d.ts +10 -0
  155. package/dist/routes/config/channels/http/handler.js +20 -0
  156. package/dist/routes/config/channels/http/handler.js.map +1 -0
  157. package/dist/routes/config/channels/http/playlistHint.d.ts +25 -0
  158. package/dist/routes/config/channels/http/playlistHint.js +40 -0
  159. package/dist/routes/config/channels/http/playlistHint.js.map +1 -0
  160. package/dist/routes/config/channels/http/serviceWarning.d.ts +10 -0
  161. package/dist/routes/config/channels/http/serviceWarning.js +28 -0
  162. package/dist/routes/config/channels/http/serviceWarning.js.map +1 -0
  163. package/dist/routes/config/channels/index.d.ts +3 -3
  164. package/dist/routes/config/channels/index.js +1 -1
  165. package/dist/routes/config/channels/index.js.map +1 -1
  166. package/dist/routes/config/channels/{routes.d.ts → setup.d.ts} +1 -1
  167. package/dist/routes/config/channels/setup.js +23 -0
  168. package/dist/routes/config/channels/setup.js.map +1 -0
  169. package/dist/routes/config/channels/table.d.ts +9 -9
  170. package/dist/routes/config/channels/table.js +124 -76
  171. package/dist/routes/config/channels/table.js.map +1 -1
  172. package/dist/routes/config/http/envelope.d.ts +111 -0
  173. package/dist/routes/config/http/envelope.js +103 -0
  174. package/dist/routes/config/http/envelope.js.map +1 -0
  175. package/dist/routes/config/index.d.ts +9 -9
  176. package/dist/routes/config/index.js +2 -2
  177. package/dist/routes/config/index.js.map +1 -1
  178. package/dist/routes/config/services.js +69 -68
  179. package/dist/routes/config/services.js.map +1 -1
  180. package/dist/routes/config/settings.d.ts +1 -1
  181. package/dist/routes/config/settings.js +45 -38
  182. package/dist/routes/config/settings.js.map +1 -1
  183. package/dist/routes/debug.js +12 -11
  184. package/dist/routes/debug.js.map +1 -1
  185. package/dist/routes/icons.js +2 -2
  186. package/dist/routes/icons.js.map +1 -1
  187. package/dist/routes/logs.js +19 -30
  188. package/dist/routes/logs.js.map +1 -1
  189. package/dist/routes/mpegts.js +1 -1
  190. package/dist/routes/playlist.d.ts +1 -1
  191. package/dist/routes/playlist.js +37 -26
  192. package/dist/routes/playlist.js.map +1 -1
  193. package/dist/routes/root/content.js +27 -6
  194. package/dist/routes/root/content.js.map +1 -1
  195. package/dist/routes/root/index.js +2 -5
  196. package/dist/routes/root/index.js.map +1 -1
  197. package/dist/routes/root/scripts/channels.js +16 -16
  198. package/dist/routes/root/scripts/channels.js.map +1 -1
  199. package/dist/routes/root/scripts/config.js +59 -59
  200. package/dist/routes/root/scripts/config.js.map +1 -1
  201. package/dist/routes/root/scripts/index.d.ts +4 -4
  202. package/dist/routes/root/scripts/shared.js +24 -10
  203. package/dist/routes/root/scripts/shared.js.map +1 -1
  204. package/dist/routes/root/scripts/status.d.ts +4 -0
  205. package/dist/routes/root/scripts/status.handlers.d.ts +182 -0
  206. package/dist/routes/root/scripts/status.handlers.js +713 -0
  207. package/dist/routes/root/scripts/status.handlers.js.map +1 -0
  208. package/dist/routes/root/scripts/status.js +86 -488
  209. package/dist/routes/root/scripts/status.js.map +1 -1
  210. package/dist/routes/root/styles.js +5 -5
  211. package/dist/routes/services.js +18 -15
  212. package/dist/routes/services.js.map +1 -1
  213. package/dist/routes/sse.d.ts +16 -0
  214. package/dist/routes/sse.js +29 -0
  215. package/dist/routes/sse.js.map +1 -0
  216. package/dist/routes/streams.js +10 -23
  217. package/dist/routes/streams.js.map +1 -1
  218. package/dist/routes/ui.d.ts +2 -2
  219. package/dist/routes/upgrade.js +6 -7
  220. package/dist/routes/upgrade.js.map +1 -1
  221. package/dist/service/commands.context.d.ts +6 -0
  222. package/dist/service/commands.context.js +47 -0
  223. package/dist/service/commands.context.js.map +1 -0
  224. package/dist/service/commands.d.ts +53 -8
  225. package/dist/service/commands.js +171 -188
  226. package/dist/service/commands.js.map +1 -1
  227. package/dist/service/generators.context.d.ts +6 -0
  228. package/dist/service/generators.context.js +45 -0
  229. package/dist/service/generators.context.js.map +1 -0
  230. package/dist/service/generators.d.ts +56 -16
  231. package/dist/service/generators.js +499 -383
  232. package/dist/service/generators.js.map +1 -1
  233. package/dist/service/index.d.ts +2 -2
  234. package/dist/streaming/clients.d.ts +2 -2
  235. package/dist/streaming/clients.js +2 -2
  236. package/dist/streaming/codec.d.ts +2 -2
  237. package/dist/streaming/codec.js +0 -1
  238. package/dist/streaming/codec.js.map +1 -1
  239. package/dist/streaming/fmp4Segmenter.d.ts +2 -2
  240. package/dist/streaming/fmp4Segmenter.js +12 -12
  241. package/dist/streaming/hls.d.ts +3 -3
  242. package/dist/streaming/hls.js +81 -70
  243. package/dist/streaming/hls.js.map +1 -1
  244. package/dist/streaming/hlsResume.d.ts +3 -3
  245. package/dist/streaming/hlsResume.js +5 -5
  246. package/dist/streaming/hlsSegments.d.ts +5 -2
  247. package/dist/streaming/hlsSegments.js +14 -9
  248. package/dist/streaming/hlsSegments.js.map +1 -1
  249. package/dist/streaming/monitor.d.ts +12 -12
  250. package/dist/streaming/monitor.js +80 -54
  251. package/dist/streaming/monitor.js.map +1 -1
  252. package/dist/streaming/mp4Parser.d.ts +34 -4
  253. package/dist/streaming/mp4Parser.js +111 -29
  254. package/dist/streaming/mp4Parser.js.map +1 -1
  255. package/dist/streaming/mpegts.d.ts +1 -1
  256. package/dist/streaming/mpegts.js +13 -10
  257. package/dist/streaming/mpegts.js.map +1 -1
  258. package/dist/streaming/playlistBuilder.d.ts +1 -1
  259. package/dist/streaming/playlistBuilder.js +2 -2
  260. package/dist/streaming/preroll.d.ts +9 -9
  261. package/dist/streaming/preroll.js +39 -41
  262. package/dist/streaming/preroll.js.map +1 -1
  263. package/dist/streaming/pretune.js +4 -4
  264. package/dist/streaming/pretune.js.map +1 -1
  265. package/dist/streaming/recovery.d.ts +3 -3
  266. package/dist/streaming/recovery.js +4 -4
  267. package/dist/streaming/registry.d.ts +6 -6
  268. package/dist/streaming/setup.d.ts +9 -8
  269. package/dist/streaming/setup.js +97 -49
  270. package/dist/streaming/setup.js.map +1 -1
  271. package/dist/streaming/showInfo.d.ts +9 -3
  272. package/dist/streaming/showInfo.js +26 -18
  273. package/dist/streaming/showInfo.js.map +1 -1
  274. package/dist/streaming/statusEmitter.d.ts +4 -4
  275. package/dist/streaming/statusEmitter.js +2 -2
  276. package/dist/types/channels.d.ts +133 -34
  277. package/dist/types/channels.js +39 -1
  278. package/dist/types/channels.js.map +1 -1
  279. package/dist/types/config.d.ts +20 -1
  280. package/dist/types/index.d.ts +9 -8
  281. package/dist/types/index.js +1 -0
  282. package/dist/types/index.js.map +1 -1
  283. package/dist/types/profiles.d.ts +5 -4
  284. package/dist/types/selection.d.ts +48 -4
  285. package/dist/types/streaming.d.ts +1 -1
  286. package/dist/upgrade/commands.context.d.ts +6 -0
  287. package/dist/upgrade/commands.context.js +29 -0
  288. package/dist/upgrade/commands.context.js.map +1 -0
  289. package/dist/upgrade/commands.d.ts +29 -2
  290. package/dist/upgrade/commands.js +87 -134
  291. package/dist/upgrade/commands.js.map +1 -1
  292. package/dist/upgrade/detection.context.d.ts +6 -0
  293. package/dist/upgrade/detection.context.js +24 -0
  294. package/dist/upgrade/detection.context.js.map +1 -0
  295. package/dist/upgrade/detection.d.ts +140 -10
  296. package/dist/upgrade/detection.js +153 -66
  297. package/dist/upgrade/detection.js.map +1 -1
  298. package/dist/upgrade/index.d.ts +2 -2
  299. package/dist/utils/chromeFetch.d.ts +1 -1
  300. package/dist/utils/chromeFetch.js +1 -1
  301. package/dist/utils/clock.d.ts +14 -0
  302. package/dist/utils/clock.js +28 -0
  303. package/dist/utils/clock.js.map +1 -0
  304. package/dist/utils/debugFilter.d.ts +1 -1
  305. package/dist/utils/debugFilter.js +6 -4
  306. package/dist/utils/debugFilter.js.map +1 -1
  307. package/dist/utils/delay.d.ts +2 -3
  308. package/dist/utils/delay.js +10 -16
  309. package/dist/utils/delay.js.map +1 -1
  310. package/dist/utils/evaluate.js +12 -14
  311. package/dist/utils/evaluate.js.map +1 -1
  312. package/dist/utils/ffmpeg.context.d.ts +6 -0
  313. package/dist/utils/ffmpeg.context.js +31 -0
  314. package/dist/utils/ffmpeg.context.js.map +1 -0
  315. package/dist/utils/ffmpeg.d.ts +85 -29
  316. package/dist/utils/ffmpeg.js +126 -129
  317. package/dist/utils/ffmpeg.js.map +1 -1
  318. package/dist/utils/fileLogger.d.ts +18 -2
  319. package/dist/utils/fileLogger.js +58 -30
  320. package/dist/utils/fileLogger.js.map +1 -1
  321. package/dist/utils/format.d.ts +1 -1
  322. package/dist/utils/format.js +2 -2
  323. package/dist/utils/format.js.map +1 -1
  324. package/dist/utils/html.js +1 -3
  325. package/dist/utils/html.js.map +1 -1
  326. package/dist/utils/index.d.ts +21 -20
  327. package/dist/utils/index.js +1 -0
  328. package/dist/utils/index.js.map +1 -1
  329. package/dist/utils/logger.js +16 -20
  330. package/dist/utils/logger.js.map +1 -1
  331. package/dist/utils/m3u.d.ts +18 -3
  332. package/dist/utils/m3u.js +28 -14
  333. package/dist/utils/m3u.js.map +1 -1
  334. package/dist/utils/memo.d.ts +19 -0
  335. package/dist/utils/memo.js +53 -0
  336. package/dist/utils/memo.js.map +1 -0
  337. package/dist/utils/morganStream.js +2 -2
  338. package/dist/utils/morganStream.js.map +1 -1
  339. package/dist/utils/pid.d.ts +2 -2
  340. package/dist/utils/pid.js +2 -2
  341. package/dist/utils/platform.d.ts +1 -1
  342. package/dist/utils/platform.js +3 -3
  343. package/dist/utils/platform.js.map +1 -1
  344. package/dist/utils/retry.d.ts +2 -0
  345. package/dist/utils/retry.js +9 -7
  346. package/dist/utils/retry.js.map +1 -1
  347. package/dist/utils/sanitize.d.ts +2 -2
  348. package/dist/utils/sanitize.js +4 -4
  349. package/dist/utils/timing.d.ts +6 -3
  350. package/dist/utils/timing.js +10 -6
  351. package/dist/utils/timing.js.map +1 -1
  352. package/dist/utils/version.d.ts +3 -2
  353. package/dist/utils/version.js +14 -27
  354. package/dist/utils/version.js.map +1 -1
  355. package/package.json +13 -7
  356. package/dist/native/intercept.d.ts +0 -35
  357. package/dist/native/intercept.js +0 -181
  358. package/dist/native/intercept.js.map +0 -1
  359. package/dist/routes/config/channels/routes.js +0 -1505
  360. package/dist/routes/config/channels/routes.js.map +0 -1
@@ -1,11 +1,7 @@
1
- /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
- *
3
- * table.ts: Channel table rendering for the PrismCast configuration interface.
4
- */
5
1
  import { ICON_BOLT, ICON_COPY, ICON_DELETE, ICON_DISABLE, ICON_EDIT, ICON_ENABLE, ICON_FILTER, ICON_HEALTH, ICON_LINK, ICON_LOGIN, ICON_MANAGE, ICON_REVERT, ICON_TRANSFER } from "../../icons.js";
6
2
  import { compareChannelSort, getAllServiceTags, getAuthDomainForChannel, getChannelServiceLabel, getChannelServiceTags, getChannelSortKey, getEnabledServices, getPredefinedDomainMap, getServiceGroup, hasMultipleServices, isChannelAvailableByService, isServiceTagEnabled, resolvePredefinedVariant, resolveServiceKey } from "../../../config/services.js";
7
3
  import { escapeHtml, formatTimeAgo } from "../../../utils/index.js";
8
- import { getActiveTagVocabulary, getChannelEffectiveTags, getChannelListing, getChannelLogo, getChannelsParseErrorMessage, getPredefinedScopeCounts, getTagRegistry, getUserChannelsFilePath, hasChannelsParseError, isPredefinedChannel, isPredefinedChannelDisabled, isUserChannel, tagsMatch } from "../../../config/userChannels.js";
4
+ import { getActiveTagVocabulary, getChannelCustomizations, getChannelEffectiveTags, getChannelListing, getChannelLogo, getChannelsParseErrorMessage, getEffectiveHdhrEnabled, getPredefinedScopeCounts, getTagRegistry, getUserChannelsFilePath, hasChannelsParseError, isPredefinedChannel, isPredefinedChannelDisabled, isUserChannel, isVisibleChannel, tagsMatch } from "../../../config/userChannels.js";
9
5
  import { getCachedProviderChannels, getProviderDomainMap, getProviderGuideUrls, getProviderModuleInfo } from "../../../browser/channelSelection.js";
10
6
  import { getChannelHealth, getDomainAuth } from "../../../config/health.js";
11
7
  import { getProfileForChannel, getProfiles } from "../../../config/profiles.js";
@@ -15,7 +11,7 @@ import { categorizeProfiles } from "../index.js";
15
11
  import { generateWizardModal } from "../../components.js";
16
12
  /**
17
13
  * Generates an annotated service display span. The client-side page-load script processes these elements via serviceIconHtml, rendering the appropriate
18
- * icon + text combination. The server just emits the text with data attributes — all icon rendering is client-side through the single serviceIconHtml path.
14
+ * icon + text combination. The server just emits the text with data attributes - all icon rendering is client-side through the single serviceIconHtml path.
19
15
  * @param name - The service display name.
20
16
  * @param domain - The service's domain for icon fallback derivation. Undefined for services without a known domain.
21
17
  * @param iconUrl - Optional explicit icon URL to try before domain-derived fallbacks.
@@ -58,8 +54,9 @@ function generateTextField(id, name, label, value, options = {}) {
58
54
  const required = options.required ? " required" : "";
59
55
  const pattern = options.pattern ? " pattern=\"" + options.pattern + "\"" : "";
60
56
  const placeholder = options.placeholder ? " placeholder=\"" + escapeHtml(options.placeholder) + "\"" : "";
61
- // Modification tracking: compare against the predefined default and render the settings form's "modified" visual treatment when they differ.
62
- const isModified = (options.defaultValue !== undefined) && (options.defaultValue !== value);
57
+ // Modification tracking is driven by storage provenance (isCustomized), not value comparison. A field is customized iff the user has explicitly set it on
58
+ // the canonical or active variant stored entry - that is the single source of truth, supplied by getChannelCustomizations in the storage layer.
59
+ const isModified = options.isCustomized === true;
63
60
  const modifiedClass = isModified ? " modified" : "";
64
61
  const modifiedDot = isModified ? "<span class=\"modified-dot\" title=\"Modified from predefined default\"></span>" : "";
65
62
  const defaultAttr = (options.defaultValue !== undefined) ? " data-default=\"" + escapeHtml(options.defaultValue) + "\"" : "";
@@ -86,14 +83,16 @@ function generateTextField(id, name, label, value, options = {}) {
86
83
  * @param selectedProfile - The currently selected profile (empty string for autodetect).
87
84
  * @param profiles - List of available profiles with descriptions and summaries.
88
85
  * @param showHint - Whether to show the hint text with profile reference link.
86
+ * @param defaultProfile - When provided, used as the data-default attribute for the per-field reset button. Should be supplied alongside isCustomized=true.
87
+ * @param isCustomized - True when the user has explicitly customized the profile field (presence in stored deltas). Drives the modified visual treatment and
88
+ * the per-field reset button. Independent of value comparison - storage provenance is the single source of truth.
89
89
  * @returns Array of HTML strings for the form row.
90
90
  */
91
- function generateProfileDropdown(id, selectedProfile, profiles, showHint = true, defaultProfile) {
91
+ function generateProfileDropdown(id, selectedProfile, profiles, showHint = true, defaultProfile, isCustomized) {
92
92
  const lines = [];
93
93
  const groups = categorizeProfiles(profiles);
94
- // Modification tracking for the profile dropdown. Same visual treatment as text fields — border accent, dot, and reset button when the current selection
95
- // differs from the predefined default.
96
- const isModified = (defaultProfile !== undefined) && (defaultProfile !== selectedProfile);
94
+ // Modification tracking is driven by storage provenance (isCustomized), not value comparison. Symmetric with generateTextField.
95
+ const isModified = isCustomized === true;
97
96
  const modifiedClass = isModified ? " modified" : "";
98
97
  const modifiedDot = isModified ? "<span class=\"modified-dot\" title=\"Modified from predefined default\"></span>" : "";
99
98
  const defaultAttr = (defaultProfile !== undefined) ? " data-default=\"" + escapeHtml(defaultProfile) + "\"" : "";
@@ -275,6 +274,12 @@ function generateProfileReference(profiles) {
275
274
  lines.push("</div>");
276
275
  return lines.join("\n");
277
276
  }
277
+ /**
278
+ * Generates HTML for the advanced fields section (station ID, channel selector, channel number, guide title, logo URL, HDHomeRun toggle, tags).
279
+ * @param idPrefix - Prefix for element IDs ("add" or "edit").
280
+ * @param options - Field values, defaults, and customization metadata. See AdvancedFieldOptions.
281
+ * @returns Array of HTML strings for the advanced fields section.
282
+ */
278
283
  function generateAdvancedFields(idPrefix, options = {}) {
279
284
  const { channelNumberValue = "", channelSelectorValue = "", guideTitleValue = "", hdhrEnabled = true, logoUrlValue = "", showHints = true, stationIdValue = "", tagsValue = "" } = options;
280
285
  const lines = [];
@@ -286,13 +291,16 @@ function generateAdvancedFields(idPrefix, options = {}) {
286
291
  // Station ID.
287
292
  const stationIdHint = showHints ? "Optional Gracenote station ID for guide data (tvc-guide-stationid)." : undefined;
288
293
  const defs = options.defaults;
289
- lines.push(...generateTextField(idPrefix + "-stationId", "stationId", "Station ID", stationIdValue, { defaultValue: defs?.stationId, hint: stationIdHint, placeholder: showHints ? "e.g., 12345" : undefined }));
294
+ const customized = options.customizedFields;
295
+ lines.push(...generateTextField(idPrefix + "-stationId", "stationId", "Station ID", stationIdValue, { defaultValue: defs?.stationId, hint: stationIdHint, isCustomized: customized?.has("stationId"), placeholder: showHints ? "e.g., 12345" : undefined }));
290
296
  // Guide title override. When set, this value replaces the channel name in the M3U playlist's tvg-name attribute and guide placeholder airings.
291
297
  const guideTitleHint = showHints ? "Optional title for guide display. When set, overrides the channel name in the M3U playlist (tvg-name)." : undefined;
292
- lines.push(...generateTextField(idPrefix + "-guideTitle", "guideTitle", "Guide Title", guideTitleValue, { defaultValue: defs?.guideTitle, hint: guideTitleHint, placeholder: showHints ? "e.g., Flighty Airport Delays" : undefined }));
298
+ lines.push(...generateTextField(idPrefix + "-guideTitle", "guideTitle", "Guide Title", guideTitleValue, { defaultValue: defs?.guideTitle, hint: guideTitleHint, isCustomized: customized?.has("guideTitle"),
299
+ placeholder: showHints ? "e.g., Flighty Airport Delays" : undefined }));
293
300
  // Custom logo URL. When set, this value is emitted as the tvg-logo attribute in the M3U playlist.
294
301
  const logoUrlHint = showHints ? "Optional logo URL for the channel. Overrides any logo derived from Channels DVR." : undefined;
295
- lines.push(...generateTextField(idPrefix + "-logoUrl", "logoUrl", "Logo URL", logoUrlValue, { defaultValue: defs?.logoUrl, hint: logoUrlHint, placeholder: showHints ? "https://example.com/logo.png" : undefined, type: "url" }));
302
+ lines.push(...generateTextField(idPrefix + "-logoUrl", "logoUrl", "Logo URL", logoUrlValue, { defaultValue: defs?.logoUrl, hint: logoUrlHint, isCustomized: customized?.has("logoUrl"),
303
+ placeholder: showHints ? "https://example.com/logo.png" : undefined, type: "url" }));
296
304
  // Channel selector. The guide-based service list is derived from the provider module registry so it stays current as services are added.
297
305
  const guideProviderNames = getProviderModuleInfo().map((p) => p.label).sort().join(", ");
298
306
  const channelSelectorHint = showHints ?
@@ -301,18 +309,21 @@ function generateAdvancedFields(idPrefix, options = {}) {
301
309
  "For tile and thumbnail profiles, right-click the channel element \u2192 Inspect \u2192 copy a unique value matching the profile's selector pattern " +
302
310
  "(typically a portion of the image src URL)." :
303
311
  undefined;
304
- lines.push(...generateTextField(idPrefix + "-channelSelector", "channelSelector", "Channel Selector", channelSelectorValue, { defaultValue: defs?.channelSelector, hint: channelSelectorHint, list: idPrefix + "-selectorList", placeholder: showHints ? "e.g., ESPN" : undefined }));
312
+ lines.push(...generateTextField(idPrefix + "-channelSelector", "channelSelector", "Channel Selector", channelSelectorValue, { defaultValue: defs?.channelSelector, hint: channelSelectorHint, isCustomized: customized?.has("channelSelector"),
313
+ list: idPrefix + "-selectorList", placeholder: showHints ? "e.g., ESPN" : undefined }));
305
314
  // Channel number for Channels DVR and Plex integration.
306
315
  const channelNumberHint = showHints ?
307
316
  "Optional numeric channel number for guide matching in Channels DVR and Plex." :
308
317
  undefined;
309
- lines.push(...generateTextField(idPrefix + "-channelNumber", "channelNumber", "Channel Number", channelNumberValue, { defaultValue: defs?.channelNumber, hint: channelNumberHint, placeholder: showHints ? "e.g., 501" : undefined }));
318
+ lines.push(...generateTextField(idPrefix + "-channelNumber", "channelNumber", "Channel Number", channelNumberValue, { defaultValue: defs?.channelNumber, hint: channelNumberHint, isCustomized: customized?.has("channelNumber"),
319
+ placeholder: showHints ? "e.g., 501" : undefined }));
310
320
  // Tags field: checkbox grid of the active tag vocabulary. Checked tags are collected into a hidden input as a comma-separated value on form submission.
311
321
  // This prevents users from entering tag names that don't exist in the managed vocabulary.
312
322
  const vocabulary = getActiveTagVocabulary();
313
323
  const currentTags = tagsValue ? tagsValue.split(",").map((t) => t.trim()).filter((t) => t.length > 0) : [];
314
324
  if (vocabulary.length > 0) {
315
- const tagsModified = (defs?.tags !== undefined) && (defs.tags !== tagsValue);
325
+ // Tags are an identity field stored on the canonical entry. Customization is presence in the canonical-stored entry, surfaced via customizedFields.
326
+ const tagsModified = customized?.has("tags") === true;
316
327
  const tagsModifiedClass = tagsModified ? " modified" : "";
317
328
  const tagsModifiedDot = tagsModified ? "<span class=\"modified-dot\" title=\"Modified from predefined default\"></span>" : "";
318
329
  const tagsDefaultAttr = (defs?.tags !== undefined) ? " data-default=\"" + escapeHtml(defs.tags) + "\"" : "";
@@ -336,11 +347,12 @@ function generateAdvancedFields(idPrefix, options = {}) {
336
347
  // HDHomeRun lineup inclusion. A hidden input provides the "false" value when the checkbox is unchecked (unchecked checkboxes are not submitted in FormData).
337
348
  // When checked, the checkbox value "true" overwrites the hidden input's "false" since it appears later in DOM order.
338
349
  const hdhrChecked = hdhrEnabled ? " checked" : "";
339
- const hdhrDefaultEnabled = defs?.hdhrEnabled ?? true;
340
- const hdhrModified = (defs?.hdhrEnabled !== undefined) && (hdhrEnabled !== hdhrDefaultEnabled);
350
+ // Customization is presence in stored deltas (identity field, lives on canonical entry); surfaced via customizedFields. The default value (when present)
351
+ // arrives pre-stringified as "true" or "false" from resetValueFor -> getEffectiveHdhrEnabled, so it goes directly into the data-default attribute.
352
+ const hdhrModified = customized?.has("hdhrEnabled") === true;
341
353
  const hdhrModifiedClass = hdhrModified ? " modified" : "";
342
354
  const hdhrModifiedDot = hdhrModified ? "<span class=\"modified-dot\" title=\"Modified from predefined default\"></span>" : "";
343
- const hdhrDefaultAttr = (defs?.hdhrEnabled !== undefined) ? " data-default=\"" + String(hdhrDefaultEnabled) + "\"" : "";
355
+ const hdhrDefaultAttr = (defs?.hdhrEnabled !== undefined) ? " data-default=\"" + defs.hdhrEnabled + "\"" : "";
344
356
  const hdhrHint = showHints ? "When unchecked, this channel is hidden from the HDHomeRun lineup and not available in Plex." : undefined;
345
357
  lines.push("<div class=\"form-row form-row-checkbox" + hdhrModifiedClass + "\">");
346
358
  lines.push(hdhrModifiedDot + "<label for=\"" + idPrefix + "-hdhrEnabled\">Include in HDHomeRun/Plex Lineup</label>");
@@ -350,7 +362,8 @@ function generateAdvancedFields(idPrefix, options = {}) {
350
362
  if (hdhrHint) {
351
363
  lines.push("<div class=\"hint\">" + hdhrHint + "</div>");
352
364
  }
353
- lines.push("</div>"); // End advanced fields.
365
+ // Close the advanced-fields wrapper opened at the top of this builder.
366
+ lines.push("</div>");
354
367
  return lines;
355
368
  }
356
369
  /**
@@ -370,8 +383,12 @@ function generateChannelSelectorData() {
370
383
  }
371
384
  // Use the resolved channel (with inheritance from the canonical) so stationId, name, and other inherited fields are populated correctly for variants.
372
385
  // The channelSelector is captured from the raw entry (already validated as non-null) to avoid non-null assertions on the resolved channel's optional field.
386
+ // resolvePredefinedVariant returns a fully-typed ResolvedChannel for both canonical and variant keys; defensive skip for the rare missing-canonical case.
373
387
  const selector = raw.channelSelector;
374
- const channel = resolvePredefinedVariant(key) ?? raw;
388
+ const channel = resolvePredefinedVariant(key);
389
+ if (!channel) {
390
+ continue;
391
+ }
375
392
  const hostname = new URL(channel.url).hostname;
376
393
  seen[hostname] ??= new Set();
377
394
  if (seen[hostname].has(selector)) {
@@ -381,19 +398,19 @@ function generateChannelSelectorData() {
381
398
  byDomain[hostname] ??= [];
382
399
  byDomain[hostname].push({ label: channel.name ?? selector, stationId: channel.stationId, value: selector });
383
400
  }
384
- // Merge cached service-discovered channels into the domain map. Predefined entries take precedence — we only add discovered channels whose channelSelector
401
+ // Merge cached service-discovered channels into the domain map. Predefined entries take precedence - we only add discovered channels whose channelSelector
385
402
  // value is not already present for that domain. This enriches the datalist with the full service lineup when precaching or prior discovery has run.
386
403
  for (const provider of getCachedProviderChannels()) {
387
- seen[provider.hostname] ??= new Set();
388
- byDomain[provider.hostname] ??= [];
404
+ const seenForHost = seen[provider.hostname] ??= new Set();
405
+ const entriesForHost = byDomain[provider.hostname] ??= [];
389
406
  for (const entry of provider.entries) {
390
- if (!seen[provider.hostname].has(entry.value)) {
391
- seen[provider.hostname].add(entry.value);
392
- byDomain[provider.hostname].push(entry);
407
+ if (!seenForHost.has(entry.value)) {
408
+ seenForHost.add(entry.value);
409
+ entriesForHost.push(entry);
393
410
  }
394
411
  }
395
412
  }
396
- // Build the hostname→slug map and slug→guideUrl map for all services (including those with cold caches) so the client-side fetch can trigger discovery for any
413
+ // Build the hostname->slug map and slug->guideUrl map for all services (including those with cold caches) so the client-side fetch can trigger discovery for any
397
414
  // service domain and the URL hint can suggest the correct guide URL.
398
415
  const serviceByDomain = getProviderDomainMap();
399
416
  const serviceGuideUrl = getProviderGuideUrls();
@@ -407,7 +424,7 @@ function generateChannelSelectorData() {
407
424
  "var predefinedByDomain = " + JSON.stringify(getPredefinedDomainMap()) + ";";
408
425
  }
409
426
  /**
410
- * Generates the tag column filter dropdown content. This is the single source of truth for the filter checkbox list — used for both the initial header render
427
+ * Generates the tag column filter dropdown content. This is the single source of truth for the filter checkbox list - used for both the initial header render
411
428
  * and for incremental updates after tag CRUD operations. The dropdown shell (button, dropdown-menu wrapper) is rendered by the header generator; this function
412
429
  * provides only the inner content (checkbox labels + "Show All" item).
413
430
  * @returns HTML string for the filter dropdown content.
@@ -427,7 +444,7 @@ export function generateTagFilterContent() {
427
444
  return lines.join("");
428
445
  }
429
446
  /**
430
- * Generates the Tag Management modal body HTML. This is the single source of truth for tag manager content — used for both the initial server render and for
447
+ * Generates the Tag Management modal body HTML. This is the single source of truth for tag manager content - used for both the initial server render and for
431
448
  * incremental updates after tag CRUD operations. The endpoints return this HTML in the response so the client can replace the modal content without a page reload.
432
449
  * @returns HTML string for the tag manager body (tag list, input field, deleted tags section).
433
450
  */
@@ -443,7 +460,7 @@ export function generateTagManagerBody() {
443
460
  "<button type=\"button\" class=\"btn-icon btn-icon-delete\" title=\"Delete tag\" onclick=\"deleteTag('" + escapeHtml(tag) +
444
461
  "')\">" + ICON_DELETE + "</button></div>");
445
462
  }
446
- // Deleted predefined tags section — show restore option for tags the user has deleted.
463
+ // Deleted predefined tags section - show restore option for tags the user has deleted.
447
464
  const deletedItems = [];
448
465
  for (const tag of registry.deletedTags) {
449
466
  deletedItems.push("<div class=\"tag-manager-item tag-deleted\" data-tag=\"" + escapeHtml(tag) + "\">" +
@@ -545,7 +562,7 @@ function generateSetupWizardModal() {
545
562
  });
546
563
  }
547
564
  /**
548
- * Generates the HTML for a single channel's table rows (display row and edit form row). All channels — predefined, override, and user-defined — get both rows.
565
+ * Generates the HTML for a single channel's table rows (display row and edit form row). All channels - predefined, override, and user-defined - get both rows.
549
566
  * The edit form is pre-populated with the effective (resolved) values so users see what they're changing. When called from generateChannelsPanel() which already
550
567
  * has the listing entry, pass it via the entry parameter to avoid redundant getChannelListing() calls. POST handlers that generate a single row omit the
551
568
  * parameter to trigger an internal lookup.
@@ -571,7 +588,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
571
588
  const serviceGroup = getServiceGroup(key);
572
589
  // Build the service tags data attribute for client-side filtering.
573
590
  const serviceTags = getChannelServiceTags(key).join(",");
574
- // Generate display row. User-created channels and predefined overrides get distinct CSS classes — user-created channels have a subtle background tint
591
+ // Generate display row. User-created channels and predefined overrides get distinct CSS classes - user-created channels have a subtle background tint
575
592
  // ("user-channel"), while predefined overrides get a left-border accent and blue dot indicator via CSS ::before ("channel-override") matching the settings
576
593
  // form's visual language for "modified from defaults." Disabled and service-filtered rows get additional classes for opacity and visibility control.
577
594
  const displayLines = [];
@@ -590,7 +607,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
590
607
  }
591
608
  const rowClassAttr = (rowClasses.length > 0) ? " class=\"" + rowClasses.join(" ") + "\"" : "";
592
609
  const rowTitleAttr = isOverride ? " title=\"Customized from predefined defaults\"" : "";
593
- // Compute effective tags early — used for both the row data attribute (tag column filter) and the Tags column cell rendering below.
610
+ // Compute effective tags early - used for both the row data attribute (tag column filter) and the Tags column cell rendering below.
594
611
  const effectiveTags = getChannelEffectiveTags(channel);
595
612
  const channelTagsAttr = (effectiveTags.length > 0) ? " data-channel-tags=\"" + escapeHtml(effectiveTags.join(",")) + "\"" : "";
596
613
  displayLines.push("<tr id=\"display-row-" + escapeHtml(key) + "\"" + rowClassAttr + rowTitleAttr + " data-provider-tags=\"" + escapeHtml(serviceTags) +
@@ -635,7 +652,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
635
652
  "\" onclick=\"startInlineEdit(this)\">" + (channel.channelNumber ? escapeHtml(String(channel.channelNumber)) : "<span class=\"text-muted\">&ndash;</span>") +
636
653
  "</td>");
637
654
  // HDHR column: inline checkbox for quick toggling. The checkbox submits changes via the toggleHdhr() client-side function.
638
- const hdhrCheckedAttr = (channel.hdhrEnabled !== false) ? " checked" : "";
655
+ const hdhrCheckedAttr = getEffectiveHdhrEnabled(channel) ? " checked" : "";
639
656
  displayLines.push("<td class=\"col-hdhr\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "hdhrEnabled")) +
640
657
  "\"><input type=\"checkbox\" data-key=\"" + cellKey + "\"" + hdhrCheckedAttr +
641
658
  " onchange=\"toggleHdhr(this)\" title=\"Include in HDHomeRun/Plex lineup\"></td>");
@@ -704,7 +721,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
704
721
  displayLines.push("<span class=\"btn-icon-placeholder\"></span>");
705
722
  }
706
723
  // Position 4: enable/disable toggle for predefined channels (regardless of override state), delete for user-defined channels. The enable/disable state and
707
- // property overrides are independent concerns — a predefined channel can be disabled AND have customizations, and the toggle always reflects the visibility state.
724
+ // property overrides are independent concerns - a predefined channel can be disabled AND have customizations, and the toggle always reflects the visibility state.
708
725
  if (isPredefined) {
709
726
  if (isDisabled) {
710
727
  displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-enable\" title=\"Enable\" aria-label=\"Enable\" onclick=\"togglePredefinedChannel('" +
@@ -742,41 +759,71 @@ export function generateChannelRowHtml(key, profiles, entry) {
742
759
  editLines.push("<form id=\"edit-channel-form-" + escapedKey + "\" onsubmit=\"return submitChannelForm(event, 'edit')\">");
743
760
  editLines.push("<input type=\"hidden\" name=\"action\" value=\"edit\">");
744
761
  editLines.push("<input type=\"hidden\" name=\"key\" value=\"" + escapedKey + "\">");
745
- // For override channels, look up the predefined defaults so each field can show the "modified from defaults" indicator when its value differs. This applies
746
- // the same visual treatment used by the settings form — border accent, blue dot, and per-field reset button — so users can immediately see which fields they
747
- // customized and reset individual fields without reverting the entire channel.
748
- const predefined = isOverride ? PREDEFINED_CHANNELS[key] : undefined;
749
- const predefinedTags = predefined ? getChannelEffectiveTags(predefined).join(", ") : undefined;
762
+ // For override channels, fetch the customization provenance once. The accessor returns which fields the user has explicitly customized (across canonical and
763
+ // active-variant stored entries) and the per-field reset value (looked up from the appropriate predefined entry, with variant-inheritance fallback). This is
764
+ // the single source of truth for the modified-field treatment - it produces no false positives when service variants are resolved, because storage provenance,
765
+ // not value comparison, drives the determination.
766
+ //
767
+ // For non-override channels (predefined-only or pure user-defined), the customization concept does not apply: predefined-only channels have no user
768
+ // customizations to mark, and pure user channels have no predefined baseline to compare against. Both paths skip the accessor and pass undefined defaults.
769
+ const customizations = isOverride ? getChannelCustomizations(key).customizations : undefined;
770
+ const customizedFields = customizations ? new Set(customizations.keys()) : undefined;
771
+ // resetValueFor stringifies the raw resetValue from the accessor for use in input data-default attributes. Returns undefined when the field is not
772
+ // customized, signalling the field generators to skip data-default and the modified treatment entirely. Array values (tags) are joined with ", " to match
773
+ // the comma-separated form representation; primitive values are stringified normally; null/undefined become the empty string. Field parameter is typed as
774
+ // CustomizableField so the call sites below get compile-time validation that they reference real customizable fields.
775
+ const resetValueFor = (field) => {
776
+ const entry = customizations?.get(field);
777
+ if (!entry) {
778
+ return undefined;
779
+ }
780
+ const value = entry.resetValue;
781
+ if ((value === undefined) || (value === null)) {
782
+ return "";
783
+ }
784
+ if (Array.isArray(value)) {
785
+ return value.join(", ");
786
+ }
787
+ if ((typeof value === "string") || (typeof value === "number") || (typeof value === "boolean")) {
788
+ return String(value);
789
+ }
790
+ return "";
791
+ };
750
792
  // Channel name.
751
793
  editLines.push(...generateTextField("edit-name-" + key, "name", "Display Name", channel.name ?? key, {
752
- defaultValue: predefined ? (predefined.name ?? key) : undefined,
794
+ defaultValue: resetValueFor("name"),
753
795
  hint: "Friendly name shown in the playlist and UI.",
796
+ isCustomized: customizedFields?.has("name"),
754
797
  required: true
755
798
  }));
756
799
  // Channel URL.
757
800
  editLines.push(...generateTextField("edit-url-" + key, "url", "Stream URL", channel.url, {
758
- defaultValue: predefined?.url,
801
+ defaultValue: resetValueFor("url"),
759
802
  hint: "The URL of the streaming page to capture.",
803
+ isCustomized: customizedFields?.has("url"),
760
804
  required: true,
761
805
  type: "url"
762
806
  }));
763
807
  // Profile dropdown.
764
- editLines.push(...generateProfileDropdown("edit-profile-" + key, channel.profile ?? "", profiles, true, isOverride ? (predefined?.profile ?? "") : undefined));
765
- // Advanced fields.
808
+ editLines.push(...generateProfileDropdown("edit-profile-" + key, channel.profile ?? "", profiles, true, resetValueFor("profile"), customizedFields?.has("profile")));
809
+ // Advanced fields. Every reset value flows through resetValueFor, which delegates to getChannelCustomizations -> computeResetValue. The accessor applies the
810
+ // effective-view rules (vocabulary-filtered tags, implicit-true hdhrEnabled) and looks up the right predefined source (canonical or variant with inheritance
811
+ // fallback) so the form sees a uniformly stringified default for every field.
766
812
  editLines.push(...generateAdvancedFields("edit-" + key, {
767
813
  channelNumberValue: channel.channelNumber ? String(channel.channelNumber) : "",
768
814
  channelSelectorValue: channel.channelSelector ?? "",
769
- defaults: predefined ? {
770
- channelNumber: predefined.channelNumber ? String(predefined.channelNumber) : "",
771
- channelSelector: predefined.channelSelector ?? "",
772
- guideTitle: predefined.guideTitle ?? "",
773
- hdhrEnabled: predefined.hdhrEnabled !== false,
774
- logoUrl: predefined.logoUrl ?? "",
775
- stationId: predefined.stationId ?? "",
776
- tags: predefinedTags
815
+ customizedFields,
816
+ defaults: isOverride ? {
817
+ channelNumber: resetValueFor("channelNumber"),
818
+ channelSelector: resetValueFor("channelSelector"),
819
+ guideTitle: resetValueFor("guideTitle"),
820
+ hdhrEnabled: resetValueFor("hdhrEnabled"),
821
+ logoUrl: resetValueFor("logoUrl"),
822
+ stationId: resetValueFor("stationId"),
823
+ tags: resetValueFor("tags")
777
824
  } : undefined,
778
825
  guideTitleValue: channel.guideTitle ?? "",
779
- hdhrEnabled: channel.hdhrEnabled !== false,
826
+ hdhrEnabled: getEffectiveHdhrEnabled(channel),
780
827
  logoUrlValue: channel.logoUrl ?? "",
781
828
  stationIdValue: channel.stationId ?? "",
782
829
  tagsValue: effectiveTags.join(", ")
@@ -798,7 +845,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
798
845
  return { displayRow, editRow };
799
846
  }
800
847
  /**
801
- * Computes the complete channel table state — summary counts and scope toggle counts — from the current channel listing. This is the server-side single source
848
+ * Computes the complete channel table state - summary counts and scope toggle counts - from the current channel listing. This is the server-side single source
802
849
  * of truth for all count computation, replacing the client-side DOM-scanning approach. Called once per mutation as part of building a patch.
803
850
  * @param listing - Optional pre-fetched listing. When omitted, fetches from getChannelListing(). Passing the listing avoids a redundant computation when
804
851
  * buildChannelTablePatch already has it.
@@ -840,7 +887,7 @@ function getHdhrCounts(listing) {
840
887
  continue;
841
888
  }
842
889
  total++;
843
- if (entry.channel.hdhrEnabled !== false) {
890
+ if (getEffectiveHdhrEnabled(entry.channel)) {
844
891
  enabled++;
845
892
  }
846
893
  }
@@ -863,8 +910,6 @@ function getTagCounts(listing) {
863
910
  }
864
911
  const result = {};
865
912
  let total = 0;
866
- // Pre-compute the vocabulary Set once for the per-channel effective tags filter.
867
- const vocabularySet = new Set(vocabulary);
868
913
  const counts = new Map();
869
914
  for (const tag of vocabulary) {
870
915
  counts.set(tag, 0);
@@ -874,7 +919,9 @@ function getTagCounts(listing) {
874
919
  continue;
875
920
  }
876
921
  total++;
877
- const effectiveTags = entry.channel.tags ? entry.channel.tags.filter((tag) => vocabularySet.has(tag)) : [];
922
+ // Use the canonical helper for vocabulary-filtered tags. Inline filtering would diverge from the rest of the codebase on the case-insensitive matching
923
+ // policy that getChannelEffectiveTags applies via tagsMatch.
924
+ const effectiveTags = getChannelEffectiveTags(entry.channel);
878
925
  for (const tag of effectiveTags) {
879
926
  const current = counts.get(tag);
880
927
  if (current !== undefined) {
@@ -972,7 +1019,7 @@ export function generateServiceFilterToolbar() {
972
1019
  * @returns HTML string for the Channels panel content.
973
1020
  */
974
1021
  export function generateChannelsPanel(channelMessage, channelError, editingChannelKey, showAddForm, formErrors, formValues) {
975
- // Get the canonical channel listing (service variants already filtered out, sorted by key). This is the single source of truth for merged channel data —
1022
+ // Get the canonical channel listing (service variants already filtered out, sorted by key). This is the single source of truth for merged channel data -
976
1023
  // it handles predefined/user merging, disabled state, and service availability.
977
1024
  const listing = getChannelListing();
978
1025
  const profiles = getProfiles();
@@ -989,7 +1036,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
989
1036
  // operations.
990
1037
  lines.push("<div class=\"channel-toolbar\">");
991
1038
  lines.push("<div class=\"toolbar-group\">");
992
- // Manage Channels dropdown — primary channel creation and setup actions.
1039
+ // Manage Channels dropdown - primary channel creation and setup actions.
993
1040
  lines.push("<div class=\"dropdown\">");
994
1041
  lines.push("<button type=\"button\" class=\"btn btn-primary btn-sm toolbar-icon-btn\" title=\"Add, browse, or set up channels\" " +
995
1042
  "onclick=\"toggleDropdown(this)\">" + ICON_MANAGE + " Manage Channels &#9662;</button>");
@@ -1017,7 +1064,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1017
1064
  " Service Setup</div>");
1018
1065
  lines.push("</div>");
1019
1066
  lines.push("</div>");
1020
- // Import / Export dropdown — data I/O operations.
1067
+ // Import / Export dropdown - data I/O operations.
1021
1068
  lines.push("<div class=\"dropdown\">");
1022
1069
  lines.push("<button type=\"button\" class=\"btn btn-secondary btn-sm toolbar-icon-btn\" title=\"Import or export channel data\" " +
1023
1070
  "onclick=\"toggleDropdown(this)\">" + ICON_TRANSFER + " Import / Export &#9662;</button>");
@@ -1039,15 +1086,15 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1039
1086
  lines.push("<input type=\"file\" id=\"import-m3u-file\" accept=\".m3u,.m3u8\" style=\"display: none;\" onchange=\"importM3U(this)\">");
1040
1087
  // Visible column set, used by Quick Actions to gate column-dependent options and by the table header to set initial hide classes.
1041
1088
  const visibleCols = new Set(CONFIG.channels.visibleColumns);
1042
- // Quick Actions dropdown — bulk operations for predefined channels.
1089
+ // Quick Actions dropdown - bulk operations for predefined channels.
1043
1090
  lines.push("<div class=\"dropdown quick-actions-dropdown\">");
1044
1091
  lines.push("<button type=\"button\" class=\"btn btn-secondary btn-sm toolbar-icon-btn\" title=\"Bulk operations for predefined channels\" " +
1045
1092
  "onclick=\"toggleDropdown(this)\">" + ICON_BOLT + " Quick Actions &#9662;</button>");
1046
1093
  lines.push("<div class=\"dropdown-menu\">");
1047
- // Compute initial toggle counts for predefined channel scopes. The server is the single source of truth — the client renders what we return here.
1094
+ // Compute initial toggle counts for predefined channel scopes. The server is the single source of truth - the client renders what we return here.
1048
1095
  const scopeCounts = getPredefinedScopeCounts();
1049
1096
  // Three toggle rows: checkbox + label + count. Clicking toggles the group via bulkTogglePredefined(). The onclick uses event.preventDefault() to stop the
1050
- // native checkbox toggle — the server response drives the update.
1097
+ // native checkbox toggle - the server response drives the update.
1051
1098
  const scopes = [
1052
1099
  { count: scopeCounts.all.enabled, label: "All Predefined", scope: "all", total: scopeCounts.all.total },
1053
1100
  { count: scopeCounts.east.enabled, label: "East Variants", scope: "east", total: scopeCounts.east.total },
@@ -1061,7 +1108,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1061
1108
  "\">" + String(s.count) + " of " + String(s.total) + " enabled</span></label>");
1062
1109
  }
1063
1110
  lines.push("<div class=\"dropdown-divider\"></div>");
1064
- // Bulk assign select — switch all multi-service channels to a single service.
1111
+ // Bulk assign select - switch all multi-service channels to a single service.
1065
1112
  const allTags = getAllServiceTags();
1066
1113
  const enabled = getEnabledServices();
1067
1114
  const hasFilter = enabled.length > 0;
@@ -1080,7 +1127,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1080
1127
  // Divider before column-gated quick actions. Hidden when neither the channel number nor HDHR column is visible.
1081
1128
  const dividerVisible = (visibleCols.has("channelNumber") || visibleCols.has("hdhrEnabled")) ? "" : " style=\"display: none;\"";
1082
1129
  lines.push("<div class=\"dropdown-divider\" id=\"quick-action-divider\"" + dividerVisible + "></div>");
1083
- // Auto-number channels — assign sequential channel numbers to visible channels in current sort order. Visible when the channel number column is shown.
1130
+ // Auto-number channels - assign sequential channel numbers to visible channels in current sort order. Visible when the channel number column is shown.
1084
1131
  const autoNumberVisible = visibleCols.has("channelNumber") ? "" : " style=\"display: none;\"";
1085
1132
  lines.push("<div id=\"quick-action-autonumber\"" + autoNumberVisible + ">");
1086
1133
  lines.push("<div class=\"bulk-assign-row\">");
@@ -1090,7 +1137,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1090
1137
  lines.push("<button type=\"button\" class=\"btn btn-sm btn-secondary\" onclick=\"dropdowns.close(); autoNumberChannels()\">Apply</button>");
1091
1138
  lines.push("</div>");
1092
1139
  lines.push("</div>");
1093
- // HDHR bulk toggle — tri-state checkbox to enable/disable all channels for the HDHomeRun lineup. Visible when the HDHR column is shown.
1140
+ // HDHR bulk toggle - tri-state checkbox to enable/disable all channels for the HDHomeRun lineup. Visible when the HDHR column is shown.
1094
1141
  const hdhrVisible = visibleCols.has("hdhrEnabled") ? "" : " style=\"display: none;\"";
1095
1142
  const hdhrCounts = getHdhrCounts(listing);
1096
1143
  const hdhrCheckedAttr = (hdhrCounts.enabled === hdhrCounts.total) ? " checked" : "";
@@ -1102,13 +1149,13 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1102
1149
  lines.push("<span class=\"quick-action-count\" id=\"hdhr-bulk-count\">" + String(hdhrCounts.enabled) + " of " + String(hdhrCounts.total) + "</span>");
1103
1150
  lines.push("</label>");
1104
1151
  lines.push("</div>");
1105
- // Bulk tag toggles — tristate checkboxes for each tag in the active vocabulary. Visible when the tags column is shown. Each checkbox shows how many enabled
1152
+ // Bulk tag toggles - tristate checkboxes for each tag in the active vocabulary. Visible when the tags column is shown. Each checkbox shows how many enabled
1106
1153
  // channels have the tag, and clicking it adds or removes the tag on all visible channels.
1107
1154
  const tagsVisible = visibleCols.has("tags") ? "" : " style=\"display: none;\"";
1108
1155
  const tagsVocabulary = getActiveTagVocabulary();
1109
1156
  if (tagsVocabulary.length > 0) {
1110
1157
  // Count how many enabled, service-available channels have each tag.
1111
- const enabledListing = listing.filter((entry) => entry.enabled && entry.availableByService);
1158
+ const enabledListing = listing.filter(isVisibleChannel);
1112
1159
  const totalEnabled = enabledListing.length;
1113
1160
  const tagCountMap = new Map();
1114
1161
  for (const tag of tagsVocabulary) {
@@ -1243,7 +1290,8 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1243
1290
  lines.push("<button type=\"button\" class=\"btn btn-secondary\" onclick=\"document.getElementById('add-channel-form').style.display='none';\">Cancel</button>");
1244
1291
  lines.push("</div>");
1245
1292
  lines.push("</form>");
1246
- lines.push("</div>"); // End add-channel-form.
1293
+ // Close the add-channel-form wrapper opened at the top of this builder.
1294
+ lines.push("</div>");
1247
1295
  // Profile reference section (hidden by default, toggled via link in profile dropdown hint).
1248
1296
  lines.push(generateProfileReference(profiles));
1249
1297
  // Channels table. Disabled predefined channels are hidden by default and revealed via the "Show disabled" toggle. The wrapper div enables horizontal scrolling on
@@ -1279,7 +1327,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1279
1327
  ...OPTIONAL_COLUMNS
1280
1328
  ];
1281
1329
  // All sortable headers use the same DOM structure: <th onclick> wraps a <span class="sort-label"> for the label text. The sort update logic targets
1282
- // .sort-label to modify only the label — never touching other children like the Tags filter dropdown. Clicking anywhere on the <th> triggers sort;
1330
+ // .sort-label to modify only the label - never touching other children like the Tags filter dropdown. Clicking anywhere on the <th> triggers sort;
1283
1331
  // additional children (like the filter button) use event.stopPropagation() to prevent sort when interacting with them.
1284
1332
  for (const hdr of sortableHeaders) {
1285
1333
  const isActive = (sortField === hdr.field);
@@ -1288,7 +1336,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1288
1336
  "\" onclick=\"channelTable.sort('" + hdr.field + "')\">");
1289
1337
  lines.push("<span class=\"sort-label\">" + hdr.label + activeIndicator + "</span>");
1290
1338
  // Tags header: additional filter dropdown alongside the sort label. The dropdown is a client-side-only view filter (transient, not persisted) that
1291
- // shows/hides rows based on their data-channel-tags attribute. The dropdown content comes from generateTagFilterContent() — the single source of truth
1339
+ // shows/hides rows based on their data-channel-tags attribute. The dropdown content comes from generateTagFilterContent() - the single source of truth
1292
1340
  // shared with the tag CRUD incremental update path.
1293
1341
  if (hdr.field === "tags") {
1294
1342
  lines.push("<div class=\"dropdown tag-filter-dropdown\" style=\"display: inline;\">");
@@ -1350,7 +1398,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
1350
1398
  // Tag Management modal. A simple dialog for creating, deleting, and restoring organizational tags. Client-side handlers drive the CRUD operations via the
1351
1399
  // tag management API endpoints.
1352
1400
  lines.push(generateTagManagementModal());
1353
- // Inline tag edit portal — a shared dropdown portaled to <body> on first use by getTagPortal() in config.ts. One instance shared across all channel rows;
1401
+ // Inline tag edit portal - a shared dropdown portaled to <body> on first use by getTagPortal() in config.ts. One instance shared across all channel rows;
1354
1402
  // populated dynamically from the clicked cell's data-tags attribute. Positioned via getBoundingClientRect() to escape the table wrapper's overflow:auto clipping.
1355
1403
  const vocabulary = getActiveTagVocabulary();
1356
1404
  lines.push("<div id=\"inline-tag-portal\" class=\"dropdown-menu inline-tag-menu\">");