@homebridge-plugins/homebridge-eufy-security 4.7.1 → 5.0.0-beta.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 (469) hide show
  1. package/AUTHORS +4 -0
  2. package/CODE_OF_CONDUCT.md +30 -0
  3. package/CONTRIBUTING.md +77 -0
  4. package/README.md +152 -49
  5. package/SECURITY.md +30 -0
  6. package/config.schema.json +92 -3
  7. package/dist/account/ownership.js +290 -0
  8. package/dist/account/ownership.js.map +1 -0
  9. package/dist/account/persistence.js +274 -0
  10. package/dist/account/persistence.js.map +1 -0
  11. package/dist/account/temporary-authentication.js +348 -0
  12. package/dist/account/temporary-authentication.js.map +1 -0
  13. package/dist/configuration.js +167 -0
  14. package/dist/configuration.js.map +1 -0
  15. package/dist/device/member-evidence.js +66 -0
  16. package/dist/device/member-evidence.js.map +1 -0
  17. package/dist/device/member-trust.js +114 -0
  18. package/dist/device/member-trust.js.map +1 -0
  19. package/dist/device/snapshot.js +190 -0
  20. package/dist/device/snapshot.js.map +1 -0
  21. package/dist/diagnostics.js +2218 -0
  22. package/dist/diagnostics.js.map +1 -0
  23. package/dist/homekit/adapter.js +2 -0
  24. package/dist/homekit/adapter.js.map +1 -0
  25. package/dist/homekit/adapters/battery.js +208 -0
  26. package/dist/homekit/adapters/battery.js.map +1 -0
  27. package/dist/homekit/adapters/camera-controls.js +527 -0
  28. package/dist/homekit/adapters/camera-controls.js.map +1 -0
  29. package/dist/homekit/adapters/camera-streaming.js +1515 -0
  30. package/dist/homekit/adapters/camera-streaming.js.map +1 -0
  31. package/dist/homekit/adapters/contact.js +138 -0
  32. package/dist/homekit/adapters/contact.js.map +1 -0
  33. package/dist/homekit/adapters/doorbell.js +57 -0
  34. package/dist/homekit/adapters/doorbell.js.map +1 -0
  35. package/dist/homekit/adapters/information.js +55 -0
  36. package/dist/homekit/adapters/information.js.map +1 -0
  37. package/dist/homekit/adapters/lock.js +489 -0
  38. package/dist/homekit/adapters/lock.js.map +1 -0
  39. package/dist/homekit/adapters/motion.js +100 -0
  40. package/dist/homekit/adapters/motion.js.map +1 -0
  41. package/dist/homekit/adapters/registry.js +26 -0
  42. package/dist/homekit/adapters/registry.js.map +1 -0
  43. package/dist/homekit/adapters/security-system.js +425 -0
  44. package/dist/homekit/adapters/security-system.js.map +1 -0
  45. package/dist/homekit/adapters/siren.js +136 -0
  46. package/dist/homekit/adapters/siren.js.map +1 -0
  47. package/dist/homekit/adapters/smart-light.js +662 -0
  48. package/dist/homekit/adapters/smart-light.js.map +1 -0
  49. package/dist/homekit/coverage-matrix.js +119 -0
  50. package/dist/homekit/coverage-matrix.js.map +1 -0
  51. package/dist/homekit/device-control.js +175 -0
  52. package/dist/homekit/device-control.js.map +1 -0
  53. package/dist/homekit/reconciler.js +319 -0
  54. package/dist/homekit/reconciler.js.map +1 -0
  55. package/dist/homekit/representation.js +23 -0
  56. package/dist/homekit/representation.js.map +1 -0
  57. package/dist/index.js +4 -9
  58. package/dist/index.js.map +1 -1
  59. package/dist/media/contracts.js +2 -0
  60. package/dist/media/contracts.js.map +1 -0
  61. package/dist/media/last-successful-image.js +208 -0
  62. package/dist/media/last-successful-image.js.map +1 -0
  63. package/dist/media/live-stream.js +1003 -0
  64. package/dist/media/live-stream.js.map +1 -0
  65. package/dist/media/recording.js +560 -0
  66. package/dist/media/recording.js.map +1 -0
  67. package/dist/media/session-budget.js +39 -0
  68. package/dist/media/session-budget.js.map +1 -0
  69. package/dist/media/snapshot.js +406 -0
  70. package/dist/media/snapshot.js.map +1 -0
  71. package/dist/media/station-live-sessions.js +101 -0
  72. package/dist/media/station-live-sessions.js.map +1 -0
  73. package/dist/platform.js +97 -769
  74. package/dist/platform.js.map +1 -1
  75. package/dist/runtime/channel.js +375 -0
  76. package/dist/runtime/channel.js.map +1 -0
  77. package/dist/runtime/owner.js +682 -0
  78. package/dist/runtime/owner.js.map +1 -0
  79. package/dist/runtime/sdk-client.js +211 -0
  80. package/dist/runtime/sdk-client.js.map +1 -0
  81. package/dist/runtime/tracker.js +211 -0
  82. package/dist/runtime/tracker.js.map +1 -0
  83. package/dist/settings.js +1 -43
  84. package/dist/settings.js.map +1 -1
  85. package/dist/storage.js +59 -0
  86. package/dist/storage.js.map +1 -0
  87. package/dist/ui/dashboard.js +181 -0
  88. package/dist/ui/dashboard.js.map +1 -0
  89. package/dist/ui/device-image.js +22 -0
  90. package/dist/ui/device-image.js.map +1 -0
  91. package/dist/ui/runtime-channel-client.js +305 -0
  92. package/dist/ui/runtime-channel-client.js.map +1 -0
  93. package/dist/ui/server.js +259 -0
  94. package/dist/ui/server.js.map +1 -0
  95. package/homebridge-ui/public/app.css +1902 -0
  96. package/homebridge-ui/public/assets/devices/clean/clean-T1250.webp +0 -0
  97. package/homebridge-ui/public/assets/devices/clean/clean-T2070.webp +0 -0
  98. package/homebridge-ui/public/assets/devices/clean/clean-T2080.webp +0 -0
  99. package/homebridge-ui/public/assets/devices/clean/clean-T2081.webp +0 -0
  100. package/homebridge-ui/public/assets/devices/clean/clean-T2103.webp +0 -0
  101. package/homebridge-ui/public/assets/devices/clean/clean-T2117.webp +0 -0
  102. package/homebridge-ui/public/assets/devices/clean/clean-T2118.webp +0 -0
  103. package/homebridge-ui/public/assets/devices/clean/clean-T2119.webp +0 -0
  104. package/homebridge-ui/public/assets/devices/clean/clean-T211A.webp +0 -0
  105. package/homebridge-ui/public/assets/devices/clean/clean-T2120.webp +0 -0
  106. package/homebridge-ui/public/assets/devices/clean/clean-T2123.webp +0 -0
  107. package/homebridge-ui/public/assets/devices/clean/clean-T2128.webp +0 -0
  108. package/homebridge-ui/public/assets/devices/clean/clean-T2130.webp +0 -0
  109. package/homebridge-ui/public/assets/devices/clean/clean-T2132.webp +0 -0
  110. package/homebridge-ui/public/assets/devices/clean/clean-T2150.webp +0 -0
  111. package/homebridge-ui/public/assets/devices/clean/clean-T2181.webp +0 -0
  112. package/homebridge-ui/public/assets/devices/clean/clean-T2182.webp +0 -0
  113. package/homebridge-ui/public/assets/devices/clean/clean-T2190.webp +0 -0
  114. package/homebridge-ui/public/assets/devices/clean/clean-T2192.webp +0 -0
  115. package/homebridge-ui/public/assets/devices/clean/clean-T2193.webp +0 -0
  116. package/homebridge-ui/public/assets/devices/clean/clean-T2194.webp +0 -0
  117. package/homebridge-ui/public/assets/devices/clean/clean-T2210.webp +0 -0
  118. package/homebridge-ui/public/assets/devices/clean/clean-T2250.webp +0 -0
  119. package/homebridge-ui/public/assets/devices/clean/clean-T2251.webp +0 -0
  120. package/homebridge-ui/public/assets/devices/clean/clean-T2252.webp +0 -0
  121. package/homebridge-ui/public/assets/devices/clean/clean-T2253.webp +0 -0
  122. package/homebridge-ui/public/assets/devices/clean/clean-T2254.webp +0 -0
  123. package/homebridge-ui/public/assets/devices/clean/clean-T2255.webp +0 -0
  124. package/homebridge-ui/public/assets/devices/clean/clean-T2256.webp +0 -0
  125. package/homebridge-ui/public/assets/devices/clean/clean-T2257.webp +0 -0
  126. package/homebridge-ui/public/assets/devices/clean/clean-T2258.webp +0 -0
  127. package/homebridge-ui/public/assets/devices/clean/clean-T2259.webp +0 -0
  128. package/homebridge-ui/public/assets/devices/clean/clean-T2261.webp +0 -0
  129. package/homebridge-ui/public/assets/devices/clean/clean-T2262.webp +0 -0
  130. package/homebridge-ui/public/assets/devices/clean/clean-T2266.webp +0 -0
  131. package/homebridge-ui/public/assets/devices/clean/clean-T2267.webp +0 -0
  132. package/homebridge-ui/public/assets/devices/clean/clean-T2268.webp +0 -0
  133. package/homebridge-ui/public/assets/devices/clean/clean-T2270.webp +0 -0
  134. package/homebridge-ui/public/assets/devices/clean/clean-T2272.webp +0 -0
  135. package/homebridge-ui/public/assets/devices/clean/clean-T2273.webp +0 -0
  136. package/homebridge-ui/public/assets/devices/clean/clean-T2276.webp +0 -0
  137. package/homebridge-ui/public/assets/devices/clean/clean-T2277.webp +0 -0
  138. package/homebridge-ui/public/assets/devices/clean/clean-T2278.webp +0 -0
  139. package/homebridge-ui/public/assets/devices/clean/clean-T2280.webp +0 -0
  140. package/homebridge-ui/public/assets/devices/clean/clean-T2292.webp +0 -0
  141. package/homebridge-ui/public/assets/devices/clean/clean-T2320.webp +0 -0
  142. package/homebridge-ui/public/assets/devices/clean/clean-T2351.webp +0 -0
  143. package/homebridge-ui/public/assets/devices/clean/clean-T2352.webp +0 -0
  144. package/homebridge-ui/public/assets/devices/clean/clean-T2353.webp +0 -0
  145. package/homebridge-ui/public/assets/devices/life/life-T8L00.webp +0 -0
  146. package/homebridge-ui/public/assets/devices/life/life-T8L01.webp +0 -0
  147. package/homebridge-ui/public/assets/devices/life/life-T8L02.webp +0 -0
  148. package/homebridge-ui/public/assets/devices/life/life-T8L04.webp +0 -0
  149. package/homebridge-ui/public/assets/devices/life/life-T8L10.webp +0 -0
  150. package/homebridge-ui/public/assets/devices/life/life-T8L20.webp +0 -0
  151. package/homebridge-ui/public/assets/devices/life/life-T8L30.webp +0 -0
  152. package/homebridge-ui/public/assets/devices/life/life-T8L40.webp +0 -0
  153. package/homebridge-ui/public/assets/devices/mower/mower-T2801.webp +0 -0
  154. package/homebridge-ui/public/assets/devices/mower/mower-T280B.webp +0 -0
  155. package/homebridge-ui/public/assets/devices/mower/mower-T2880.webp +0 -0
  156. package/homebridge-ui/public/assets/devices/security/security-T7400.webp +0 -0
  157. package/homebridge-ui/public/assets/devices/security/security-T7401.webp +0 -0
  158. package/homebridge-ui/public/assets/devices/security/security-T8001.webp +0 -0
  159. package/homebridge-ui/public/assets/devices/security/security-T8002.webp +0 -0
  160. package/homebridge-ui/public/assets/devices/security/security-T8010.webp +0 -0
  161. package/homebridge-ui/public/assets/devices/security/security-T8021.webp +0 -0
  162. package/homebridge-ui/public/assets/devices/security/security-T8023.webp +0 -0
  163. package/homebridge-ui/public/assets/devices/security/security-T8025.webp +0 -0
  164. package/homebridge-ui/public/assets/devices/security/security-T8030.webp +0 -0
  165. package/homebridge-ui/public/assets/devices/security/security-T8110.webp +0 -0
  166. package/homebridge-ui/public/assets/devices/security/security-T8111.webp +0 -0
  167. package/homebridge-ui/public/assets/devices/security/security-T8112.webp +0 -0
  168. package/homebridge-ui/public/assets/devices/security/security-T8113.webp +0 -0
  169. package/homebridge-ui/public/assets/devices/security/security-T8114.webp +0 -0
  170. package/homebridge-ui/public/assets/devices/security/security-T8122.webp +0 -0
  171. package/homebridge-ui/public/assets/devices/security/security-T8123.webp +0 -0
  172. package/homebridge-ui/public/assets/devices/security/security-T8124.webp +0 -0
  173. package/homebridge-ui/public/assets/devices/security/security-T8130.webp +0 -0
  174. package/homebridge-ui/public/assets/devices/security/security-T8131.webp +0 -0
  175. package/homebridge-ui/public/assets/devices/security/security-T8134.webp +0 -0
  176. package/homebridge-ui/public/assets/devices/security/security-T8140.webp +0 -0
  177. package/homebridge-ui/public/assets/devices/security/security-T8141.webp +0 -0
  178. package/homebridge-ui/public/assets/devices/security/security-T8150.webp +0 -0
  179. package/homebridge-ui/public/assets/devices/security/security-T8151.webp +0 -0
  180. package/homebridge-ui/public/assets/devices/security/security-T8152.webp +0 -0
  181. package/homebridge-ui/public/assets/devices/security/security-T8153.webp +0 -0
  182. package/homebridge-ui/public/assets/devices/security/security-T8160.webp +0 -0
  183. package/homebridge-ui/public/assets/devices/security/security-T8161.webp +0 -0
  184. package/homebridge-ui/public/assets/devices/security/security-T8162.webp +0 -0
  185. package/homebridge-ui/public/assets/devices/security/security-T8170.webp +0 -0
  186. package/homebridge-ui/public/assets/devices/security/security-T8171.webp +0 -0
  187. package/homebridge-ui/public/assets/devices/security/security-T8172.webp +0 -0
  188. package/homebridge-ui/public/assets/devices/security/security-T8173.webp +0 -0
  189. package/homebridge-ui/public/assets/devices/security/security-T8200.webp +0 -0
  190. package/homebridge-ui/public/assets/devices/security/security-T8200X.webp +0 -0
  191. package/homebridge-ui/public/assets/devices/security/security-T8201.webp +0 -0
  192. package/homebridge-ui/public/assets/devices/security/security-T8203.webp +0 -0
  193. package/homebridge-ui/public/assets/devices/security/security-T8210.webp +0 -0
  194. package/homebridge-ui/public/assets/devices/security/security-T8213.webp +0 -0
  195. package/homebridge-ui/public/assets/devices/security/security-T8214.webp +0 -0
  196. package/homebridge-ui/public/assets/devices/security/security-T8222.webp +0 -0
  197. package/homebridge-ui/public/assets/devices/security/security-T8400.webp +0 -0
  198. package/homebridge-ui/public/assets/devices/security/security-T8401.webp +0 -0
  199. package/homebridge-ui/public/assets/devices/security/security-T8410.webp +0 -0
  200. package/homebridge-ui/public/assets/devices/security/security-T8411.webp +0 -0
  201. package/homebridge-ui/public/assets/devices/security/security-T8414.webp +0 -0
  202. package/homebridge-ui/public/assets/devices/security/security-T8416.webp +0 -0
  203. package/homebridge-ui/public/assets/devices/security/security-T8417.webp +0 -0
  204. package/homebridge-ui/public/assets/devices/security/security-T8419.webp +0 -0
  205. package/homebridge-ui/public/assets/devices/security/security-T8419N.webp +0 -0
  206. package/homebridge-ui/public/assets/devices/security/security-T8420.webp +0 -0
  207. package/homebridge-ui/public/assets/devices/security/security-T8420X.webp +0 -0
  208. package/homebridge-ui/public/assets/devices/security/security-T8422.webp +0 -0
  209. package/homebridge-ui/public/assets/devices/security/security-T8423.webp +0 -0
  210. package/homebridge-ui/public/assets/devices/security/security-T8424.webp +0 -0
  211. package/homebridge-ui/public/assets/devices/security/security-T8425.webp +0 -0
  212. package/homebridge-ui/public/assets/devices/security/security-T8441.webp +0 -0
  213. package/homebridge-ui/public/assets/devices/security/security-T8442.webp +0 -0
  214. package/homebridge-ui/public/assets/devices/security/security-T8452.webp +0 -0
  215. package/homebridge-ui/public/assets/devices/security/security-T84A0.webp +0 -0
  216. package/homebridge-ui/public/assets/devices/security/security-T84A1.webp +0 -0
  217. package/homebridge-ui/public/assets/devices/security/security-T8500.webp +0 -0
  218. package/homebridge-ui/public/assets/devices/security/security-T8502.webp +0 -0
  219. package/homebridge-ui/public/assets/devices/security/security-T8503.webp +0 -0
  220. package/homebridge-ui/public/assets/devices/security/security-T8504.webp +0 -0
  221. package/homebridge-ui/public/assets/devices/security/security-T8506.webp +0 -0
  222. package/homebridge-ui/public/assets/devices/security/security-T8510.webp +0 -0
  223. package/homebridge-ui/public/assets/devices/security/security-T8510P.webp +0 -0
  224. package/homebridge-ui/public/assets/devices/security/security-T8520.webp +0 -0
  225. package/homebridge-ui/public/assets/devices/security/security-T8520P.webp +0 -0
  226. package/homebridge-ui/public/assets/devices/security/security-T8530.webp +0 -0
  227. package/homebridge-ui/public/assets/devices/security/security-T8531.webp +0 -0
  228. package/homebridge-ui/public/assets/devices/security/security-T85D0.webp +0 -0
  229. package/homebridge-ui/public/assets/devices/security/security-T85L0.webp +0 -0
  230. package/homebridge-ui/public/assets/devices/security/security-T85P0.webp +0 -0
  231. package/homebridge-ui/public/assets/devices/security/security-T85V0.webp +0 -0
  232. package/homebridge-ui/public/assets/devices/security/security-T8600.webp +0 -0
  233. package/homebridge-ui/public/assets/devices/security/security-T86P2.webp +0 -0
  234. package/homebridge-ui/public/assets/devices/security/security-T8790.webp +0 -0
  235. package/homebridge-ui/public/assets/devices/security/security-T87B0.webp +0 -0
  236. package/homebridge-ui/public/assets/devices/security/security-T87B2.webp +0 -0
  237. package/homebridge-ui/public/assets/devices/security/security-T8900.webp +0 -0
  238. package/homebridge-ui/public/assets/devices/security/security-T8910.webp +0 -0
  239. package/homebridge-ui/public/assets/devices/security/security-T8920.webp +0 -0
  240. package/homebridge-ui/public/assets/devices/security/security-T8960.webp +0 -0
  241. package/homebridge-ui/public/assets/devices/security/security-T8B0.webp +0 -0
  242. package/homebridge-ui/public/assets/devices/security/security-T8E00.webp +0 -0
  243. package/homebridge-ui/public/assets/devices/security/security-T8N00.webp +0 -0
  244. package/homebridge-ui/public/assets/devices/security/security-T8W11C.webp +0 -0
  245. package/homebridge-ui/public/assets/devices/security/security-T90F0.webp +0 -0
  246. package/homebridge-ui/public/assets/devices/security/security-T90M0.webp +0 -0
  247. package/homebridge-ui/public/assets/devices/security/security-T90R0.webp +0 -0
  248. package/homebridge-ui/public/assets/devices/security/security-T90S0.webp +0 -0
  249. package/homebridge-ui/public/assets/icons/inventory.svg +1 -1
  250. package/homebridge-ui/public/assets/icons/troubleshoot.svg +1 -0
  251. package/homebridge-ui/public/assets/icons/warning.svg +1 -1
  252. package/homebridge-ui/public/assets/logo-dark.svg +10 -0
  253. package/homebridge-ui/public/assets/logo.svg +10 -0
  254. package/homebridge-ui/public/i18n/en.json +218 -0
  255. package/homebridge-ui/public/i18n/fr.json +218 -0
  256. package/homebridge-ui/public/index.html +377 -28
  257. package/homebridge-ui/public/js/app.js +1065 -0
  258. package/homebridge-ui/public/js/dashboard.js +385 -0
  259. package/homebridge-ui/public/js/legacy-settings.js +37 -0
  260. package/homebridge-ui/public/js/profile-wizard.js +68 -0
  261. package/homebridge-ui/server.js +2 -1393
  262. package/i18n/runtime/en.json +100 -0
  263. package/media/Snapshot-Unavailable.jpg +0 -0
  264. package/media/Snapshot-black.jpg +0 -0
  265. package/media/camera-disabled.jpg +0 -0
  266. package/media/camera-offline.jpg +0 -0
  267. package/package.json +29 -24
  268. package/.claude/CLAUDE.md +0 -175
  269. package/.claude/PRD.md +0 -241
  270. package/.claude/docs/debug-recording-lessons.md +0 -91
  271. package/.claude/docs/hksv-recording-fix.md +0 -160
  272. package/.claude/skills/architect/SKILL.md +0 -76
  273. package/.claude/skills/developer/SKILL.md +0 -59
  274. package/.claude/skills/new-device-support/SKILL.md +0 -212
  275. package/.claude/skills/new-device-support/check-device.mjs +0 -363
  276. package/.claude/skills/new-device-support/map-properties.mjs +0 -450
  277. package/.claude/skills/new-device-support/verify-device.mjs +0 -272
  278. package/.claude/skills/planner/SKILL.md +0 -100
  279. package/.claude/skills/qa/SKILL.md +0 -79
  280. package/.claude/skills/support/SKILL.md +0 -175
  281. package/.prettierignore +0 -7
  282. package/.prettierrc.json +0 -7
  283. package/FUNDING.yml +0 -1
  284. package/dist/accessories/AutoSyncStationAccessory.js +0 -157
  285. package/dist/accessories/AutoSyncStationAccessory.js.map +0 -1
  286. package/dist/accessories/BaseAccessory.js +0 -261
  287. package/dist/accessories/BaseAccessory.js.map +0 -1
  288. package/dist/accessories/CameraAccessory.js +0 -431
  289. package/dist/accessories/CameraAccessory.js.map +0 -1
  290. package/dist/accessories/Device.js +0 -65
  291. package/dist/accessories/Device.js.map +0 -1
  292. package/dist/accessories/EntrySensorAccessory.js +0 -48
  293. package/dist/accessories/EntrySensorAccessory.js.map +0 -1
  294. package/dist/accessories/LockAccessory.js +0 -142
  295. package/dist/accessories/LockAccessory.js.map +0 -1
  296. package/dist/accessories/MotionSensorAccessory.js +0 -48
  297. package/dist/accessories/MotionSensorAccessory.js.map +0 -1
  298. package/dist/accessories/SmartDropAccessory.js +0 -141
  299. package/dist/accessories/SmartDropAccessory.js.map +0 -1
  300. package/dist/accessories/StationAccessory.js +0 -373
  301. package/dist/accessories/StationAccessory.js.map +0 -1
  302. package/dist/config.js +0 -26
  303. package/dist/config.js.map +0 -1
  304. package/dist/controller/DebugRecordingManager.js +0 -402
  305. package/dist/controller/DebugRecordingManager.js.map +0 -1
  306. package/dist/controller/LocalLivestreamManager.js +0 -371
  307. package/dist/controller/LocalLivestreamManager.js.map +0 -1
  308. package/dist/controller/recordingDelegate.js +0 -252
  309. package/dist/controller/recordingDelegate.js.map +0 -1
  310. package/dist/controller/snapshotDelegate.js +0 -348
  311. package/dist/controller/snapshotDelegate.js.map +0 -1
  312. package/dist/controller/streamingDelegate.js +0 -389
  313. package/dist/controller/streamingDelegate.js.map +0 -1
  314. package/dist/interfaces.js +0 -2
  315. package/dist/interfaces.js.map +0 -1
  316. package/dist/media/Snapshot-Unavailable.png +0 -0
  317. package/dist/media/Snapshot-Unavailable.xcf +0 -0
  318. package/dist/media/Snapshot-black.png +0 -0
  319. package/dist/media/camera-disabled.png +0 -0
  320. package/dist/media/camera-offline.png +0 -0
  321. package/dist/media/media/Snapshot-Unavailable.png +0 -0
  322. package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
  323. package/dist/media/media/Snapshot-black.png +0 -0
  324. package/dist/media/media/camera-disabled.png +0 -0
  325. package/dist/media/media/camera-offline.png +0 -0
  326. package/dist/utils/Talkback.js +0 -162
  327. package/dist/utils/Talkback.js.map +0 -1
  328. package/dist/utils/accessoriesStore.js +0 -207
  329. package/dist/utils/accessoriesStore.js.map +0 -1
  330. package/dist/utils/configTypes.js +0 -36
  331. package/dist/utils/configTypes.js.map +0 -1
  332. package/dist/utils/ffmpeg.js +0 -1111
  333. package/dist/utils/ffmpeg.js.map +0 -1
  334. package/dist/utils/interfaces.js +0 -8
  335. package/dist/utils/interfaces.js.map +0 -1
  336. package/dist/utils/utils.js +0 -224
  337. package/dist/utils/utils.js.map +0 -1
  338. package/dist/version.js +0 -2
  339. package/dist/version.js.map +0 -1
  340. package/homebridge-eufy-security.png +0 -0
  341. package/homebridge-ui/diagnosticsCrypto.js +0 -112
  342. package/homebridge-ui/public/app.js +0 -237
  343. package/homebridge-ui/public/assets/devices/4g_lte_cam_s330_large.png +0 -0
  344. package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
  345. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
  346. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
  347. package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
  348. package/homebridge-ui/public/assets/devices/batterydoorbell2k_large.png +0 -0
  349. package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
  350. package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
  351. package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
  352. package/homebridge-ui/public/assets/devices/eufyCamS4_large.png +0 -0
  353. package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
  354. package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
  355. package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
  356. package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
  357. package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
  358. package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
  359. package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
  360. package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
  361. package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
  362. package/homebridge-ui/public/assets/devices/flood_freeze_sensor_e20_t90F0_large.png +0 -0
  363. package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
  364. package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
  365. package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
  366. package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
  367. package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.png +0 -0
  368. package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
  369. package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
  370. package/homebridge-ui/public/assets/devices/homebase_large.jpg +0 -0
  371. package/homebridge-ui/public/assets/devices/homebasemini_large.png +0 -0
  372. package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
  373. package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
  374. package/homebridge-ui/public/assets/devices/indoorcamC220_t8419N_large.png +0 -0
  375. package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
  376. package/homebridge-ui/public/assets/devices/indoorcamc120_large.png +0 -0
  377. package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
  378. package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
  379. package/homebridge-ui/public/assets/devices/indoorcams350_large.png +0 -0
  380. package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
  381. package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
  382. package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
  383. package/homebridge-ui/public/assets/devices/nvr_s4_max_T8N00_large.png +0 -0
  384. package/homebridge-ui/public/assets/devices/pir_sensor_e20_large.png +0 -0
  385. package/homebridge-ui/public/assets/devices/poe_bullet_ptz_cam_s4_T8E00_large.png +0 -0
  386. package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
  387. package/homebridge-ui/public/assets/devices/siren_sensor_e20_large.png +0 -0
  388. package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
  389. package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
  390. package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
  391. package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
  392. package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
  393. package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
  394. package/homebridge-ui/public/assets/devices/smartlock_t85D0_large.png +0 -0
  395. package/homebridge-ui/public/assets/devices/smartlock_t85L0_large.png +0 -0
  396. package/homebridge-ui/public/assets/devices/smartlock_t85P0_large.png +0 -0
  397. package/homebridge-ui/public/assets/devices/smartlock_t85V0_large.png +0 -0
  398. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
  399. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
  400. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
  401. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
  402. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
  403. package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
  404. package/homebridge-ui/public/assets/devices/smartlock_video_t8531_large.png +0 -0
  405. package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
  406. package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
  407. package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
  408. package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
  409. package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
  410. package/homebridge-ui/public/assets/devices/smoke_alarm_e10_t90S0_large.png +0 -0
  411. package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
  412. package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
  413. package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
  414. package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
  415. package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
  416. package/homebridge-ui/public/assets/devices/solocame42_large.png +0 -0
  417. package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
  418. package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
  419. package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
  420. package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
  421. package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
  422. package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
  423. package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
  424. package/homebridge-ui/public/assets/devices/unknown.png +0 -0
  425. package/homebridge-ui/public/assets/devices/walllight_s100_large.png +0 -0
  426. package/homebridge-ui/public/assets/devices/walllight_s120_large.png +0 -0
  427. package/homebridge-ui/public/assets/devices/water_freeze_sensor_t8920_large.png +0 -0
  428. package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
  429. package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
  430. package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
  431. package/homebridge-ui/public/assets/icons/attach.svg +0 -1
  432. package/homebridge-ui/public/assets/icons/battery_0.svg +0 -1
  433. package/homebridge-ui/public/assets/icons/battery_1.svg +0 -1
  434. package/homebridge-ui/public/assets/icons/battery_2.svg +0 -1
  435. package/homebridge-ui/public/assets/icons/battery_3.svg +0 -1
  436. package/homebridge-ui/public/assets/icons/battery_4.svg +0 -1
  437. package/homebridge-ui/public/assets/icons/battery_5.svg +0 -1
  438. package/homebridge-ui/public/assets/icons/battery_6.svg +0 -1
  439. package/homebridge-ui/public/assets/icons/copy.svg +0 -1
  440. package/homebridge-ui/public/assets/icons/delete.svg +0 -1
  441. package/homebridge-ui/public/assets/icons/download.svg +0 -1
  442. package/homebridge-ui/public/assets/icons/info.svg +0 -1
  443. package/homebridge-ui/public/assets/icons/refresh.svg +0 -1
  444. package/homebridge-ui/public/assets/icons/satellite_alt.svg +0 -1
  445. package/homebridge-ui/public/assets/icons/solar_power.svg +0 -1
  446. package/homebridge-ui/public/components/device-card.js +0 -174
  447. package/homebridge-ui/public/components/guard-modes.js +0 -88
  448. package/homebridge-ui/public/components/number-input.js +0 -121
  449. package/homebridge-ui/public/components/select.js +0 -73
  450. package/homebridge-ui/public/components/toggle.js +0 -68
  451. package/homebridge-ui/public/services/api.js +0 -250
  452. package/homebridge-ui/public/services/config.js +0 -144
  453. package/homebridge-ui/public/style.css +0 -818
  454. package/homebridge-ui/public/utils/countries.js +0 -73
  455. package/homebridge-ui/public/utils/device-images.js +0 -102
  456. package/homebridge-ui/public/utils/helpers.js +0 -115
  457. package/homebridge-ui/public/views/dashboard.js +0 -226
  458. package/homebridge-ui/public/views/device-detail.js +0 -626
  459. package/homebridge-ui/public/views/diagnostics.js +0 -402
  460. package/homebridge-ui/public/views/login.js +0 -650
  461. package/homebridge-ui/public/views/recordings.js +0 -319
  462. package/homebridge-ui/public/views/settings.js +0 -197
  463. package/homebridge-ui/public/views/unsupported-detail.js +0 -296
  464. package/media/Snapshot-Unavailable.png +0 -0
  465. package/media/Snapshot-Unavailable.xcf +0 -0
  466. package/media/Snapshot-black.png +0 -0
  467. package/media/camera-disabled.png +0 -0
  468. package/media/camera-offline.png +0 -0
  469. package/scripts/decrypt-diagnostics.mjs +0 -257
@@ -0,0 +1,1065 @@
1
+ const shell = document.querySelector('[data-ui-shell]');
2
+ const masthead = document.querySelector('[data-masthead]');
3
+ const firstSetup = document.querySelector('[data-first-setup]');
4
+ const setupContent = document.querySelector('[data-setup-content]');
5
+ const acknowledgement = document.querySelector('[data-first-setup-ack]');
6
+ const continueButton = document.querySelector('[data-first-setup-continue]');
7
+ const authForm = document.querySelector('[data-auth-form]');
8
+ const challengeForm = document.querySelector('[data-challenge-form]');
9
+ const accountInput = document.querySelector('[data-account]');
10
+ const passwordInput = document.querySelector('[data-password]');
11
+ const countryInput = document.querySelector('[data-country]');
12
+ const trustedDeviceInput = document.querySelector('[data-trusted-device-name]');
13
+ const challengeAnswer = document.querySelector('[data-challenge-answer]');
14
+ const challengeImage = document.querySelector('[data-challenge-image]');
15
+ const challengeLabel = document.querySelector('[data-challenge-label]');
16
+ const authStatus = document.querySelector('[data-auth-status]');
17
+ const authSubmit = document.querySelector('[data-auth-submit]');
18
+ const challengeSubmit = document.querySelector('[data-challenge-submit]');
19
+ const dashboard = document.querySelector('[data-dashboard]');
20
+ const dashboardState = document.querySelector('[data-dashboard-state]');
21
+ const dashboardTitle = document.querySelector('[data-dashboard-title]');
22
+ const dashboardBadge = document.querySelector('[data-dashboard-badge]');
23
+ const dashboardSummary = document.querySelector('[data-dashboard-summary]');
24
+ const dashboardAuthenticate = document.querySelector('[data-dashboard-authenticate]');
25
+ const deviceGroups = document.querySelector('[data-device-groups]');
26
+ const updatePending = document.querySelector('[data-update-pending]');
27
+ const updatePendingSummary = document.querySelector('[data-update-pending-summary]');
28
+ const updatePendingVersion = document.querySelector('[data-update-pending-version]');
29
+ const updateRestart = document.querySelector('[data-update-restart]');
30
+ const pageTitle = document.querySelector('[data-page-title]');
31
+ const legacyNotice = document.querySelector('[data-legacy-notice]');
32
+ const legacySettings = document.querySelector('[data-legacy-settings]');
33
+ const legacyAcknowledge = document.querySelector('[data-legacy-acknowledge]');
34
+ const menuDiagnostics = document.querySelector('[data-menu-diagnostics]');
35
+ const menuAdvanced = document.querySelector('[data-menu-advanced]');
36
+ const diagnosticsPanel = document.querySelector('[data-diagnostics]');
37
+ const mastheadDiagnostics = document.querySelector('[data-masthead-diagnostics]');
38
+ const diagnosticsClose = document.querySelector('[data-diagnostics-close]');
39
+ const diagnosticsWizardPanel = document.querySelector('[data-diagnostics-wizard]');
40
+ const diagnosticsQuestion = document.querySelector('[data-diagnostics-question]');
41
+ const diagnosticsQuestionText = document.querySelector('[data-diagnostics-question-text]');
42
+ const diagnosticsYes = document.querySelector('[data-diagnostics-answer="yes"]');
43
+ const diagnosticsNo = document.querySelector('[data-diagnostics-answer="no"]');
44
+ const diagnosticsDirect = document.querySelector('[data-diagnostics-direct]');
45
+ const diagnosticsDirectPanel = document.querySelector('[data-diagnostics-direct-panel]');
46
+ const diagnosticsDirectChoose = document.querySelector('[data-diagnostics-direct-choose]');
47
+ const diagnosticsDirectBack = document.querySelector('[data-diagnostics-direct-back]');
48
+ const diagnosticsFrequency = document.querySelector('[data-diagnostics-frequency]');
49
+ const diagnosticsFrequencyHeading = document.querySelector('#diagnostics-frequency-heading');
50
+ const diagnosticsFrequencyNow = document.querySelector('[data-diagnostics-frequency-answer="now"]');
51
+ const diagnosticsFrequencyIntermittent = document.querySelector('[data-diagnostics-frequency-answer="intermittent"]');
52
+ const diagnosticsFrequencyBack = document.querySelector('[data-diagnostics-frequency-back]');
53
+ const diagnosticsMatch = document.querySelector('[data-diagnostics-match]');
54
+ const diagnosticsReject = document.querySelector('[data-diagnostics-reject]');
55
+ const diagnosticsProfile = document.querySelector('[data-diagnostics-profile]');
56
+ const diagnosticsAuthorize = document.querySelector('[data-diagnostics-authorize]');
57
+ const diagnosticsReproduction = document.querySelector('[data-diagnostics-reproduction]');
58
+ const diagnosticsStatus = document.querySelector('[data-diagnostics-status]');
59
+ const diagnosticsIssue = document.querySelector('[data-diagnostics-issue]');
60
+ const diagnosticsResult = document.querySelector('[data-diagnostics-result]');
61
+ const diagnosticsActions = document.querySelector('[data-diagnostics-actions]');
62
+ const diagnosticsGuidanceTitle = document.querySelector('[data-diagnostics-guidance-title]');
63
+ const diagnosticsModeSummary = document.querySelector('[data-diagnostics-mode-summary]');
64
+ const diagnosticsGuidance = document.querySelector('[data-diagnostics-guidance]');
65
+ const diagnosticsPhaseTitle = document.querySelector('[data-diagnostics-phase-title]');
66
+ const diagnosticsGuidanceBeforeSection = document.querySelector('[data-diagnostics-guidance-before-section]');
67
+ const diagnosticsGuidanceBefore = document.querySelector('[data-diagnostics-guidance-before]');
68
+ const diagnosticsGuidanceAction = document.querySelector('[data-diagnostics-guidance-action]');
69
+ const diagnosticsReview = document.querySelector('[data-diagnostics-review]');
70
+ const diagnosticsManifest = document.querySelector('[data-diagnostics-manifest]');
71
+ const diagnosticsReviewConfirm = document.querySelector('[data-diagnostics-review-confirm]');
72
+ const diagnosticsReviewConfirmLabel = document.querySelector('[data-diagnostics-review-confirm-label]');
73
+ const diagnosticsExport = document.querySelector('[data-diagnostics-export]');
74
+ const diagnosticsResultHeading = document.querySelector('[data-diagnostics-result-heading]');
75
+ const diagnosticsStartAnother = document.querySelector('[data-diagnostics-start-another]');
76
+ const advancedPanel = document.querySelector('[data-advanced-settings]');
77
+ const dashboardDiagnose = document.querySelector('[data-dashboard-diagnose]');
78
+ const devicePanel = document.querySelector('[data-device-settings]');
79
+ const deviceClose = document.querySelector('[data-device-close]');
80
+ const deviceSettingsTitle = document.querySelector('[data-device-settings-title]');
81
+ const deviceSettingsEyebrow = document.querySelector('[data-device-settings-eyebrow]');
82
+ const deviceSettingsControls = document.querySelector('[data-device-settings-controls]');
83
+ const advancedClose = document.querySelector('[data-advanced-close]');
84
+ const advancedPolling = document.querySelector('[data-advanced-polling]');
85
+ const advancedConcurrentMedia = document.querySelector('[data-advanced-concurrent-media]');
86
+ const advancedFfmpeg = document.querySelector('[data-advanced-ffmpeg]');
87
+ const warmUpAvailable = document.querySelector('[data-warm-up-available]');
88
+ const warmUpChosen = document.querySelector('[data-warm-up-chosen]');
89
+ const warmUpAdd = document.querySelector('[data-warm-up-add]');
90
+ const warmUpAddAll = document.querySelector('[data-warm-up-add-all]');
91
+ const warmUpRemove = document.querySelector('[data-warm-up-remove]');
92
+ const warmUpRemoveAll = document.querySelector('[data-warm-up-remove-all]');
93
+ const advancedStatus = document.querySelector('[data-advanced-status]');
94
+
95
+ let messages = {};
96
+ let pluginConfig = [];
97
+ let savedConfigSignature = '';
98
+ let pendingConfig;
99
+ let challenge = '';
100
+ let legacyNames = [];
101
+ let legacyAcknowledged = false;
102
+ let diagnosticsState = { status: 'inactive', missingEvidence: [] };
103
+ let diagnosticsReviewId = '';
104
+ let diagnosticsStartingAnother = false;
105
+ let panelReturn;
106
+ let dashboardPanelTrigger;
107
+ const dashboardView = window.HomebridgeEufyDashboard;
108
+ const legacySettingsView = window.HomebridgeEufyLegacySettings;
109
+ const diagnosticsWizard = window.HomebridgeEufyDiagnosticsWizard;
110
+ let diagnosticsWizardState = diagnosticsWizard.start();
111
+ const dashboardElements = {
112
+ dashboard,
113
+ title: dashboardTitle,
114
+ diagnose: dashboardDiagnose,
115
+ summary: dashboardSummary,
116
+ authenticate: dashboardAuthenticate,
117
+ groups: deviceGroups,
118
+ deviceSettingsTitle,
119
+ deviceSettingsEyebrow,
120
+ deviceSettingsControls,
121
+ updatePending,
122
+ updatePendingSummary,
123
+ updatePendingVersion,
124
+ updateRestart,
125
+ setup: setupContent,
126
+ pageTitle,
127
+ masthead,
128
+ };
129
+
130
+ function requestWithinDeadline(path, body, timeoutMs = 320000) {
131
+ let timer;
132
+ const timeout = new Promise((_, reject) => {
133
+ timer = setTimeout(() => reject(new Error('Request timed out')), timeoutMs);
134
+ });
135
+ return Promise.race([homebridge.request(path, body), timeout]).finally(() => clearTimeout(timer));
136
+ }
137
+
138
+ function isDashboardUiReproducing(state = diagnosticsState) {
139
+ return diagnosticsWizard.backgroundActive(state);
140
+ }
141
+
142
+ async function recordActiveUiEvent(event) {
143
+ if (!isDashboardUiReproducing()) return;
144
+ await requestWithinDeadline('/diagnostics/ui-event', { event }, 12000);
145
+ }
146
+
147
+ function recordActiveUiEventBestEffort(event) {
148
+ void recordActiveUiEvent(event).catch(() => undefined);
149
+ }
150
+
151
+ acknowledgement.addEventListener('change', () => {
152
+ continueButton.disabled = !acknowledgement.checked;
153
+ });
154
+
155
+ continueButton.addEventListener('click', () => {
156
+ if (!acknowledgement.checked) return;
157
+ firstSetup.hidden = true;
158
+ setupContent.hidden = false;
159
+ });
160
+
161
+ /**
162
+ * Declares the resolved locale on both the document root and the shell.
163
+ *
164
+ * The root element is what assistive technology reads to choose pronunciation and what a translation tool
165
+ * reads to identify the page's language; the shell attribute is what this script's own date formatting reads.
166
+ */
167
+ function applyLocale(locale) {
168
+ document.documentElement.lang = locale;
169
+ shell.lang = locale;
170
+ }
171
+
172
+ async function loadMessages(locale) {
173
+ const response = await fetch(`i18n/${locale}.json`);
174
+ if (!response.ok) {
175
+ throw new Error(`Unable to load ${locale} translations`);
176
+ }
177
+ return response.json();
178
+ }
179
+
180
+ async function applyTranslations(language) {
181
+ let locale = language.toLowerCase().split('-')[0] === 'fr' ? 'fr' : 'en';
182
+ try {
183
+ messages = await loadMessages(locale);
184
+ } catch {
185
+ locale = 'en';
186
+ try {
187
+ messages = await loadMessages(locale);
188
+ } catch {
189
+ applyLocale(locale);
190
+ return;
191
+ }
192
+ }
193
+
194
+ applyLocale(locale);
195
+ document.querySelectorAll('[data-i18n]').forEach((element) => {
196
+ element.textContent = messages[element.dataset.i18n] ?? element.textContent;
197
+ });
198
+ document.querySelectorAll('[data-i18n-aria-label]').forEach((element) => {
199
+ element.setAttribute('aria-label', messages[element.dataset.i18nAriaLabel] ?? '');
200
+ });
201
+ }
202
+
203
+ function setBusy(busy) {
204
+ authSubmit.disabled = busy;
205
+ challengeSubmit.disabled = busy;
206
+ }
207
+
208
+ const diagnosticsProfiles = {
209
+ 'startup-authentication': {
210
+ title: 'diagnosticsProfileStartup',
211
+ summary: 'diagnosticsStartupSummary',
212
+ before: 'diagnosticsStartupBefore',
213
+ action: 'diagnosticsStartupAction',
214
+ },
215
+ 'device-representation': {
216
+ title: 'diagnosticsProfileDevices',
217
+ summary: 'diagnosticsDevicesSummary',
218
+ before: 'diagnosticsDevicesBefore',
219
+ action: 'diagnosticsDevicesAction',
220
+ },
221
+ 'control-state': {
222
+ title: 'diagnosticsProfileControl',
223
+ summary: 'diagnosticsControlSummary',
224
+ before: 'diagnosticsControlBefore',
225
+ action: 'diagnosticsControlAction',
226
+ },
227
+ 'live-media': {
228
+ title: 'diagnosticsProfileLiveMedia',
229
+ summary: 'diagnosticsLiveSummary',
230
+ before: 'diagnosticsLiveBefore',
231
+ action: 'diagnosticsLiveAction',
232
+ },
233
+ 'hksv-recording': {
234
+ title: 'diagnosticsProfileRecording',
235
+ summary: 'diagnosticsRecordingSummary',
236
+ before: 'diagnosticsRecordingBefore',
237
+ action: 'diagnosticsRecordingAction',
238
+ },
239
+ 'dashboard-ui': {
240
+ title: 'diagnosticsProfileDashboard',
241
+ summary: 'diagnosticsDashboardSummary',
242
+ before: 'diagnosticsDashboardBefore',
243
+ action: 'diagnosticsDashboardAction',
244
+ },
245
+ other: {
246
+ title: 'diagnosticsProfileOther',
247
+ summary: 'diagnosticsOtherSummary',
248
+ before: 'diagnosticsOtherBefore',
249
+ action: 'diagnosticsOtherAction',
250
+ },
251
+ };
252
+
253
+ function renderDiagnosticsGuidance(profile) {
254
+ const guidance = diagnosticsProfiles[profile] ?? diagnosticsProfiles.other;
255
+ diagnosticsGuidanceTitle.textContent = messages[guidance.title] ?? '';
256
+ diagnosticsPhaseTitle.textContent = messages[guidance.title] ?? '';
257
+ diagnosticsGuidanceBefore.textContent = messages[guidance.before] ?? '';
258
+ diagnosticsGuidanceAction.textContent = messages[guidance.action] ?? '';
259
+ }
260
+
261
+ function renderDiagnosticsWizard() {
262
+ diagnosticsQuestion.hidden = diagnosticsWizardState.mode !== 'questions';
263
+ diagnosticsDirectPanel.hidden = diagnosticsWizardState.mode !== 'direct';
264
+ diagnosticsFrequency.hidden = diagnosticsWizardState.mode !== 'frequency';
265
+ diagnosticsMatch.hidden = diagnosticsWizardState.mode !== 'match';
266
+ if (diagnosticsWizardState.mode === 'questions') {
267
+ const question = diagnosticsWizard.questions[diagnosticsWizardState.questionIndex];
268
+ diagnosticsQuestionText.textContent = messages[question.message] ?? '';
269
+ }
270
+ if (diagnosticsWizardState.mode === 'match') {
271
+ diagnosticsProfile.value = diagnosticsWizardState.profile;
272
+ renderDiagnosticsGuidance(diagnosticsWizardState.profile);
273
+ diagnosticsModeSummary.textContent =
274
+ messages[
275
+ diagnosticsWizardState.reproductionMode === 'intermittent'
276
+ ? 'diagnosticsModeIntermittentSummary'
277
+ : 'diagnosticsModeNowSummary'
278
+ ] ?? '';
279
+ }
280
+ }
281
+
282
+ function renderDiagnostics(state) {
283
+ diagnosticsState = state;
284
+ const screen = diagnosticsWizard.screen(state, diagnosticsStartingAnother);
285
+ const choosing = screen === 'choose';
286
+ const reviewing = screen === 'review';
287
+ const reproductionMode = state.reproductionMode ?? 'now';
288
+ const collecting = isDashboardUiReproducing(state);
289
+ for (const entry of [mastheadDiagnostics, menuDiagnostics]) {
290
+ if (collecting) entry.dataset.collecting = 'true';
291
+ else delete entry.dataset.collecting;
292
+ entry.setAttribute(
293
+ 'aria-label',
294
+ messages[collecting ? 'diagnosticsCollectingFinishHere' : 'menuDiagnostics'] ?? '',
295
+ );
296
+ }
297
+ diagnosticsWizardPanel.hidden = !choosing;
298
+ diagnosticsGuidance.hidden = screen !== 'reproduce';
299
+ diagnosticsGuidanceBeforeSection.hidden = state.status === 'reproducing';
300
+ diagnosticsActions.hidden = screen !== 'reproduce';
301
+ diagnosticsReproduction.disabled = !['authorized', 'reproducing'].includes(state.status);
302
+ diagnosticsReproduction.textContent =
303
+ reproductionMode === 'intermittent'
304
+ ? state.status === 'reproducing'
305
+ ? messages.diagnosticsIntermittentIssueHappened
306
+ : messages.diagnosticsIntermittentStartWaiting
307
+ : state.status === 'reproducing'
308
+ ? messages.diagnosticsNowFinish
309
+ : messages.diagnosticsStartRecording;
310
+ diagnosticsIssue.hidden = !reviewing;
311
+ diagnosticsIssue.href = state.issueUrl ?? '';
312
+ diagnosticsResult.hidden = !reviewing;
313
+ diagnosticsReview.hidden = !reviewing;
314
+ diagnosticsManifest.hidden = true;
315
+ diagnosticsReviewConfirmLabel.hidden = true;
316
+ diagnosticsReviewConfirm.checked = false;
317
+ diagnosticsExport.hidden = true;
318
+ diagnosticsExport.disabled = true;
319
+ diagnosticsReviewId = '';
320
+ if (state.profile) diagnosticsProfile.value = state.profile;
321
+ if (screen === 'reproduce') {
322
+ renderDiagnosticsGuidance(state.profile);
323
+ if (reproductionMode === 'intermittent') {
324
+ diagnosticsGuidanceBeforeSection.hidden = true;
325
+ diagnosticsGuidanceAction.textContent =
326
+ messages[
327
+ state.status === 'reproducing'
328
+ ? 'diagnosticsIntermittentWaitingGuidance'
329
+ : 'diagnosticsIntermittentReadyGuidance'
330
+ ] ?? '';
331
+ }
332
+ }
333
+ if (choosing) renderDiagnosticsWizard();
334
+ const statusKey = reviewing
335
+ ? state.missingEvidence?.length
336
+ ? 'diagnosticsMissingEvidence'
337
+ : 'diagnosticsComplete'
338
+ : {
339
+ inactive: 'diagnosticsInactive',
340
+ authorized:
341
+ reproductionMode === 'intermittent' ? 'diagnosticsIntermittentAuthorized' : 'diagnosticsAuthorized',
342
+ reproducing:
343
+ reproductionMode === 'intermittent' ? 'diagnosticsIntermittentReproducing' : 'diagnosticsReproducing',
344
+ complete: state.missingEvidence?.length ? 'diagnosticsMissingEvidence' : 'diagnosticsComplete',
345
+ expired: 'diagnosticsExpired',
346
+ }[state.status];
347
+ diagnosticsStatus.textContent = (
348
+ choosing && (state.status === 'inactive' || diagnosticsStartingAnother) ? '' : (messages[statusKey] ?? '')
349
+ ).replace('{evidence}', state.missingEvidence?.join(', ') ?? '');
350
+ }
351
+
352
+ function renderArchiveManifest(manifest) {
353
+ diagnosticsManifest.replaceChildren();
354
+ const summary = document.createElement('p');
355
+ const mode =
356
+ messages[manifest.reproductionMode === 'intermittent' ? 'diagnosticsModeIntermittent' : 'diagnosticsModeNow'] ??
357
+ manifest.reproductionMode;
358
+ summary.textContent = `${manifest.archiveFormat} v${manifest.version} · ${manifest.keyId} · ${messages.diagnosticsArchiveMode ?? ''} ${mode} · ${(messages.diagnosticsArchiveExpires ?? '').replace('{expiresAt}', new Date(manifest.archiveExpiresAt).toLocaleString(shell.lang || 'en'))}`;
359
+ const evidence = document.createElement('ul');
360
+ for (const item of manifest.evidence ?? []) {
361
+ const row = document.createElement('li');
362
+ const size = item.bytes === undefined ? '' : ` · ${item.bytes} B`;
363
+ const truncated = item.truncated ? ` · ${messages.diagnosticsArchiveTruncated ?? ''}` : '';
364
+ const missing = item.missingReason ? ` · ${item.missingReason}` : '';
365
+ const fields = (item.fields ?? []).map((field) => `${field.field}: ${field.privacyClass}`).join(', ');
366
+ row.textContent = `${item.evidence} · ${item.privacyClass} · ${item.status}${missing}${size}${truncated}${fields ? ` · ${messages.diagnosticsArchiveFields ?? ''} ${fields}` : ''}`;
367
+ evidence.append(row);
368
+ }
369
+ const exclusions = document.createElement('p');
370
+ exclusions.textContent = `${messages.diagnosticsArchiveExcluded ?? ''} ${(manifest.excludedClasses ?? []).join(', ')}`;
371
+ diagnosticsManifest.append(summary, evidence, exclusions);
372
+ const uncovered = (manifest.evidence ?? []).filter((item) => item.coversReproduction === false);
373
+ if (uncovered.length > 0) {
374
+ const gap = document.createElement('p');
375
+ gap.textContent = (messages.diagnosticsArchiveCoverageGap ?? '')
376
+ .replace('{classes}', uncovered.map((item) => item.evidence).join(', '))
377
+ .replace(
378
+ '{retainedFrom}',
379
+ new Date(
380
+ Math.min(...uncovered.map((item) => Date.parse(item.retainedFrom))),
381
+ ).toLocaleString(shell.lang || 'en'),
382
+ );
383
+ diagnosticsManifest.append(gap);
384
+ }
385
+ diagnosticsManifest.hidden = false;
386
+ }
387
+
388
+ diagnosticsReview.addEventListener('click', async () => {
389
+ diagnosticsReview.disabled = true;
390
+ try {
391
+ const review = await requestWithinDeadline('/diagnostics/archive/review', undefined, 12000);
392
+ diagnosticsReviewId = review.reviewId;
393
+ renderArchiveManifest(review.manifest);
394
+ diagnosticsReviewConfirmLabel.hidden = false;
395
+ diagnosticsExport.hidden = false;
396
+ } catch {
397
+ try {
398
+ renderDiagnostics(await requestWithinDeadline('/diagnostics/status', undefined, 12000));
399
+ diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
400
+ } catch {
401
+ diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
402
+ }
403
+ } finally {
404
+ diagnosticsReview.disabled = false;
405
+ }
406
+ });
407
+
408
+ diagnosticsReviewConfirm.addEventListener('change', () => {
409
+ diagnosticsExport.disabled = !diagnosticsReviewConfirm.checked || !diagnosticsReviewId;
410
+ });
411
+
412
+ diagnosticsExport.addEventListener('click', async () => {
413
+ if (!diagnosticsReviewConfirm.checked || !diagnosticsReviewId) return;
414
+ diagnosticsExport.disabled = true;
415
+ try {
416
+ const exported = await requestWithinDeadline(
417
+ '/diagnostics/archive/export',
418
+ { reviewId: diagnosticsReviewId },
419
+ 12000,
420
+ );
421
+ const download = document.createElement('a');
422
+ download.href = `data:${exported.mediaType};base64,${exported.archive}`;
423
+ download.download = exported.filename;
424
+ document.body.appendChild(download);
425
+ download.click();
426
+ document.body.removeChild(download);
427
+ diagnosticsReviewId = '';
428
+ diagnosticsReviewConfirm.checked = false;
429
+ } catch {
430
+ diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
431
+ }
432
+ });
433
+
434
+ diagnosticsAuthorize.addEventListener('click', async () => {
435
+ diagnosticsAuthorize.disabled = true;
436
+ let focusAuthorizeAfter = false;
437
+ const previousStatus = diagnosticsState.status;
438
+ try {
439
+ const authorized = await requestWithinDeadline(
440
+ '/diagnostics/authorize',
441
+ {
442
+ profile: diagnosticsWizardState.profile,
443
+ reproductionMode: diagnosticsWizardState.reproductionMode,
444
+ },
445
+ 12000,
446
+ );
447
+ diagnosticsStartingAnother = false;
448
+ renderDiagnostics(authorized);
449
+ if (diagnosticsState.status === 'authorized') diagnosticsGuidance.focus?.();
450
+ } catch {
451
+ try {
452
+ const refreshed = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
453
+ if (refreshed.status !== previousStatus) diagnosticsStartingAnother = false;
454
+ renderDiagnostics(refreshed);
455
+ if (refreshed.status === previousStatus) {
456
+ diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
457
+ focusAuthorizeAfter = true;
458
+ } else if (refreshed.status === 'authorized') diagnosticsGuidance.focus?.();
459
+ } catch {
460
+ diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
461
+ }
462
+ } finally {
463
+ diagnosticsAuthorize.disabled = false;
464
+ if (focusAuthorizeAfter) diagnosticsAuthorize.focus?.();
465
+ }
466
+ });
467
+
468
+ diagnosticsYes.addEventListener('click', () => {
469
+ diagnosticsWizardState = diagnosticsWizard.answer(diagnosticsWizardState, true);
470
+ renderDiagnosticsWizard();
471
+ diagnosticsFrequencyHeading.focus?.();
472
+ });
473
+
474
+ diagnosticsNo.addEventListener('click', () => {
475
+ diagnosticsWizardState = diagnosticsWizard.answer(diagnosticsWizardState, false);
476
+ renderDiagnosticsWizard();
477
+ if (diagnosticsWizardState.mode === 'frequency') diagnosticsFrequencyHeading.focus?.();
478
+ else diagnosticsQuestionText.focus?.();
479
+ });
480
+
481
+ diagnosticsDirect.addEventListener('click', () => {
482
+ diagnosticsWizardState = diagnosticsWizard.direct();
483
+ renderDiagnosticsWizard();
484
+ diagnosticsProfile.focus?.();
485
+ });
486
+
487
+ diagnosticsDirectChoose.addEventListener('click', () => {
488
+ diagnosticsWizardState = diagnosticsWizard.selectDirect(diagnosticsWizardState, diagnosticsProfile.value);
489
+ renderDiagnosticsWizard();
490
+ diagnosticsFrequencyHeading.focus?.();
491
+ });
492
+
493
+ diagnosticsDirectBack.addEventListener('click', () => {
494
+ diagnosticsWizardState = diagnosticsWizard.start();
495
+ renderDiagnosticsWizard();
496
+ diagnosticsQuestionText.focus?.();
497
+ });
498
+
499
+ function chooseDiagnosticsReproductionMode(reproductionMode) {
500
+ diagnosticsWizardState = diagnosticsWizard.chooseReproductionMode(diagnosticsWizardState, reproductionMode);
501
+ renderDiagnosticsWizard();
502
+ diagnosticsGuidanceTitle.focus?.();
503
+ }
504
+
505
+ diagnosticsFrequencyNow.addEventListener('click', () => chooseDiagnosticsReproductionMode('now'));
506
+ diagnosticsFrequencyIntermittent.addEventListener('click', () => chooseDiagnosticsReproductionMode('intermittent'));
507
+
508
+ diagnosticsFrequencyBack.addEventListener('click', () => {
509
+ diagnosticsWizardState = diagnosticsWizard.backFromFrequency(diagnosticsWizardState);
510
+ renderDiagnosticsWizard();
511
+ if (diagnosticsWizardState.mode === 'direct') diagnosticsProfile.focus?.();
512
+ else diagnosticsQuestionText.focus?.();
513
+ });
514
+
515
+ diagnosticsReject.addEventListener('click', () => {
516
+ diagnosticsWizardState = diagnosticsWizard.reject(diagnosticsWizardState);
517
+ renderDiagnosticsWizard();
518
+ if (diagnosticsWizardState.mode === 'direct') diagnosticsProfile.focus?.();
519
+ else diagnosticsQuestionText.focus?.();
520
+ });
521
+
522
+ diagnosticsStartAnother.addEventListener('click', () => {
523
+ diagnosticsStartingAnother = true;
524
+ diagnosticsWizardState = diagnosticsWizard.start();
525
+ renderDiagnostics(diagnosticsState);
526
+ diagnosticsQuestionText.focus?.();
527
+ });
528
+
529
+ diagnosticsReproduction.addEventListener('click', async () => {
530
+ diagnosticsReproduction.disabled = true;
531
+ const previousStatus = diagnosticsState.status;
532
+ const path =
533
+ diagnosticsState.status === 'reproducing' ? '/diagnostics/reproduction/end' : '/diagnostics/reproduction/start';
534
+ try {
535
+ let state = await requestWithinDeadline(path, undefined, 12000);
536
+ if (state.status === 'complete' && state.missingEvidence?.length) {
537
+ await new Promise((resolve) => setTimeout(resolve, 250));
538
+ state = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
539
+ }
540
+ renderDiagnostics(state);
541
+ if (previousStatus === 'authorized' && isDashboardUiReproducing(state)) {
542
+ await recordActiveUiEvent('background-started').catch(() => undefined);
543
+ closeDashboardPanel();
544
+ return;
545
+ }
546
+ if (state.status === 'complete' || state.partialExportAvailable) diagnosticsResultHeading.focus?.();
547
+ } catch {
548
+ try {
549
+ const refreshed = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
550
+ renderDiagnostics(refreshed);
551
+ if (refreshed.status === previousStatus) diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
552
+ if (refreshed.status === 'complete' || refreshed.partialExportAvailable) diagnosticsResultHeading.focus?.();
553
+ else if (refreshed.status === 'authorized' || refreshed.status === 'reproducing') diagnosticsGuidance.focus?.();
554
+ } catch {
555
+ diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
556
+ }
557
+ } finally {
558
+ diagnosticsReproduction.disabled = !['authorized', 'reproducing'].includes(diagnosticsState.status);
559
+ }
560
+ });
561
+
562
+ /**
563
+ * Opens one dashboard panel over whichever screen is currently showing.
564
+ *
565
+ * The diagnostics panel is a sibling of the dashboard rather than a child of it, so it can open before an
566
+ * account exists — a startup or sign-in failure is visible on the setup and authentication screens, which is
567
+ * exactly where the dashboard is hidden. The advanced panel still lives inside the dashboard, because every
568
+ * setting it holds needs an account to mean anything.
569
+ */
570
+ function openDashboardPanel(panel, trigger) {
571
+ dashboardPanelTrigger = trigger;
572
+ panelReturn = {
573
+ masthead: masthead.hidden,
574
+ firstSetup: firstSetup.hidden,
575
+ setupContent: setupContent.hidden,
576
+ dashboard: dashboard.hidden,
577
+ };
578
+ masthead.hidden = true;
579
+ firstSetup.hidden = true;
580
+ setupContent.hidden = true;
581
+ const insideDashboard = panel !== diagnosticsPanel;
582
+ dashboard.hidden = !insideDashboard;
583
+ dashboardState.hidden = insideDashboard;
584
+ dashboardSummary.hidden = insideDashboard;
585
+ deviceGroups.hidden = insideDashboard;
586
+ for (const other of [diagnosticsPanel, advancedPanel, devicePanel]) {
587
+ other.hidden = other !== panel;
588
+ }
589
+ panel.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
590
+ panel.querySelector('.dashboard-page-back')?.focus?.();
591
+ }
592
+
593
+ /** Restores the screen a panel was opened over, rather than assuming it was the dashboard. */
594
+ function closeDashboardPanel() {
595
+ diagnosticsPanel.hidden = true;
596
+ advancedPanel.hidden = true;
597
+ devicePanel.hidden = true;
598
+ dashboardState.hidden = false;
599
+ dashboardSummary.hidden = false;
600
+ deviceGroups.hidden = false;
601
+ const restored = panelReturn;
602
+ if (restored) {
603
+ masthead.hidden = restored.masthead;
604
+ firstSetup.hidden = restored.firstSetup;
605
+ setupContent.hidden = restored.setupContent;
606
+ dashboard.hidden = restored.dashboard;
607
+ panelReturn = undefined;
608
+ }
609
+ dashboardPanelTrigger?.focus?.();
610
+ if (!restored || restored.dashboard === false) {
611
+ recordActiveUiEventBestEffort('dashboard-opened');
612
+ }
613
+ }
614
+
615
+ mastheadDiagnostics.addEventListener('click', () => menuDiagnostics.click());
616
+
617
+ // The dashboard's own offer of a run goes the same way the menu does, so there is one path into the wizard.
618
+ dashboardDiagnose.addEventListener('click', () => menuDiagnostics.click());
619
+
620
+ menuDiagnostics.addEventListener('click', async () => {
621
+ openDashboardPanel(diagnosticsPanel, menuDiagnostics);
622
+ // Drawn on opening, because the wizard's first question is only ever put on the page by this call.
623
+ renderDiagnosticsWizard();
624
+ try {
625
+ renderDiagnostics(await requestWithinDeadline('/diagnostics/status', undefined, 12000));
626
+ } catch {
627
+ diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
628
+ recordActiveUiEventBestEffort('request-failed');
629
+ }
630
+ });
631
+ menuAdvanced.addEventListener('click', async () => {
632
+ const config = configuredBlock() ?? {};
633
+ advancedPolling.value = String(config.pollingIntervalMinutes ?? 10);
634
+ advancedConcurrentMedia.value = String(config.maxConcurrentMediaSessions ?? 0);
635
+ warmUpSelection = [...new Set(Array.isArray(config.warmUpEvents) ? config.warmUpEvents : ['doorbellPress'])].sort();
636
+ warmUpMarked = new Set();
637
+ renderWarmUp();
638
+ advancedFfmpeg.value = config.ffmpegPath ?? '';
639
+ openDashboardPanel(advancedPanel, menuAdvanced);
640
+ /**
641
+ * The panel asks for its own candidates rather than relying on the devices view having been opened first.
642
+ * Without this a user who came straight here sees an empty left column, and clearing the right one would leave
643
+ * the setting with no way back. Best effort: an unanswered request leaves whatever is already chosen.
644
+ */
645
+ if (warmUpCandidates.length === 0) {
646
+ try {
647
+ const snapshot = await requestWithinDeadline('/dashboard', { representationPreferences: {} }, 12000);
648
+ warmUpCandidates = Array.isArray(snapshot.warmUpCandidates) ? snapshot.warmUpCandidates : [];
649
+ renderWarmUp();
650
+ } catch {
651
+ recordActiveUiEventBestEffort('request-failed');
652
+ }
653
+ }
654
+ });
655
+ diagnosticsClose.addEventListener('click', () => {
656
+ diagnosticsStartingAnother = false;
657
+ renderDiagnostics(diagnosticsState);
658
+ closeDashboardPanel();
659
+ });
660
+ advancedClose.addEventListener('click', () => {
661
+ closeDashboardPanel();
662
+ });
663
+
664
+ deviceClose.addEventListener('click', () => {
665
+ closeDashboardPanel();
666
+ });
667
+
668
+ async function updateAdvancedSettings() {
669
+ const existing = configuredBlock();
670
+ if (!existing) return;
671
+ const rawPolling = advancedPolling.value.trim();
672
+ const pollingIntervalMinutes = rawPolling === '' ? 10 : Number(rawPolling);
673
+ if (!Number.isInteger(pollingIntervalMinutes) || pollingIntervalMinutes < 0) {
674
+ advancedPolling.setCustomValidity?.(messages.advancedPollingInvalid ?? '');
675
+ advancedPolling.reportValidity?.();
676
+ return;
677
+ }
678
+ advancedPolling.setCustomValidity?.('');
679
+ const rawConcurrentMedia = advancedConcurrentMedia.value.trim();
680
+ const maxConcurrentMediaSessions = rawConcurrentMedia === '' ? 0 : Number(rawConcurrentMedia);
681
+ if (!Number.isInteger(maxConcurrentMediaSessions) || maxConcurrentMediaSessions < 0) {
682
+ advancedConcurrentMedia.setCustomValidity?.(messages.advancedConcurrentMediaInvalid ?? '');
683
+ advancedConcurrentMedia.reportValidity?.();
684
+ return;
685
+ }
686
+ advancedConcurrentMedia.setCustomValidity?.('');
687
+ const ffmpegPath = advancedFfmpeg.value.trim();
688
+ const next = { ...existing };
689
+ if (pollingIntervalMinutes === 10) delete next.pollingIntervalMinutes;
690
+ else next.pollingIntervalMinutes = pollingIntervalMinutes;
691
+ if (maxConcurrentMediaSessions === 0) delete next.maxConcurrentMediaSessions;
692
+ else next.maxConcurrentMediaSessions = maxConcurrentMediaSessions;
693
+ if (ffmpegPath) next.ffmpegPath = ffmpegPath;
694
+ else delete next.ffmpegPath;
695
+ // The default is what the plugin applies when the key is absent, so storing it would only pin today's default.
696
+ if (warmUpSelection.length === 1 && warmUpSelection[0] === 'doorbellPress') delete next.warmUpEvents;
697
+ else next.warmUpEvents = [...warmUpSelection];
698
+ try {
699
+ await updateConfig(next);
700
+ advancedStatus.textContent = '';
701
+ } catch {
702
+ advancedStatus.textContent = messages.advancedSaveFailed ?? '';
703
+ recordActiveUiEventBestEffort('request-failed');
704
+ }
705
+ }
706
+
707
+ advancedPolling.addEventListener('change', updateAdvancedSettings);
708
+ advancedConcurrentMedia.addEventListener('change', updateAdvancedSettings);
709
+ advancedFfmpeg.addEventListener('change', updateAdvancedSettings);
710
+ warmUpAdd.addEventListener('click', () => moveWarmUp('available'));
711
+ warmUpAddAll.addEventListener('click', () => moveWarmUp('available', true));
712
+ warmUpRemove.addEventListener('click', () => moveWarmUp('chosen'));
713
+ warmUpRemoveAll.addEventListener('click', () => moveWarmUp('chosen', true));
714
+
715
+ let warmUpSelection = ['doorbellPress'];
716
+ let warmUpCandidates = [];
717
+ // The devices the dashboard last drew, so opening a tile can find the one it speaks for.
718
+ let dashboardDevices = [];
719
+ /**
720
+ * The entries the user has marked, in whichever column they sit.
721
+ *
722
+ * One set is enough because an event is in exactly one column: the column follows from whether the selection
723
+ * holds it. Marking is what makes the single arrows act on a choice rather than on whatever came first.
724
+ */
725
+ let warmUpMarked = new Set();
726
+
727
+ /**
728
+ * The events this interface may offer: whatever the discovered devices report, plus anything already chosen.
729
+ *
730
+ * Keeping a chosen event that no device currently reports is deliberate — a camera may be offline, and dropping
731
+ * the entry would silently rewrite the user's setting on the next save.
732
+ */
733
+ function warmUpOffered() {
734
+ return [...new Set([...warmUpCandidates, ...warmUpSelection])].sort();
735
+ }
736
+
737
+ /**
738
+ * A readable name for one event: the translation where this build has one, and otherwise a label derived from
739
+ * the reported name. Deriving it is what lets a newly reported event appear without a release here.
740
+ */
741
+ function warmUpLabel(event) {
742
+ const translated = messages[`advancedWarmUpEvent_${event}`];
743
+ if (translated) return translated;
744
+ const spaced = event.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase();
745
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
746
+ }
747
+
748
+ /**
749
+ * Draws both columns from the selection.
750
+ *
751
+ * Each entry is a button rather than an option, so a keyboard reaches it and a screen reader announces the
752
+ * column it belongs to. The move buttons are disabled while there is nothing to move, because a control that
753
+ * looks available and does nothing is worse than one that says it cannot.
754
+ */
755
+ function renderWarmUp() {
756
+ if (!warmUpAvailable || !warmUpChosen) return;
757
+ const offered = warmUpOffered();
758
+ const columns = [
759
+ { list: warmUpAvailable, column: 'available', events: offered.filter((e) => !warmUpSelection.includes(e)) },
760
+ { list: warmUpChosen, column: 'chosen', events: offered.filter((e) => warmUpSelection.includes(e)) },
761
+ ];
762
+ for (const { list, column, events } of columns) {
763
+ list.textContent = '';
764
+ for (const event of events) {
765
+ const item = document.createElement('li');
766
+ const button = document.createElement('button');
767
+ button.type = 'button';
768
+ button.textContent = warmUpLabel(event);
769
+ button.setAttribute('aria-pressed', String(warmUpMarked.has(event)));
770
+ button.addEventListener('click', () => {
771
+ if (warmUpMarked.has(event)) warmUpMarked.delete(event);
772
+ else warmUpMarked.add(event);
773
+ renderWarmUp();
774
+ });
775
+ item.append(button);
776
+ list.append(item);
777
+ }
778
+ }
779
+ const availableEvents = offered.filter((event) => !warmUpSelection.includes(event));
780
+ // A control that looks available and moves nothing is worse than one that says it cannot.
781
+ if (warmUpAdd) warmUpAdd.disabled = !availableEvents.some((event) => warmUpMarked.has(event));
782
+ if (warmUpAddAll) warmUpAddAll.disabled = availableEvents.length === 0;
783
+ if (warmUpRemove) warmUpRemove.disabled = !warmUpSelection.some((event) => warmUpMarked.has(event));
784
+ if (warmUpRemoveAll) warmUpRemoveAll.disabled = warmUpSelection.length === 0;
785
+ }
786
+
787
+ /**
788
+ * Moves every marked entry of one column across, or every entry of it when asked for all.
789
+ *
790
+ * The marks are cleared afterwards rather than carried over: the entry has visibly changed column, which is the
791
+ * feedback, and leaving it marked would arm the opposite arrow with what was just moved.
792
+ */
793
+ function moveWarmUp(from, all = false) {
794
+ const offered = warmUpOffered();
795
+ const source = from === 'available' ? offered.filter((event) => !warmUpSelection.includes(event)) : warmUpSelection;
796
+ const moving = all ? source : source.filter((event) => warmUpMarked.has(event));
797
+ if (moving.length === 0) return;
798
+ warmUpSelection =
799
+ from === 'available'
800
+ ? [...new Set([...warmUpSelection, ...moving])].sort()
801
+ : warmUpSelection.filter((event) => !moving.includes(event));
802
+ warmUpMarked = new Set();
803
+ renderWarmUp();
804
+ void updateAdvancedSettings();
805
+ }
806
+
807
+ function configuredBlock() {
808
+ return pluginConfig.find((block) => block.platform === 'HomebridgeEufy');
809
+ }
810
+
811
+ function stableConfigValue(value) {
812
+ if (Array.isArray(value)) return value.map(stableConfigValue);
813
+ if (!value || typeof value !== 'object') return value;
814
+ return Object.fromEntries(
815
+ Object.keys(value)
816
+ .sort()
817
+ .map((key) => [key, stableConfigValue(value[key])]),
818
+ );
819
+ }
820
+
821
+ function configSignature(config) {
822
+ return JSON.stringify(stableConfigValue(config));
823
+ }
824
+
825
+ async function updateConfig(block) {
826
+ const nextConfig = pluginConfig.map((candidate) => (candidate.platform === 'HomebridgeEufy' ? block : candidate));
827
+ if (!nextConfig.includes(block)) nextConfig.push(block);
828
+ await homebridge.updatePluginConfig(nextConfig);
829
+ pluginConfig = nextConfig;
830
+ if (configSignature(nextConfig) === savedConfigSignature) homebridge.disableSaveButton();
831
+ else homebridge.enableSaveButton();
832
+ }
833
+
834
+ dashboardView.bindPreferences(dashboardElements, configuredBlock, updateConfig, () => messages, (serial, trigger) => {
835
+ const device = dashboardDevices.find((candidate) => candidate.serial === serial);
836
+ if (!device) return;
837
+ dashboardView.renderDeviceSettings(device, configuredBlock() ?? {}, messages, dashboardElements);
838
+ openDashboardPanel(devicePanel, trigger);
839
+ });
840
+
841
+ dashboardAuthenticate.addEventListener('click', () => {
842
+ diagnosticsPanel.hidden = true;
843
+ advancedPanel.hidden = true;
844
+ dashboard.hidden = true;
845
+ setupContent.hidden = false;
846
+ masthead.hidden = false;
847
+ pageTitle.textContent = messages.pageTitle;
848
+ accountInput.focus?.();
849
+ recordActiveUiEventBestEffort('authentication-opened');
850
+ });
851
+
852
+ legacyAcknowledge.addEventListener('click', async () => {
853
+ legacyAcknowledged = true;
854
+ const existing = configuredBlock();
855
+ if (existing) {
856
+ try {
857
+ await updateConfig({ ...existing, discardedV4Settings: legacyNames, discardedV4Acknowledged: true });
858
+ } catch {
859
+ return;
860
+ }
861
+ }
862
+ legacyNotice.hidden = true;
863
+ });
864
+
865
+ async function saveAuthenticatedConfig() {
866
+ homebridge.enableSaveButton();
867
+ try {
868
+ await homebridge.updatePluginConfig([pendingConfig]);
869
+ await homebridge.savePluginConfig();
870
+ pluginConfig = [pendingConfig];
871
+ savedConfigSignature = configSignature(pluginConfig);
872
+ homebridge.enableSaveButton();
873
+ } catch {
874
+ } finally {
875
+ passwordInput.value = '';
876
+ pendingConfig = undefined;
877
+ }
878
+ }
879
+
880
+ // Draws the device list from the plugin's own record of it, and shows it in place of the setup flow.
881
+ async function showDashboard() {
882
+ const representationPreferences = Object.fromEntries(
883
+ Object.entries(configuredBlock()?.entityPreferences ?? {})
884
+ .filter(([, preference]) => typeof preference.represented === 'boolean')
885
+ .map(([serial, preference]) => [serial, preference.represented]),
886
+ );
887
+ try {
888
+ const snapshot = await requestWithinDeadline('/dashboard', { representationPreferences }, 12000);
889
+ // What the warm-up setting may offer comes from the devices themselves, so it is learnt here.
890
+ warmUpCandidates = Array.isArray(snapshot.warmUpCandidates) ? snapshot.warmUpCandidates : [];
891
+ // Kept beside the render that drew them, so opening a tile finds the device that tile stands for.
892
+ dashboardDevices = snapshot.devices ?? [];
893
+ dashboardView.render(snapshot, configuredBlock() ?? {}, messages, dashboardElements);
894
+ // The image pass runs beside the rendered dashboard, so its failure is dropped here rather than escaping.
895
+ void dashboardView
896
+ .applyDeviceImages(dashboardElements, (serial) => requestWithinDeadline('/device/image', { serial }, 12000))
897
+ .catch(() => undefined);
898
+ } catch {
899
+ dashboardView.render({ state: 'missing', devices: [] }, configuredBlock() ?? {}, messages, dashboardElements);
900
+ recordActiveUiEventBestEffort('request-failed');
901
+ }
902
+ recordActiveUiEventBestEffort('dashboard-opened');
903
+ }
904
+
905
+ async function handleResult(result) {
906
+ if (result.status === 'captcha') {
907
+ challenge = 'captcha';
908
+ authForm.hidden = true;
909
+ challengeImage.src = result.image;
910
+ challengeImage.hidden = false;
911
+ challengeLabel.textContent = messages.captchaLabel ?? '';
912
+ challengeForm.hidden = false;
913
+ authStatus.textContent = '';
914
+ return;
915
+ }
916
+ if (result.status === 'two-factor') {
917
+ challenge = 'two-factor';
918
+ authForm.hidden = true;
919
+ challengeImage.hidden = true;
920
+ challengeLabel.textContent = messages.twoFactorLabel ?? '';
921
+ challengeForm.hidden = false;
922
+ authStatus.textContent = result.method;
923
+ return;
924
+ }
925
+
926
+ challengeForm.hidden = true;
927
+ if (result.status === 'restart-required') {
928
+ authForm.hidden = true;
929
+ authStatus.textContent = messages.authSuccess ?? '';
930
+ await saveAuthenticatedConfig();
931
+ // The devices this sign-in discovered are already recorded, so the flow ends on them rather than on a notice.
932
+ await showDashboard();
933
+ } else if (result.status === 'blocked' || result.status === 'plugin-running') {
934
+ authForm.hidden = false;
935
+ authStatus.textContent = messages.authBlocked ?? '';
936
+ } else if (result.status === 'timed-out') {
937
+ authForm.hidden = false;
938
+ authStatus.textContent = messages.authTimedOut ?? '';
939
+ } else {
940
+ authForm.hidden = false;
941
+ authStatus.textContent = messages.authFailed ?? '';
942
+ }
943
+ }
944
+
945
+ authForm.addEventListener('submit', async (event) => {
946
+ event.preventDefault();
947
+ const existing = pluginConfig.find((block) => block.platform === 'HomebridgeEufy') ?? {};
948
+ const retained = Object.fromEntries(
949
+ [
950
+ 'pollingIntervalMinutes',
951
+ 'sessionWarmUp',
952
+ 'ffmpegPath',
953
+ 'entityPreferences',
954
+ 'discardedV4Settings',
955
+ 'discardedV4Acknowledged',
956
+ ]
957
+ .filter((key) => Object.hasOwn(existing, key))
958
+ .map((key) => [key, existing[key]]),
959
+ );
960
+ pendingConfig = {
961
+ ...retained,
962
+ platform: 'HomebridgeEufy',
963
+ username: accountInput.value.trim(),
964
+ password: passwordInput.value,
965
+ country: countryInput.value.trim().toUpperCase(),
966
+ trustedDeviceName: trustedDeviceInput.value.trim(),
967
+ ...(legacyNames.length > 0 ? { discardedV4Settings: legacyNames } : {}),
968
+ ...(legacyAcknowledged ? { discardedV4Acknowledged: true } : {}),
969
+ };
970
+ setBusy(true);
971
+ try {
972
+ await handleResult(await requestWithinDeadline('/auth/start', { configuration: pendingConfig }));
973
+ } catch {
974
+ authStatus.textContent = messages.authFailed ?? '';
975
+ recordActiveUiEventBestEffort('request-failed');
976
+ } finally {
977
+ setBusy(false);
978
+ }
979
+ });
980
+
981
+ challengeForm.addEventListener('submit', async (event) => {
982
+ event.preventDefault();
983
+ const path = challenge === 'captcha' ? '/auth/captcha' : '/auth/two-factor';
984
+ const body = challenge === 'captcha' ? { answer: challengeAnswer.value } : { code: challengeAnswer.value };
985
+ challengeAnswer.value = '';
986
+ setBusy(true);
987
+ try {
988
+ await handleResult(await requestWithinDeadline(path, body));
989
+ } catch {
990
+ authStatus.textContent = messages.authFailed ?? '';
991
+ recordActiveUiEventBestEffort('request-failed');
992
+ } finally {
993
+ setBusy(false);
994
+ }
995
+ });
996
+
997
+ window.addEventListener('pagehide', () => {
998
+ void requestWithinDeadline('/auth/close', undefined, 12000).catch(() => undefined);
999
+ });
1000
+
1001
+ if (updateRestart) {
1002
+ /**
1003
+ * Asks the older build to end so Homebridge brings up the installed one, then reloads what this page knows.
1004
+ *
1005
+ * The button is disabled for the whole attempt, because a second press would land on a process that is already
1006
+ * going away. A refusal restores it and leaves the notice standing, which is the honest outcome: the older
1007
+ * build is still the one answering.
1008
+ */
1009
+ updateRestart.addEventListener('click', async () => {
1010
+ updateRestart.disabled = true;
1011
+ updateRestart.textContent = messages.updatePendingBusy;
1012
+ try {
1013
+ await requestWithinDeadline('/update/restart', {}, 20000);
1014
+ await new Promise((resolve) => setTimeout(resolve, 6000));
1015
+ await showDashboard();
1016
+ } catch {
1017
+ updatePendingSummary.textContent = messages.updatePendingFailed;
1018
+ } finally {
1019
+ updateRestart.disabled = false;
1020
+ updateRestart.textContent = messages.updatePendingAction;
1021
+ }
1022
+ });
1023
+ }
1024
+
1025
+ homebridge.addEventListener('ready', async () => {
1026
+ homebridge.disableSaveButton();
1027
+ shell.dataset.theme = await homebridge.userCurrentLightingMode();
1028
+ const language = await homebridge.i18nCurrentLang();
1029
+ pluginConfig = await homebridge.getPluginConfig();
1030
+ savedConfigSignature = configSignature(pluginConfig);
1031
+ await applyTranslations(language);
1032
+ try {
1033
+ renderDiagnostics(await requestWithinDeadline('/diagnostics/status', undefined, 12000));
1034
+ } catch {
1035
+ diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
1036
+ }
1037
+ const configured = pluginConfig.find(
1038
+ (block) =>
1039
+ block.platform === 'HomebridgeEufy' && typeof block.username === 'string' && block.username.trim().length > 0,
1040
+ );
1041
+ const v5Block = configuredBlock();
1042
+ if (v5Block?.discardedV4Settings?.length) {
1043
+ legacyNames = v5Block.discardedV4Settings;
1044
+ legacyAcknowledged = v5Block.discardedV4Acknowledged === true;
1045
+ } else {
1046
+ const legacyBlock = pluginConfig.find((block) => block.platform === 'EufySecurity');
1047
+ if (legacyBlock) {
1048
+ legacyNames = legacySettingsView.names(legacyBlock);
1049
+ }
1050
+ }
1051
+ legacyNotice.hidden = legacyNames.length === 0 || legacyAcknowledged;
1052
+ legacySettings.textContent = legacyNames.join(', ');
1053
+
1054
+ firstSetup.hidden = Boolean(configured);
1055
+ setupContent.hidden = !configured;
1056
+ acknowledgement.checked = false;
1057
+ continueButton.disabled = true;
1058
+ accountInput.value = configured?.username ?? '';
1059
+ passwordInput.value = configured?.password ?? '';
1060
+ countryInput.value = configured?.country ?? 'US';
1061
+ trustedDeviceInput.value = configured?.trustedDeviceName ?? 'Homebridge Eufy';
1062
+ if (configured) {
1063
+ await showDashboard();
1064
+ }
1065
+ });