prismcast 1.12.1 → 1.14.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 (655) hide show
  1. package/dist/app.d.ts +88 -7
  2. package/dist/app.js +295 -161
  3. package/dist/app.js.map +1 -1
  4. package/dist/browser/blockedPage.d.ts +0 -1
  5. package/dist/browser/browserSupervisor.d.ts +4 -4
  6. package/dist/browser/browserSupervisor.js.map +1 -1
  7. package/dist/browser/cdp.d.ts +8 -20
  8. package/dist/browser/cdp.js +11 -61
  9. package/dist/browser/cdp.js.map +1 -1
  10. package/dist/browser/channelSelection.d.ts +16 -4
  11. package/dist/browser/channelSelection.js +42 -31
  12. package/dist/browser/channelSelection.js.map +1 -1
  13. package/dist/browser/consent.d.ts +17 -4
  14. package/dist/browser/consent.js +25 -40
  15. package/dist/browser/consent.js.map +1 -1
  16. package/dist/browser/display.d.ts +0 -1
  17. package/dist/browser/hlsPlaylistObserver.d.ts +3 -2
  18. package/dist/browser/hlsPlaylistObserver.js +9 -4
  19. package/dist/browser/hlsPlaylistObserver.js.map +1 -1
  20. package/dist/browser/index.d.ts +179 -46
  21. package/dist/browser/index.js +473 -306
  22. package/dist/browser/index.js.map +1 -1
  23. package/dist/browser/launchGovernor.d.ts +0 -1
  24. package/dist/browser/login.d.ts +8 -7
  25. package/dist/browser/login.js +25 -16
  26. package/dist/browser/login.js.map +1 -1
  27. package/dist/browser/manifestInterceptor.d.ts +14 -5
  28. package/dist/browser/manifestInterceptor.js +25 -12
  29. package/dist/browser/manifestInterceptor.js.map +1 -1
  30. package/dist/browser/navigation.d.ts +28 -0
  31. package/dist/browser/navigation.js +57 -0
  32. package/dist/browser/navigation.js.map +1 -0
  33. package/dist/browser/pageStaleness.d.ts +0 -1
  34. package/dist/browser/precaching.d.ts +27 -11
  35. package/dist/browser/precaching.js +262 -107
  36. package/dist/browser/precaching.js.map +1 -1
  37. package/dist/browser/tabCapture.d.ts +7 -10
  38. package/dist/browser/tabCapture.js +37 -50
  39. package/dist/browser/tabCapture.js.map +1 -1
  40. package/dist/browser/tabNetworkObserver.d.ts +0 -1
  41. package/dist/browser/tabSelection.d.ts +5 -5
  42. package/dist/browser/tabSelection.js +11 -10
  43. package/dist/browser/tabSelection.js.map +1 -1
  44. package/dist/browser/tuning/cache.d.ts +0 -1
  45. package/dist/browser/tuning/comcastPolymer.d.ts +0 -1
  46. package/dist/browser/tuning/comcastPolymer.js +29 -31
  47. package/dist/browser/tuning/comcastPolymer.js.map +1 -1
  48. package/dist/browser/tuning/cox.d.ts +0 -1
  49. package/dist/browser/tuning/directv.d.ts +0 -1
  50. package/dist/browser/tuning/directv.js +11 -12
  51. package/dist/browser/tuning/directv.js.map +1 -1
  52. package/dist/browser/tuning/fox.d.ts +0 -1
  53. package/dist/browser/tuning/fox.js +2 -2
  54. package/dist/browser/tuning/fox.js.map +1 -1
  55. package/dist/browser/tuning/gridSearch.d.ts +0 -1
  56. package/dist/browser/tuning/hbo.d.ts +0 -1
  57. package/dist/browser/tuning/hbo.js +34 -17
  58. package/dist/browser/tuning/hbo.js.map +1 -1
  59. package/dist/browser/tuning/hulu.d.ts +14 -2
  60. package/dist/browser/tuning/hulu.js +144 -21
  61. package/dist/browser/tuning/hulu.js.map +1 -1
  62. package/dist/browser/tuning/shared.d.ts +0 -1
  63. package/dist/browser/tuning/sling.d.ts +0 -1
  64. package/dist/browser/tuning/sling.js +17 -22
  65. package/dist/browser/tuning/sling.js.map +1 -1
  66. package/dist/browser/tuning/spectrum.d.ts +0 -1
  67. package/dist/browser/tuning/thumbnailRow.d.ts +0 -1
  68. package/dist/browser/tuning/tileClick.d.ts +0 -1
  69. package/dist/browser/tuning/xfinity.d.ts +0 -1
  70. package/dist/browser/tuning/youtubeTv.d.ts +0 -1
  71. package/dist/browser/video.d.ts +22 -20
  72. package/dist/browser/video.js +102 -119
  73. package/dist/browser/video.js.map +1 -1
  74. package/dist/browser/windowSync.d.ts +0 -1
  75. package/dist/channels/index.d.ts +0 -1
  76. package/dist/channels/index.js +5 -5
  77. package/dist/channels/index.js.map +1 -1
  78. package/dist/cliHelp.d.ts +11 -0
  79. package/dist/cliHelp.js +148 -0
  80. package/dist/cliHelp.js.map +1 -0
  81. package/dist/config/channelForm.d.ts +0 -1
  82. package/dist/config/channelForm.js +5 -4
  83. package/dist/config/channelForm.js.map +1 -1
  84. package/dist/config/channelIdentity.d.ts +27 -0
  85. package/dist/config/channelIdentity.js +72 -0
  86. package/dist/config/channelIdentity.js.map +1 -0
  87. package/dist/config/channelSort.d.ts +25 -0
  88. package/dist/config/channelSort.js +101 -0
  89. package/dist/config/channelSort.js.map +1 -0
  90. package/dist/config/consistencyProbe.d.ts +2 -5
  91. package/dist/config/consistencyProbe.js +8 -51
  92. package/dist/config/consistencyProbe.js.map +1 -1
  93. package/dist/config/health.d.ts +3 -2
  94. package/dist/config/health.js +41 -28
  95. package/dist/config/health.js.map +1 -1
  96. package/dist/config/index.d.ts +107 -56
  97. package/dist/config/index.js +499 -267
  98. package/dist/config/index.js.map +1 -1
  99. package/dist/config/paths.d.ts +13 -9
  100. package/dist/config/paths.js +21 -25
  101. package/dist/config/paths.js.map +1 -1
  102. package/dist/config/persistence.context.d.ts +0 -1
  103. package/dist/config/persistence.d.ts +57 -12
  104. package/dist/config/persistence.js +127 -44
  105. package/dist/config/persistence.js.map +1 -1
  106. package/dist/config/presets.d.ts +0 -1
  107. package/dist/config/profiles.d.ts +9 -4
  108. package/dist/config/profiles.js +37 -3
  109. package/dist/config/profiles.js.map +1 -1
  110. package/dist/config/providerLineups.d.ts +0 -1
  111. package/dist/config/reactivity.d.ts +60 -37
  112. package/dist/config/reactivity.js +89 -116
  113. package/dist/config/reactivity.js.map +1 -1
  114. package/dist/config/servicePacks.d.ts +0 -1
  115. package/dist/config/services.d.ts +62 -73
  116. package/dist/config/services.js +140 -202
  117. package/dist/config/services.js.map +1 -1
  118. package/dist/config/sites.d.ts +0 -9
  119. package/dist/config/sites.js +5 -40
  120. package/dist/config/sites.js.map +1 -1
  121. package/dist/config/streamSettings.d.ts +24 -0
  122. package/dist/config/streamSettings.js +17 -0
  123. package/dist/config/streamSettings.js.map +1 -0
  124. package/dist/config/userChannels.d.ts +82 -80
  125. package/dist/config/userChannels.js +184 -248
  126. package/dist/config/userChannels.js.map +1 -1
  127. package/dist/config/userConfig.d.ts +225 -59
  128. package/dist/config/userConfig.js +352 -131
  129. package/dist/config/userConfig.js.map +1 -1
  130. package/dist/config/userProfiles.d.ts +2 -3
  131. package/dist/config/userProfiles.js +6 -2
  132. package/dist/config/userProfiles.js.map +1 -1
  133. package/dist/hdhr/channelMap.d.ts +0 -1
  134. package/dist/hdhr/discover.d.ts +7 -2
  135. package/dist/hdhr/discover.js +12 -6
  136. package/dist/hdhr/discover.js.map +1 -1
  137. package/dist/hdhr/getHandlers.d.ts +0 -1
  138. package/dist/hdhr/identity.d.ts +0 -1
  139. package/dist/hdhr/index.d.ts +51 -11
  140. package/dist/hdhr/index.js +108 -112
  141. package/dist/hdhr/index.js.map +1 -1
  142. package/dist/hdhr/protocol.d.ts +0 -1
  143. package/dist/hdhr/tunerState.d.ts +0 -1
  144. package/dist/hdhr/udp.d.ts +11 -5
  145. package/dist/hdhr/udp.js +25 -20
  146. package/dist/hdhr/udp.js.map +1 -1
  147. package/dist/identity.d.ts +0 -1
  148. package/dist/index.d.ts +0 -1
  149. package/dist/index.js +16 -143
  150. package/dist/index.js.map +1 -1
  151. package/dist/native/codecInference.d.ts +5 -4
  152. package/dist/native/codecInference.js +12 -7
  153. package/dist/native/codecInference.js.map +1 -1
  154. package/dist/native/decrypt.d.ts +10 -3
  155. package/dist/native/decrypt.js +9 -5
  156. package/dist/native/decrypt.js.map +1 -1
  157. package/dist/native/index.d.ts +25 -23
  158. package/dist/native/index.js +27 -20
  159. package/dist/native/index.js.map +1 -1
  160. package/dist/native/probe.d.ts +7 -10
  161. package/dist/native/probe.js +40 -36
  162. package/dist/native/probe.js.map +1 -1
  163. package/dist/native/proxy.d.ts +3 -4
  164. package/dist/native/proxy.js +122 -92
  165. package/dist/native/proxy.js.map +1 -1
  166. package/dist/native/tokenExpiry.d.ts +0 -1
  167. package/dist/routes/assets.d.ts +0 -1
  168. package/dist/routes/auth.d.ts +0 -1
  169. package/dist/routes/auth.js +1 -1
  170. package/dist/routes/auth.js.map +1 -1
  171. package/dist/routes/cdp.d.ts +18 -11
  172. package/dist/routes/cdp.js +99 -70
  173. package/dist/routes/cdp.js.map +1 -1
  174. package/dist/routes/channels.d.ts +0 -1
  175. package/dist/routes/clientActions.d.ts +0 -1
  176. package/dist/routes/components.d.ts +23 -6
  177. package/dist/routes/components.js +21 -4
  178. package/dist/routes/components.js.map +1 -1
  179. package/dist/routes/config/channels/endpoints/browse.d.ts +0 -1
  180. package/dist/routes/config/channels/endpoints/browse.js +39 -29
  181. package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
  182. package/dist/routes/config/channels/endpoints/bulk.d.ts +0 -1
  183. package/dist/routes/config/channels/endpoints/bulk.js +3 -7
  184. package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
  185. package/dist/routes/config/channels/endpoints/crud.d.ts +0 -1
  186. package/dist/routes/config/channels/endpoints/crud.js +3 -2
  187. package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
  188. package/dist/routes/config/channels/endpoints/importExport.d.ts +0 -1
  189. package/dist/routes/config/channels/endpoints/importExport.js +7 -3
  190. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
  191. package/dist/routes/config/channels/endpoints/predefined.d.ts +0 -1
  192. package/dist/routes/config/channels/endpoints/predefined.js +5 -15
  193. package/dist/routes/config/channels/endpoints/predefined.js.map +1 -1
  194. package/dist/routes/config/channels/endpoints/prefs.d.ts +0 -1
  195. package/dist/routes/config/channels/endpoints/prefs.js +8 -7
  196. package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
  197. package/dist/routes/config/channels/endpoints/service.d.ts +0 -1
  198. package/dist/routes/config/channels/endpoints/service.js +13 -9
  199. package/dist/routes/config/channels/endpoints/service.js.map +1 -1
  200. package/dist/routes/config/channels/endpoints/tags.d.ts +0 -1
  201. package/dist/routes/config/channels/endpoints/tags.js +4 -16
  202. package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
  203. package/dist/routes/config/channels/healthBridge.d.ts +0 -1
  204. package/dist/routes/config/channels/http/handler.d.ts +0 -1
  205. package/dist/routes/config/channels/http/playlistHint.d.ts +0 -1
  206. package/dist/routes/config/channels/http/serviceWarning.d.ts +0 -1
  207. package/dist/routes/config/channels/http/serviceWarning.js +1 -1
  208. package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
  209. package/dist/routes/config/channels/index.d.ts +0 -1
  210. package/dist/routes/config/channels/setup.d.ts +0 -1
  211. package/dist/routes/config/channels/table.d.ts +2 -2
  212. package/dist/routes/config/channels/table.js +13 -14
  213. package/dist/routes/config/channels/table.js.map +1 -1
  214. package/dist/routes/config/http/envelope.d.ts +6 -4
  215. package/dist/routes/config/http/envelope.js +18 -5
  216. package/dist/routes/config/http/envelope.js.map +1 -1
  217. package/dist/routes/config/index.d.ts +21 -14
  218. package/dist/routes/config/index.js +49 -35
  219. package/dist/routes/config/index.js.map +1 -1
  220. package/dist/routes/config/services.d.ts +0 -1
  221. package/dist/routes/config/services.js +4 -3
  222. package/dist/routes/config/services.js.map +1 -1
  223. package/dist/routes/config/settings.d.ts +43 -7
  224. package/dist/routes/config/settings.js +227 -126
  225. package/dist/routes/config/settings.js.map +1 -1
  226. package/dist/routes/config/vocabulary.d.ts +24 -0
  227. package/dist/routes/config/vocabulary.js +33 -0
  228. package/dist/routes/config/vocabulary.js.map +1 -0
  229. package/dist/routes/debug.d.ts +0 -1
  230. package/dist/routes/debug.js +28 -13
  231. package/dist/routes/debug.js.map +1 -1
  232. package/dist/routes/health.d.ts +1 -2
  233. package/dist/routes/health.js +5 -2
  234. package/dist/routes/health.js.map +1 -1
  235. package/dist/routes/hls.d.ts +0 -1
  236. package/dist/routes/icons.d.ts +0 -1
  237. package/dist/routes/index.d.ts +0 -1
  238. package/dist/routes/logs.d.ts +3 -2
  239. package/dist/routes/logs.js +4 -2
  240. package/dist/routes/logs.js.map +1 -1
  241. package/dist/routes/mpegts.d.ts +0 -1
  242. package/dist/routes/play.d.ts +0 -1
  243. package/dist/routes/playlist.d.ts +0 -1
  244. package/dist/routes/playlist.js +2 -1
  245. package/dist/routes/playlist.js.map +1 -1
  246. package/dist/routes/root/content.d.ts +0 -1
  247. package/dist/routes/root/content.js +33 -26
  248. package/dist/routes/root/content.js.map +1 -1
  249. package/dist/routes/root/index.d.ts +0 -1
  250. package/dist/routes/root/scripts/channels.d.ts +0 -1
  251. package/dist/routes/root/scripts/channels.js +25 -13
  252. package/dist/routes/root/scripts/channels.js.map +1 -1
  253. package/dist/routes/root/scripts/clientEscape.d.ts +0 -1
  254. package/dist/routes/root/scripts/clientUrl.d.ts +0 -1
  255. package/dist/routes/root/scripts/config.d.ts +1 -2
  256. package/dist/routes/root/scripts/config.js +132 -105
  257. package/dist/routes/root/scripts/config.js.map +1 -1
  258. package/dist/routes/root/scripts/index.d.ts +0 -1
  259. package/dist/routes/root/scripts/shared.d.ts +0 -1
  260. package/dist/routes/root/scripts/shared.js +34 -23
  261. package/dist/routes/root/scripts/shared.js.map +1 -1
  262. package/dist/routes/root/scripts/status.d.ts +0 -1
  263. package/dist/routes/root/scripts/status.handlers.d.ts +8 -14
  264. package/dist/routes/root/scripts/status.handlers.js +38 -23
  265. package/dist/routes/root/scripts/status.handlers.js.map +1 -1
  266. package/dist/routes/root/styles.d.ts +0 -1
  267. package/dist/routes/root/styles.js +13 -16
  268. package/dist/routes/root/styles.js.map +1 -1
  269. package/dist/routes/services.d.ts +0 -1
  270. package/dist/routes/services.js +13 -17
  271. package/dist/routes/services.js.map +1 -1
  272. package/dist/routes/sse.d.ts +3 -2
  273. package/dist/routes/sse.js +7 -3
  274. package/dist/routes/sse.js.map +1 -1
  275. package/dist/routes/streams.d.ts +4 -2
  276. package/dist/routes/streams.js +8 -5
  277. package/dist/routes/streams.js.map +1 -1
  278. package/dist/routes/theme.d.ts +0 -1
  279. package/dist/routes/theme.js +12 -12
  280. package/dist/routes/theme.js.map +1 -1
  281. package/dist/routes/ui.d.ts +4 -4
  282. package/dist/routes/ui.js +17 -13
  283. package/dist/routes/ui.js.map +1 -1
  284. package/dist/routes/upgrade.d.ts +0 -1
  285. package/dist/routes/upgrade.js +2 -1
  286. package/dist/routes/upgrade.js.map +1 -1
  287. package/dist/service/commands.context.d.ts +0 -1
  288. package/dist/service/commands.context.js +7 -1
  289. package/dist/service/commands.context.js.map +1 -1
  290. package/dist/service/commands.d.ts +0 -1
  291. package/dist/service/generators.context.d.ts +0 -1
  292. package/dist/service/generators.d.ts +0 -1
  293. package/dist/service/generators.js +12 -9
  294. package/dist/service/generators.js.map +1 -1
  295. package/dist/service/index.d.ts +0 -1
  296. package/dist/streaming/captureLock.d.ts +1 -2
  297. package/dist/streaming/captureLock.js +6 -5
  298. package/dist/streaming/captureLock.js.map +1 -1
  299. package/dist/streaming/captureSession.d.ts +4 -5
  300. package/dist/streaming/captureSession.js +6 -7
  301. package/dist/streaming/captureSession.js.map +1 -1
  302. package/dist/streaming/clients.d.ts +4 -3
  303. package/dist/streaming/clients.js +5 -4
  304. package/dist/streaming/clients.js.map +1 -1
  305. package/dist/streaming/codec.d.ts +12 -3
  306. package/dist/streaming/codec.js +40 -28
  307. package/dist/streaming/codec.js.map +1 -1
  308. package/dist/streaming/fmp4Segmenter.d.ts +16 -5
  309. package/dist/streaming/fmp4Segmenter.js +110 -89
  310. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  311. package/dist/streaming/hls.d.ts +55 -15
  312. package/dist/streaming/hls.js +255 -162
  313. package/dist/streaming/hls.js.map +1 -1
  314. package/dist/streaming/hlsResume.d.ts +42 -16
  315. package/dist/streaming/hlsResume.js +75 -42
  316. package/dist/streaming/hlsResume.js.map +1 -1
  317. package/dist/streaming/hlsSegments.d.ts +9 -7
  318. package/dist/streaming/hlsSegments.js +19 -14
  319. package/dist/streaming/hlsSegments.js.map +1 -1
  320. package/dist/streaming/lifecycle.d.ts +5 -4
  321. package/dist/streaming/lifecycle.js +25 -13
  322. package/dist/streaming/lifecycle.js.map +1 -1
  323. package/dist/streaming/monitor.d.ts +8 -5
  324. package/dist/streaming/monitor.js +109 -93
  325. package/dist/streaming/monitor.js.map +1 -1
  326. package/dist/streaming/mp4Parser.d.ts +29 -5
  327. package/dist/streaming/mp4Parser.js +49 -7
  328. package/dist/streaming/mp4Parser.js.map +1 -1
  329. package/dist/streaming/mpegts.d.ts +0 -1
  330. package/dist/streaming/mpegts.js +23 -7
  331. package/dist/streaming/mpegts.js.map +1 -1
  332. package/dist/streaming/playlistBuilder.d.ts +1 -3
  333. package/dist/streaming/playlistBuilder.js +2 -3
  334. package/dist/streaming/playlistBuilder.js.map +1 -1
  335. package/dist/streaming/preroll.d.ts +76 -30
  336. package/dist/streaming/preroll.js +141 -89
  337. package/dist/streaming/preroll.js.map +1 -1
  338. package/dist/streaming/pretune.d.ts +3 -4
  339. package/dist/streaming/pretune.js +50 -46
  340. package/dist/streaming/pretune.js.map +1 -1
  341. package/dist/streaming/pretuneTimers.d.ts +16 -12
  342. package/dist/streaming/pretuneTimers.js +27 -37
  343. package/dist/streaming/pretuneTimers.js.map +1 -1
  344. package/dist/streaming/recovery.d.ts +36 -7
  345. package/dist/streaming/recovery.js +53 -11
  346. package/dist/streaming/recovery.js.map +1 -1
  347. package/dist/streaming/registry.d.ts +35 -14
  348. package/dist/streaming/registry.js +23 -8
  349. package/dist/streaming/registry.js.map +1 -1
  350. package/dist/streaming/setup.d.ts +66 -25
  351. package/dist/streaming/setup.js +305 -229
  352. package/dist/streaming/setup.js.map +1 -1
  353. package/dist/streaming/showInfo.d.ts +29 -13
  354. package/dist/streaming/showInfo.js +156 -140
  355. package/dist/streaming/showInfo.js.map +1 -1
  356. package/dist/streaming/statusEmitter.d.ts +16 -12
  357. package/dist/streaming/statusEmitter.js +25 -21
  358. package/dist/streaming/statusEmitter.js.map +1 -1
  359. package/dist/types/channels.d.ts +0 -1
  360. package/dist/types/config.d.ts +24 -14
  361. package/dist/types/config.js +5 -1
  362. package/dist/types/config.js.map +1 -1
  363. package/dist/types/index.d.ts +4 -4
  364. package/dist/types/index.js +2 -1
  365. package/dist/types/index.js.map +1 -1
  366. package/dist/types/profiles.d.ts +3 -3
  367. package/dist/types/selection.d.ts +19 -8
  368. package/dist/types/shared.d.ts +0 -1
  369. package/dist/types/streaming.d.ts +12 -6
  370. package/dist/types/streaming.js +12 -5
  371. package/dist/types/streaming.js.map +1 -1
  372. package/dist/upgrade/commands.context.d.ts +0 -1
  373. package/dist/upgrade/commands.d.ts +0 -1
  374. package/dist/upgrade/detection.context.d.ts +0 -1
  375. package/dist/upgrade/detection.d.ts +0 -1
  376. package/dist/upgrade/index.d.ts +0 -1
  377. package/dist/upgrade/lifecycle.context.d.ts +2 -1
  378. package/dist/upgrade/lifecycle.context.js +9 -4
  379. package/dist/upgrade/lifecycle.context.js.map +1 -1
  380. package/dist/upgrade/lifecycle.d.ts +0 -1
  381. package/dist/upgrade/pathHandle.d.ts +0 -1
  382. package/dist/utils/bootSession.context.d.ts +0 -1
  383. package/dist/utils/bootSession.context.js +2 -1
  384. package/dist/utils/bootSession.context.js.map +1 -1
  385. package/dist/utils/bootSession.d.ts +0 -1
  386. package/dist/utils/chromeFetch.d.ts +0 -1
  387. package/dist/utils/cliOutput.d.ts +0 -1
  388. package/dist/utils/debugFilter.d.ts +3 -3
  389. package/dist/utils/debugFilter.js +7 -5
  390. package/dist/utils/debugFilter.js.map +1 -1
  391. package/dist/utils/delay.d.ts +47 -25
  392. package/dist/utils/delay.js +74 -37
  393. package/dist/utils/delay.js.map +1 -1
  394. package/dist/{hdhr → utils}/deviceId.d.ts +1 -2
  395. package/dist/{hdhr → utils}/deviceId.js +11 -29
  396. package/dist/utils/deviceId.js.map +1 -0
  397. package/dist/utils/errors.d.ts +7 -1
  398. package/dist/utils/errors.js +12 -3
  399. package/dist/utils/errors.js.map +1 -1
  400. package/dist/utils/evaluate.d.ts +11 -3
  401. package/dist/utils/evaluate.js +4 -3
  402. package/dist/utils/evaluate.js.map +1 -1
  403. package/dist/utils/ffmpeg.context.d.ts +0 -1
  404. package/dist/utils/ffmpeg.d.ts +0 -1
  405. package/dist/utils/fileLogger.d.ts +26 -14
  406. package/dist/utils/fileLogger.js +89 -49
  407. package/dist/utils/fileLogger.js.map +1 -1
  408. package/dist/utils/format.d.ts +12 -3
  409. package/dist/utils/format.js +29 -11
  410. package/dist/utils/format.js.map +1 -1
  411. package/dist/utils/httpErrors.d.ts +13 -0
  412. package/dist/utils/httpErrors.js +22 -0
  413. package/dist/utils/httpErrors.js.map +1 -0
  414. package/dist/utils/index.d.ts +3 -2
  415. package/dist/utils/index.js +3 -1
  416. package/dist/utils/index.js.map +1 -1
  417. package/dist/utils/logEmitter.d.ts +0 -1
  418. package/dist/utils/logger.d.ts +9 -9
  419. package/dist/utils/logger.js +64 -20
  420. package/dist/utils/logger.js.map +1 -1
  421. package/dist/utils/m3u.d.ts +0 -1
  422. package/dist/utils/markup.d.ts +0 -1
  423. package/dist/utils/markup.js +8 -3
  424. package/dist/utils/markup.js.map +1 -1
  425. package/dist/utils/memo.d.ts +0 -1
  426. package/dist/utils/morganStream.d.ts +0 -1
  427. package/dist/utils/network.d.ts +0 -1
  428. package/dist/utils/never.d.ts +0 -1
  429. package/dist/utils/pid.d.ts +0 -1
  430. package/dist/utils/plainObject.d.ts +6 -0
  431. package/dist/utils/plainObject.js +18 -0
  432. package/dist/utils/plainObject.js.map +1 -0
  433. package/dist/utils/platform.d.ts +0 -1
  434. package/dist/utils/processInspector.context.d.ts +32 -2
  435. package/dist/utils/processInspector.context.js +117 -1
  436. package/dist/utils/processInspector.context.js.map +1 -1
  437. package/dist/utils/processInspector.d.ts +0 -32
  438. package/dist/utils/processInspector.js +10 -117
  439. package/dist/utils/processInspector.js.map +1 -1
  440. package/dist/utils/retry.d.ts +5 -4
  441. package/dist/utils/retry.js +43 -33
  442. package/dist/utils/retry.js.map +1 -1
  443. package/dist/utils/runtimeIdentity.context.d.ts +0 -1
  444. package/dist/utils/runtimeIdentity.context.js +4 -30
  445. package/dist/utils/runtimeIdentity.context.js.map +1 -1
  446. package/dist/utils/runtimeIdentity.d.ts +18 -7
  447. package/dist/utils/runtimeIdentity.js +51 -18
  448. package/dist/utils/runtimeIdentity.js.map +1 -1
  449. package/dist/utils/sanitize.d.ts +0 -1
  450. package/dist/utils/streamContext.d.ts +0 -1
  451. package/dist/utils/timing.d.ts +3 -4
  452. package/dist/utils/timing.js +4 -4
  453. package/dist/utils/timing.js.map +1 -1
  454. package/dist/utils/version.d.ts +5 -4
  455. package/dist/utils/version.js +35 -24
  456. package/dist/utils/version.js.map +1 -1
  457. package/package.json +9 -9
  458. package/dist/app.d.ts.map +0 -1
  459. package/dist/browser/blockedPage.d.ts.map +0 -1
  460. package/dist/browser/browserSupervisor.d.ts.map +0 -1
  461. package/dist/browser/cdp.d.ts.map +0 -1
  462. package/dist/browser/channelSelection.d.ts.map +0 -1
  463. package/dist/browser/consent.d.ts.map +0 -1
  464. package/dist/browser/display.d.ts.map +0 -1
  465. package/dist/browser/hlsPlaylistObserver.d.ts.map +0 -1
  466. package/dist/browser/index.d.ts.map +0 -1
  467. package/dist/browser/launchGovernor.d.ts.map +0 -1
  468. package/dist/browser/login.d.ts.map +0 -1
  469. package/dist/browser/manifestInterceptor.d.ts.map +0 -1
  470. package/dist/browser/pageStaleness.d.ts.map +0 -1
  471. package/dist/browser/precaching.d.ts.map +0 -1
  472. package/dist/browser/tabCapture.d.ts.map +0 -1
  473. package/dist/browser/tabNetworkObserver.d.ts.map +0 -1
  474. package/dist/browser/tabSelection.d.ts.map +0 -1
  475. package/dist/browser/tuning/cache.d.ts.map +0 -1
  476. package/dist/browser/tuning/comcastPolymer.d.ts.map +0 -1
  477. package/dist/browser/tuning/cox.d.ts.map +0 -1
  478. package/dist/browser/tuning/directv.d.ts.map +0 -1
  479. package/dist/browser/tuning/fox.d.ts.map +0 -1
  480. package/dist/browser/tuning/gridSearch.d.ts.map +0 -1
  481. package/dist/browser/tuning/hbo.d.ts.map +0 -1
  482. package/dist/browser/tuning/hulu.d.ts.map +0 -1
  483. package/dist/browser/tuning/shared.d.ts.map +0 -1
  484. package/dist/browser/tuning/sling.d.ts.map +0 -1
  485. package/dist/browser/tuning/spectrum.d.ts.map +0 -1
  486. package/dist/browser/tuning/thumbnailRow.d.ts.map +0 -1
  487. package/dist/browser/tuning/tileClick.d.ts.map +0 -1
  488. package/dist/browser/tuning/xfinity.d.ts.map +0 -1
  489. package/dist/browser/tuning/youtubeTv.d.ts.map +0 -1
  490. package/dist/browser/video.d.ts.map +0 -1
  491. package/dist/browser/windowSync.d.ts.map +0 -1
  492. package/dist/channels/index.d.ts.map +0 -1
  493. package/dist/config/channelForm.d.ts.map +0 -1
  494. package/dist/config/consistencyProbe.d.ts.map +0 -1
  495. package/dist/config/health.d.ts.map +0 -1
  496. package/dist/config/index.d.ts.map +0 -1
  497. package/dist/config/paths.d.ts.map +0 -1
  498. package/dist/config/persistence.context.d.ts.map +0 -1
  499. package/dist/config/persistence.d.ts.map +0 -1
  500. package/dist/config/presets.d.ts.map +0 -1
  501. package/dist/config/profiles.d.ts.map +0 -1
  502. package/dist/config/providerLineups.d.ts.map +0 -1
  503. package/dist/config/reactivity.d.ts.map +0 -1
  504. package/dist/config/servicePacks.d.ts.map +0 -1
  505. package/dist/config/services.d.ts.map +0 -1
  506. package/dist/config/sites.d.ts.map +0 -1
  507. package/dist/config/userChannels.d.ts.map +0 -1
  508. package/dist/config/userConfig.d.ts.map +0 -1
  509. package/dist/config/userProfiles.d.ts.map +0 -1
  510. package/dist/hdhr/channelMap.d.ts.map +0 -1
  511. package/dist/hdhr/deviceId.d.ts.map +0 -1
  512. package/dist/hdhr/deviceId.js.map +0 -1
  513. package/dist/hdhr/discover.d.ts.map +0 -1
  514. package/dist/hdhr/getHandlers.d.ts.map +0 -1
  515. package/dist/hdhr/identity.d.ts.map +0 -1
  516. package/dist/hdhr/index.d.ts.map +0 -1
  517. package/dist/hdhr/protocol.d.ts.map +0 -1
  518. package/dist/hdhr/tunerState.d.ts.map +0 -1
  519. package/dist/hdhr/udp.d.ts.map +0 -1
  520. package/dist/identity.d.ts.map +0 -1
  521. package/dist/index.d.ts.map +0 -1
  522. package/dist/native/codecInference.d.ts.map +0 -1
  523. package/dist/native/decrypt.d.ts.map +0 -1
  524. package/dist/native/index.d.ts.map +0 -1
  525. package/dist/native/probe.d.ts.map +0 -1
  526. package/dist/native/proxy.d.ts.map +0 -1
  527. package/dist/native/tokenExpiry.d.ts.map +0 -1
  528. package/dist/routes/assets.d.ts.map +0 -1
  529. package/dist/routes/auth.d.ts.map +0 -1
  530. package/dist/routes/cdp.d.ts.map +0 -1
  531. package/dist/routes/channels.d.ts.map +0 -1
  532. package/dist/routes/clientActions.d.ts.map +0 -1
  533. package/dist/routes/components.d.ts.map +0 -1
  534. package/dist/routes/config/channels/endpoints/browse.d.ts.map +0 -1
  535. package/dist/routes/config/channels/endpoints/bulk.d.ts.map +0 -1
  536. package/dist/routes/config/channels/endpoints/crud.d.ts.map +0 -1
  537. package/dist/routes/config/channels/endpoints/importExport.d.ts.map +0 -1
  538. package/dist/routes/config/channels/endpoints/predefined.d.ts.map +0 -1
  539. package/dist/routes/config/channels/endpoints/prefs.d.ts.map +0 -1
  540. package/dist/routes/config/channels/endpoints/service.d.ts.map +0 -1
  541. package/dist/routes/config/channels/endpoints/tags.d.ts.map +0 -1
  542. package/dist/routes/config/channels/healthBridge.d.ts.map +0 -1
  543. package/dist/routes/config/channels/http/handler.d.ts.map +0 -1
  544. package/dist/routes/config/channels/http/playlistHint.d.ts.map +0 -1
  545. package/dist/routes/config/channels/http/serviceWarning.d.ts.map +0 -1
  546. package/dist/routes/config/channels/index.d.ts.map +0 -1
  547. package/dist/routes/config/channels/setup.d.ts.map +0 -1
  548. package/dist/routes/config/channels/table.d.ts.map +0 -1
  549. package/dist/routes/config/http/envelope.d.ts.map +0 -1
  550. package/dist/routes/config/index.d.ts.map +0 -1
  551. package/dist/routes/config/services.d.ts.map +0 -1
  552. package/dist/routes/config/settings.d.ts.map +0 -1
  553. package/dist/routes/debug.d.ts.map +0 -1
  554. package/dist/routes/health.d.ts.map +0 -1
  555. package/dist/routes/hls.d.ts.map +0 -1
  556. package/dist/routes/icons.d.ts.map +0 -1
  557. package/dist/routes/index.d.ts.map +0 -1
  558. package/dist/routes/logs.d.ts.map +0 -1
  559. package/dist/routes/mpegts.d.ts.map +0 -1
  560. package/dist/routes/play.d.ts.map +0 -1
  561. package/dist/routes/playlist.d.ts.map +0 -1
  562. package/dist/routes/root/content.d.ts.map +0 -1
  563. package/dist/routes/root/index.d.ts.map +0 -1
  564. package/dist/routes/root/scripts/channels.d.ts.map +0 -1
  565. package/dist/routes/root/scripts/clientEscape.d.ts.map +0 -1
  566. package/dist/routes/root/scripts/clientUrl.d.ts.map +0 -1
  567. package/dist/routes/root/scripts/config.d.ts.map +0 -1
  568. package/dist/routes/root/scripts/index.d.ts.map +0 -1
  569. package/dist/routes/root/scripts/shared.d.ts.map +0 -1
  570. package/dist/routes/root/scripts/status.d.ts.map +0 -1
  571. package/dist/routes/root/scripts/status.handlers.d.ts.map +0 -1
  572. package/dist/routes/root/styles.d.ts.map +0 -1
  573. package/dist/routes/services.d.ts.map +0 -1
  574. package/dist/routes/sse.d.ts.map +0 -1
  575. package/dist/routes/streams.d.ts.map +0 -1
  576. package/dist/routes/theme.d.ts.map +0 -1
  577. package/dist/routes/ui.d.ts.map +0 -1
  578. package/dist/routes/upgrade.d.ts.map +0 -1
  579. package/dist/service/commands.context.d.ts.map +0 -1
  580. package/dist/service/commands.d.ts.map +0 -1
  581. package/dist/service/generators.context.d.ts.map +0 -1
  582. package/dist/service/generators.d.ts.map +0 -1
  583. package/dist/service/index.d.ts.map +0 -1
  584. package/dist/streaming/captureLock.d.ts.map +0 -1
  585. package/dist/streaming/captureSession.d.ts.map +0 -1
  586. package/dist/streaming/clients.d.ts.map +0 -1
  587. package/dist/streaming/codec.d.ts.map +0 -1
  588. package/dist/streaming/fmp4Segmenter.d.ts.map +0 -1
  589. package/dist/streaming/hls.d.ts.map +0 -1
  590. package/dist/streaming/hlsResume.d.ts.map +0 -1
  591. package/dist/streaming/hlsSegments.d.ts.map +0 -1
  592. package/dist/streaming/lifecycle.d.ts.map +0 -1
  593. package/dist/streaming/monitor.d.ts.map +0 -1
  594. package/dist/streaming/mp4Parser.d.ts.map +0 -1
  595. package/dist/streaming/mpegts.d.ts.map +0 -1
  596. package/dist/streaming/playlistBuilder.d.ts.map +0 -1
  597. package/dist/streaming/preroll.d.ts.map +0 -1
  598. package/dist/streaming/pretune.d.ts.map +0 -1
  599. package/dist/streaming/pretuneTimers.d.ts.map +0 -1
  600. package/dist/streaming/recovery.d.ts.map +0 -1
  601. package/dist/streaming/registry.d.ts.map +0 -1
  602. package/dist/streaming/setup.d.ts.map +0 -1
  603. package/dist/streaming/showInfo.d.ts.map +0 -1
  604. package/dist/streaming/statusEmitter.d.ts.map +0 -1
  605. package/dist/types/channels.d.ts.map +0 -1
  606. package/dist/types/config.d.ts.map +0 -1
  607. package/dist/types/index.d.ts.map +0 -1
  608. package/dist/types/profiles.d.ts.map +0 -1
  609. package/dist/types/selection.d.ts.map +0 -1
  610. package/dist/types/shared.d.ts.map +0 -1
  611. package/dist/types/streaming.d.ts.map +0 -1
  612. package/dist/upgrade/commands.context.d.ts.map +0 -1
  613. package/dist/upgrade/commands.d.ts.map +0 -1
  614. package/dist/upgrade/detection.context.d.ts.map +0 -1
  615. package/dist/upgrade/detection.d.ts.map +0 -1
  616. package/dist/upgrade/index.d.ts.map +0 -1
  617. package/dist/upgrade/lifecycle.context.d.ts.map +0 -1
  618. package/dist/upgrade/lifecycle.d.ts.map +0 -1
  619. package/dist/upgrade/pathHandle.d.ts.map +0 -1
  620. package/dist/utils/bootSession.context.d.ts.map +0 -1
  621. package/dist/utils/bootSession.d.ts.map +0 -1
  622. package/dist/utils/chromeFetch.d.ts.map +0 -1
  623. package/dist/utils/cliOutput.d.ts.map +0 -1
  624. package/dist/utils/clock.d.ts +0 -15
  625. package/dist/utils/clock.d.ts.map +0 -1
  626. package/dist/utils/clock.js +0 -31
  627. package/dist/utils/clock.js.map +0 -1
  628. package/dist/utils/debugFilter.d.ts.map +0 -1
  629. package/dist/utils/delay.d.ts.map +0 -1
  630. package/dist/utils/errors.d.ts.map +0 -1
  631. package/dist/utils/evaluate.d.ts.map +0 -1
  632. package/dist/utils/ffmpeg.context.d.ts.map +0 -1
  633. package/dist/utils/ffmpeg.d.ts.map +0 -1
  634. package/dist/utils/fileLogger.d.ts.map +0 -1
  635. package/dist/utils/format.d.ts.map +0 -1
  636. package/dist/utils/index.d.ts.map +0 -1
  637. package/dist/utils/logEmitter.d.ts.map +0 -1
  638. package/dist/utils/logger.d.ts.map +0 -1
  639. package/dist/utils/m3u.d.ts.map +0 -1
  640. package/dist/utils/markup.d.ts.map +0 -1
  641. package/dist/utils/memo.d.ts.map +0 -1
  642. package/dist/utils/morganStream.d.ts.map +0 -1
  643. package/dist/utils/network.d.ts.map +0 -1
  644. package/dist/utils/never.d.ts.map +0 -1
  645. package/dist/utils/pid.d.ts.map +0 -1
  646. package/dist/utils/platform.d.ts.map +0 -1
  647. package/dist/utils/processInspector.context.d.ts.map +0 -1
  648. package/dist/utils/processInspector.d.ts.map +0 -1
  649. package/dist/utils/retry.d.ts.map +0 -1
  650. package/dist/utils/runtimeIdentity.context.d.ts.map +0 -1
  651. package/dist/utils/runtimeIdentity.d.ts.map +0 -1
  652. package/dist/utils/sanitize.d.ts.map +0 -1
  653. package/dist/utils/streamContext.d.ts.map +0 -1
  654. package/dist/utils/timing.d.ts.map +0 -1
  655. package/dist/utils/version.d.ts.map +0 -1
@@ -1,22 +1,35 @@
1
- import { CONFIG, getDefaults, validatePositiveInt, validatePositiveNumber } from "../../config/index.js";
2
- import { CONFIG_METADATA, getAdvancedSections, getEnvOverrides, getNestedValue, getSettingsTabSections, getUITabs, isEqualToDefault, mutateConfig, readConfig, setNestedValue } from "../../config/userConfig.js";
3
- import { LOG, escapeHtml, isRunningAsService, sanitizeString, stringifySorted } from "../../utils/index.js";
1
+ import { CAPTURE_BASELINE_CODEC, RECOGNIZED_CODECS } from "../../types/index.js";
2
+ import { CONFIG_METADATA, getAdvancedSections, getEnvOverrides, getNestedValue, getSettingsTabSections, getUITabs, isEqualToDefault, readConfig, setNestedValue } from "../../config/userConfig.js";
3
+ import { ConfigurationRejectedError, getConfigurationGap, getDefaults, getLoadedConfiguration, validateInteger, validateNumber } from "../../config/index.js";
4
+ import { LOG, escapeHtml, isRunningAsService, sanitizeString, serializeAttrs, stringifySorted } from "../../utils/index.js";
5
+ import { PENDING_PATH_ATTRIBUTE, REACTIVITY_BADGES, formatSettingCount } from "./vocabulary.js";
4
6
  import { applyConfigurationChange, describeConfigurationOutcome } from "./index.js";
5
7
  import { sendErrorResponse, sendFormErrors, sendSuccess, sendValidationError } from "./http/envelope.js";
6
8
  import { ACTIONS } from "../clientActions.js";
7
9
  import { VIDEO_QUALITY_PRESETS } from "../../config/presets.js";
10
+ import { generateBadge } from "../components.js";
8
11
  import { getConfigFilePath } from "../../config/paths.js";
9
- import { getGpuCapabilities } from "../../browser/display.js";
10
12
  import { getProviderModuleInfo } from "../../browser/channelSelection.js";
13
+ import { isCaptureCodecSupported } from "../../streaming/codec.js";
14
+ import { systemClock } from "homebridge-plugin-utils";
15
+ /* The capture-codec items' words: each recognized codec's label, and the reason each codec other than the baseline states when this GPU cannot capture it. Each
16
+ * table is keyed by the codec types, so a codec added to the recognized list cannot compile until the form decides its label and, unless it is the baseline,
17
+ * its reason. Which codecs are fixed and which are available is the codec module's answer, so the form holds only these words. A label names its codec alone,
18
+ * and the " (always enabled)" suffix is the fixed item's, which the provider below appends to whichever codec is the baseline.
19
+ */
20
+ const CAPTURE_CODEC_LABELS = {
21
+ h264: "H.264",
22
+ hevc: "HEVC"
23
+ };
24
+ const CAPTURE_CODEC_REASONS = {
25
+ hevc: "Requires GPU with HEVC hardware encoding."
26
+ };
11
27
  // Registry of list item providers for checkboxList settings. Each key matches a listItemsKey value in CONFIG_METADATA.
12
28
  const LIST_ITEM_PROVIDERS = {
13
- captureCodecs: () => {
14
- const gpuCaps = getGpuCapabilities();
15
- return [
16
- { fixed: true, label: "H.264 (always enabled)", value: "h264" },
17
- { disabled: !gpuCaps?.hevcHardwareEncoding, disabledReason: "Requires GPU with HEVC hardware encoding.", label: "HEVC", value: "hevc" }
18
- ];
19
- },
29
+ // Every recognized codec in order: the baseline as the fixed item, and every other codec disabled while this GPU cannot capture it.
30
+ captureCodecs: () => RECOGNIZED_CODECS.map((codec) => (codec === CAPTURE_BASELINE_CODEC) ?
31
+ { fixed: true, label: CAPTURE_CODEC_LABELS[codec] + " (always enabled)", value: codec } :
32
+ { disabled: !isCaptureCodecSupported(codec), disabledReason: CAPTURE_CODEC_REASONS[codec], label: CAPTURE_CODEC_LABELS[codec], value: codec }),
20
33
  providerModules: () => getProviderModuleInfo().map((p) => ({ label: p.label, value: p.slug }))
21
34
  };
22
35
  /**
@@ -185,15 +198,83 @@ function getFieldWidthClass(setting) {
185
198
  // Generic strings get wide width.
186
199
  return "field-wide";
187
200
  }
201
+ // The marker sentence of each kind, given the running value as the form displays it. Keyed by the kind union, so a kind added later cannot compile until
202
+ // it decides its sentence.
203
+ const PENDING_SENTENCES = {
204
+ restart: (running) => REACTIVITY_BADGES.restart.title + " Running value: " + running + ".",
205
+ unrealized: (running) => "Saved, but not applied yet. Running value: " + running + ". The next save retries it."
206
+ };
188
207
  /**
189
- * Generates HTML for a single setting form field. Supports text inputs, number inputs, and select dropdowns based on the setting type and validValues.
208
+ * Reads a setting's value for a person: the one reading the default line and the pending markers both use, so a value reads the same wherever the form names
209
+ * it. A checkbox list reads as its members joined, or none when it has no member; a missing value reads as autodetect; a number reads in display units, grouped
210
+ * unless the setting is a port, with its unit pluralized by the number; any other value reads as its display text with its unit appended as declared.
211
+ * @param value - The value, in storage units.
190
212
  * @param setting - The setting metadata.
191
- * @param currentValue - The current effective value (in storage units).
192
- * @param defaultValue - The default value (in storage units).
193
- * @param envOverride - The environment variable value if overridden, undefined otherwise.
213
+ * @returns The value's text with its display unit.
214
+ */
215
+ function formatSettingValue(value, setting) {
216
+ const displayUnit = getDisplayUnit(setting);
217
+ const displayValue = toDisplayValue(value, setting);
218
+ let text;
219
+ if (setting.type === "checkboxList") {
220
+ const members = Array.isArray(value) ? value : [];
221
+ text = (members.length > 0) ? members.join(", ") : "none";
222
+ }
223
+ else if (displayValue === null) {
224
+ text = "autodetect";
225
+ }
226
+ else if (typeof displayValue === "number") {
227
+ text = formatValueForDisplay(displayValue, setting.type);
228
+ }
229
+ else {
230
+ text = displayValue;
231
+ }
232
+ if (!displayUnit) {
233
+ return text;
234
+ }
235
+ return text + " " + ((typeof displayValue === "number") ? formatUnitForValue(displayValue, displayUnit) : displayUnit);
236
+ }
237
+ /**
238
+ * Derives the pending view of a configuration gap: one entry for each saved setting the running process has not taken, keyed by path in the settings' declared
239
+ * order, each carrying the marker sentence the form shows. The view is derived from the gap on every call and stored nowhere, which is why a save's response
240
+ * carries all of it rather than a change against an earlier view.
241
+ * @param gap - The gap to project, the live gap between the running configuration and the loaded snapshot by default.
242
+ * @returns The pending settings, keyed by path.
243
+ */
244
+ export function collectPendingSettings(gap = getConfigurationGap()) {
245
+ const buckets = {
246
+ held: { changes: gap.held, kind: "restart" },
247
+ live: { changes: gap.live, kind: "unrealized" },
248
+ nextStream: { changes: gap.nextStream, kind: "unrealized" }
249
+ };
250
+ const filed = new Map();
251
+ for (const bucket of Object.values(buckets)) {
252
+ for (const change of bucket.changes) {
253
+ filed.set(change.path, { change, kind: bucket.kind });
254
+ }
255
+ }
256
+ // Walking the metadata settings rather than looking each gap path up means no lookup can miss, and the view comes out in the order the form renders.
257
+ const pending = new Map();
258
+ for (const setting of Object.values(CONFIG_METADATA).flat()) {
259
+ const entry = filed.get(setting.path);
260
+ if (!entry) {
261
+ continue;
262
+ }
263
+ // The gap accessor diffs the running configuration against the loaded snapshot, so a change's previous value is the running one the marker names.
264
+ const running = entry.change.previous;
265
+ pending.set(setting.path, { kind: entry.kind, path: setting.path, text: PENDING_SENTENCES[entry.kind](formatSettingValue(running, setting)) });
266
+ }
267
+ return pending;
268
+ }
269
+ /**
270
+ * Generates HTML for a single setting form field. The setting's validValues and type choose the control: a select dropdown when validValues is non-empty,
271
+ * otherwise a hidden-input-plus-checkbox pair for a boolean, a hidden JSON input with a checkbox grid for a checkboxList, and a text or number input for every
272
+ * other type. The field also carries its pending marker, its default hint and, where they apply, the reset button and the environment override notice.
273
+ * @param options - The setting, the saved value the form shows, its default, its environment override, and its pending entry.
194
274
  * @returns HTML string for the form field.
195
275
  */
196
- function generateSettingField(setting, currentValue, defaultValue, envOverride) {
276
+ function generateSettingField(options) {
277
+ const { currentValue, defaultValue, envOverride, pending, setting } = options;
197
278
  const isDisabled = (envOverride !== undefined) || (setting.disabledReason !== undefined);
198
279
  const inputId = setting.path.replaceAll(".", "-");
199
280
  const isModified = !isDisabled && !isEqualToDefault(currentValue, defaultValue);
@@ -205,10 +286,11 @@ function generateSettingField(setting, currentValue, defaultValue, envOverride)
205
286
  const displayMax = getDisplayMax(setting);
206
287
  // Determine if this should be a select dropdown.
207
288
  const hasValidValues = setting.validValues && (setting.validValues.length > 0);
208
- // Check if this setting depends on a boolean toggle that is currently disabled. The depends-disabled class applies a visual grey-out without actually
209
- // disabling the inputs, so values are still submitted during save.
289
+ // Check if this setting depends on a boolean toggle the saved configuration turns off. The depends-disabled class applies a visual grey-out without actually
290
+ // disabling the inputs, so values are still submitted during save. The toggle is read from the saved configuration every field renders from, so the grey-out
291
+ // agrees with the toggle the form shows.
210
292
  const dependsOnId = setting.dependsOn ? setting.dependsOn.replaceAll(".", "-") : undefined;
211
- const isDependencyDisabled = setting.dependsOn ? !getNestedValue(CONFIG, setting.dependsOn) : false;
293
+ const isDependencyDisabled = setting.dependsOn ? !getNestedValue(getLoadedConfiguration(), setting.dependsOn) : false;
212
294
  // Build CSS classes for the form group.
213
295
  const groupClasses = ["form-group"];
214
296
  if (isDisabled) {
@@ -232,8 +314,13 @@ function generateSettingField(setting, currentValue, defaultValue, envOverride)
232
314
  lines.push("<span class=\"modified-dot\" title=\"Modified from default\"></span>");
233
315
  }
234
316
  lines.push(escapeHtml(setting.label));
317
+ // The class badge tells the user when a save of the setting takes effect, before the click. A class whose promise a save already makes carries none.
318
+ const classBadge = REACTIVITY_BADGES[setting.reactivity];
319
+ if (classBadge) {
320
+ lines.push(generateBadge(classBadge.label, { title: classBadge.title, variant: classBadge.variant }));
321
+ }
235
322
  if (envOverride !== undefined) {
236
- lines.push("<span class=\"env-badge\">ENV</span>");
323
+ lines.push(generateBadge("ENV", { variant: "env" }));
237
324
  }
238
325
  lines.push("</label>");
239
326
  // Block-level content that must appear outside the form-row flex container. Type branches that produce content too large for the inline flex layout (grids,
@@ -277,8 +364,8 @@ function generateSettingField(setting, currentValue, defaultValue, envOverride)
277
364
  lines.push("</select>");
278
365
  }
279
366
  else if (setting.type === "boolean") {
280
- // Render boolean as a checkbox. A hidden input with value "false" precedes the checkbox so that unchecking submits "false" rather than omitting the field
281
- // entirely (which would cause the server to skip it and fall back to the default).
367
+ // Render boolean as a checkbox. A hidden input with value "false" precedes the checkbox so that unchecking submits "false". Without it an unchecked box
368
+ // submits nothing, the save handler skips the path, and the merge keeps the saved true, so the uncheck would be lost.
282
369
  const isChecked = (currentValue === true) || (currentValue === "true");
283
370
  const defaultStr = defaultValue ? "true" : "false";
284
371
  lines.push("<input type=\"hidden\" name=\"" + setting.path + "\" value=\"false\">");
@@ -335,9 +422,10 @@ function generateSettingField(setting, currentValue, defaultValue, envOverride)
335
422
  else {
336
423
  // Render as input field.
337
424
  const inputType = (setting.type === "float") ? "number" : (((setting.type === "integer") || (setting.type === "port")) ? "number" : "text");
338
- // Calculate step for arrow key increments. Auto-derived from min/displayDivisor: when the min in display units is between 0 and 1 (exclusive), use it as the
339
- // step (e.g., 500ms -> 0.5s step); otherwise step is 1 whole display unit. This gives meaningful arrow increments and constrains input to a sensible value
340
- // grid (e.g., 0.5, 1.0, 1.5, ... for half-second steps).
425
+ // Calculate step for arrow key increments. With a displayDivisor and a min, the step is auto-derived: when the min in display units is between 0 and 1
426
+ // (exclusive), use it as the step (e.g., 500ms -> 0.5s step); otherwise the step is 1 whole display unit. A float with no displayDivisor steps by 0.01 so
427
+ // two-decimal values stay reachable, and every other number steps by one display unit. This gives meaningful arrow increments and constrains input to a
428
+ // sensible value grid (e.g., 0.5, 1.0, 1.5, ... for half-second steps).
341
429
  let step = "1";
342
430
  if (setting.displayDivisor && (setting.min !== undefined)) {
343
431
  const displayMin = setting.min / setting.displayDivisor;
@@ -393,35 +481,16 @@ function generateSettingField(setting, currentValue, defaultValue, envOverride)
393
481
  for (const content of postDescription) {
394
482
  lines.push(content);
395
483
  }
484
+ // Every field carries one marker slot, holding the server's sentence when the saved value is not the running one and hidden otherwise, so a save's response
485
+ // fills or clears it as text without creating markup.
486
+ lines.push("<div" + serializeAttrs({ "class": "form-pending" }) + serializeAttrs({ [PENDING_PATH_ATTRIBUTE]: setting.path, hidden: pending === undefined }) +
487
+ ">" + escapeHtml(pending?.text ?? "") + "</div>");
396
488
  // Add disabled reason warning when a setting is locked out due to an upstream issue.
397
489
  if (setting.disabledReason) {
398
490
  lines.push("<div class=\"form-warning\">" + escapeHtml(setting.disabledReason) + "</div>");
399
491
  }
400
- // Add default value hint with properly pluralized unit.
401
- let defaultDisplay;
402
- if (setting.type === "checkboxList") {
403
- // For checkbox lists, show the default items as a comma-separated list, or "none" for empty arrays.
404
- const defaultArr = Array.isArray(defaultValue) ? defaultValue : [];
405
- defaultDisplay = defaultArr.length > 0 ? defaultArr.join(", ") : "none";
406
- }
407
- else if (displayDefault === null) {
408
- defaultDisplay = "autodetect";
409
- }
410
- else if (typeof displayDefault === "number") {
411
- defaultDisplay = formatValueForDisplay(displayDefault, setting.type);
412
- }
413
- else {
414
- defaultDisplay = displayDefault;
415
- }
416
- // Format the unit with correct pluralization based on the default value.
417
- let formattedUnit = "";
418
- if (displayUnit && (typeof displayDefault === "number")) {
419
- formattedUnit = " " + formatUnitForValue(displayDefault, displayUnit);
420
- }
421
- else if (displayUnit) {
422
- formattedUnit = " " + displayUnit;
423
- }
424
- lines.push("<div class=\"form-default\">Default: " + escapeHtml(defaultDisplay) + formattedUnit + "</div>");
492
+ // Add the default value hint, read as the pending markers read a value.
493
+ lines.push("<div class=\"form-default\">Default: " + escapeHtml(formatSettingValue(defaultValue, setting)) + "</div>");
425
494
  // Add env var override notice if applicable.
426
495
  if (isDisabled && setting.envVar && envOverride) {
427
496
  lines.push("<div class=\"form-env\">Overridden by environment variable: <code>" + escapeHtml(setting.envVar) + "=" +
@@ -449,7 +518,7 @@ function validateSettingValue(setting, value) {
449
518
  // Validate string type with validValues.
450
519
  if ((setting.type === "string") && setting.validValues && (setting.validValues.length > 0)) {
451
520
  if (!setting.validValues.includes(value)) {
452
- return setting.label + " must be one of: " + setting.validValues.join(", ");
521
+ return setting.label + " must be one of: " + setting.validValues.join(", ") + ".";
453
522
  }
454
523
  return undefined;
455
524
  }
@@ -457,7 +526,7 @@ function validateSettingValue(setting, value) {
457
526
  switch (setting.type) {
458
527
  case "boolean": {
459
528
  if (typeof value !== "boolean") {
460
- return setting.label + " must be true or false";
529
+ return setting.label + " must be true or false.";
461
530
  }
462
531
  return undefined;
463
532
  }
@@ -478,35 +547,35 @@ function validateSettingValue(setting, value) {
478
547
  case "integer":
479
548
  case "port": {
480
549
  if (typeof value !== "number") {
481
- return setting.label + " must be a number";
550
+ return setting.label + " must be a number.";
482
551
  }
483
- const error = validatePositiveInt(setting.label, value, setting.min, setting.max);
552
+ const error = validateInteger(setting.label, value, setting.min, setting.max);
484
553
  return error ?? undefined;
485
554
  }
486
555
  case "float": {
487
556
  if (typeof value !== "number") {
488
- return setting.label + " must be a number";
557
+ return setting.label + " must be a number.";
489
558
  }
490
- const error = validatePositiveNumber(setting.label, value, setting.min, setting.max);
559
+ const error = validateNumber(setting.label, value, setting.min, setting.max);
491
560
  return error ?? undefined;
492
561
  }
493
562
  case "host": {
494
563
  if ((typeof value !== "string") || (value.trim() === "")) {
495
- return setting.label + " must be a non-empty string";
564
+ return setting.label + " must be a non-empty string.";
496
565
  }
497
566
  return undefined;
498
567
  }
499
568
  case "path": {
500
569
  // A path is any string. The empty and null forms mean autodetect and were already accepted by the guard at the top.
501
570
  if (typeof value !== "string") {
502
- return setting.label + " must be a string";
571
+ return setting.label + " must be a string.";
503
572
  }
504
573
  return undefined;
505
574
  }
506
575
  case "string": {
507
576
  // A free-form string carries no value constraint beyond being one. Settings that restrict their values declare validValues and were handled above.
508
577
  if (typeof value !== "string") {
509
- return setting.label + " must be a string";
578
+ return setting.label + " must be a string.";
510
579
  }
511
580
  return undefined;
512
581
  }
@@ -568,16 +637,34 @@ function parseFormValue(setting, value) {
568
637
  }
569
638
  }
570
639
  }
640
+ /**
641
+ * Generates the fields of a run of settings, the one per-field loop the Settings tab and every collapsible section share.
642
+ * @param settings - The settings to render, in order.
643
+ * @param context - The environment overrides and the pending view the page render resolved once.
644
+ * @returns One HTML string per field.
645
+ */
646
+ function generateSettingFields(settings, context) {
647
+ const defaults = getDefaults();
648
+ // The form shows the saved configuration, the loaded snapshot, rather than the running one. After a save that holds a change for a restart, a page load shows
649
+ // that change, so submitting the page cannot post the running value back over it.
650
+ const saved = getLoadedConfiguration();
651
+ return settings.map((setting) => generateSettingField({
652
+ currentValue: getNestedValue(saved, setting.path),
653
+ defaultValue: getNestedValue(defaults, setting.path),
654
+ envOverride: context.envOverrides.get(setting.path),
655
+ pending: context.pending.get(setting.path),
656
+ setting
657
+ }));
658
+ }
571
659
  /**
572
660
  * Generates the content for the Settings tab with non-collapsible section headers.
573
- * @param envOverrides - The environment overrides the page render resolved once and shares with every section it draws.
661
+ * @param context - The environment overrides and the pending view the page render resolved once and shares with every section it draws.
574
662
  * @returns HTML string for the Settings tab content.
575
663
  */
576
- export function generateSettingsTabContent(envOverrides) {
664
+ export function generateSettingsTabContent(context) {
577
665
  const sections = getSettingsTabSections();
578
666
  const tabs = getUITabs();
579
667
  const settingsTab = tabs.find((t) => t.id === "settings");
580
- const defaults = getDefaults();
581
668
  const lines = [];
582
669
  // Panel header with description and reset button.
583
670
  lines.push("<div class=\"panel-header\">");
@@ -590,12 +677,7 @@ export function generateSettingsTabContent(envOverrides) {
590
677
  lines.push("<div class=\"settings-section\">");
591
678
  lines.push("<div class=\"settings-section-header\">" + escapeHtml(section.displayName) + "</div>");
592
679
  // Generate setting fields for this section.
593
- for (const setting of section.settings) {
594
- const currentValue = getNestedValue(CONFIG, setting.path);
595
- const defaultValue = getNestedValue(defaults, setting.path);
596
- const envOverride = envOverrides.get(setting.path);
597
- lines.push(generateSettingField(setting, currentValue, defaultValue, envOverride));
598
- }
680
+ lines.push(...generateSettingFields(section.settings, context));
599
681
  lines.push("</div>");
600
682
  }
601
683
  return lines.join("\n");
@@ -603,30 +685,28 @@ export function generateSettingsTabContent(envOverrides) {
603
685
  /**
604
686
  * Generates the content for a collapsible section within the Advanced tab.
605
687
  * @param section - The section definition.
606
- * @param envOverrides - The environment overrides the page render resolved once and shares with every section it draws.
688
+ * @param context - The environment overrides and the pending view the page render resolved once and shares with every section it draws.
607
689
  * @returns HTML string for the section.
608
690
  */
609
- export function generateCollapsibleSection(section, envOverrides) {
610
- const defaults = getDefaults();
691
+ export function generateCollapsibleSection(section, context) {
611
692
  const lines = [];
612
- const settingCount = section.settings.length;
693
+ /* A section holding a pending setting is opened on the server, so a pending marker is never hidden behind a collapsed header, and nothing on the client has
694
+ * to remember it. The client's stored section state only ever adds the expanded class, so a section opened here stays open on load.
695
+ */
696
+ const expanded = section.settings.some((setting) => context.pending.has(setting.path)) ? " expanded" : "";
613
697
  // Section container.
614
698
  lines.push("<div class=\"advanced-section\" data-section=\"" + escapeHtml(section.id) + "\">");
615
699
  // Section header with chevron, title, and count.
616
- lines.push("<div class=\"section-header\" data-click-action=\"" + ACTIONS.toggleSection + "\" data-section-id=\"" + escapeHtml(section.id) + "\">");
700
+ lines.push("<div class=\"section-header" + expanded + "\" data-click-action=\"" + ACTIONS.toggleSection + "\" data-section-id=\"" + escapeHtml(section.id) +
701
+ "\">");
617
702
  lines.push("<span class=\"section-chevron\">&#9654;</span>");
618
703
  lines.push("<span class=\"section-title\">" + escapeHtml(section.displayName) + "</span>");
619
- lines.push("<span class=\"section-count\">(" + String(settingCount) + " setting" + (settingCount === 1 ? "" : "s") + ")</span>");
704
+ lines.push("<span class=\"section-count\">(" + formatSettingCount(section.settings.length) + ")</span>");
620
705
  lines.push("</div>");
621
- // Section content (collapsed by default).
622
- lines.push("<div class=\"section-content\">");
706
+ // Section content, collapsed unless the section holds a pending setting.
707
+ lines.push("<div class=\"section-content" + expanded + "\">");
623
708
  // Generate setting fields for this section.
624
- for (const setting of section.settings) {
625
- const currentValue = getNestedValue(CONFIG, setting.path);
626
- const defaultValue = getNestedValue(defaults, setting.path);
627
- const envOverride = envOverrides.get(setting.path);
628
- lines.push(generateSettingField(setting, currentValue, defaultValue, envOverride));
629
- }
709
+ lines.push(...generateSettingFields(section.settings, context));
630
710
  // Close section-content, then advanced-section, in reverse order of the opens above.
631
711
  lines.push("</div>");
632
712
  lines.push("</div>");
@@ -634,10 +714,10 @@ export function generateCollapsibleSection(section, envOverrides) {
634
714
  }
635
715
  /**
636
716
  * Generates the content for the Advanced tab with collapsible sections.
637
- * @param envOverrides - The environment overrides the page render resolved once and shares with every section it draws.
717
+ * @param context - The environment overrides and the pending view the page render resolved once and shares with every section it draws.
638
718
  * @returns HTML string for the Advanced tab content.
639
719
  */
640
- export function generateAdvancedTabContent(envOverrides) {
720
+ export function generateAdvancedTabContent(context) {
641
721
  const sections = getAdvancedSections();
642
722
  const tabs = getUITabs();
643
723
  const advancedTab = tabs.find((t) => t.id === "advanced");
@@ -650,7 +730,7 @@ export function generateAdvancedTabContent(envOverrides) {
650
730
  lines.push("</div>");
651
731
  // Generate each collapsible section.
652
732
  for (const section of sections) {
653
- lines.push(generateCollapsibleSection(section, envOverrides));
733
+ lines.push(generateCollapsibleSection(section, context));
654
734
  }
655
735
  return lines.join("\n");
656
736
  }
@@ -661,17 +741,37 @@ export function generateAdvancedTabContent(envOverrides) {
661
741
  export function generateSettingsFormFooter() {
662
742
  return "<div class=\"config-path\">Configuration file: <code>" + escapeHtml(getConfigFilePath()) + "</code></div>";
663
743
  }
744
+ /**
745
+ * The keys that name an object's prototype machinery rather than a setting. A document parsed from JSON carries any of them as an ordinary key, and assigning
746
+ * through one reaches a prototype instead of the configuration, so the merge refuses each of them at every level it walks.
747
+ */
748
+ const PROTOTYPE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
749
+ /**
750
+ * Refuses a document key that would reach an object's prototype rather than name a setting. The merge runs inside the save's store mutation, so the refusal
751
+ * writes nothing, and the route answers it as the validation error it is.
752
+ * @param key - The key the merge is about to assign through.
753
+ * @throws ConfigurationRejectedError when the key names prototype machinery.
754
+ */
755
+ function refusePrototypeKey(key) {
756
+ if (PROTOTYPE_KEYS.has(key)) {
757
+ throw new ConfigurationRejectedError("The configuration document carries the key " + key + ", which is not a setting.");
758
+ }
759
+ }
664
760
  /**
665
761
  * Shallow-merges a source config into a target config. Top-level object values are merged one level deep (preserving sibling keys within each category).
666
- * Non-object values are assigned directly. This is the single merge strategy used by both the settings form save and config import endpoints.
762
+ * Non-object values are assigned directly. This is the single merge strategy the settings form save and the config import share. A key that names prototype
763
+ * machinery, at any level the merge walks, is refused before anything is assigned through it.
667
764
  * @param target - The existing config to merge into (modified in place).
668
765
  * @param source - The new values to merge.
766
+ * @throws ConfigurationRejectedError when the source carries a key that is not a setting.
669
767
  */
670
768
  function mergeConfigValues(target, source) {
671
769
  for (const [path, value] of Object.entries(source)) {
770
+ refusePrototypeKey(path);
672
771
  if ((typeof value === "object") && (value !== null) && !Array.isArray(value)) {
673
772
  target[path] ??= {};
674
773
  for (const [subPath, subValue] of Object.entries(value)) {
774
+ refusePrototypeKey(subPath);
675
775
  target[path][subPath] = subValue;
676
776
  }
677
777
  }
@@ -680,6 +780,31 @@ function mergeConfigValues(target, source) {
680
780
  }
681
781
  }
682
782
  }
783
+ /**
784
+ * Builds the data envelope of a save response from the save's outcome. The settings save and the import share it, so each answers with the same fields: the
785
+ * restart members the API reference documents, the outcome by setting under changes, and the pending view under pending. changes.applied and
786
+ * changes.nextStream are everything this save's reconcile realized, a value an earlier save introduced and this save's retry realized included, while
787
+ * changes.deferred and changes.rejected are this save's own. pending is the whole view after the save, read here once the save has resolved, and the client
788
+ * replaces its markers from it rather than merging them. Exported so a unit row holds the mapping of every bucket, the next-stream bucket included, which a
789
+ * route reaches only through a setting of that class.
790
+ * @param outcome - The save's apply and restart result.
791
+ * @returns The response data.
792
+ */
793
+ export function buildSaveResponseData(outcome) {
794
+ const paths = (changes) => changes.map((change) => change.path);
795
+ return {
796
+ activeStreams: outcome.restart?.activeStreams ?? 0,
797
+ changes: {
798
+ applied: paths(outcome.apply.applied),
799
+ deferred: paths(outcome.apply.deferred),
800
+ nextStream: paths(outcome.apply.nextStream),
801
+ rejected: outcome.apply.rejected.map(({ change, reason }) => ({ path: change.path, reason }))
802
+ },
803
+ deferred: outcome.restart?.deferred ?? false,
804
+ pending: Array.from(collectPendingSettings().values()),
805
+ willRestart: outcome.restart?.willRestart ?? false
806
+ };
807
+ }
683
808
  /**
684
809
  * Installs all settings-related route handlers on the Express application.
685
810
  * @param app - The Express application.
@@ -725,28 +850,15 @@ export function setupSettingsRoutes(app) {
725
850
  sendFormErrors(res, validationErrors);
726
851
  return;
727
852
  }
728
- // Merge form values into the existing config via mutateConfig. The settings form only manages CONFIG_METADATA fields, but config.json also stores fields
729
- // managed by separate endpoints (disabledPredefined, enabledServices, visibleColumns, setupCompleted, channelSortField, channelSortDirection,
730
- // channelsDvr.host, hdhr.deviceId, etc.). Merging form values into the existing config preserves all non-form fields automatically - no carry-forward
731
- // list to maintain.
732
- await mutateConfig((existing) => {
853
+ // Merge form values into the existing config through the validated save. The settings form only manages CONFIG_METADATA fields, but config.json also
854
+ // stores fields managed by separate endpoints (disabledPredefined, enabledServices, visibleColumns, setupCompleted, channelSortField,
855
+ // channelSortDirection, channelsDvr.host, hdhr.deviceId, etc.). Merging form values into the existing config preserves all non-form fields
856
+ // automatically - no carry-forward list to maintain. The save then reconciles the running configuration against the file: each change takes effect as
857
+ // its reactivity class allows, and a restart is scheduled only when this save holds a restart-class change for one.
858
+ const outcome = await applyConfigurationChange("to apply configuration changes", (existing) => {
733
859
  mergeConfigValues(existing, newConfig);
734
860
  });
735
- // Reload the in-memory CONFIG from the freshly-written disk state and dispatch the diff to registered subsystem handlers. Subsystems that opted in to
736
- // live application (HDHR is the first) make their changes immediately; everything else is reported as deferred and triggers a service restart so the
737
- // change lands on the next boot.
738
- const outcome = await applyConfigurationChange("to apply configuration changes");
739
- sendSuccess(res, {
740
- data: {
741
- activeStreams: outcome.restart?.activeStreams ?? 0,
742
- appliedCount: outcome.apply.applied.length,
743
- deferred: outcome.restart?.deferred ?? false,
744
- deferredCount: outcome.apply.deferred.length,
745
- rejectedCount: outcome.apply.rejected.length,
746
- willRestart: outcome.restart?.willRestart ?? false
747
- },
748
- message: describeConfigurationOutcome(outcome)
749
- });
861
+ sendSuccess(res, { data: buildSaveResponseData(outcome), message: describeConfigurationOutcome(outcome) });
750
862
  }
751
863
  catch (error) {
752
864
  sendErrorResponse(res, error, "save configuration");
@@ -814,11 +926,13 @@ export function setupSettingsRoutes(app) {
814
926
  sendValidationError(res, "Validation errors:\n" + validationErrors.join("\n"));
815
927
  return;
816
928
  }
817
- // Import replaces the user settings layer and preserves the system state layer. CONFIG_METADATA is the SSOT for which fields are user settings
818
- // (port, timeouts, quality preset, etc.) vs system state (channelsDvr.host, deviceId, disabledPredefined, enabledServices, etc.). Clearing all
819
- // CONFIG_METADATA-tracked paths before merging ensures that settings not present in the import file revert to defaults rather than surviving
820
- // from the previous config. System state fields are untouched because they're not in CONFIG_METADATA.
821
- await mutateConfig((existing) => {
929
+ // Import replaces the user settings layer. CONFIG_METADATA is the SSOT for which fields are user settings (port, timeouts, quality preset, etc.) vs
930
+ // system state (channelsDvr.host, deviceId, disabledPredefined, enabledServices, etc.). Clearing all CONFIG_METADATA-tracked paths before merging
931
+ // ensures that settings not present in the import file revert to defaults rather than surviving from the previous config. The merge then copies every
932
+ // key the document carries, so a system-state field missing from the document keeps its current value, while one present in the document - as in a
933
+ // file written by the export, which carries the whole configuration - is merged over the current one. The save validates the result before anything
934
+ // reaches disk and reconciles the running configuration against it, the same path every write to the settings takes.
935
+ const outcome = await applyConfigurationChange("after configuration import", (existing) => {
822
936
  // Clear all user settings from the existing config, leaving only system state.
823
937
  for (const settings of Object.values(CONFIG_METADATA)) {
824
938
  for (const setting of settings) {
@@ -835,20 +949,7 @@ export function setupSettingsRoutes(app) {
835
949
  // Apply imported settings on top of the preserved system state.
836
950
  mergeConfigValues(existing, importedConfig);
837
951
  });
838
- // Reload and dispatch the diff. Import frequently changes more fields at once than the form save flow, so a higher fraction of imports will land in the
839
- // "restart required" path - but the same live-apply machinery still picks off any subsystem that opted in.
840
- const outcome = await applyConfigurationChange("after configuration import");
841
- sendSuccess(res, {
842
- data: {
843
- activeStreams: outcome.restart?.activeStreams ?? 0,
844
- appliedCount: outcome.apply.applied.length,
845
- deferred: outcome.restart?.deferred ?? false,
846
- deferredCount: outcome.apply.deferred.length,
847
- rejectedCount: outcome.apply.rejected.length,
848
- willRestart: outcome.restart?.willRestart ?? false
849
- },
850
- message: describeConfigurationOutcome(outcome)
851
- });
952
+ sendSuccess(res, { data: buildSaveResponseData(outcome), message: describeConfigurationOutcome(outcome) });
852
953
  }
853
954
  catch (error) {
854
955
  sendErrorResponse(res, error, "import configuration");
@@ -864,7 +965,7 @@ export function setupSettingsRoutes(app) {
864
965
  sendSuccess(res, { message: "Server is restarting..." });
865
966
  // Close the browser first to avoid orphan Chrome processes. The delay gives the success response above time to
866
967
  // reach the client before the process exits.
867
- setTimeout(() => {
968
+ systemClock.schedule(() => {
868
969
  LOG.info("Exiting for forced service manager restart.");
869
970
  void import("../../browser/index.js").then(async (mod) => mod.closeBrowser()).then(() => { process.exit(0); }).catch(() => { process.exit(1); });
870
971
  }, 500);