prismcast 1.8.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 (392) hide show
  1. package/README.md +6 -6
  2. package/dist/app.d.ts +1 -1
  3. package/dist/app.js +41 -13
  4. package/dist/app.js.map +1 -1
  5. package/dist/browser/cdp.d.ts +4 -6
  6. package/dist/browser/cdp.js +18 -22
  7. package/dist/browser/cdp.js.map +1 -1
  8. package/dist/browser/channelSelection.d.ts +41 -11
  9. package/dist/browser/channelSelection.js +75 -24
  10. package/dist/browser/channelSelection.js.map +1 -1
  11. package/dist/browser/display.d.ts +1 -1
  12. package/dist/browser/index.d.ts +8 -8
  13. package/dist/browser/index.js +44 -45
  14. package/dist/browser/index.js.map +1 -1
  15. package/dist/browser/login.d.ts +2 -2
  16. package/dist/browser/login.js +1 -1
  17. package/dist/browser/manifestInterceptor.d.ts +77 -0
  18. package/dist/browser/manifestInterceptor.js +323 -0
  19. package/dist/browser/manifestInterceptor.js.map +1 -0
  20. package/dist/browser/precaching.d.ts +1 -1
  21. package/dist/browser/precaching.js +23 -23
  22. package/dist/browser/precaching.js.map +1 -1
  23. package/dist/browser/tuning/comcastPolymer.d.ts +1 -1
  24. package/dist/browser/tuning/cox.d.ts +1 -1
  25. package/dist/browser/tuning/directv.d.ts +1 -1
  26. package/dist/browser/tuning/directv.js +65 -69
  27. package/dist/browser/tuning/directv.js.map +1 -1
  28. package/dist/browser/tuning/fox.d.ts +1 -1
  29. package/dist/browser/tuning/fox.js +172 -15
  30. package/dist/browser/tuning/fox.js.map +1 -1
  31. package/dist/browser/tuning/hbo.d.ts +1 -1
  32. package/dist/browser/tuning/hbo.js +7 -7
  33. package/dist/browser/tuning/hbo.js.map +1 -1
  34. package/dist/browser/tuning/hulu.d.ts +1 -1
  35. package/dist/browser/tuning/hulu.js +120 -113
  36. package/dist/browser/tuning/hulu.js.map +1 -1
  37. package/dist/browser/tuning/shared.d.ts +3 -3
  38. package/dist/browser/tuning/shared.js +5 -5
  39. package/dist/browser/tuning/sling.d.ts +1 -1
  40. package/dist/browser/tuning/sling.js +30 -30
  41. package/dist/browser/tuning/sling.js.map +1 -1
  42. package/dist/browser/tuning/spectrum.d.ts +1 -1
  43. package/dist/browser/tuning/spectrum.js +21 -22
  44. package/dist/browser/tuning/spectrum.js.map +1 -1
  45. package/dist/browser/tuning/thumbnailRow.d.ts +1 -1
  46. package/dist/browser/tuning/thumbnailRow.js +1 -1
  47. package/dist/browser/tuning/tileClick.d.ts +1 -1
  48. package/dist/browser/tuning/tileClick.js +5 -5
  49. package/dist/browser/tuning/xfinity.d.ts +1 -1
  50. package/dist/browser/tuning/youtubeTv.d.ts +1 -1
  51. package/dist/browser/tuning/youtubeTv.js +17 -18
  52. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  53. package/dist/browser/video.d.ts +20 -6
  54. package/dist/browser/video.js +30 -38
  55. package/dist/browser/video.js.map +1 -1
  56. package/dist/channels/index.d.ts +1 -2
  57. package/dist/channels/index.js +475 -442
  58. package/dist/channels/index.js.map +1 -1
  59. package/dist/config/channelForm.d.ts +58 -0
  60. package/dist/config/channelForm.js +129 -0
  61. package/dist/config/channelForm.js.map +1 -0
  62. package/dist/config/consistencyProbe.d.ts +7 -0
  63. package/dist/config/consistencyProbe.js +143 -0
  64. package/dist/config/consistencyProbe.js.map +1 -0
  65. package/dist/config/health.d.ts +7 -6
  66. package/dist/config/health.js +78 -65
  67. package/dist/config/health.js.map +1 -1
  68. package/dist/config/index.d.ts +1 -1
  69. package/dist/config/index.js +21 -14
  70. package/dist/config/index.js.map +1 -1
  71. package/dist/config/paths.d.ts +9 -3
  72. package/dist/config/paths.js +12 -4
  73. package/dist/config/paths.js.map +1 -1
  74. package/dist/config/persistence.context.d.ts +8 -0
  75. package/dist/config/persistence.context.js +30 -0
  76. package/dist/config/persistence.context.js.map +1 -0
  77. package/dist/config/persistence.d.ts +157 -0
  78. package/dist/config/persistence.js +446 -0
  79. package/dist/config/persistence.js.map +1 -0
  80. package/dist/config/presets.d.ts +14 -4
  81. package/dist/config/presets.js +84 -75
  82. package/dist/config/presets.js.map +1 -1
  83. package/dist/config/profiles.d.ts +2 -2
  84. package/dist/config/profiles.js +7 -9
  85. package/dist/config/profiles.js.map +1 -1
  86. package/dist/config/servicePacks.d.ts +46 -0
  87. package/dist/config/{providerPacks.js → servicePacks.js} +50 -53
  88. package/dist/config/servicePacks.js.map +1 -0
  89. package/dist/config/services.d.ts +228 -0
  90. package/dist/config/services.js +772 -0
  91. package/dist/config/services.js.map +1 -0
  92. package/dist/config/sites.d.ts +3 -3
  93. package/dist/config/sites.js +75 -75
  94. package/dist/config/sites.js.map +1 -1
  95. package/dist/config/userChannels.d.ts +522 -54
  96. package/dist/config/userChannels.js +1343 -366
  97. package/dist/config/userChannels.js.map +1 -1
  98. package/dist/config/userConfig.d.ts +108 -13
  99. package/dist/config/userConfig.js +296 -143
  100. package/dist/config/userConfig.js.map +1 -1
  101. package/dist/config/userProfiles.d.ts +34 -16
  102. package/dist/config/userProfiles.js +189 -100
  103. package/dist/config/userProfiles.js.map +1 -1
  104. package/dist/hdhr/channelMap.d.ts +1 -1
  105. package/dist/hdhr/channelMap.js +10 -16
  106. package/dist/hdhr/channelMap.js.map +1 -1
  107. package/dist/hdhr/deviceId.js +10 -9
  108. package/dist/hdhr/deviceId.js.map +1 -1
  109. package/dist/hdhr/discover.js +4 -5
  110. package/dist/hdhr/discover.js.map +1 -1
  111. package/dist/hdhr/index.js +12 -19
  112. package/dist/hdhr/index.js.map +1 -1
  113. package/dist/index.js +12 -12
  114. package/dist/index.js.map +1 -1
  115. package/dist/native/codecInference.d.ts +44 -0
  116. package/dist/native/codecInference.js +327 -0
  117. package/dist/native/codecInference.js.map +1 -0
  118. package/dist/native/decrypt.d.ts +1 -1
  119. package/dist/native/index.d.ts +5 -5
  120. package/dist/native/index.js +45 -67
  121. package/dist/native/index.js.map +1 -1
  122. package/dist/native/probe.d.ts +27 -9
  123. package/dist/native/probe.js +159 -68
  124. package/dist/native/probe.js.map +1 -1
  125. package/dist/native/proxy.d.ts +4 -2
  126. package/dist/native/proxy.js +56 -53
  127. package/dist/native/proxy.js.map +1 -1
  128. package/dist/native/tokenExpiry.d.ts +22 -0
  129. package/dist/native/tokenExpiry.js +153 -0
  130. package/dist/native/tokenExpiry.js.map +1 -0
  131. package/dist/routes/assets.js +3 -3
  132. package/dist/routes/assets.js.map +1 -1
  133. package/dist/routes/auth.js +9 -13
  134. package/dist/routes/auth.js.map +1 -1
  135. package/dist/routes/components.js +2 -2
  136. package/dist/routes/components.js.map +1 -1
  137. package/dist/routes/config/channels/endpoints/browse.d.ts +6 -0
  138. package/dist/routes/config/channels/endpoints/browse.js +195 -0
  139. package/dist/routes/config/channels/endpoints/browse.js.map +1 -0
  140. package/dist/routes/config/channels/endpoints/bulk.d.ts +6 -0
  141. package/dist/routes/config/channels/endpoints/bulk.js +128 -0
  142. package/dist/routes/config/channels/endpoints/bulk.js.map +1 -0
  143. package/dist/routes/config/channels/endpoints/crud.d.ts +6 -0
  144. package/dist/routes/config/channels/endpoints/crud.js +504 -0
  145. package/dist/routes/config/channels/endpoints/crud.js.map +1 -0
  146. package/dist/routes/config/channels/endpoints/importExport.d.ts +6 -0
  147. package/dist/routes/config/channels/endpoints/importExport.js +131 -0
  148. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -0
  149. package/dist/routes/config/channels/endpoints/predefined.d.ts +6 -0
  150. package/dist/routes/config/channels/endpoints/predefined.js +78 -0
  151. package/dist/routes/config/channels/endpoints/predefined.js.map +1 -0
  152. package/dist/routes/config/channels/endpoints/prefs.d.ts +6 -0
  153. package/dist/routes/config/channels/endpoints/prefs.js +58 -0
  154. package/dist/routes/config/channels/endpoints/prefs.js.map +1 -0
  155. package/dist/routes/config/channels/endpoints/service.d.ts +6 -0
  156. package/dist/routes/config/channels/endpoints/service.js +149 -0
  157. package/dist/routes/config/channels/endpoints/service.js.map +1 -0
  158. package/dist/routes/config/channels/endpoints/tags.d.ts +6 -0
  159. package/dist/routes/config/channels/endpoints/tags.js +185 -0
  160. package/dist/routes/config/channels/endpoints/tags.js.map +1 -0
  161. package/dist/routes/config/channels/http/handler.d.ts +10 -0
  162. package/dist/routes/config/channels/http/handler.js +20 -0
  163. package/dist/routes/config/channels/http/handler.js.map +1 -0
  164. package/dist/routes/config/channels/http/playlistHint.d.ts +25 -0
  165. package/dist/routes/config/channels/http/playlistHint.js +40 -0
  166. package/dist/routes/config/channels/http/playlistHint.js.map +1 -0
  167. package/dist/routes/config/channels/http/serviceWarning.d.ts +10 -0
  168. package/dist/routes/config/channels/http/serviceWarning.js +28 -0
  169. package/dist/routes/config/channels/http/serviceWarning.js.map +1 -0
  170. package/dist/routes/config/channels/index.d.ts +3 -3
  171. package/dist/routes/config/channels/index.js +2 -2
  172. package/dist/routes/config/channels/index.js.map +1 -1
  173. package/dist/routes/config/channels/{routes.d.ts → setup.d.ts} +1 -1
  174. package/dist/routes/config/channels/setup.js +23 -0
  175. package/dist/routes/config/channels/setup.js.map +1 -0
  176. package/dist/routes/config/channels/table.d.ts +13 -13
  177. package/dist/routes/config/channels/table.js +291 -184
  178. package/dist/routes/config/channels/table.js.map +1 -1
  179. package/dist/routes/config/http/envelope.d.ts +111 -0
  180. package/dist/routes/config/http/envelope.js +103 -0
  181. package/dist/routes/config/http/envelope.js.map +1 -0
  182. package/dist/routes/config/index.d.ts +9 -9
  183. package/dist/routes/config/index.js +5 -5
  184. package/dist/routes/config/index.js.map +1 -1
  185. package/dist/routes/config/{providers.js → services.js} +105 -99
  186. package/dist/routes/config/services.js.map +1 -0
  187. package/dist/routes/config/settings.d.ts +1 -1
  188. package/dist/routes/config/settings.js +85 -60
  189. package/dist/routes/config/settings.js.map +1 -1
  190. package/dist/routes/debug.js +17 -18
  191. package/dist/routes/debug.js.map +1 -1
  192. package/dist/routes/health.js +1 -1
  193. package/dist/routes/health.js.map +1 -1
  194. package/dist/routes/icons.js +2 -2
  195. package/dist/routes/icons.js.map +1 -1
  196. package/dist/routes/index.js +2 -2
  197. package/dist/routes/index.js.map +1 -1
  198. package/dist/routes/logs.js +23 -34
  199. package/dist/routes/logs.js.map +1 -1
  200. package/dist/routes/mpegts.js +1 -1
  201. package/dist/routes/playlist.d.ts +5 -5
  202. package/dist/routes/playlist.js +84 -61
  203. package/dist/routes/playlist.js.map +1 -1
  204. package/dist/routes/root/content.js +82 -61
  205. package/dist/routes/root/content.js.map +1 -1
  206. package/dist/routes/root/index.js +2 -5
  207. package/dist/routes/root/index.js.map +1 -1
  208. package/dist/routes/root/scripts/channels.js +538 -581
  209. package/dist/routes/root/scripts/channels.js.map +1 -1
  210. package/dist/routes/root/scripts/config.js +868 -1022
  211. package/dist/routes/root/scripts/config.js.map +1 -1
  212. package/dist/routes/root/scripts/index.d.ts +4 -4
  213. package/dist/routes/root/scripts/shared.js +445 -197
  214. package/dist/routes/root/scripts/shared.js.map +1 -1
  215. package/dist/routes/root/scripts/status.d.ts +4 -0
  216. package/dist/routes/root/scripts/status.handlers.d.ts +182 -0
  217. package/dist/routes/root/scripts/status.handlers.js +713 -0
  218. package/dist/routes/root/scripts/status.handlers.js.map +1 -0
  219. package/dist/routes/root/scripts/status.js +85 -436
  220. package/dist/routes/root/scripts/status.js.map +1 -1
  221. package/dist/routes/root/styles.js +16 -8
  222. package/dist/routes/root/styles.js.map +1 -1
  223. package/dist/routes/services.d.ts +6 -0
  224. package/dist/routes/{providers.js → services.js} +58 -55
  225. package/dist/routes/services.js.map +1 -0
  226. package/dist/routes/sse.d.ts +16 -0
  227. package/dist/routes/sse.js +29 -0
  228. package/dist/routes/sse.js.map +1 -0
  229. package/dist/routes/streams.js +10 -23
  230. package/dist/routes/streams.js.map +1 -1
  231. package/dist/routes/ui.d.ts +2 -2
  232. package/dist/routes/ui.js +2 -0
  233. package/dist/routes/ui.js.map +1 -1
  234. package/dist/routes/upgrade.js +6 -7
  235. package/dist/routes/upgrade.js.map +1 -1
  236. package/dist/service/commands.context.d.ts +6 -0
  237. package/dist/service/commands.context.js +47 -0
  238. package/dist/service/commands.context.js.map +1 -0
  239. package/dist/service/commands.d.ts +53 -8
  240. package/dist/service/commands.js +172 -192
  241. package/dist/service/commands.js.map +1 -1
  242. package/dist/service/generators.context.d.ts +6 -0
  243. package/dist/service/generators.context.js +45 -0
  244. package/dist/service/generators.context.js.map +1 -0
  245. package/dist/service/generators.d.ts +56 -16
  246. package/dist/service/generators.js +509 -291
  247. package/dist/service/generators.js.map +1 -1
  248. package/dist/service/index.d.ts +2 -2
  249. package/dist/streaming/clients.d.ts +2 -2
  250. package/dist/streaming/clients.js +2 -2
  251. package/dist/streaming/codec.d.ts +2 -2
  252. package/dist/streaming/codec.js +0 -1
  253. package/dist/streaming/codec.js.map +1 -1
  254. package/dist/streaming/fmp4Segmenter.d.ts +2 -2
  255. package/dist/streaming/fmp4Segmenter.js +12 -12
  256. package/dist/streaming/hls.d.ts +7 -7
  257. package/dist/streaming/hls.js +104 -92
  258. package/dist/streaming/hls.js.map +1 -1
  259. package/dist/streaming/hlsResume.d.ts +3 -3
  260. package/dist/streaming/hlsResume.js +6 -6
  261. package/dist/streaming/hlsResume.js.map +1 -1
  262. package/dist/streaming/hlsSegments.d.ts +5 -2
  263. package/dist/streaming/hlsSegments.js +14 -9
  264. package/dist/streaming/hlsSegments.js.map +1 -1
  265. package/dist/streaming/monitor.d.ts +12 -12
  266. package/dist/streaming/monitor.js +98 -66
  267. package/dist/streaming/monitor.js.map +1 -1
  268. package/dist/streaming/mp4Parser.d.ts +34 -4
  269. package/dist/streaming/mp4Parser.js +111 -29
  270. package/dist/streaming/mp4Parser.js.map +1 -1
  271. package/dist/streaming/mpegts.d.ts +1 -1
  272. package/dist/streaming/mpegts.js +13 -10
  273. package/dist/streaming/mpegts.js.map +1 -1
  274. package/dist/streaming/playlistBuilder.d.ts +1 -1
  275. package/dist/streaming/playlistBuilder.js +2 -2
  276. package/dist/streaming/preroll.d.ts +9 -9
  277. package/dist/streaming/preroll.js +40 -42
  278. package/dist/streaming/preroll.js.map +1 -1
  279. package/dist/streaming/pretune.js +4 -4
  280. package/dist/streaming/pretune.js.map +1 -1
  281. package/dist/streaming/recovery.d.ts +3 -3
  282. package/dist/streaming/recovery.js +4 -4
  283. package/dist/streaming/registry.d.ts +6 -6
  284. package/dist/streaming/registry.js +4 -8
  285. package/dist/streaming/registry.js.map +1 -1
  286. package/dist/streaming/setup.d.ts +11 -10
  287. package/dist/streaming/setup.js +120 -71
  288. package/dist/streaming/setup.js.map +1 -1
  289. package/dist/streaming/showInfo.d.ts +9 -3
  290. package/dist/streaming/showInfo.js +33 -27
  291. package/dist/streaming/showInfo.js.map +1 -1
  292. package/dist/streaming/statusEmitter.d.ts +8 -7
  293. package/dist/streaming/statusEmitter.js +5 -5
  294. package/dist/streaming/statusEmitter.js.map +1 -1
  295. package/dist/types/channels.d.ts +139 -36
  296. package/dist/types/channels.js +39 -1
  297. package/dist/types/channels.js.map +1 -1
  298. package/dist/types/config.d.ts +22 -3
  299. package/dist/types/index.d.ts +9 -8
  300. package/dist/types/index.js +1 -0
  301. package/dist/types/index.js.map +1 -1
  302. package/dist/types/profiles.d.ts +14 -13
  303. package/dist/types/selection.d.ts +49 -5
  304. package/dist/types/shared.d.ts +1 -1
  305. package/dist/types/streaming.d.ts +2 -2
  306. package/dist/upgrade/commands.context.d.ts +6 -0
  307. package/dist/upgrade/commands.context.js +29 -0
  308. package/dist/upgrade/commands.context.js.map +1 -0
  309. package/dist/upgrade/commands.d.ts +29 -2
  310. package/dist/upgrade/commands.js +87 -134
  311. package/dist/upgrade/commands.js.map +1 -1
  312. package/dist/upgrade/detection.context.d.ts +6 -0
  313. package/dist/upgrade/detection.context.js +24 -0
  314. package/dist/upgrade/detection.context.js.map +1 -0
  315. package/dist/upgrade/detection.d.ts +140 -10
  316. package/dist/upgrade/detection.js +153 -66
  317. package/dist/upgrade/detection.js.map +1 -1
  318. package/dist/upgrade/index.d.ts +2 -2
  319. package/dist/utils/chromeFetch.d.ts +1 -1
  320. package/dist/utils/chromeFetch.js +2 -2
  321. package/dist/utils/clock.d.ts +14 -0
  322. package/dist/utils/clock.js +28 -0
  323. package/dist/utils/clock.js.map +1 -0
  324. package/dist/utils/debugFilter.d.ts +1 -1
  325. package/dist/utils/debugFilter.js +8 -6
  326. package/dist/utils/debugFilter.js.map +1 -1
  327. package/dist/utils/delay.d.ts +2 -3
  328. package/dist/utils/delay.js +10 -16
  329. package/dist/utils/delay.js.map +1 -1
  330. package/dist/utils/evaluate.js +12 -14
  331. package/dist/utils/evaluate.js.map +1 -1
  332. package/dist/utils/ffmpeg.context.d.ts +6 -0
  333. package/dist/utils/ffmpeg.context.js +31 -0
  334. package/dist/utils/ffmpeg.context.js.map +1 -0
  335. package/dist/utils/ffmpeg.d.ts +85 -29
  336. package/dist/utils/ffmpeg.js +126 -129
  337. package/dist/utils/ffmpeg.js.map +1 -1
  338. package/dist/utils/fileLogger.d.ts +18 -2
  339. package/dist/utils/fileLogger.js +60 -31
  340. package/dist/utils/fileLogger.js.map +1 -1
  341. package/dist/utils/format.d.ts +7 -1
  342. package/dist/utils/format.js +22 -2
  343. package/dist/utils/format.js.map +1 -1
  344. package/dist/utils/html.js +1 -3
  345. package/dist/utils/html.js.map +1 -1
  346. package/dist/utils/index.d.ts +21 -20
  347. package/dist/utils/index.js +1 -0
  348. package/dist/utils/index.js.map +1 -1
  349. package/dist/utils/logger.js +31 -45
  350. package/dist/utils/logger.js.map +1 -1
  351. package/dist/utils/m3u.d.ts +18 -3
  352. package/dist/utils/m3u.js +28 -14
  353. package/dist/utils/m3u.js.map +1 -1
  354. package/dist/utils/memo.d.ts +19 -0
  355. package/dist/utils/memo.js +53 -0
  356. package/dist/utils/memo.js.map +1 -0
  357. package/dist/utils/morganStream.js +4 -4
  358. package/dist/utils/morganStream.js.map +1 -1
  359. package/dist/utils/pid.d.ts +2 -2
  360. package/dist/utils/pid.js +2 -2
  361. package/dist/utils/platform.d.ts +1 -1
  362. package/dist/utils/platform.js +3 -2
  363. package/dist/utils/platform.js.map +1 -1
  364. package/dist/utils/retry.d.ts +2 -0
  365. package/dist/utils/retry.js +9 -7
  366. package/dist/utils/retry.js.map +1 -1
  367. package/dist/utils/sanitize.d.ts +2 -2
  368. package/dist/utils/sanitize.js +4 -4
  369. package/dist/utils/streamContext.d.ts +7 -0
  370. package/dist/utils/streamContext.js +10 -1
  371. package/dist/utils/streamContext.js.map +1 -1
  372. package/dist/utils/timing.d.ts +6 -3
  373. package/dist/utils/timing.js +10 -6
  374. package/dist/utils/timing.js.map +1 -1
  375. package/dist/utils/version.d.ts +3 -2
  376. package/dist/utils/version.js +15 -28
  377. package/dist/utils/version.js.map +1 -1
  378. package/package.json +14 -10
  379. package/dist/config/providerPacks.d.ts +0 -46
  380. package/dist/config/providerPacks.js.map +0 -1
  381. package/dist/config/providers.d.ts +0 -175
  382. package/dist/config/providers.js +0 -691
  383. package/dist/config/providers.js.map +0 -1
  384. package/dist/native/intercept.d.ts +0 -35
  385. package/dist/native/intercept.js +0 -184
  386. package/dist/native/intercept.js.map +0 -1
  387. package/dist/routes/config/channels/routes.js +0 -1477
  388. package/dist/routes/config/channels/routes.js.map +0 -1
  389. package/dist/routes/config/providers.js.map +0 -1
  390. package/dist/routes/providers.d.ts +0 -6
  391. package/dist/routes/providers.js.map +0 -1
  392. /package/dist/routes/config/{providers.d.ts → services.d.ts} +0 -0
@@ -0,0 +1,713 @@
1
+ /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * status.handlers.ts: Pure handler logic for the PrismCast status display script.
4
+ *
5
+ * The architecture is ports-and-adapters with the same shape used across upgrade/, service/, utils/ffmpeg.ts, and utils/clock.ts. Every formatter, renderer, DOM
6
+ * mutator, SSE event handler, and lifecycle helper that the status display needs is defined here as a free-standing TypeScript function over a HandlerContext.
7
+ *
8
+ * - HandlerContext (this file) - the runtime-input port; the document, the mutable client state, and the window.*-resolved externals
9
+ * - ClientState (this file) - the mutable record of streamData / systemData / expandedStreams / staleness counters / rAF gates
10
+ * - ClientExternals (this file) - the readonly handle to sibling-script window.* APIs (channelTable, dropdowns, copyToClipboard, etc.)
11
+ * - HANDLER_CONSTANTS (this file) - the registry of script-side constants (health color CSS vars, label maps, row tints) emitted into the script body
12
+ * - HANDLER_FUNCTIONS (this file) - the registry of pure functions whose .toString() output is concatenated into the emitted script body
13
+ * - generateStatusScript (status.ts) - the boundary; the only place that builds strings, that wires window.*, and that constructs the EventSource
14
+ *
15
+ * Why this file exists. Free-standing handler functions are directly importable and callable from Node tests with synthetic context literals, which avoids the
16
+ * happy-dom EventSource gap that would otherwise force every SSE behavior test to run inside a synthetic DOM. The IIFE shell in status.ts is a thin glue layer;
17
+ * the test surface is this handler module.
18
+ *
19
+ * Single source of truth. Every function defined here is consumed twice - once by Node tests that import and call it directly, and once by the browser via
20
+ * Function.prototype.toString() concatenation in generateStatusScript(). The TypeScript function IS the source; there is no parallel hand-mirrored implementation
21
+ * to drift away from it. The constraint this places on function bodies is that they may reference only their parameters, browser globals (document, JSON, Date,
22
+ * Math, URL, Object, Number, requestAnimationFrame, setInterval, EventSource, window, CustomEvent), or sibling functions that are also emitted. No imports, no
23
+ * closures over module-scope TS variables, no Node-only APIs. The constants are emitted via JSON.stringify so handler bodies can reference them by their TS-side
24
+ * identifier.
25
+ */
26
+ /**
27
+ * Builds an empty client state. The IIFE calls this at startup; tests call it for fixtures. Defaults to "no streams, no system data, watchdog timestamps zeroed,
28
+ * rAF gates open." lastStatusEventTime is zeroed here; the IIFE reseeds it to Date.now() right before opening the EventSource so the staleness watchdog has a
29
+ * meaningful baseline. Tests that exercise the watchdog seed it explicitly.
30
+ */
31
+ export function createInitialState() {
32
+ return {
33
+ expandedStreams: {},
34
+ hiddenSince: 0,
35
+ lastStatusEventTime: 0,
36
+ popoverRenderPending: false,
37
+ streamData: {},
38
+ systemData: null,
39
+ tableRenderPending: false
40
+ };
41
+ }
42
+ // CSS-variable-backed color tokens for the health-state badge dot. The recovering/stalled/etc. literals are the discriminants the server emits. Typed as a wide
43
+ // string-record so script-side handlers can index by any health value with a `?? fallback` pattern (consistent with how the other lookup tables below are
44
+ // structured).
45
+ const healthColorVars = {
46
+ buffering: "var(--stream-buffering)",
47
+ error: "var(--stream-error)",
48
+ healthy: "var(--stream-healthy)",
49
+ recovering: "var(--stream-recovering)",
50
+ stalled: "var(--stream-stalled)"
51
+ };
52
+ // Static labels for each top-level health state. The recovering state delegates to getRecoveringLabel which dispatches on the escalation level.
53
+ const healthLabels = {
54
+ buffering: "Buffering",
55
+ error: "Error",
56
+ healthy: "Healthy",
57
+ stalled: "Stalled"
58
+ };
59
+ // Client-type display labels for the detail row's client breakdown. Hoisted to module scope so formatClients does not reallocate the map on every render.
60
+ const clientTypeLabels = {
61
+ hls: "HLS",
62
+ mpegts: "MPEG-TS"
63
+ };
64
+ // Native HLS variant resolution display labels. Hoisted so renderStreamsTable does not reallocate on every stream row.
65
+ const nativeResolutionLabels = {
66
+ "1080": "1080p",
67
+ "2160": "4K",
68
+ "360": "360p",
69
+ "480": "480p",
70
+ "720": "720p"
71
+ };
72
+ // Row background tints keyed by health state. Module-scope constant so renderStreamsTable reads from a single shared map instead of allocating a fresh object
73
+ // for every stream row. Consistent with clientTypeLabels and nativeResolutionLabels above - accessed inline at the call site, no wrapper helper.
74
+ const rowTints = {
75
+ buffering: "var(--stream-tint-buffering)",
76
+ error: "var(--stream-tint-error)",
77
+ healthy: "transparent",
78
+ recovering: "var(--stream-tint-recovering)",
79
+ stalled: "var(--stream-tint-stalled)"
80
+ };
81
+ /**
82
+ * The script-side constant registry. generateStatusScript() emits each entry as `const NAME = <JSON.stringify(value)>;`. Naming is the IIFE-side identifier
83
+ * (matches the closure references inside the function bodies below). Adding a new constant means appending here and using its name in handler bodies; nothing
84
+ * else changes.
85
+ */
86
+ export const HANDLER_CONSTANTS = [
87
+ { name: "clientTypeLabels", value: clientTypeLabels },
88
+ { name: "healthColorVars", value: healthColorVars },
89
+ { name: "healthLabels", value: healthLabels },
90
+ { name: "nativeResolutionLabels", value: nativeResolutionLabels },
91
+ { name: "rowTints", value: rowTints }
92
+ ];
93
+ // Pure formatters. Free of DOM and free of state; only depend on their parameters. Tests call them directly with literal inputs.
94
+ // Format a duration in seconds to a human-readable label. Threshold ladder: <60s -> seconds, <3600s -> minutes+seconds, otherwise hours+minutes.
95
+ function formatDuration(seconds) {
96
+ if (seconds < 60) {
97
+ return String(seconds) + "s";
98
+ }
99
+ if (seconds < 3600) {
100
+ return String(Math.floor(seconds / 60)) + "m " + String(seconds % 60) + "s";
101
+ }
102
+ const h = Math.floor(seconds / 3600);
103
+ const m = Math.floor((seconds % 3600) / 60);
104
+ return String(h) + "h " + String(m) + "m";
105
+ }
106
+ // Format a byte count to a human-readable label. KB/MB are 1024-based. Used for the per-stream segment-buffer memory line.
107
+ function formatBytes(bytes) {
108
+ if (bytes === 0) {
109
+ return "0 B";
110
+ }
111
+ if (bytes < 1024) {
112
+ return String(bytes) + " B";
113
+ }
114
+ if (bytes < 1048576) {
115
+ return (bytes / 1024).toFixed(1) + " KB";
116
+ }
117
+ return (bytes / 1048576).toFixed(1) + " MB";
118
+ }
119
+ // Format an absolute time (e.g., "6:54 AM" same-day, or "Jan 14, 6:54 AM" cross-day). 12-hour clock; no seconds.
120
+ function formatTime(isoString) {
121
+ const date = new Date(isoString);
122
+ const now = new Date();
123
+ let hours = date.getHours();
124
+ const minutes = date.getMinutes();
125
+ const ampm = hours >= 12 ? "PM" : "AM";
126
+ hours = hours % 12;
127
+ hours = hours ? hours : 12;
128
+ let timeStr = String(hours) + ":" + (minutes < 10 ? "0" : "") + String(minutes) + " " + ampm;
129
+ if (date.toDateString() !== now.toDateString()) {
130
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
131
+ timeStr = String(months[date.getMonth()]) + " " + String(date.getDate()) + ", " + timeStr;
132
+ }
133
+ return timeStr;
134
+ }
135
+ // Format a timestamp as "N {unit}s ago" relative to now. Used by the channel/domain health icons in the Channels tab to show how recent the last result was.
136
+ function formatTimeAgo(ts) {
137
+ const seconds = Math.floor((Date.now() - ts) / 1000);
138
+ if (seconds < 60) {
139
+ return "just now";
140
+ }
141
+ const minutes = Math.floor(seconds / 60);
142
+ if (minutes < 60) {
143
+ return String(minutes) + (minutes === 1 ? " minute ago" : " minutes ago");
144
+ }
145
+ const hours = Math.floor(minutes / 60);
146
+ if (hours < 24) {
147
+ return String(hours) + (hours === 1 ? " hour ago" : " hours ago");
148
+ }
149
+ const days = Math.floor(hours / 24);
150
+ return String(days) + (days === 1 ? " day ago" : " days ago");
151
+ }
152
+ // Extract a concise display domain from a URL (last two hostname parts). Mirrors the server-side extractDomain() in utils/format.ts so the popover label and the
153
+ // table fallback agree on what "the domain" means.
154
+ function getDomain(url) {
155
+ const parsed = URL.parse(url);
156
+ if (!parsed) {
157
+ return url;
158
+ }
159
+ const parts = parsed.hostname.split(".");
160
+ return (parts.length > 2) ? parts.slice(-2).join(".") : parts.join(".");
161
+ }
162
+ // Resolve the recovery-level label for a stream in the recovering state. Kept as a dedicated helper because the mapping is level-based, not string-based, so it
163
+ // cannot fold into the same lookup map as the top-level health labels. Escalation level semantics defined in monitor.ts: L1=play/unmute, L2=seek, L3=source
164
+ // reload, L4=page navigation.
165
+ function getRecoveringLabel(level) {
166
+ switch (level) {
167
+ case 1: return "Resuming playback";
168
+ case 2: return "Syncing to live";
169
+ case 3: return "Reloading player";
170
+ default: return (level >= 4) ? "Reloading page" : "Recovering";
171
+ }
172
+ }
173
+ // Get health badge HTML using CSS variables for theme-aware colors.
174
+ function getHealthBadge(health, level) {
175
+ const label = (health === "recovering") ? getRecoveringLabel(level) : (healthLabels[health] ?? health);
176
+ return "<span class=\"status-dot\" style=\"color: " + (healthColorVars[health] ?? "var(--text-muted)") + ";\">&#9679;</span> " +
177
+ "<span style=\"color: var(--text-secondary);\">" + label + "</span>";
178
+ }
179
+ // Format the per-stream "last issue" line for the detail panel. Returns "None" for streams that have not yet hit a health event; otherwise an "{Issue} at {time}
180
+ // ({status})" form where status is "(recovered)" if currently healthy or "(recovering)" otherwise.
181
+ function formatLastIssue(s) {
182
+ if (!s.lastIssueType || !s.lastIssueTime) {
183
+ return "None";
184
+ }
185
+ const issueLabel = s.lastIssueType.charAt(0).toUpperCase() + s.lastIssueType.slice(1);
186
+ const timeStr = formatTime(new Date(s.lastIssueTime).toISOString());
187
+ const status = (s.health === "healthy") ? " (recovered)" : " (recovering)";
188
+ return issueLabel + " at " + timeStr + status;
189
+ }
190
+ // Format the per-stream "auto-recovery" line for the detail panel. "N/A" when no attempts have happened; otherwise a count and an optional page-reload tail.
191
+ function formatAutoRecovery(s) {
192
+ const attempts = s.recoveryAttempts;
193
+ const reloads = s.pageReloadsInWindow;
194
+ if (attempts === 0) {
195
+ return "N/A";
196
+ }
197
+ let str = String(attempts) + (attempts === 1 ? " attempt" : " attempts");
198
+ if (reloads > 0) {
199
+ str += ", " + String(reloads) + (reloads === 1 ? " page reload" : " page reloads");
200
+ }
201
+ return str;
202
+ }
203
+ // Format the client breakdown for the detail row. "None" when the stream has zero clients; otherwise "{count} {type}" entries joined by commas.
204
+ function formatClients(s) {
205
+ if (s.clientCount === 0) {
206
+ return "None";
207
+ }
208
+ const parts = [];
209
+ for (const c of s.clients) {
210
+ parts.push(String(c.count) + " " + (clientTypeLabels[c.type] ?? c.type));
211
+ }
212
+ return parts.join(", ");
213
+ }
214
+ // Health cell HTML: client-count indicator (when clients > 0) followed by the health badge. Shared between the full-table render and the targeted-row update so
215
+ // the two paths always produce identical health cells.
216
+ function renderHealthCellContent(s) {
217
+ let clientIndicator = "";
218
+ if (s.clientCount > 0) {
219
+ const title = String(s.clientCount) + (s.clientCount !== 1 ? " clients" : " client");
220
+ clientIndicator = "<span class=\"client-count\" title=\"" + title + "\">&#9673; " + String(s.clientCount) + "</span> ";
221
+ }
222
+ return clientIndicator + getHealthBadge(s.health, s.escalationLevel ?? 0);
223
+ }
224
+ // Detail panel codec line: "{Codec} ({Mode}){suffix}" where suffix carries native-HLS bandwidth and resolution if present. Hardware-accelerated codecs prefix
225
+ // the lightning bolt; native HLS without a captureCodec fills "Native HLS" instead.
226
+ function renderDetailCodec(s) {
227
+ const codecLabel = s.captureCodec ? (s.hardwareAccelerated ? "⚡ " + s.captureCodec : s.captureCodec) :
228
+ (s.streamingMode === "native" ? "Native HLS" : "Unknown");
229
+ const modeLabel = s.streamingMode === "native" ? "Native HLS" : "Capture";
230
+ let qualitySuffix = "";
231
+ if (s.streamingMode === "native") {
232
+ const qParts = [];
233
+ if ((s.nativeBandwidth ?? 0) > 0) {
234
+ qParts.push(((s.nativeBandwidth ?? 0) / 1000000).toFixed(1) + "Mbps");
235
+ }
236
+ if (s.nativeResolution) {
237
+ const h = s.nativeResolution.split("x")[1];
238
+ qParts.push((h ? nativeResolutionLabels[h] : undefined) ?? s.nativeResolution);
239
+ }
240
+ if (qParts.length > 0) {
241
+ qualitySuffix = " - " + qParts.join(" ");
242
+ }
243
+ }
244
+ return "<strong>Codec:</strong> " + codecLabel + " (" + modeLabel + ")" + qualitySuffix;
245
+ }
246
+ // Detail panel "Started:" line with optional "(N HLS, M MPEG-TS)" client suffix when clients > 0.
247
+ function renderDetailStarted(s) {
248
+ const clientSuffix = s.clientCount > 0 ? " &middot; " + formatClients(s) : "";
249
+ return "<strong>Started:</strong> " + formatTime(s.startTime) + clientSuffix;
250
+ }
251
+ // DOM mutators. Each takes a HandlerContext (and any per-call inputs) and reads/writes ctx.document and ctx.state. Tests call them directly against happy-dom
252
+ // or stub documents.
253
+ // Update the system-status display in the page header. Shows browser-connected status (green dot when connected, red dot + "Browser offline" when not) plus a
254
+ // stream count summary.
255
+ function updateSystemStatus(ctx) {
256
+ if (!ctx.state.systemData) {
257
+ return;
258
+ }
259
+ const healthEl = ctx.document.getElementById("system-health");
260
+ const streamEl = ctx.document.getElementById("stream-count");
261
+ if (!healthEl || !streamEl) {
262
+ return;
263
+ }
264
+ if (ctx.state.systemData.browser.connected) {
265
+ healthEl.innerHTML = "<span class=\"status-dot\" style=\"color: var(--stream-healthy);\">&#9679;</span>";
266
+ }
267
+ else {
268
+ healthEl.innerHTML = "<span class=\"status-dot\" style=\"color: var(--stream-error);\">&#9679;</span> Browser offline";
269
+ }
270
+ const active = ctx.state.systemData.streams.active;
271
+ const limit = ctx.state.systemData.streams.limit;
272
+ if (active === 0) {
273
+ streamEl.textContent = "0 streams";
274
+ streamEl.classList.remove("clickable");
275
+ const popMenu = ctx.document.getElementById("stream-popover-menu");
276
+ if (popMenu) {
277
+ popMenu.classList.remove("show");
278
+ }
279
+ }
280
+ else {
281
+ streamEl.textContent = String(active) + "/" + String(limit) + " streams";
282
+ streamEl.classList.add("clickable");
283
+ }
284
+ }
285
+ // Build the stream popover content from streamData. Populates the supplied menu element with one row per active stream.
286
+ function buildStreamPopoverContent(menu, ctx) {
287
+ let html = "";
288
+ const now = Date.now();
289
+ for (const s of Object.values(ctx.state.streamData)) {
290
+ // The fallback chain uses || (not ??) so empty-string channel/serviceName values fall through to getDomain. The server may emit an empty string when the
291
+ // channel is not yet identified.
292
+ const color = healthColorVars[s.health] ?? "var(--text-muted)";
293
+ const name = (s.channel ?? "") || (s.serviceName ?? "") || getDomain(s.url);
294
+ const dur = Math.floor((now - new Date(s.startTime).getTime()) / 1000);
295
+ const hwBadge = s.hardwareAccelerated ? " <span title=\"Hardware accelerated\">⚡</span>" : "";
296
+ const showSuffix = s.showName ? " <span class=\"stream-popover-show\">" + s.showName + "</span>" : "";
297
+ html += "<div class=\"stream-popover-row\">";
298
+ html += "<span class=\"status-dot\" style=\"color: " + color + ";\">&#9679;</span>";
299
+ html += ctx.externals.channelDisplayHtml(s.logoUrl, name, "stream-popover-logo", "stream-popover-channel");
300
+ html += hwBadge + showSuffix;
301
+ html += "<span class=\"stream-popover-duration\">" + formatDuration(dur) + "</span>";
302
+ html += "</div>";
303
+ }
304
+ menu.innerHTML = html;
305
+ }
306
+ // Refresh an already-open stream popover with current data. No-op if the popover is closed or there are no streams (in which case it auto-closes).
307
+ function updateStreamPopover(ctx) {
308
+ const menu = ctx.document.getElementById("stream-popover-menu");
309
+ if (!menu?.classList.contains("show")) {
310
+ return;
311
+ }
312
+ const ids = Object.keys(ctx.state.streamData);
313
+ if (ids.length === 0) {
314
+ menu.classList.remove("show");
315
+ return;
316
+ }
317
+ buildStreamPopoverContent(menu, ctx);
318
+ }
319
+ // Render scheduling. The DOM-writing render functions funnel through requestAnimationFrame gates so multiple SSE events arriving within the same frame produce a
320
+ // single DOM write instead of redundant back-to-back rebuilds that destroy and recreate image elements.
321
+ // Schedule a full-table render on the next animation frame. Idempotent within a frame - repeated calls collapse into one render.
322
+ function scheduleTableRender(ctx) {
323
+ if (!ctx.state.tableRenderPending) {
324
+ ctx.state.tableRenderPending = true;
325
+ requestAnimationFrame(() => {
326
+ ctx.state.tableRenderPending = false;
327
+ renderStreamsTable(ctx);
328
+ });
329
+ }
330
+ }
331
+ // Schedule a popover refresh on the next animation frame. Idempotent within a frame.
332
+ function schedulePopoverRender(ctx) {
333
+ if (!ctx.state.popoverRenderPending) {
334
+ ctx.state.popoverRenderPending = true;
335
+ requestAnimationFrame(() => {
336
+ ctx.state.popoverRenderPending = false;
337
+ updateStreamPopover(ctx);
338
+ });
339
+ }
340
+ }
341
+ // Render the full streams table. Used by the snapshot/streamAdded/streamRemoved handlers and as the fallback when updateStreamRow cannot find an existing row.
342
+ function renderStreamsTable(ctx) {
343
+ const tbody = ctx.document.getElementById("streams-tbody");
344
+ if (!tbody) {
345
+ return;
346
+ }
347
+ const entries = Object.entries(ctx.state.streamData);
348
+ if (entries.length === 0) {
349
+ tbody.innerHTML = "<tr class=\"empty-row\"><td colspan=\"4\">No active streams</td></tr>";
350
+ return;
351
+ }
352
+ let html = "";
353
+ for (const [id, s] of entries) {
354
+ const isExpanded = ctx.state.expandedStreams[id];
355
+ const chevron = isExpanded ? "&#9660;" : "&#9654;";
356
+ const rowTint = rowTints[s.health] ?? "transparent";
357
+ // The channel/serviceName fallback uses || semantics so empty strings (server may emit them when identification is pending) fall through to the domain.
358
+ const channelText = (s.channel ?? "") || (s.serviceName ?? "") || getDomain(s.url);
359
+ const channelDisplay = ctx.externals.channelDisplayHtml(s.logoUrl, channelText, "channel-logo", "channel-text");
360
+ html += "<tr class=\"stream-row\" data-id=\"" + id + "\" onclick=\"toggleStreamDetails(" + id + ")\" style=\"background-color: " + rowTint + ";\">";
361
+ html += "<td class=\"chevron\">" + chevron + "</td>";
362
+ const hwIcon = s.hardwareAccelerated ? "⚡ " : "";
363
+ let nativeBadge = "";
364
+ if (s.streamingMode === "native") {
365
+ nativeBadge = " <span class=\"native-badge\" title=\"Native HLS\">Native</span>";
366
+ }
367
+ else if (s.hardwareAccelerated) {
368
+ nativeBadge = " <span class=\"native-badge\" title=\"Hardware accelerated\">" + hwIcon + (s.captureCodec ?? "") + "</span>";
369
+ }
370
+ const durationSpan = "<span class=\"stream-duration\" id=\"duration-" + id + "\">· " + formatDuration(s.duration) + "</span>";
371
+ html += "<td class=\"stream-info\">" + channelDisplay + nativeBadge + " " + durationSpan + "</td>";
372
+ const showDisplay = s.showName ?? "";
373
+ html += "<td class=\"stream-show\">" + showDisplay + "</td>";
374
+ html += "<td class=\"stream-health\">" + renderHealthCellContent(s) + "</td>";
375
+ html += "</tr>";
376
+ if (isExpanded) {
377
+ html += "<tr class=\"stream-details\" data-id=\"" + id + "\">";
378
+ html += "<td colspan=\"4\">";
379
+ html += "<div class=\"details-content\">";
380
+ html += "<div class=\"details-header\">";
381
+ html += "<div class=\"details-url\">" + s.url + "</div>";
382
+ html += "<div class=\"details-started\">" + renderDetailStarted(s) + "</div>";
383
+ html += "</div>";
384
+ html += "<div class=\"details-metrics\">";
385
+ html += "<div class=\"details-codec\">" + renderDetailCodec(s) + "</div>";
386
+ html += "<div class=\"details-issue\"><strong>Last issue:</strong> " + formatLastIssue(s) + "</div>";
387
+ html += "<div class=\"details-recovery\"><strong>Recovery:</strong> " + formatAutoRecovery(s) + "</div>";
388
+ html += "<div class=\"details-memory\"><strong>Memory:</strong> " + formatBytes(s.memoryBytes) + "</div>";
389
+ html += "</div>";
390
+ html += "</div>";
391
+ html += "</td></tr>";
392
+ }
393
+ }
394
+ tbody.innerHTML = html;
395
+ }
396
+ // Targeted update for a single stream row. Updates only the cells that change between health ticks (health badge, show name, client count, row tint, and detail
397
+ // panel metrics if expanded). Leaves the logo, channel name, badge, and structural elements untouched so image elements are never destroyed and recreated. Falls
398
+ // back to a full table render if the row does not exist yet (e.g., race between streamAdded and streamHealthChanged).
399
+ function updateStreamRow(s, ctx) {
400
+ const row = ctx.document.querySelector(".stream-row[data-id=\"" + String(s.id) + "\"]");
401
+ if (!row) {
402
+ scheduleTableRender(ctx);
403
+ return;
404
+ }
405
+ row.style.backgroundColor = rowTints[s.health] ?? "transparent";
406
+ const healthCell = row.querySelector(".stream-health");
407
+ if (healthCell) {
408
+ healthCell.innerHTML = renderHealthCellContent(s);
409
+ }
410
+ const showCell = row.querySelector(".stream-show");
411
+ if (showCell) {
412
+ showCell.textContent = s.showName ?? "";
413
+ }
414
+ if (ctx.state.expandedStreams[s.id]) {
415
+ const detailRow = ctx.document.querySelector(".stream-details[data-id=\"" + String(s.id) + "\"]");
416
+ if (detailRow) {
417
+ const issueEl = detailRow.querySelector(".details-issue");
418
+ if (issueEl) {
419
+ issueEl.innerHTML = "<strong>Last issue:</strong> " + formatLastIssue(s);
420
+ }
421
+ const recoveryEl = detailRow.querySelector(".details-recovery");
422
+ if (recoveryEl) {
423
+ recoveryEl.innerHTML = "<strong>Recovery:</strong> " + formatAutoRecovery(s);
424
+ }
425
+ const memoryEl = detailRow.querySelector(".details-memory");
426
+ if (memoryEl) {
427
+ memoryEl.innerHTML = "<strong>Memory:</strong> " + formatBytes(s.memoryBytes);
428
+ }
429
+ const codecEl = detailRow.querySelector(".details-codec");
430
+ if (codecEl) {
431
+ codecEl.innerHTML = renderDetailCodec(s);
432
+ }
433
+ const startedEl = detailRow.querySelector(".details-started");
434
+ if (startedEl) {
435
+ startedEl.innerHTML = renderDetailStarted(s);
436
+ }
437
+ }
438
+ }
439
+ }
440
+ // Toggle the expanded/collapsed state of a stream's detail panel. Triggered by inline onclick from the table row; the IIFE exposes a window-bound trampoline.
441
+ function toggleStreamDetails(id, ctx) {
442
+ ctx.state.expandedStreams[id] = !ctx.state.expandedStreams[id];
443
+ renderStreamsTable(ctx);
444
+ }
445
+ // Refresh all stream-duration cells. Computed from the immutable startTime so the displayed duration is always accurate regardless of any staleness in
446
+ // server-sent updates. Also schedules a popover refresh so the popover's duration column ticks alongside the table.
447
+ function updateDurations(ctx) {
448
+ const now = Date.now();
449
+ for (const [id, s] of Object.entries(ctx.state.streamData)) {
450
+ const durationSec = Math.floor((now - new Date(s.startTime).getTime()) / 1000);
451
+ const el = ctx.document.getElementById("duration-" + id);
452
+ if (el) {
453
+ el.textContent = "· " + formatDuration(durationSec);
454
+ }
455
+ }
456
+ schedulePopoverRender(ctx);
457
+ }
458
+ // Update the per-channel health icon in the Channels tab. Domain match check guards against stale events from a prior service binding - the login button carries
459
+ // a data-auth-domain attribute that must agree with the event's domain (when supplied) for the update to apply.
460
+ function updateChannelHealth(channelKey, status, timestamp, domain, ctx) {
461
+ const row = ctx.document.getElementById("display-row-" + channelKey);
462
+ if (!row) {
463
+ return;
464
+ }
465
+ if (domain) {
466
+ const loginBtn = row.querySelector(".btn-icon-login");
467
+ if (loginBtn && (loginBtn.getAttribute("data-auth-domain") !== domain)) {
468
+ return;
469
+ }
470
+ }
471
+ const icon = row.querySelector(".btn-icon-health");
472
+ if (!icon) {
473
+ return;
474
+ }
475
+ icon.classList.remove("health-success", "health-failed");
476
+ if (status === "success") {
477
+ icon.classList.add("health-success");
478
+ }
479
+ else {
480
+ icon.classList.add("health-failed");
481
+ }
482
+ icon.title = (status === "success" ? "Succeeded " : "Failed ") + formatTimeAgo(timestamp);
483
+ }
484
+ // Mark every login button bound to the given domain as verified, with a "Verified {time-ago}" tooltip. Called for successful health events and for snapshot
485
+ // domain entries.
486
+ function updateDomainAuth(domain, timestamp, ctx) {
487
+ const buttons = ctx.document.querySelectorAll(".btn-icon-login[data-auth-domain=\"" + domain + "\"]");
488
+ for (const button of Array.from(buttons)) {
489
+ button.classList.add("health-success");
490
+ button.title = "Verified " + formatTimeAgo(timestamp);
491
+ }
492
+ }
493
+ // Apply a full health snapshot. Walks the channels map then the domains map, applying the per-key updaters for each entry.
494
+ function applyHealthSnapshot(data, ctx) {
495
+ for (const [channelKey, entry] of Object.entries(data.channels)) {
496
+ updateChannelHealth(channelKey, entry.status, entry.timestamp, entry.domain, ctx);
497
+ }
498
+ for (const [domain, timestamp] of Object.entries(data.domains)) {
499
+ updateDomainAuth(domain, timestamp, ctx);
500
+ }
501
+ }
502
+ // SSE handlers. Each takes the parsed event payload and the HandlerContext. The IIFE wires JSON.parse and event-listener registration; the handlers themselves
503
+ // are pure logic.
504
+ // Snapshot handler. Replaces all client state with the snapshot's contents and re-renders everything, then applies the optional health snapshot.
505
+ function handleSnapshot(data, ctx) {
506
+ ctx.state.systemData = data.system;
507
+ ctx.state.streamData = {};
508
+ for (const stream of data.streams) {
509
+ ctx.state.streamData[stream.id] = stream;
510
+ }
511
+ updateSystemStatus(ctx);
512
+ renderStreamsTable(ctx);
513
+ updateStreamPopover(ctx);
514
+ if (data.health) {
515
+ applyHealthSnapshot(data.health, ctx);
516
+ }
517
+ }
518
+ // streamAdded handler. Inserts the new stream into streamData and triggers a full table render plus popover refresh.
519
+ function handleStreamAdded(data, ctx) {
520
+ ctx.state.streamData[data.id] = data;
521
+ renderStreamsTable(ctx);
522
+ updateStreamPopover(ctx);
523
+ }
524
+ // streamRemoved handler. Drops the stream from streamData and expandedStreams, re-renders, and invokes the deferred-restart callback if config.ts has registered
525
+ // one. The optional ?.() form is used because the callback may not be registered yet when this handler fires.
526
+ function handleStreamRemoved(data, ctx) {
527
+ // The dynamic-delete lint rule fires because TS cannot statically verify the key exists. Here the key is a stream id that may or may not be present in the
528
+ // record, so the delete is intentionally tolerant. eslint-disable on these two lines is the right call; the alternatives (Map<string, T>,
529
+ // Record<string, T | undefined>) would either change the on-the-wire data shape or leave dead "undefined" entries that Object.values would still iterate.
530
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
531
+ delete ctx.state.streamData[data.id];
532
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
533
+ delete ctx.state.expandedStreams[data.id];
534
+ renderStreamsTable(ctx);
535
+ updateStreamPopover(ctx);
536
+ ctx.externals.updateRestartDialogStatus?.();
537
+ }
538
+ // streamHealthChanged handler. Replaces the stream's record and chooses between a targeted row update (cheap, image-preserving) and a full table render
539
+ // (structural change). Also schedules a popover refresh so the duration/badge columns tick.
540
+ function handleStreamHealthChanged(data, ctx) {
541
+ const prev = ctx.state.streamData[data.id];
542
+ if (!prev) {
543
+ return;
544
+ }
545
+ const structuralChange = (prev.logoUrl !== data.logoUrl) || (prev.streamingMode !== data.streamingMode) ||
546
+ (prev.hardwareAccelerated !== data.hardwareAccelerated) || (prev.captureCodec !== data.captureCodec);
547
+ ctx.state.streamData[data.id] = data;
548
+ if (structuralChange) {
549
+ scheduleTableRender(ctx);
550
+ }
551
+ else {
552
+ updateStreamRow(data, ctx);
553
+ }
554
+ schedulePopoverRender(ctx);
555
+ }
556
+ // systemStatusChanged handler. Replaces systemData and refreshes the header.
557
+ function handleSystemStatusChanged(data, ctx) {
558
+ ctx.state.systemData = data;
559
+ updateSystemStatus(ctx);
560
+ }
561
+ // healthChanged handler. Targeted update for a single channel; if the result is success, also refreshes the domain-auth icons for that domain.
562
+ function handleHealthChanged(data, ctx) {
563
+ updateChannelHealth(data.channelKey, data.status, data.timestamp, data.domain, ctx);
564
+ if ((data.status === "success") && data.domain) {
565
+ updateDomainAuth(data.domain, data.timestamp, ctx);
566
+ }
567
+ }
568
+ // channelUpdate handler. Forwards the patch to the shared channelTable namespace exposed by shared.ts.
569
+ function handleChannelUpdate(data, ctx) {
570
+ ctx.externals.channelTable.applyPatch(data);
571
+ }
572
+ // onerror handler for the EventSource. Replaces the system-health badge with a stalled-color dot and "Updates paused" label so operators see the connection
573
+ // problem immediately.
574
+ function handleSseError(ctx) {
575
+ const el = ctx.document.getElementById("system-health");
576
+ if (el) {
577
+ el.innerHTML = "<span class=\"status-dot\" style=\"color: var(--stream-stalled);\">&#9679;</span> Updates paused";
578
+ }
579
+ }
580
+ // Window-bound trampolines and lifecycle helpers. These are invoked from the IIFE either via window.* assignment or directly.
581
+ // Toggle the stream popover open/closed. Wired to window.toggleStreamPopover by the IIFE so the header button's onclick can reach it.
582
+ function toggleStreamPopover(ctx) {
583
+ const ids = Object.keys(ctx.state.streamData);
584
+ if (ids.length === 0) {
585
+ return;
586
+ }
587
+ const menu = ctx.document.getElementById("stream-popover-menu");
588
+ if (!menu) {
589
+ return;
590
+ }
591
+ const isOpen = menu.classList.contains("show");
592
+ ctx.externals.dropdowns.close();
593
+ if (!isOpen) {
594
+ buildStreamPopoverContent(menu, ctx);
595
+ menu.classList.add("show");
596
+ }
597
+ }
598
+ // Copy the Quick Start playlist URL to the clipboard. Wired to window.copyOverviewPlaylistUrl by the IIFE so the Copy button's onclick can reach it.
599
+ function copyOverviewPlaylistUrl(ctx) {
600
+ const urlEl = ctx.document.getElementById("overview-playlist-url");
601
+ if (urlEl) {
602
+ ctx.externals.copyToClipboard(urlEl.textContent, "Playlist URL copied to clipboard.");
603
+ }
604
+ }
605
+ // visibilitychange handler. When the page returns from being hidden for more than 30 seconds, reconnect the SSE stream and re-activate the current tab so the
606
+ // logs stream reconnects naturally through its existing tabactivated listener. The reconnect callback is supplied by the IIFE - it owns the EventSource
607
+ // instance.
608
+ function handleVisibilityChange(ctx, reconnect) {
609
+ if (ctx.document.hidden) {
610
+ ctx.state.hiddenSince = Date.now();
611
+ }
612
+ else if ((ctx.state.hiddenSince > 0) && ((Date.now() - ctx.state.hiddenSince) > 30000)) {
613
+ ctx.state.hiddenSince = 0;
614
+ reconnect();
615
+ const activeTab = ctx.document.querySelector(".tab-btn.active");
616
+ if (activeTab) {
617
+ ctx.document.dispatchEvent(new CustomEvent("tabactivated", { detail: { category: activeTab.getAttribute("data-category") } }));
618
+ }
619
+ }
620
+ else {
621
+ ctx.state.hiddenSince = 0;
622
+ }
623
+ }
624
+ // Initialize hover-emulating tooltips for devices whose primary input cannot hover (iPadOS Safari). A single shared tooltip element is positioned via
625
+ // getBoundingClientRect on hover and hidden on leave; immune to overflow containers and stacking contexts.
626
+ function initIPadTooltips(ctx) {
627
+ if (!window.matchMedia("(hover: none)").matches) {
628
+ return;
629
+ }
630
+ const tip = ctx.document.createElement("div");
631
+ tip.className = "btn-icon-tooltip";
632
+ ctx.document.body.appendChild(tip);
633
+ ctx.document.addEventListener("mouseenter", (e) => {
634
+ const target = e.target;
635
+ const btn = target?.closest(".btn-icon[aria-label]");
636
+ if (!btn) {
637
+ return;
638
+ }
639
+ const label = btn.getAttribute("title") ?? btn.getAttribute("aria-label");
640
+ if (!label) {
641
+ return;
642
+ }
643
+ const rect = btn.getBoundingClientRect();
644
+ tip.textContent = label;
645
+ tip.classList.add("visible");
646
+ tip.style.top = String(rect.bottom + 6 + (window.scrollY || 0)) + "px";
647
+ tip.style.left = String(rect.left + (rect.width / 2) + (window.scrollX || 0)) + "px";
648
+ tip.style.transform = "translateX(-50%)";
649
+ }, true);
650
+ ctx.document.addEventListener("mouseleave", (e) => {
651
+ const target = e.target;
652
+ const src = target?.closest(".btn-icon[aria-label]");
653
+ if (!src) {
654
+ return;
655
+ }
656
+ const related = e.relatedTarget;
657
+ const dest = related?.closest(".btn-icon[aria-label]");
658
+ if (dest === src) {
659
+ return;
660
+ }
661
+ tip.classList.remove("visible");
662
+ }, true);
663
+ }
664
+ /**
665
+ * The script-side function registry. generateStatusScript() emits each entry's .toString() output in order. Order is hoisting-irrelevant for function
666
+ * declarations but reflects the logical groups: pure formatters, state-derived renderers, DOM mutators, render schedulers, SSE handlers, then trampolines and
667
+ * lifecycle helpers. Adding a new function means appending here and using its identifier in the IIFE; nothing else changes.
668
+ *
669
+ * The exported individual functions remain importable for tests; this registry is just the emission order for the script body.
670
+ */
671
+ export const HANDLER_FUNCTIONS = [
672
+ formatDuration,
673
+ formatBytes,
674
+ formatTime,
675
+ formatTimeAgo,
676
+ getDomain,
677
+ getRecoveringLabel,
678
+ getHealthBadge,
679
+ formatLastIssue,
680
+ formatAutoRecovery,
681
+ formatClients,
682
+ renderHealthCellContent,
683
+ renderDetailCodec,
684
+ renderDetailStarted,
685
+ updateSystemStatus,
686
+ buildStreamPopoverContent,
687
+ updateStreamPopover,
688
+ scheduleTableRender,
689
+ schedulePopoverRender,
690
+ renderStreamsTable,
691
+ updateStreamRow,
692
+ toggleStreamDetails,
693
+ updateDurations,
694
+ updateChannelHealth,
695
+ updateDomainAuth,
696
+ applyHealthSnapshot,
697
+ handleSnapshot,
698
+ handleStreamAdded,
699
+ handleStreamRemoved,
700
+ handleStreamHealthChanged,
701
+ handleSystemStatusChanged,
702
+ handleHealthChanged,
703
+ handleChannelUpdate,
704
+ handleSseError,
705
+ toggleStreamPopover,
706
+ copyOverviewPlaylistUrl,
707
+ handleVisibilityChange,
708
+ initIPadTooltips
709
+ ];
710
+ // Test surface. Each function is exported so status.handlers.test.ts and the DOM-runtime suite at test/e2e/dom-runtime/status-handlers-runtime.test.ts can
711
+ // import and call them directly.
712
+ export { applyHealthSnapshot, buildStreamPopoverContent, copyOverviewPlaylistUrl, formatAutoRecovery, formatBytes, formatClients, formatDuration, formatLastIssue, formatTime, formatTimeAgo, getDomain, getHealthBadge, getRecoveringLabel, handleChannelUpdate, handleHealthChanged, handleSnapshot, handleSseError, handleStreamAdded, handleStreamHealthChanged, handleStreamRemoved, handleSystemStatusChanged, handleVisibilityChange, initIPadTooltips, renderDetailCodec, renderDetailStarted, renderHealthCellContent, renderStreamsTable, schedulePopoverRender, scheduleTableRender, toggleStreamDetails, toggleStreamPopover, updateChannelHealth, updateDomainAuth, updateDurations, updateStreamPopover, updateStreamRow, updateSystemStatus };
713
+ //# sourceMappingURL=status.handlers.js.map