@fastybird/smart-panel-backend 1.1.0-alpha.2 → 1.1.0-alpha.21

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 (403) hide show
  1. package/dist/app.module.js +8 -0
  2. package/dist/app.module.js.map +1 -1
  3. package/dist/common/filters/conflict-exception.filter.d.ts +6 -0
  4. package/dist/common/filters/conflict-exception.filter.js +59 -0
  5. package/dist/common/filters/conflict-exception.filter.js.map +1 -0
  6. package/dist/common/filters/global-error.filter.js +1 -4
  7. package/dist/common/filters/global-error.filter.js.map +1 -1
  8. package/dist/common/filters/global-filters.d.ts +3 -0
  9. package/dist/common/filters/global-filters.js +22 -0
  10. package/dist/common/filters/global-filters.js.map +1 -0
  11. package/dist/common/filters/unprocessable-entity-exception.filter.d.ts +1 -0
  12. package/dist/common/filters/unprocessable-entity-exception.filter.js +15 -3
  13. package/dist/common/filters/unprocessable-entity-exception.filter.js.map +1 -1
  14. package/dist/common/utils/transform.utils.js +3 -0
  15. package/dist/common/utils/transform.utils.js.map +1 -1
  16. package/dist/dataSource.js +1 -0
  17. package/dist/dataSource.js.map +1 -1
  18. package/dist/main.js +2 -7
  19. package/dist/main.js.map +1 -1
  20. package/dist/migrations/1000000000026-AddHardwareInputCategories.d.ts +7 -0
  21. package/dist/migrations/1000000000026-AddHardwareInputCategories.js +364 -0
  22. package/dist/migrations/1000000000026-AddHardwareInputCategories.js.map +1 -0
  23. package/dist/modules/devices/controllers/channels.controller.d.ts +5 -1
  24. package/dist/modules/devices/controllers/channels.controller.js +34 -2
  25. package/dist/modules/devices/controllers/channels.controller.js.map +1 -1
  26. package/dist/modules/devices/controllers/channels.properties.controller.js +21 -4
  27. package/dist/modules/devices/controllers/channels.properties.controller.js.map +1 -1
  28. package/dist/modules/devices/controllers/devices.channels.controller.d.ts +5 -1
  29. package/dist/modules/devices/controllers/devices.channels.controller.js +38 -2
  30. package/dist/modules/devices/controllers/devices.channels.controller.js.map +1 -1
  31. package/dist/modules/devices/controllers/devices.channels.properties.controller.js +21 -4
  32. package/dist/modules/devices/controllers/devices.channels.properties.controller.js.map +1 -1
  33. package/dist/modules/devices/devices.constants.d.ts +6 -0
  34. package/dist/modules/devices/devices.constants.js +6 -0
  35. package/dist/modules/devices/devices.constants.js.map +1 -1
  36. package/dist/modules/devices/devices.module.js +18 -0
  37. package/dist/modules/devices/devices.module.js.map +1 -1
  38. package/dist/modules/devices/devices.openapi.d.ts +3 -1
  39. package/dist/modules/devices/devices.openapi.js +7 -0
  40. package/dist/modules/devices/devices.openapi.js.map +1 -1
  41. package/dist/modules/devices/dto/channel-input-occurrence.dto.d.ts +10 -0
  42. package/dist/modules/devices/dto/channel-input-occurrence.dto.js +99 -0
  43. package/dist/modules/devices/dto/channel-input-occurrence.dto.js.map +1 -0
  44. package/dist/modules/devices/models/channel-input-capabilities.model.d.ts +20 -0
  45. package/dist/modules/devices/models/channel-input-capabilities.model.js +111 -0
  46. package/dist/modules/devices/models/channel-input-capabilities.model.js.map +1 -0
  47. package/dist/modules/devices/models/channel-input-occurrence.model.d.ts +22 -0
  48. package/dist/modules/devices/models/channel-input-occurrence.model.js +154 -0
  49. package/dist/modules/devices/models/channel-input-occurrence.model.js.map +1 -0
  50. package/dist/modules/devices/services/channel-input-deduplication.service.d.ts +17 -0
  51. package/dist/modules/devices/services/channel-input-deduplication.service.js +88 -0
  52. package/dist/modules/devices/services/channel-input-deduplication.service.js.map +1 -0
  53. package/dist/modules/devices/services/channel-input-occurrences.service.d.ts +21 -0
  54. package/dist/modules/devices/services/channel-input-occurrences.service.js +163 -0
  55. package/dist/modules/devices/services/channel-input-occurrences.service.js.map +1 -0
  56. package/dist/modules/devices/services/channels.properties.service.d.ts +27 -3
  57. package/dist/modules/devices/services/channels.properties.service.js +304 -28
  58. package/dist/modules/devices/services/channels.properties.service.js.map +1 -1
  59. package/dist/modules/devices/services/channels.service.js +42 -38
  60. package/dist/modules/devices/services/channels.service.js.map +1 -1
  61. package/dist/modules/devices/services/command-latency-trace-collector.service.d.ts +101 -0
  62. package/dist/modules/devices/services/command-latency-trace-collector.service.js +451 -0
  63. package/dist/modules/devices/services/command-latency-trace-collector.service.js.map +1 -0
  64. package/dist/modules/devices/services/device-structure-lock.service.d.ts +10 -2
  65. package/dist/modules/devices/services/device-structure-lock.service.js +86 -7
  66. package/dist/modules/devices/services/device-structure-lock.service.js.map +1 -1
  67. package/dist/modules/devices/services/devices.service.js +37 -33
  68. package/dist/modules/devices/services/devices.service.js.map +1 -1
  69. package/dist/modules/devices/services/module-reset.service.d.ts +3 -1
  70. package/dist/modules/devices/services/module-reset.service.js +21 -16
  71. package/dist/modules/devices/services/module-reset.service.js.map +1 -1
  72. package/dist/modules/devices/services/property-command-dispatch.service.d.ts +45 -0
  73. package/dist/modules/devices/services/property-command-dispatch.service.js +227 -0
  74. package/dist/modules/devices/services/property-command-dispatch.service.js.map +1 -0
  75. package/dist/modules/devices/services/property-command-window.service.d.ts +71 -0
  76. package/dist/modules/devices/services/property-command-window.service.js +204 -0
  77. package/dist/modules/devices/services/property-command-window.service.js.map +1 -0
  78. package/dist/modules/devices/services/property-command.service.d.ts +20 -3
  79. package/dist/modules/devices/services/property-command.service.js +56 -10
  80. package/dist/modules/devices/services/property-command.service.js.map +1 -1
  81. package/dist/modules/devices/services/property-state-coordinator.service.d.ts +4 -0
  82. package/dist/modules/devices/services/property-state-coordinator.service.js +35 -0
  83. package/dist/modules/devices/services/property-state-coordinator.service.js.map +1 -0
  84. package/dist/modules/devices/services/property-value.service.d.ts +1 -0
  85. package/dist/modules/devices/services/property-value.service.js +4 -0
  86. package/dist/modules/devices/services/property-value.service.js.map +1 -1
  87. package/dist/modules/devices/utils/schema.utils.js +1 -1
  88. package/dist/modules/devices/utils/schema.utils.js.map +1 -1
  89. package/dist/modules/platform/services/platform.service.d.ts +13 -4
  90. package/dist/modules/platform/services/platform.service.js +57 -35
  91. package/dist/modules/platform/services/platform.service.js.map +1 -1
  92. package/dist/modules/remote-access/dto/update-config.dto.js +5 -0
  93. package/dist/modules/remote-access/dto/update-config.dto.js.map +1 -1
  94. package/dist/modules/remote-access/validators/is-remote-access-url.validator.js +5 -2
  95. package/dist/modules/remote-access/validators/is-remote-access-url.validator.js.map +1 -1
  96. package/dist/modules/system/scripts/update-worker.sh +983 -61
  97. package/dist/modules/system/services/privileged-worker.service.d.ts +10 -0
  98. package/dist/modules/system/services/privileged-worker.service.js +137 -11
  99. package/dist/modules/system/services/privileged-worker.service.js.map +1 -1
  100. package/dist/modules/system/services/update-executor.service.d.ts +20 -2
  101. package/dist/modules/system/services/update-executor.service.js +257 -25
  102. package/dist/modules/system/services/update-executor.service.js.map +1 -1
  103. package/dist/modules/websocket/gateway/websocket.gateway.d.ts +4 -1
  104. package/dist/modules/websocket/gateway/websocket.gateway.js +28 -5
  105. package/dist/modules/websocket/gateway/websocket.gateway.js.map +1 -1
  106. package/dist/modules/websocket/services/command-acknowledgement-trace.service.d.ts +143 -0
  107. package/dist/modules/websocket/services/command-acknowledgement-trace.service.js +523 -0
  108. package/dist/modules/websocket/services/command-acknowledgement-trace.service.js.map +1 -0
  109. package/dist/modules/websocket/websocket.module.js +9 -1
  110. package/dist/modules/websocket/websocket.module.js.map +1 -1
  111. package/dist/plugins/devices-home-assistant/devices-home-assistant.openapi.d.ts +2 -2
  112. package/dist/plugins/devices-home-assistant/devices-home-assistant.plugin.d.ts +6 -1
  113. package/dist/plugins/devices-home-assistant/devices-home-assistant.plugin.js +16 -1
  114. package/dist/plugins/devices-home-assistant/devices-home-assistant.plugin.js.map +1 -1
  115. package/dist/plugins/devices-home-assistant/dto/mapping-preview.dto.d.ts +1 -0
  116. package/dist/plugins/devices-home-assistant/dto/mapping-preview.dto.js +11 -0
  117. package/dist/plugins/devices-home-assistant/dto/mapping-preview.dto.js.map +1 -1
  118. package/dist/plugins/devices-home-assistant/mappers/button.entity.mapper.service.d.ts +19 -0
  119. package/dist/plugins/devices-home-assistant/mappers/button.entity.mapper.service.js +51 -0
  120. package/dist/plugins/devices-home-assistant/mappers/button.entity.mapper.service.js.map +1 -0
  121. package/dist/plugins/devices-home-assistant/mappings/definitions/entity-mappings.yaml +56 -7
  122. package/dist/plugins/devices-home-assistant/models/mapping-preview.model.d.ts +1 -0
  123. package/dist/plugins/devices-home-assistant/models/mapping-preview.model.js +11 -0
  124. package/dist/plugins/devices-home-assistant/models/mapping-preview.model.js.map +1 -1
  125. package/dist/plugins/devices-home-assistant/services/device-adoption.service.js +17 -0
  126. package/dist/plugins/devices-home-assistant/services/device-adoption.service.js.map +1 -1
  127. package/dist/plugins/devices-home-assistant/services/home-assistant-raw-event.service.d.ts +27 -0
  128. package/dist/plugins/devices-home-assistant/services/home-assistant-raw-event.service.js +271 -0
  129. package/dist/plugins/devices-home-assistant/services/home-assistant-raw-event.service.js.map +1 -0
  130. package/dist/plugins/devices-home-assistant/services/home-assistant.ws.service.d.ts +2 -2
  131. package/dist/plugins/devices-home-assistant/services/home-assistant.ws.service.js +33 -14
  132. package/dist/plugins/devices-home-assistant/services/home-assistant.ws.service.js.map +1 -1
  133. package/dist/plugins/devices-home-assistant/services/mapping-preview.service.js +24 -4
  134. package/dist/plugins/devices-home-assistant/services/mapping-preview.service.js.map +1 -1
  135. package/dist/plugins/devices-home-assistant/services/state-changed.event.service.d.ts +4 -1
  136. package/dist/plugins/devices-home-assistant/services/state-changed.event.service.js +84 -5
  137. package/dist/plugins/devices-home-assistant/services/state-changed.event.service.js.map +1 -1
  138. package/dist/plugins/devices-home-assistant/services/wizard.service.js +1 -0
  139. package/dist/plugins/devices-home-assistant/services/wizard.service.js.map +1 -1
  140. package/dist/plugins/devices-home-assistant/utils/ha-event.utils.d.ts +2 -0
  141. package/dist/plugins/devices-home-assistant/utils/ha-event.utils.js +50 -0
  142. package/dist/plugins/devices-home-assistant/utils/ha-event.utils.js.map +1 -0
  143. package/dist/plugins/devices-homekit/listeners/homekit-event.listener.d.ts +2 -0
  144. package/dist/plugins/devices-homekit/listeners/homekit-event.listener.js +28 -1
  145. package/dist/plugins/devices-homekit/listeners/homekit-event.listener.js.map +1 -1
  146. package/dist/plugins/devices-homekit/mappers/base.mapper.js +27 -4
  147. package/dist/plugins/devices-homekit/mappers/base.mapper.js.map +1 -1
  148. package/dist/plugins/devices-homekit/mappers/button.mapper.d.ts +15 -0
  149. package/dist/plugins/devices-homekit/mappers/button.mapper.js +143 -0
  150. package/dist/plugins/devices-homekit/mappers/button.mapper.js.map +1 -0
  151. package/dist/plugins/devices-homekit/mappers/homekit-mapper.interface.d.ts +16 -0
  152. package/dist/plugins/devices-homekit/mappers/thermostat-coordinator.d.ts +7 -0
  153. package/dist/plugins/devices-homekit/mappers/thermostat-coordinator.js +84 -73
  154. package/dist/plugins/devices-homekit/mappers/thermostat-coordinator.js.map +1 -1
  155. package/dist/plugins/devices-homekit/services/homekit-mapper-registry.service.d.ts +5 -1
  156. package/dist/plugins/devices-homekit/services/homekit-mapper-registry.service.js +42 -0
  157. package/dist/plugins/devices-homekit/services/homekit-mapper-registry.service.js.map +1 -1
  158. package/dist/plugins/devices-homey/mappings/definitions/channels.yaml +50 -0
  159. package/dist/plugins/devices-homey/mappings/definitions/devices.yaml +18 -0
  160. package/dist/plugins/devices-homey/mappings/definitions/properties.yaml +55 -0
  161. package/dist/plugins/devices-homey/mappings/device-support.js +10 -0
  162. package/dist/plugins/devices-homey/mappings/device-support.js.map +1 -1
  163. package/dist/plugins/devices-homey/mappings/mapping-loader.service.d.ts +1 -0
  164. package/dist/plugins/devices-homey/mappings/mapping-loader.service.js +66 -8
  165. package/dist/plugins/devices-homey/mappings/mapping-loader.service.js.map +1 -1
  166. package/dist/plugins/devices-homey/models/inventory.model.d.ts +2 -1
  167. package/dist/plugins/devices-homey/models/inventory.model.js +1 -0
  168. package/dist/plugins/devices-homey/models/inventory.model.js.map +1 -1
  169. package/dist/plugins/devices-homey/models/mapping-preview.model.d.ts +1 -0
  170. package/dist/plugins/devices-homey/models/mapping-preview.model.js +1 -0
  171. package/dist/plugins/devices-homey/models/mapping-preview.model.js.map +1 -1
  172. package/dist/plugins/devices-homey/services/homey-mapping-preview.service.js +7 -0
  173. package/dist/plugins/devices-homey/services/homey-mapping-preview.service.js.map +1 -1
  174. package/dist/plugins/devices-homey/services/homey-synchronizer.service.d.ts +3 -1
  175. package/dist/plugins/devices-homey/services/homey-synchronizer.service.js +37 -6
  176. package/dist/plugins/devices-homey/services/homey-synchronizer.service.js.map +1 -1
  177. package/dist/plugins/devices-reterminal/services/reterminal-button.service.d.ts +7 -3
  178. package/dist/plugins/devices-reterminal/services/reterminal-button.service.js +63 -17
  179. package/dist/plugins/devices-reterminal/services/reterminal-button.service.js.map +1 -1
  180. package/dist/plugins/devices-shelly-ng/delegates/delegates-manager.service.d.ts +38 -3
  181. package/dist/plugins/devices-shelly-ng/delegates/delegates-manager.service.js +840 -134
  182. package/dist/plugins/devices-shelly-ng/delegates/delegates-manager.service.js.map +1 -1
  183. package/dist/plugins/devices-shelly-ng/delegates/shelly-device.delegate.d.ts +15 -1
  184. package/dist/plugins/devices-shelly-ng/delegates/shelly-device.delegate.js +117 -10
  185. package/dist/plugins/devices-shelly-ng/delegates/shelly-device.delegate.js.map +1 -1
  186. package/dist/plugins/devices-shelly-ng/devices-shelly-ng.constants.js +11 -13
  187. package/dist/plugins/devices-shelly-ng/devices-shelly-ng.constants.js.map +1 -1
  188. package/dist/plugins/devices-shelly-ng/devices-shelly-ng.plugin.js +2 -0
  189. package/dist/plugins/devices-shelly-ng/devices-shelly-ng.plugin.js.map +1 -1
  190. package/dist/plugins/devices-shelly-ng/mappings/definitions/inputs.yaml +80 -0
  191. package/dist/plugins/devices-shelly-ng/mappings/mapping-loader.service.js +11 -1
  192. package/dist/plugins/devices-shelly-ng/mappings/mapping-loader.service.js.map +1 -1
  193. package/dist/plugins/devices-shelly-ng/mappings/mapping.types.d.ts +6 -3
  194. package/dist/plugins/devices-shelly-ng/mappings/schema/mapping-schema.json +5 -0
  195. package/dist/plugins/devices-shelly-ng/services/device-manager.service.js +130 -7
  196. package/dist/plugins/devices-shelly-ng/services/device-manager.service.js.map +1 -1
  197. package/dist/plugins/devices-shelly-ng/services/poll-placement-diagnostics.service.d.ts +48 -0
  198. package/dist/plugins/devices-shelly-ng/services/poll-placement-diagnostics.service.js +457 -0
  199. package/dist/plugins/devices-shelly-ng/services/poll-placement-diagnostics.service.js.map +1 -0
  200. package/dist/plugins/devices-shelly-ng/services/shelly-ng.service.d.ts +12 -2
  201. package/dist/plugins/devices-shelly-ng/services/shelly-ng.service.js +86 -28
  202. package/dist/plugins/devices-shelly-ng/services/shelly-ng.service.js.map +1 -1
  203. package/dist/plugins/devices-shelly-ng/services/shelly-rpc-client.service.js +3 -0
  204. package/dist/plugins/devices-shelly-ng/services/shelly-rpc-client.service.js.map +1 -1
  205. package/dist/plugins/devices-shelly-ng/services/shelly-ws-server.service.d.ts +1 -1
  206. package/dist/plugins/devices-shelly-ng/services/shelly-ws-server.service.js +18 -13
  207. package/dist/plugins/devices-shelly-ng/services/shelly-ws-server.service.js.map +1 -1
  208. package/dist/plugins/devices-shelly-ng/utils/transform.utils.d.ts +2 -0
  209. package/dist/plugins/devices-shelly-ng/utils/transform.utils.js +14 -1
  210. package/dist/plugins/devices-shelly-ng/utils/transform.utils.js.map +1 -1
  211. package/dist/plugins/devices-shelly-v1/devices-shelly-v1.constants.d.ts +2 -0
  212. package/dist/plugins/devices-shelly-v1/devices-shelly-v1.constants.js +66 -4
  213. package/dist/plugins/devices-shelly-v1/devices-shelly-v1.constants.js.map +1 -1
  214. package/dist/plugins/devices-shelly-v1/devices-shelly-v1.plugin.js +2 -0
  215. package/dist/plugins/devices-shelly-v1/devices-shelly-v1.plugin.js.map +1 -1
  216. package/dist/plugins/devices-shelly-v1/platforms/shelly-v1.device.platform.js +7 -0
  217. package/dist/plugins/devices-shelly-v1/platforms/shelly-v1.device.platform.js.map +1 -1
  218. package/dist/plugins/devices-shelly-v1/services/device-mapper.service.d.ts +3 -1
  219. package/dist/plugins/devices-shelly-v1/services/device-mapper.service.js +20 -2
  220. package/dist/plugins/devices-shelly-v1/services/device-mapper.service.js.map +1 -1
  221. package/dist/plugins/devices-shelly-v1/services/shelly-v1-input-tracker.service.d.ts +16 -0
  222. package/dist/plugins/devices-shelly-v1/services/shelly-v1-input-tracker.service.js +107 -0
  223. package/dist/plugins/devices-shelly-v1/services/shelly-v1-input-tracker.service.js.map +1 -0
  224. package/dist/plugins/devices-shelly-v1/services/shelly-v1.service.d.ts +7 -1
  225. package/dist/plugins/devices-shelly-v1/services/shelly-v1.service.js +98 -3
  226. package/dist/plugins/devices-shelly-v1/services/shelly-v1.service.js.map +1 -1
  227. package/dist/plugins/devices-third-party/controllers/third-party-inputs.controller.d.ts +17 -0
  228. package/dist/plugins/devices-third-party/controllers/third-party-inputs.controller.js +205 -0
  229. package/dist/plugins/devices-third-party/controllers/third-party-inputs.controller.js.map +1 -0
  230. package/dist/plugins/devices-third-party/devices-third-party.openapi.d.ts +1 -1
  231. package/dist/plugins/devices-third-party/devices-third-party.plugin.js +2 -1
  232. package/dist/plugins/devices-third-party/devices-third-party.plugin.js.map +1 -1
  233. package/dist/plugins/devices-third-party/dto/report-input-occurrence-response.dto.d.ts +5 -0
  234. package/dist/plugins/devices-third-party/dto/report-input-occurrence-response.dto.js +50 -0
  235. package/dist/plugins/devices-third-party/dto/report-input-occurrence-response.dto.js.map +1 -0
  236. package/dist/plugins/devices-third-party/dto/report-input-occurrence.dto.d.ts +8 -0
  237. package/dist/plugins/devices-third-party/dto/report-input-occurrence.dto.js +81 -0
  238. package/dist/plugins/devices-third-party/dto/report-input-occurrence.dto.js.map +1 -0
  239. package/dist/plugins/devices-third-party/platforms/third-party-device.platform.js +19 -4
  240. package/dist/plugins/devices-third-party/platforms/third-party-device.platform.js.map +1 -1
  241. package/dist/plugins/devices-virtual/listeners/virtual-projection.listener.d.ts +2 -0
  242. package/dist/plugins/devices-virtual/listeners/virtual-projection.listener.js +57 -0
  243. package/dist/plugins/devices-virtual/listeners/virtual-projection.listener.js.map +1 -1
  244. package/dist/plugins/devices-zigbee2mqtt/devices-zigbee2mqtt.constants.d.ts +3 -0
  245. package/dist/plugins/devices-zigbee2mqtt/devices-zigbee2mqtt.constants.js +41 -5
  246. package/dist/plugins/devices-zigbee2mqtt/devices-zigbee2mqtt.constants.js.map +1 -1
  247. package/dist/plugins/devices-zigbee2mqtt/interfaces/zigbee2mqtt.interface.d.ts +7 -1
  248. package/dist/plugins/devices-zigbee2mqtt/mappings/config-driven.converter.js +16 -15
  249. package/dist/plugins/devices-zigbee2mqtt/mappings/config-driven.converter.js.map +1 -1
  250. package/dist/plugins/devices-zigbee2mqtt/mappings/definitions/remotes.yaml +505 -74
  251. package/dist/plugins/devices-zigbee2mqtt/mappings/schema/mapping-schema.json +3 -3
  252. package/dist/plugins/devices-zigbee2mqtt/mappings/transformers/transformer.types.d.ts +1 -1
  253. package/dist/plugins/devices-zigbee2mqtt/services/base-client-adapter.d.ts +8 -2
  254. package/dist/plugins/devices-zigbee2mqtt/services/base-client-adapter.js +6 -5
  255. package/dist/plugins/devices-zigbee2mqtt/services/base-client-adapter.js.map +1 -1
  256. package/dist/plugins/devices-zigbee2mqtt/services/device-adoption.service.js +18 -11
  257. package/dist/plugins/devices-zigbee2mqtt/services/device-adoption.service.js.map +1 -1
  258. package/dist/plugins/devices-zigbee2mqtt/services/device-mapper.service.d.ts +9 -3
  259. package/dist/plugins/devices-zigbee2mqtt/services/device-mapper.service.js +166 -3
  260. package/dist/plugins/devices-zigbee2mqtt/services/device-mapper.service.js.map +1 -1
  261. package/dist/plugins/devices-zigbee2mqtt/services/mqtt-client-adapter.service.js +5 -2
  262. package/dist/plugins/devices-zigbee2mqtt/services/mqtt-client-adapter.service.js.map +1 -1
  263. package/dist/plugins/devices-zigbee2mqtt/services/zigbee2mqtt.service.d.ts +1 -0
  264. package/dist/plugins/devices-zigbee2mqtt/services/zigbee2mqtt.service.js +44 -4
  265. package/dist/plugins/devices-zigbee2mqtt/services/zigbee2mqtt.service.js.map +1 -1
  266. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.d.ts +17 -0
  267. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.js +147 -0
  268. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.js.map +1 -0
  269. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.d.ts +15 -0
  270. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.js +98 -0
  271. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.js.map +1 -0
  272. package/dist/plugins/remote-access-cloudflare-tunnel/dto/update-config.dto.d.ts +9 -0
  273. package/dist/plugins/remote-access-cloudflare-tunnel/dto/update-config.dto.js +94 -0
  274. package/dist/plugins/remote-access-cloudflare-tunnel/dto/update-config.dto.js.map +1 -0
  275. package/dist/plugins/remote-access-cloudflare-tunnel/models/config.model.d.ts +9 -0
  276. package/dist/plugins/remote-access-cloudflare-tunnel/models/config.model.js +92 -0
  277. package/dist/plugins/remote-access-cloudflare-tunnel/models/config.model.js.map +1 -0
  278. package/dist/plugins/remote-access-cloudflare-tunnel/models/install.model.d.ts +7 -0
  279. package/dist/plugins/remote-access-cloudflare-tunnel/models/install.model.js +48 -0
  280. package/dist/plugins/remote-access-cloudflare-tunnel/models/install.model.js.map +1 -0
  281. package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.d.ts +42 -0
  282. package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.js +352 -0
  283. package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.js.map +1 -0
  284. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.constants.d.ts +24 -0
  285. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.constants.js +27 -0
  286. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.constants.js.map +1 -0
  287. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.exceptions.d.ts +8 -0
  288. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.exceptions.js +19 -0
  289. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.exceptions.js.map +1 -0
  290. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.openapi.d.ts +4 -0
  291. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.openapi.js +20 -0
  292. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.openapi.js.map +1 -0
  293. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.plugin.d.ts +21 -0
  294. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.plugin.js +135 -0
  295. package/dist/plugins/remote-access-cloudflare-tunnel/remote-access-cloudflare-tunnel.plugin.js.map +1 -0
  296. package/dist/plugins/remote-access-cloudflare-tunnel/scripts/cloudflared-setup.sh +185 -0
  297. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.d.ts +82 -0
  298. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.js +507 -0
  299. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.js.map +1 -0
  300. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.d.ts +10 -0
  301. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.js +37 -0
  302. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.js.map +1 -0
  303. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-setup.service.d.ts +38 -0
  304. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-setup.service.js +147 -0
  305. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-setup.service.js.map +1 -0
  306. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.d.ts +15 -0
  307. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.js +67 -0
  308. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.js.map +1 -0
  309. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.d.ts +8 -0
  310. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.js +46 -0
  311. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.js.map +1 -0
  312. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-process.service.d.ts +27 -0
  313. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-process.service.js +139 -0
  314. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-process.service.js.map +1 -0
  315. package/dist/plugins/remote-access-tailscale/controllers/setup.controller.d.ts +1 -0
  316. package/dist/plugins/remote-access-tailscale/controllers/setup.controller.js +26 -10
  317. package/dist/plugins/remote-access-tailscale/controllers/setup.controller.js.map +1 -1
  318. package/dist/plugins/remote-access-tailscale/controllers/status.controller.d.ts +6 -1
  319. package/dist/plugins/remote-access-tailscale/controllers/status.controller.js +28 -3
  320. package/dist/plugins/remote-access-tailscale/controllers/status.controller.js.map +1 -1
  321. package/dist/plugins/remote-access-tailscale/models/status.model.d.ts +19 -0
  322. package/dist/plugins/remote-access-tailscale/models/status.model.js +158 -1
  323. package/dist/plugins/remote-access-tailscale/models/status.model.js.map +1 -1
  324. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.d.ts +3 -0
  325. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.js +4 -1
  326. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.js.map +1 -1
  327. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.d.ts +13 -0
  328. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.js +27 -0
  329. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.js.map +1 -0
  330. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.d.ts +2 -2
  331. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.js +2 -0
  332. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.js.map +1 -1
  333. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.plugin.js +1 -1
  334. package/dist/plugins/remote-access-tailscale/scripts/tailscale-setup.sh +61 -0
  335. package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.d.ts +9 -0
  336. package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.js +12 -0
  337. package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.js.map +1 -1
  338. package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.d.ts +1 -0
  339. package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.js +27 -2
  340. package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.js.map +1 -1
  341. package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.d.ts +31 -1
  342. package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.js +316 -30
  343. package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.js.map +1 -1
  344. package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.d.ts +8 -3
  345. package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.js +39 -12
  346. package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.js.map +1 -1
  347. package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.d.ts +14 -5
  348. package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.js +51 -16
  349. package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.js.map +1 -1
  350. package/dist/plugins/scenes-local/platforms/local-scene.platform.d.ts +3 -1
  351. package/dist/plugins/scenes-local/platforms/local-scene.platform.js +12 -7
  352. package/dist/plugins/scenes-local/platforms/local-scene.platform.js.map +1 -1
  353. package/dist/plugins/simulator/controllers/simulator.controller.d.ts +8 -2
  354. package/dist/plugins/simulator/controllers/simulator.controller.js +102 -2
  355. package/dist/plugins/simulator/controllers/simulator.controller.js.map +1 -1
  356. package/dist/plugins/simulator/dto/simulate-occurrence.dto.d.ts +12 -0
  357. package/dist/plugins/simulator/dto/simulate-occurrence.dto.js +122 -0
  358. package/dist/plugins/simulator/dto/simulate-occurrence.dto.js.map +1 -0
  359. package/dist/plugins/simulator/models/simulator-response.model.d.ts +12 -0
  360. package/dist/plugins/simulator/models/simulator-response.model.js +84 -1
  361. package/dist/plugins/simulator/models/simulator-response.model.js.map +1 -1
  362. package/dist/plugins/simulator/scenarios/definitions/small-apartment.yaml +78 -0
  363. package/dist/plugins/simulator/services/scenario-executor.service.js +3 -1
  364. package/dist/plugins/simulator/services/scenario-executor.service.js.map +1 -1
  365. package/dist/plugins/simulator/services/simulation.service.js +10 -1
  366. package/dist/plugins/simulator/services/simulation.service.js.map +1 -1
  367. package/dist/plugins/simulator/simulator.openapi.d.ts +3 -2
  368. package/dist/plugins/simulator/simulator.openapi.js +5 -0
  369. package/dist/plugins/simulator/simulator.openapi.js.map +1 -1
  370. package/dist/plugins/simulator/simulators/device-simulator.interface.d.ts +2 -0
  371. package/dist/plugins/simulator/simulators/device-simulator.interface.js +83 -0
  372. package/dist/plugins/simulator/simulators/device-simulator.interface.js.map +1 -1
  373. package/dist/plugins/simulator/simulators/index.d.ts +1 -0
  374. package/dist/plugins/simulator/simulators/index.js +1 -0
  375. package/dist/plugins/simulator/simulators/index.js.map +1 -1
  376. package/dist/plugins/simulator/simulators/input-controller.simulator.d.ts +10 -0
  377. package/dist/plugins/simulator/simulators/input-controller.simulator.js +57 -0
  378. package/dist/plugins/simulator/simulators/input-controller.simulator.js.map +1 -0
  379. package/dist/plugins/simulator/simulators/lighting.simulator.js +1 -0
  380. package/dist/plugins/simulator/simulators/lighting.simulator.js.map +1 -1
  381. package/dist/plugins/spaces-home-control/services/climate-intent.service.d.ts +3 -1
  382. package/dist/plugins/spaces-home-control/services/climate-intent.service.js +6 -3
  383. package/dist/plugins/spaces-home-control/services/climate-intent.service.js.map +1 -1
  384. package/dist/plugins/spaces-home-control/services/covers-intent.service.d.ts +3 -1
  385. package/dist/plugins/spaces-home-control/services/covers-intent.service.js +6 -3
  386. package/dist/plugins/spaces-home-control/services/covers-intent.service.js.map +1 -1
  387. package/dist/plugins/spaces-home-control/services/lighting-intent.service.d.ts +3 -1
  388. package/dist/plugins/spaces-home-control/services/lighting-intent.service.js +7 -4
  389. package/dist/plugins/spaces-home-control/services/lighting-intent.service.js.map +1 -1
  390. package/dist/plugins/spaces-home-control/services/space-media-activity.service.d.ts +3 -1
  391. package/dist/plugins/spaces-home-control/services/space-media-activity.service.js +11 -5
  392. package/dist/plugins/spaces-home-control/services/space-media-activity.service.js.map +1 -1
  393. package/dist/plugins/spaces-home-control/services/space-undo-history.service.d.ts +3 -1
  394. package/dist/plugins/spaces-home-control/services/space-undo-history.service.js +8 -5
  395. package/dist/plugins/spaces-home-control/services/space-undo-history.service.js.map +1 -1
  396. package/dist/spec/channels.d.ts +51 -1
  397. package/dist/spec/channels.js +64 -1
  398. package/dist/spec/channels.js.map +1 -1
  399. package/dist/spec/devices.d.ts +155 -2
  400. package/dist/spec/devices.js +161 -2
  401. package/dist/spec/devices.js.map +1 -1
  402. package/dist/tsconfig.build.tsbuildinfo +1 -1
  403. package/package.json +2 -2
@@ -6,6 +6,666 @@ STATUS_FILE="${STATUS_FILE:-/var/lib/smart-panel/update-status.json}"
6
6
  INSTALL_TYPE="${INSTALL_TYPE:-npm}"
7
7
  IMAGE_BASE_DIR="${IMAGE_BASE_DIR:-/opt/smart-panel}"
8
8
  DOWNLOAD_URL="${DOWNLOAD_URL:-}"
9
+ HEALTH_URL="${HEALTH_URL:-}"
10
+ HEALTH_EXPECTED_VERSION="${HEALTH_EXPECTED_VERSION:-$VERSION}"
11
+ UPDATE_START_MODE="${UPDATE_START_MODE:-running-service}"
12
+
13
+ # `stopped-maintenance` is an operator-only mode. The API always supplies the default
14
+ # running-service mode; this mode is intentionally guarded by a complete, explicit identity
15
+ # packet and is never inferred from a stale status file or an ambient process environment.
16
+ if [ "$UPDATE_START_MODE" != "running-service" ] && [ "$UPDATE_START_MODE" != "stopped-maintenance" ]; then
17
+ printf 'Unsupported update start mode: %s\n' "$UPDATE_START_MODE" >&2
18
+ exit 64
19
+ fi
20
+
21
+ # The attempt record and lock live beside the public status file, never inside a versioned image
22
+ # directory. This lets the record survive a backend restart, target-directory cleanup or a
23
+ # failed migration, while keeping it outside the release tarball and private to the service user.
24
+ ATTEMPT_DIR="${ATTEMPT_DIR:-$(dirname "$STATUS_FILE")/update-attempt}"
25
+ ATTEMPT_FILE="${ATTEMPT_DIR}/attempt.json"
26
+ ATTEMPT_LOCK="${ATTEMPT_DIR}/lock"
27
+ ATTEMPT_ID=""
28
+ MIGRATION_ENTERED="false"
29
+ FINALIZED="false"
30
+ MIGRATION_LOG=""
31
+ CURRENT_PHASE="preparing"
32
+ SERVICE_NAME="smart-panel"
33
+ SERVICE_UNIT="${SERVICE_UNIT:-${SERVICE_NAME}.service}"
34
+ QUIESCENCE_TIMEOUT_SECONDS="${QUIESCENCE_TIMEOUT_SECONDS:-30}"
35
+ START_TIMEOUT_SECONDS="${START_TIMEOUT_SECONDS:-60}"
36
+ CAPTURED_SERVICE_CGROUP_PROCS=""
37
+ CAPTURED_SERVICE_CGROUP_DIR=""
38
+ CAPTURED_SERVICE_CGROUP_EVENTS=""
39
+ CAPTURED_SERVICE_MEMBER_IDENTITIES=""
40
+ CGROUP_MEMBERS=""
41
+ CGROUP_POPULATED=""
42
+ MAINTENANCE_DIR=""
43
+ DB_BACKUP_PATH="${DB_BACKUP_PATH:-}"
44
+ BACKUP_TMP_PATH=""
45
+ SOURCE_DB_PATH=""
46
+ STOPPED_SERVICE_BASELINE=""
47
+
48
+ json_escape() {
49
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/[[:cntrl:]]//g'
50
+ }
51
+
52
+ source_database_is_valid() {
53
+ [ -n "${SOURCE_DB_PATH:-}" ] || return 1
54
+ [ ! -L "$SOURCE_DB_PATH" ] && [ -f "$SOURCE_DB_PATH" ] && [ -s "$SOURCE_DB_PATH" ]
55
+ }
56
+
57
+ write_attempt() {
58
+ local state="$1"
59
+ local phase="$2"
60
+ local error="${3:-}"
61
+ local recovery_required="${4:-false}"
62
+ local migration_entered="${5:-$MIGRATION_ENTERED}"
63
+ local tmp_file="${ATTEMPT_FILE}.tmp.$$"
64
+ CURRENT_PHASE="$phase"
65
+
66
+ mkdir -p "$ATTEMPT_DIR"
67
+ chmod 700 "$ATTEMPT_DIR"
68
+
69
+ {
70
+ printf '{\n'
71
+ printf '\t"attemptId": "%s",\n' "$(json_escape "$ATTEMPT_ID")"
72
+ printf '\t"ownerPid": %s,\n' "$$"
73
+ printf '\t"targetVersion": "%s",\n' "$(json_escape "$VERSION")"
74
+ printf '\t"state": "%s",\n' "$(json_escape "$state")"
75
+ printf '\t"phase": "%s",\n' "$(json_escape "$phase")"
76
+ printf '\t"migrationEntered": %s,\n' "$migration_entered"
77
+ printf '\t"recoveryRequired": %s,\n' "$recovery_required"
78
+ printf '\t"migrationLog": "%s",\n' "$(json_escape "$MIGRATION_LOG")"
79
+ printf '\t"updatedAt": "%s",\n' "$(date -Iseconds)"
80
+ printf '\t"error": "%s"\n' "$(json_escape "$error")"
81
+ printf '}\n'
82
+ } > "$tmp_file"
83
+
84
+ chmod 600 "$tmp_file"
85
+ mv -f "$tmp_file" "$ATTEMPT_FILE"
86
+ }
87
+
88
+ attempt_is_unresolved() {
89
+ if [ -f "$ATTEMPT_FILE" ] && grep -q '"state": "complete"' "$ATTEMPT_FILE"; then
90
+ # A crash after the durable completion record but before rmdir must not strand a clean
91
+ # attempt forever; completion is the evidence that makes this lock removable.
92
+ rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
93
+
94
+ return 1
95
+ fi
96
+
97
+ if [ -d "$ATTEMPT_LOCK" ]; then
98
+ return 0
99
+ fi
100
+
101
+ if [ ! -f "$ATTEMPT_FILE" ]; then
102
+ return 1
103
+ fi
104
+
105
+ grep -q '"state": "active"' "$ATTEMPT_FILE" || grep -q '"recoveryRequired": true' "$ATTEMPT_FILE"
106
+ }
107
+
108
+ acquire_attempt() {
109
+ mkdir -p "$ATTEMPT_DIR"
110
+ chmod 700 "$ATTEMPT_DIR"
111
+
112
+ if attempt_is_unresolved; then
113
+ return 1
114
+ fi
115
+
116
+ # mkdir is the portable atomic OS-level lock used by the legacy launcher. An orphaned lock is
117
+ # deliberately retained; an expired timestamp cannot prove that a migration did not commit.
118
+ if ! mkdir "$ATTEMPT_LOCK" 2>/dev/null; then
119
+ return 1
120
+ fi
121
+ chmod 700 "$ATTEMPT_LOCK"
122
+
123
+ ATTEMPT_ID="$(date +%s)-$$"
124
+ write_attempt "active" "preparing" "" "false" "false"
125
+ }
126
+
127
+ release_attempt() {
128
+ if [ -n "$ATTEMPT_ID" ]; then
129
+ write_attempt "complete" "complete" "" "false" "$MIGRATION_ENTERED"
130
+ fi
131
+
132
+ rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
133
+ }
134
+
135
+ hold_attempt() {
136
+ local phase="$1"
137
+ local error="$2"
138
+
139
+ write_attempt "recovery_required" "$phase" "$error" "true" "$MIGRATION_ENTERED"
140
+ }
141
+
142
+ finish_before_migration() {
143
+ local phase="$1"
144
+ local error="$2"
145
+
146
+ if [ -n "${BACKUP_TMP_PATH:-}" ]; then
147
+ rm -f "$BACKUP_TMP_PATH" 2>/dev/null || true
148
+ BACKUP_TMP_PATH=""
149
+ fi
150
+ if [ -n "${TMP_TARBALL:-}" ]; then
151
+ rm -f "$TMP_TARBALL" 2>/dev/null || true
152
+ fi
153
+
154
+ write_attempt "failed_preflight" "$phase" "$error" "false" "false"
155
+ rmdir "$ATTEMPT_LOCK" 2>/dev/null || true
156
+ }
157
+
158
+ phase_requires_hold() {
159
+ case "$CURRENT_PHASE" in
160
+ stopping|quiesced|switching) return 0 ;;
161
+ *) return 1 ;;
162
+ esac
163
+ }
164
+
165
+ fail_before_migration() {
166
+ local phase="$1"
167
+ local error="$2"
168
+
169
+ if [ -n "$ATTEMPT_ID" ]; then
170
+ finish_before_migration "$phase" "$error"
171
+ fi
172
+
173
+ update_status "failed" "failed" "$error"
174
+ FINALIZED="true"
175
+ exit 1
176
+ }
177
+
178
+ restore_before_migration() {
179
+ local previous_target="$1"
180
+ local current_link="$2"
181
+ local new_version_dir="$3"
182
+ local resolved_previous resolved_current
183
+
184
+ [ -n "$previous_target" ] || return 1
185
+ sudo -n ln -sfn "$previous_target" "$current_link" || return 1
186
+
187
+ resolved_previous="$(realpath "$previous_target" 2>/dev/null || true)"
188
+ resolved_current="$(realpath "$current_link" 2>/dev/null || true)"
189
+ [ -n "$resolved_previous" ] && [ "$resolved_previous" = "$resolved_current" ] || return 1
190
+
191
+ rm -rf "$new_version_dir" || return 1
192
+ sudo -n systemctl start "$SERVICE_NAME" 2>/dev/null || return 1
193
+ wait_for_started_service
194
+ }
195
+
196
+ service_property() {
197
+ local property="$1"
198
+
199
+ systemctl show "$SERVICE_NAME" --property="$property" --value
200
+ }
201
+
202
+ manager_processes_are_empty() {
203
+ local output token
204
+
205
+ # Tests may provide a deterministic manager enumeration. Production uses the systemd Manager
206
+ # D-Bus method instead of treating MainPID=0 as an independent process proof.
207
+ if [ -n "${SERVICE_MANAGER_PROCS_FILE:-}" ]; then
208
+ [ -r "$SERVICE_MANAGER_PROCS_FILE" ] || return 2
209
+ output="$(cat "$SERVICE_MANAGER_PROCS_FILE" 2>/dev/null)" || return 2
210
+ else
211
+ command -v busctl >/dev/null 2>&1 || return 2
212
+ output="$(busctl --system call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
213
+ org.freedesktop.systemd1.Manager GetUnitProcesses s "$SERVICE_UNIT" 2>/dev/null)" || return 2
214
+ fi
215
+
216
+ # busctl returns an array header followed by tuples. Any positive integer in the tuple stream
217
+ # is a process owned by the unit; zero is the empty-array count/fixture sentinel.
218
+ while IFS= read -r token; do
219
+ case "$token" in
220
+ ''|0) ;;
221
+ *[!0-9]*) ;;
222
+ *) [ "$token" -eq 0 ] || return 1 ;;
223
+ esac
224
+ done <<EOF
225
+ $(printf '%s\n' "$output" | grep -Eo '[0-9]+' || true)
226
+ EOF
227
+ }
228
+
229
+ stopped_service_quiesced() {
230
+ local active_state main_pid control_pid job_id control_group cgroup_dir cgroup_procs cgroup_events
231
+ local member_status population_status probe_status members subtree_file baseline current_baseline property value cgroup_mount_type
232
+
233
+ active_state="$(service_property ActiveState 2>/dev/null)" || return 2
234
+ if [ -n "${SERVICE_CGROUP_MOUNT_TYPE:-}" ]; then
235
+ [ "$SERVICE_CGROUP_MOUNT_TYPE" = "cgroup2fs" ] || return 2
236
+ else
237
+ cgroup_mount_type="$(LC_ALL=C stat -f -c '%T' "${SERVICE_CGROUP_MOUNT:-/sys/fs/cgroup}" 2>/dev/null || true)"
238
+ [ "$cgroup_mount_type" = "cgroup2fs" ] || return 2
239
+ fi
240
+ main_pid="$(service_property MainPID 2>/dev/null)" || return 2
241
+ control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
242
+ job_id="$(service_property Job 2>/dev/null)" || return 2
243
+ case "$active_state" in inactive|failed) ;; *) return 1 ;; esac
244
+ [ "$main_pid" = "0" ] || return 1
245
+ [ "$control_pid" = "0" ] || return 1
246
+ # systemctl --value prints an empty value when the unit has no pending job.
247
+ [ -z "$job_id" ] || [ "$job_id" = "0" ] || return 1
248
+
249
+ manager_processes_are_empty || return $?
250
+ baseline=""
251
+ for property in FragmentPath ExecStart Slice Delegate KillMode InvocationID Result; do
252
+ value="$(service_property "$property" 2>/dev/null || true)"
253
+ [ -n "$value" ] || return 2
254
+ baseline="${baseline}${property}=${value}\n"
255
+ done
256
+ if [ -n "$STOPPED_SERVICE_BASELINE" ]; then
257
+ current_baseline="$baseline"
258
+ [ "$current_baseline" = "$STOPPED_SERVICE_BASELINE" ] || return 1
259
+ else
260
+ STOPPED_SERVICE_BASELINE="$baseline"
261
+ fi
262
+
263
+ control_group="$(service_property ControlGroup 2>/dev/null || true)"
264
+ # systemd may clear ControlGroup after a stopped unit is reaped. The canonical unit cgroup is
265
+ # still the only acceptable fallback; it is checked against the cgroup-v2 mount below.
266
+ [ -n "$control_group" ] || control_group="/system.slice/${SERVICE_NAME}.service"
267
+ case "$control_group" in
268
+ /*) ;;
269
+ *) return 2 ;;
270
+ esac
271
+ cgroup_procs="${SERVICE_CGROUP_PROCS_FILE:-/sys/fs/cgroup${control_group}/cgroup.procs}"
272
+ cgroup_dir="${SERVICE_CGROUP_DIR:-${cgroup_procs%/cgroup.procs}}"
273
+ cgroup_events="${SERVICE_CGROUP_EVENTS_FILE:-${cgroup_dir}/cgroup.events}"
274
+
275
+ if probe_cgroup_directory "$cgroup_dir"; then
276
+ [ -r "$cgroup_procs" ] || return 2
277
+ [ -r "$cgroup_events" ] || return 2
278
+ read_cgroup_members "$cgroup_procs" || return $?
279
+ members="$CGROUP_MEMBERS"
280
+ [ -z "$members" ] || return 1
281
+ read_cgroup_population "$cgroup_events" || return $?
282
+ [ "$CGROUP_POPULATED" = "0" ] || return 1
283
+ # Check descendants as well as the unit's direct cgroup. A delegated child subtree is not
284
+ # quiescent merely because the parent cgroup.procs is empty.
285
+ while IFS= read -r subtree_file; do
286
+ [ "$subtree_file" = "$cgroup_procs" ] && continue
287
+ read_cgroup_members "$subtree_file" || return $?
288
+ [ -z "$CGROUP_MEMBERS" ] || return 1
289
+ done < <(find "$cgroup_dir" -type f -name cgroup.procs -print 2>/dev/null)
290
+ return 0
291
+ else
292
+ probe_status=$?
293
+ fi
294
+ [ "$probe_status" -eq 1 ] || return 2
295
+
296
+ # A removed original cgroup is valid only when the cgroup-v2 mount and its stable parent remain
297
+ # readable; manager enumeration above is the independent proof that no unit process survived.
298
+ [ -d "${SERVICE_CGROUP_MOUNT:-/sys/fs/cgroup}" ] || return 2
299
+ [ -d "$(dirname "$cgroup_dir")" ] || return 2
300
+ return 0
301
+ }
302
+
303
+ wait_for_stopped_service() {
304
+ local deadline=$((SECONDS + QUIESCENCE_TIMEOUT_SECONDS)) result
305
+
306
+ while [ "$SECONDS" -le "$deadline" ]; do
307
+ if stopped_service_quiesced; then return 0; else result=$?; fi
308
+ [ "$result" -eq 2 ] || sleep 1
309
+ [ "$result" -eq 2 ] && return 2
310
+ done
311
+ return 1
312
+ }
313
+
314
+ target_health_is_valid() {
315
+ local payload
316
+
317
+ [ -n "$HEALTH_URL" ] || return 1
318
+ payload="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null)" || return 1
319
+
320
+ printf '%s' "$payload" | HEALTH_EXPECTED_VERSION="$HEALTH_EXPECTED_VERSION" node -e '
321
+ let input = "";
322
+ process.stdin.setEncoding("utf8");
323
+ process.stdin.on("data", (chunk) => { input += chunk; });
324
+ process.stdin.on("end", () => {
325
+ try {
326
+ const body = JSON.parse(input);
327
+ const data = body && body.data;
328
+ process.exit(data?.status === "ok" && data.version === process.env.HEALTH_EXPECTED_VERSION ? 0 : 1);
329
+ } catch {
330
+ process.exit(1);
331
+ }
332
+ });
333
+ '
334
+ }
335
+
336
+ target_process_is_stable() {
337
+ local state main_pid start_identity resolved_target expected_target
338
+
339
+ state="$(service_property ActiveState 2>/dev/null || true)"
340
+ main_pid="$(service_property MainPID 2>/dev/null || true)"
341
+ start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
342
+ resolved_target="$(readlink -f "$CURRENT_LINK" 2>/dev/null || true)"
343
+ expected_target="$(realpath "$NEW_VERSION_DIR" 2>/dev/null || true)"
344
+
345
+ [ "$state" = "active" ] || return 1
346
+ [ "$main_pid" != "0" ] && [ -n "$main_pid" ] || return 1
347
+ [ -n "$start_identity" ] || return 1
348
+ [ -n "$CURRENT_LINK" ] && [ -n "$NEW_VERSION_DIR" ] || return 1
349
+ [ -n "$expected_target" ] && [ "$resolved_target" = "$expected_target" ] || return 1
350
+ }
351
+
352
+ process_identity() {
353
+ local pid="$1"
354
+ local proc_root="${SERVICE_PROCESS_ROOT:-/proc}"
355
+ local proc_dir="${proc_root}/${pid}"
356
+ local stat_line start_time executable command
357
+
358
+ [ "$pid" != "0" ] && [ -n "$pid" ] || return 1
359
+
360
+ # SERVICE_PROCESS_ROOT is a private fixture hook; the production default is /proc.
361
+ if [ -r "${proc_dir}/identity" ]; then
362
+ printf '%s|%s\n' "$pid" "$(cat "${proc_dir}/identity")"
363
+ return 0
364
+ fi
365
+
366
+ if [ -r "${proc_dir}/stat" ]; then
367
+ stat_line="$(cat "${proc_dir}/stat")" || return 2
368
+ start_time="$(printf '%s\n' "$stat_line" | awk '{ sub(/^.*\\) /, ""); print $20 }')" || return 2
369
+ [ -n "$start_time" ] || return 2
370
+ executable="$(readlink "${proc_dir}/exe" 2>/dev/null)" || return 2
371
+ [ -n "$executable" ] || return 2
372
+ printf '%s|%s|%s\n' "$pid" "$start_time" "$executable"
373
+ return 0
374
+ fi
375
+
376
+ if ! kill -0 "$pid" 2>/dev/null; then
377
+ return 1
378
+ fi
379
+
380
+ command="$(ps -p "$pid" -o command= 2>/dev/null)" || return 2
381
+ [ -n "$command" ] || return 2
382
+ printf '%s|%s\n' "$pid" "$command"
383
+ }
384
+
385
+ read_cgroup_members() {
386
+ local cgroup_procs="$1"
387
+ local contents members
388
+
389
+ CGROUP_MEMBERS=""
390
+ if ! contents="$(cat "$cgroup_procs" 2>/dev/null)"; then
391
+ return 2
392
+ fi
393
+
394
+ if ! members="$(printf '%s\n' "$contents" | awk '
395
+ length($0) > 0 {
396
+ if ($0 !~ /^[0-9]+$/) {
397
+ exit 2
398
+ }
399
+ print
400
+ }
401
+ ')"; then
402
+ return 2
403
+ fi
404
+
405
+ CGROUP_MEMBERS="$members"
406
+ }
407
+
408
+ read_cgroup_population() {
409
+ local cgroup_events="$1"
410
+ local contents populated
411
+
412
+ CGROUP_POPULATED=""
413
+ if ! contents="$(cat "$cgroup_events" 2>/dev/null)"; then
414
+ return 2
415
+ fi
416
+
417
+ if ! populated="$(printf '%s\n' "$contents" | awk '
418
+ $1 == "populated" {
419
+ count++
420
+ if (NF != 2 || $2 !~ /^[01]$/) {
421
+ invalid=1
422
+ } else {
423
+ value=$2
424
+ }
425
+ }
426
+ END {
427
+ if (count != 1 || invalid) {
428
+ exit 2
429
+ }
430
+ print value
431
+ }
432
+ ')"; then
433
+ return 2
434
+ fi
435
+
436
+ CGROUP_POPULATED="$populated"
437
+ }
438
+
439
+ probe_cgroup_directory() {
440
+ local cgroup_dir="$1"
441
+ local gnu_stat bsd_stat
442
+
443
+ if [ ! -e "$cgroup_dir" ]; then
444
+ [ -d "$(dirname "$cgroup_dir")" ] || return 2
445
+ return 1
446
+ fi
447
+ [ -d "$cgroup_dir" ] || return 2
448
+
449
+ if gnu_stat="$(LC_ALL=C stat -c '%F' "$cgroup_dir" 2>&1)"; then
450
+ case "$gnu_stat" in
451
+ directory) return 0 ;;
452
+ *) return 2 ;;
453
+ esac
454
+ fi
455
+
456
+ if bsd_stat="$(LC_ALL=C stat -f '%HT' "$cgroup_dir" 2>&1)"; then
457
+ case "$bsd_stat" in
458
+ Directory|directory) return 0 ;;
459
+ *) return 2 ;;
460
+ esac
461
+ fi
462
+
463
+ case "$gnu_stat\n$bsd_stat" in
464
+ *"No such file or directory"*|*"no such file or directory"*) return 1 ;;
465
+ *) return 2 ;;
466
+ esac
467
+ }
468
+
469
+ capture_service_identity() {
470
+ local active_state main_pid control_pid control_group cgroup_procs cgroup_dir cgroup_events members pid identity member_status
471
+
472
+ active_state="$(service_property ActiveState 2>/dev/null)" || return 2
473
+ main_pid="$(service_property MainPID 2>/dev/null)" || return 2
474
+ control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
475
+ control_group="$(service_property ControlGroup 2>/dev/null)" || return 2
476
+
477
+ case "$active_state" in
478
+ active|activating) ;;
479
+ *) return 1 ;;
480
+ esac
481
+
482
+ [ "$main_pid" -gt 0 ] 2>/dev/null || return 1
483
+ [ "$control_pid" = "0" ] || [ "$control_pid" -gt 0 ] 2>/dev/null || return 2
484
+ [ -n "$control_group" ] || return 2
485
+
486
+ cgroup_procs="${SERVICE_CGROUP_PROCS_FILE:-/sys/fs/cgroup${control_group}/cgroup.procs}"
487
+ cgroup_dir="${SERVICE_CGROUP_DIR:-${cgroup_procs%/cgroup.procs}}"
488
+ cgroup_events="${SERVICE_CGROUP_EVENTS_FILE:-${cgroup_dir}/cgroup.events}"
489
+ if probe_cgroup_directory "$cgroup_dir"; then :; else return 2; fi
490
+ [ -r "$cgroup_procs" ] || return 2
491
+ [ -r "$cgroup_events" ] || return 2
492
+ if read_cgroup_members "$cgroup_procs"; then
493
+ member_status=0
494
+ else
495
+ member_status=$?
496
+ fi
497
+ [ "$member_status" -eq 0 ] || return "$member_status"
498
+ members="$CGROUP_MEMBERS"
499
+ [ -n "$members" ] || return 1
500
+
501
+ CAPTURED_SERVICE_MEMBER_IDENTITIES=""
502
+ while IFS= read -r pid; do
503
+ [ -n "$pid" ] || continue
504
+ identity="$(process_identity "$pid")" || return 2
505
+ CAPTURED_SERVICE_MEMBER_IDENTITIES="${CAPTURED_SERVICE_MEMBER_IDENTITIES}${identity}\n"
506
+ done <<EOF
507
+ $members
508
+ EOF
509
+
510
+ printf '%s\n' "$members" | grep -qx "$main_pid" || return 1
511
+ CAPTURED_SERVICE_CGROUP_PROCS="$cgroup_procs"
512
+ CAPTURED_SERVICE_CGROUP_DIR="$cgroup_dir"
513
+ CAPTURED_SERVICE_CGROUP_EVENTS="$cgroup_events"
514
+ }
515
+
516
+ service_quiesced() {
517
+ local active_state main_pid control_pid cgroup_dir_status member_status population_status probe_status pid captured_identity current_identity identity_status
518
+
519
+ [ -n "$CAPTURED_SERVICE_CGROUP_PROCS" ] || return 2
520
+ active_state="$(service_property ActiveState 2>/dev/null)" || return 2
521
+ main_pid="$(service_property MainPID 2>/dev/null)" || return 2
522
+ control_pid="$(service_property ControlPID 2>/dev/null)" || return 2
523
+
524
+ case "$active_state" in
525
+ inactive|failed) ;;
526
+ *) return 1 ;;
527
+ esac
528
+
529
+ [ "$main_pid" = "0" ] || return 1
530
+ [ "$control_pid" = "0" ] || return 1
531
+
532
+ # Check every member captured before stop independently. This catches KillMode=process
533
+ # survivors even when systemd has already removed the original cgroup directory.
534
+ while IFS= read -r captured_identity; do
535
+ [ -n "$captured_identity" ] || continue
536
+ pid="${captured_identity%%|*}"
537
+ current_identity=""
538
+ if current_identity="$(process_identity "$pid")"; then
539
+ identity_status=0
540
+ else
541
+ identity_status=$?
542
+ fi
543
+ case "$identity_status" in
544
+ 0) [ "$current_identity" != "$captured_identity" ] || return 1 ;;
545
+ 1) ;;
546
+ *) return 2 ;;
547
+ esac
548
+ done <<EOF
549
+ $(printf '%b' "$CAPTURED_SERVICE_MEMBER_IDENTITIES")
550
+ EOF
551
+
552
+ # The original cgroup may be empty or removed after the service reaches inactive/failed with
553
+ # zero MainPID/ControlPID. A missing membership file while its directory remains is an
554
+ # unavailable probe, not proof that the subtree is empty.
555
+ if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
556
+ if read_cgroup_members "$CAPTURED_SERVICE_CGROUP_PROCS"; then
557
+ member_status=0
558
+ else
559
+ member_status=$?
560
+ fi
561
+ if [ "$member_status" -eq 2 ]; then
562
+ if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
563
+ return 2
564
+ else
565
+ probe_status=$?
566
+ fi
567
+ [ "$probe_status" -eq 1 ] || return 2
568
+ return 0
569
+ fi
570
+ [ "$member_status" -eq 0 ] || return "$member_status"
571
+ [ -z "$CGROUP_MEMBERS" ] || return 1
572
+
573
+ if read_cgroup_population "$CAPTURED_SERVICE_CGROUP_EVENTS"; then
574
+ population_status=0
575
+ else
576
+ population_status=$?
577
+ fi
578
+ if [ "$population_status" -eq 2 ]; then
579
+ if probe_cgroup_directory "$CAPTURED_SERVICE_CGROUP_DIR"; then
580
+ return 2
581
+ else
582
+ probe_status=$?
583
+ fi
584
+ [ "$probe_status" -eq 1 ] || return 2
585
+ return 0
586
+ fi
587
+ [ "$population_status" -eq 0 ] || return "$population_status"
588
+ [ "$CGROUP_POPULATED" = "0" ] || return 1
589
+ else
590
+ cgroup_dir_status=$?
591
+ case "$cgroup_dir_status" in
592
+ 1) ;;
593
+ *) return 2 ;;
594
+ esac
595
+ fi
596
+
597
+ return 0
598
+ }
599
+
600
+ wait_for_quiescence() {
601
+ local deadline=$((SECONDS + QUIESCENCE_TIMEOUT_SECONDS))
602
+ local result
603
+
604
+ while [ "$SECONDS" -le "$deadline" ]; do
605
+ if service_quiesced; then
606
+ return 0
607
+ else
608
+ result=$?
609
+ fi
610
+ if [ "$result" -eq 2 ]; then
611
+ return 2
612
+ fi
613
+
614
+ sleep 1
615
+ done
616
+
617
+ return 1
618
+ }
619
+
620
+ wait_for_started_service() {
621
+ local deadline=$((SECONDS + START_TIMEOUT_SECONDS))
622
+ local previous_main_pid=""
623
+ local previous_start_identity=""
624
+ local before_main_pid before_start_identity main_pid start_identity
625
+
626
+ while [ "$SECONDS" -le "$deadline" ]; do
627
+ if target_process_is_stable; then
628
+ before_main_pid="$(service_property MainPID 2>/dev/null || true)"
629
+ before_start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
630
+
631
+ if [ "$before_main_pid" = "0" ] || [ -z "$before_main_pid" ] || [ -z "$before_start_identity" ]; then
632
+ previous_main_pid=""
633
+ previous_start_identity=""
634
+ sleep 1
635
+ continue
636
+ fi
637
+
638
+ if target_health_is_valid && target_process_is_stable; then
639
+ main_pid="$(service_property MainPID 2>/dev/null || true)"
640
+ start_identity="$(service_property ExecMainStartTimestampMonotonic 2>/dev/null || true)"
641
+
642
+ if [ "$before_main_pid" = "$main_pid" ] &&
643
+ [ "$before_start_identity" = "$start_identity" ]; then
644
+ if [ "$main_pid" = "$previous_main_pid" ] &&
645
+ [ "$start_identity" = "$previous_start_identity" ]; then
646
+ return 0
647
+ fi
648
+
649
+ previous_main_pid="$main_pid"
650
+ previous_start_identity="$start_identity"
651
+ else
652
+ previous_main_pid=""
653
+ previous_start_identity=""
654
+ fi
655
+ else
656
+ previous_main_pid=""
657
+ previous_start_identity=""
658
+ fi
659
+ else
660
+ previous_main_pid=""
661
+ previous_start_identity=""
662
+ fi
663
+
664
+ sleep 1
665
+ done
666
+
667
+ return 1
668
+ }
9
669
 
10
670
  # Capture once — used for all status writes so timeout detection works correctly
11
671
  STARTED_AT="$(date -Iseconds)"
@@ -45,7 +705,17 @@ update_status() {
45
705
  cleanup() {
46
706
  local exit_code=$?
47
707
 
48
- if [ $exit_code -ne 0 ]; then
708
+ if [ -n "${BACKUP_TMP_PATH:-}" ]; then
709
+ rm -f "$BACKUP_TMP_PATH" 2>/dev/null || true
710
+ BACKUP_TMP_PATH=""
711
+ fi
712
+ if [ "$exit_code" -ne 0 ] && [ "$FINALIZED" != "true" ]; then
713
+ if [ -n "$ATTEMPT_ID" ] && { [ "$MIGRATION_ENTERED" = "true" ] || phase_requires_hold; }; then
714
+ hold_attempt "interrupted" "Update worker exited with code $exit_code during a state-changing phase"
715
+ elif [ -n "$ATTEMPT_ID" ]; then
716
+ finish_before_migration "failed" "Update process exited with code $exit_code"
717
+ fi
718
+
49
719
  # Only write a generic failure if a specific error wasn't already recorded
50
720
  if [ -f "$STATUS_FILE" ] && grep -q '"status": "failed"' "$STATUS_FILE" 2>/dev/null; then
51
721
  return
@@ -56,6 +726,27 @@ cleanup() {
56
726
 
57
727
  trap cleanup EXIT
58
728
 
729
+ handle_signal() {
730
+ local signal_name="$1"
731
+ local signal_code="$2"
732
+
733
+ if [ -n "$ATTEMPT_ID" ]; then
734
+ if [ "$MIGRATION_ENTERED" = "true" ] || phase_requires_hold; then
735
+ hold_attempt "interrupted" "Update worker received SIG${signal_name} after migration entry"
736
+ else
737
+ finish_before_migration "interrupted" "Update worker received SIG${signal_name} before migration entry"
738
+ fi
739
+ fi
740
+
741
+ update_status "failed" "failed" "Update worker received SIG${signal_name}"
742
+ FINALIZED="true"
743
+ exit "$signal_code"
744
+ }
745
+
746
+ trap 'handle_signal TERM 143' TERM
747
+ trap 'handle_signal INT 130' INT
748
+ trap 'handle_signal HUP 129' HUP
749
+
59
750
  # ──────────────────────────────────────────────────────────────
60
751
  # Image-based update (Raspbian image installs)
61
752
  # ──────────────────────────────────────────────────────────────
@@ -64,6 +755,85 @@ if [ "$INSTALL_TYPE" = "image" ]; then
64
755
  CLEAN_VERSION="${VERSION#v}"
65
756
  NEW_VERSION_DIR="${IMAGE_BASE_DIR}/v${CLEAN_VERSION}"
66
757
  CURRENT_LINK="${IMAGE_BASE_DIR}/current"
758
+ ENV_FILE="${ENV_FILE:-/etc/smart-panel/environment}"
759
+
760
+ if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
761
+ case "$VERSION" in latest|vlatest|'')
762
+ update_status "failed" "failed" "Stopped maintenance requires an exact target version"
763
+ FINALIZED="true"
764
+ exit 1
765
+ ;; esac
766
+ for required in EXPECTED_CURRENT_VERSION TARGET_ARCHIVE_SHA256 TARGET_WORKER_SHA256 EXPECTED_IMAGE_BASE_DIR \
767
+ EXPECTED_STATUS_FILE EXPECTED_ATTEMPT_DIR EXPECTED_DB_PATH DB_BACKUP_PATH; do
768
+ if [ -z "${!required:-}" ]; then
769
+ update_status "failed" "failed" "Stopped maintenance identity is missing: ${required}"
770
+ FINALIZED="true"
771
+ exit 1
772
+ fi
773
+ done
774
+ TARGET_ARCHIVE_SHA256="$(printf '%s' "$TARGET_ARCHIVE_SHA256" | tr '[:upper:]' '[:lower:]')"
775
+ TARGET_WORKER_SHA256="$(printf '%s' "$TARGET_WORKER_SHA256" | tr '[:upper:]' '[:lower:]')"
776
+ case "$TARGET_ARCHIVE_SHA256$TARGET_WORKER_SHA256" in *[!0123456789abcdef]*)
777
+ update_status "failed" "failed" "Stopped maintenance checksum is invalid"
778
+ FINALIZED="true"
779
+ exit 1
780
+ ;; esac
781
+ [ "${#TARGET_ARCHIVE_SHA256}" -eq 64 ] && [ "${#TARGET_WORKER_SHA256}" -eq 64 ] || {
782
+ update_status "failed" "failed" "Stopped maintenance checksum must be SHA-256"
783
+ FINALIZED="true"
784
+ exit 1
785
+ }
786
+ [ "$EXPECTED_IMAGE_BASE_DIR" = "$IMAGE_BASE_DIR" ] || {
787
+ update_status "failed" "failed" "Stopped maintenance image base identity mismatch"
788
+ FINALIZED="true"
789
+ exit 1
790
+ }
791
+ [ "$EXPECTED_STATUS_FILE" = "$STATUS_FILE" ] || {
792
+ update_status "failed" "failed" "Stopped maintenance status identity mismatch"
793
+ FINALIZED="true"
794
+ exit 1
795
+ }
796
+ [ "$EXPECTED_ATTEMPT_DIR" = "$ATTEMPT_DIR" ] || {
797
+ update_status "failed" "failed" "Stopped maintenance attempt identity mismatch"
798
+ FINALIZED="true"
799
+ exit 1
800
+ }
801
+ [ "$EXPECTED_DB_PATH" = "${FB_DB_PATH:-}" ] || {
802
+ update_status "failed" "failed" "Stopped maintenance database identity mismatch"
803
+ FINALIZED="true"
804
+ exit 1
805
+ }
806
+ if [ -f "$ENV_FILE" ]; then
807
+ configured_db_path="$(
808
+ set -a
809
+ # shellcheck source=/dev/null
810
+ . "$ENV_FILE"
811
+ printf '%s' "${FB_DB_PATH:-}"
812
+ )"
813
+ [ "$configured_db_path" = "$EXPECTED_DB_PATH" ] || \
814
+ fail_before_migration "preflight" "Configured database identity does not match stopped-maintenance packet"
815
+ fi
816
+ if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
817
+ fail_before_migration "preflight" "Stopped maintenance backup path already exists"
818
+ fi
819
+ SOURCE_DB_PATH="${FB_DB_PATH%/}/database.sqlite"
820
+ source_database_is_valid || fail_before_migration "preflight" "Stopped maintenance source database is missing, symlinked, or empty"
821
+ fi
822
+
823
+ # Image updates must verify the target release through the existing local health route. A
824
+ # missing route would otherwise silently reduce readiness to a PID check and reintroduce the
825
+ # startup/worker circular wait that the updater is designed to avoid.
826
+ if [ -z "$HEALTH_URL" ]; then
827
+ update_status "failed" "failed" "No local health URL configured for image update"
828
+ FINALIZED="true"
829
+ exit 1
830
+ fi
831
+
832
+ if ! acquire_attempt; then
833
+ update_status "failed" "failed" "An unresolved update attempt already owns the updater"
834
+ FINALIZED="true"
835
+ exit 1
836
+ fi
67
837
 
68
838
  # Verify passwordless sudo is available for all required commands.
69
839
  # Without -n, sudo may hang waiting for a password on a detached process.
@@ -77,8 +847,7 @@ if [ "$INSTALL_TYPE" = "image" ]; then
77
847
 
78
848
  for cmd in "${SUDO_CMDS[@]}"; do
79
849
  if ! sudo -n -l $cmd >/dev/null 2>&1; then
80
- update_status "failed" "failed" "Passwordless sudo not available for: $cmd"
81
- exit 1
850
+ fail_before_migration "preflight" "Passwordless sudo not available for: $cmd"
82
851
  fi
83
852
  done
84
853
  PREVIOUS_TARGET=""
@@ -86,6 +855,24 @@ if [ "$INSTALL_TYPE" = "image" ]; then
86
855
  # Save the current version for rollback
87
856
  if [ -L "$CURRENT_LINK" ]; then
88
857
  PREVIOUS_TARGET=$(readlink "$CURRENT_LINK")
858
+ case "$PREVIOUS_TARGET" in
859
+ /*) ;;
860
+ *) PREVIOUS_TARGET="${IMAGE_BASE_DIR}/${PREVIOUS_TARGET}" ;;
861
+ esac
862
+ fi
863
+
864
+ if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
865
+ EXPECTED_CURRENT_LINK="${IMAGE_BASE_DIR}/v${EXPECTED_CURRENT_VERSION#v}"
866
+ RESOLVED_CURRENT="$(realpath "$CURRENT_LINK" 2>/dev/null || true)"
867
+ RESOLVED_EXPECTED="$(realpath "$EXPECTED_CURRENT_LINK" 2>/dev/null || true)"
868
+ [ -n "$RESOLVED_CURRENT" ] && [ "$RESOLVED_CURRENT" = "$RESOLVED_EXPECTED" ] || \
869
+ fail_before_migration "preflight" "Current release/link identity does not match stopped-maintenance packet"
870
+ if ! wait_for_stopped_service; then
871
+ hold_attempt "stopping" "Stopped service did not reach verified quiescence"
872
+ update_status "failed" "failed" "Stopped service quiescence could not be verified; update is held for recovery"
873
+ FINALIZED="true"
874
+ exit 1
875
+ fi
89
876
  fi
90
877
 
91
878
  # Guard: refuse to overwrite the currently running version
@@ -94,25 +881,28 @@ if [ "$INSTALL_TYPE" = "image" ]; then
94
881
  RESOLVED_NEW=$(cd "$IMAGE_BASE_DIR" && realpath "$NEW_VERSION_DIR" 2>/dev/null || true)
95
882
 
96
883
  if [ -n "$PREVIOUS_TARGET" ] && [ -n "$RESOLVED_PREV" ] && [ -n "$RESOLVED_NEW" ] && [ "$RESOLVED_PREV" = "$RESOLVED_NEW" ]; then
97
- update_status "failed" "failed" "Target version v${CLEAN_VERSION} is already the active version"
98
- exit 1
884
+ fail_before_migration "preflight" "Target version v${CLEAN_VERSION} is already the active version"
99
885
  fi
100
886
 
101
887
  # ── Download ──
102
888
  update_status "downloading" "downloading"
103
889
 
104
890
  if [ -z "$DOWNLOAD_URL" ]; then
105
- update_status "failed" "failed" "No download URL provided for image update"
106
- exit 1
891
+ fail_before_migration "downloading" "No download URL provided for image update"
107
892
  fi
108
893
 
109
894
  TMP_TARBALL="/tmp/smart-panel-backend-v${CLEAN_VERSION}.tar.gz"
110
895
 
111
896
  curl -fSL -o "$TMP_TARBALL" "$DOWNLOAD_URL" 2>&1 || {
112
- update_status "failed" "failed" "Download failed from ${DOWNLOAD_URL}"
113
- exit 1
897
+ fail_before_migration "downloading" "Download failed from ${DOWNLOAD_URL}"
114
898
  }
115
899
 
900
+ if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
901
+ actual_archive_sha256="$(sha256sum "$TMP_TARBALL" 2>/dev/null | awk '{print $1}')"
902
+ [ "$actual_archive_sha256" = "$TARGET_ARCHIVE_SHA256" ] || \
903
+ fail_before_migration "downloading" "Downloaded archive checksum does not match maintenance packet"
904
+ fi
905
+
116
906
  # ── Extract ──
117
907
  update_status "installing" "installing"
118
908
 
@@ -121,17 +911,36 @@ if [ "$INSTALL_TYPE" = "image" ]; then
121
911
  tar -xzf "$TMP_TARBALL" -C "$NEW_VERSION_DIR" 2>&1 || {
122
912
  rm -rf "$NEW_VERSION_DIR"
123
913
  rm -f "$TMP_TARBALL"
124
- update_status "failed" "failed" "Failed to extract update archive"
125
- exit 1
914
+ fail_before_migration "installing" "Failed to extract update archive"
126
915
  }
127
916
 
128
917
  rm -f "$TMP_TARBALL"
129
918
 
919
+ if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
920
+ target_worker="${NEW_VERSION_DIR}/dist/modules/system/scripts/update-worker.sh"
921
+ [ -f "$target_worker" ] || {
922
+ rm -rf "$NEW_VERSION_DIR"
923
+ fail_before_migration "installing" "Target release worker is missing"
924
+ }
925
+ actual_worker_sha256="$(sha256sum "$target_worker" 2>/dev/null | awk '{print $1}')"
926
+ [ "$actual_worker_sha256" = "$TARGET_WORKER_SHA256" ] || {
927
+ rm -rf "$NEW_VERSION_DIR"
928
+ fail_before_migration "installing" "Target release worker checksum does not match maintenance packet"
929
+ }
930
+ MAINTENANCE_DIR="${MAINTENANCE_DIR:-${ATTEMPT_DIR}/maintenance-${ATTEMPT_ID}}"
931
+ case "$MAINTENANCE_DIR" in
932
+ "$IMAGE_BASE_DIR"/*|"$NEW_VERSION_DIR"/*) fail_before_migration "installing" "Maintenance directory is inside a release tree" ;;
933
+ esac
934
+ mkdir -p "$MAINTENANCE_DIR"
935
+ chmod 700 "$MAINTENANCE_DIR"
936
+ cp "$target_worker" "$MAINTENANCE_DIR/update-worker.sh"
937
+ chmod 700 "$MAINTENANCE_DIR/update-worker.sh"
938
+ fi
939
+
130
940
  # Create the image-install marker in the new version
131
941
  touch "${NEW_VERSION_DIR}/.image-install" || {
132
942
  rm -rf "$NEW_VERSION_DIR"
133
- update_status "failed" "failed" "Failed to create image-install marker"
134
- exit 1
943
+ fail_before_migration "installing" "Failed to create image-install marker"
135
944
  }
136
945
 
137
946
  # ── Verify dependencies ──
@@ -143,8 +952,7 @@ if [ "$INSTALL_TYPE" = "image" ]; then
143
952
  if [ ! -d "node_modules" ]; then
144
953
  npm install --omit=dev 2>&1 || {
145
954
  rm -rf "$NEW_VERSION_DIR"
146
- update_status "failed" "failed" "npm install failed"
147
- exit 1
955
+ fail_before_migration "installing" "npm install failed"
148
956
  }
149
957
  fi
150
958
 
@@ -153,72 +961,184 @@ if [ "$INSTALL_TYPE" = "image" ]; then
153
961
  # hanging on password prompt when running as a detached process.
154
962
  sudo -n chown -R smart-panel:smart-panel "$NEW_VERSION_DIR" || {
155
963
  rm -rf "$NEW_VERSION_DIR"
156
- update_status "failed" "failed" "Failed to set ownership on ${NEW_VERSION_DIR}"
157
- exit 1
964
+ fail_before_migration "installing" "Failed to set ownership on ${NEW_VERSION_DIR}"
158
965
  }
159
966
 
160
- # ── Stop service ──
967
+ # ── Stop/qualify service ──
161
968
  update_status "stopping" "stopping"
162
- sudo -n systemctl stop smart-panel 2>/dev/null || true
969
+ write_attempt "active" "stopping" "" "false" "$MIGRATION_ENTERED"
970
+ if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
971
+ if ! wait_for_stopped_service; then
972
+ hold_attempt "stopping" "Stopped service changed or failed quiescence recheck"
973
+ update_status "failed" "failed" "Stopped service quiescence could not be rechecked; update is held for recovery"
974
+ FINALIZED="true"
975
+ exit 1
976
+ fi
977
+ else
978
+ if ! capture_service_identity; then
979
+ hold_attempt "stopping" "Could not capture service process/cgroup identity before stop"
980
+ update_status "failed" "failed" "Service process/cgroup identity could not be captured; update is held for recovery"
981
+ FINALIZED="true"
982
+ exit 1
983
+ fi
984
+
985
+ if ! sudo -n systemctl stop "$SERVICE_NAME" 2>/dev/null; then
986
+ hold_attempt "stopping" "Service stop failed; writer quiescence is unknown"
987
+ update_status "failed" "failed" "Could not stop ${SERVICE_NAME}; update is held for recovery"
988
+ FINALIZED="true"
989
+ exit 1
990
+ fi
991
+
992
+ if ! wait_for_quiescence; then
993
+ hold_attempt "stopping" "Service did not reach verified quiescence before migration"
994
+ update_status "failed" "failed" "Service quiescence could not be verified; update is held for recovery"
995
+ FINALIZED="true"
996
+ exit 1
997
+ fi
998
+
999
+ write_attempt "active" "quiesced" "" "false" "$MIGRATION_ENTERED"
1000
+ fi
1001
+
1002
+ if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
1003
+ # A consistent backup is an input safeguard, never a rollback mechanism. SQLite's own
1004
+ # backup API is required so a copied WAL database cannot be mistaken for a snapshot.
1005
+ command -v sqlite3 >/dev/null 2>&1 || {
1006
+ rm -rf "$NEW_VERSION_DIR"
1007
+ fail_before_migration "quiesced" "sqlite3 is required for stopped-maintenance backup"
1008
+ }
1009
+ if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
1010
+ rm -rf "$NEW_VERSION_DIR"
1011
+ fail_before_migration "quiesced" "Stopped maintenance backup path was created during the run"
1012
+ fi
1013
+ source_database_is_valid || {
1014
+ rm -rf "$NEW_VERSION_DIR"
1015
+ fail_before_migration "quiesced" "Stopped maintenance source database became unavailable"
1016
+ }
1017
+ mkdir -p "$(dirname "$DB_BACKUP_PATH")" || {
1018
+ rm -rf "$NEW_VERSION_DIR"
1019
+ fail_before_migration "quiesced" "Could not create SQLite backup directory"
1020
+ }
1021
+ BACKUP_TMP_PATH="$(mktemp "${DB_BACKUP_PATH}.tmp-${ATTEMPT_ID}.XXXXXX")" || {
1022
+ rm -rf "$NEW_VERSION_DIR"
1023
+ fail_before_migration "quiesced" "Could not create temporary SQLite backup"
1024
+ }
1025
+ if ! sqlite3 "${SOURCE_DB_PATH}" ".backup '${BACKUP_TMP_PATH}'"; then
1026
+ rm -f "$BACKUP_TMP_PATH"
1027
+ BACKUP_TMP_PATH=""
1028
+ rm -rf "$NEW_VERSION_DIR"
1029
+ fail_before_migration "quiesced" "Consistent SQLite backup failed"
1030
+ fi
1031
+ if [ ! -s "$BACKUP_TMP_PATH" ]; then
1032
+ rm -f "$BACKUP_TMP_PATH"
1033
+ BACKUP_TMP_PATH=""
1034
+ rm -rf "$NEW_VERSION_DIR"
1035
+ fail_before_migration "quiesced" "Consistent SQLite backup is empty"
1036
+ fi
1037
+ if ! ln "$BACKUP_TMP_PATH" "$DB_BACKUP_PATH" 2>/dev/null; then
1038
+ rm -f "$BACKUP_TMP_PATH"
1039
+ BACKUP_TMP_PATH=""
1040
+ rm -rf "$NEW_VERSION_DIR"
1041
+ if [ -e "$DB_BACKUP_PATH" ] || [ -L "$DB_BACKUP_PATH" ]; then
1042
+ fail_before_migration "quiesced" "Stopped maintenance backup path was created during the run"
1043
+ fi
1044
+ fail_before_migration "quiesced" "Consistent SQLite backup publish failed"
1045
+ fi
1046
+ rm -f "$BACKUP_TMP_PATH"
1047
+ BACKUP_TMP_PATH=""
1048
+ fi
163
1049
 
164
1050
  # ── Switch symlink (atomic on same filesystem) ──
165
1051
  sudo -n ln -sfn "$NEW_VERSION_DIR" "$CURRENT_LINK" || {
166
- # Revert on failure
167
- if [ -n "$PREVIOUS_TARGET" ]; then
168
- sudo -n ln -sfn "$PREVIOUS_TARGET" "$CURRENT_LINK"
1052
+ if [ "$UPDATE_START_MODE" = "stopped-maintenance" ]; then
1053
+ hold_attempt "switching" "Failed to switch version symlink in stopped maintenance"
1054
+ update_status "failed" "failed" "Failed to switch version symlink; update is held for recovery"
1055
+ FINALIZED="true"
1056
+ exit 1
169
1057
  fi
170
- sudo -n systemctl start smart-panel 2>/dev/null || true
171
- update_status "failed" "failed" "Failed to switch version symlink"
1058
+ # Revert only when the old link, target cleanup and old-service health are all verified.
1059
+ if restore_before_migration "$PREVIOUS_TARGET" "$CURRENT_LINK" "$NEW_VERSION_DIR"; then
1060
+ fail_before_migration "switching" "Failed to switch version symlink"
1061
+ fi
1062
+
1063
+ hold_attempt "switching" "Failed to switch version symlink and recovery start was not verified"
1064
+ update_status "failed" "failed" "Failed to switch version symlink; update is held for recovery"
1065
+ FINALIZED="true"
172
1066
  exit 1
173
1067
  }
174
1068
 
175
1069
  # ── Run database migrations ──
176
1070
  update_status "migrating" "migrating"
177
1071
 
178
- ENV_FILE="/etc/smart-panel/environment"
179
-
180
- if [ -f "${NEW_VERSION_DIR}/dist/dataSource.js" ]; then
181
- (
182
- set -a
183
- # shellcheck source=/dev/null
184
- [ -f "$ENV_FILE" ] && . "$ENV_FILE"
185
- set +a
186
- cd "$NEW_VERSION_DIR"
187
- node node_modules/typeorm/cli.js migration:run -d dist/dataSource.js
188
- ) 2>&1 || {
189
- # Migration failed — revert symlink if possible
190
- update_status "stopping" "stopping"
191
- sudo -n systemctl stop smart-panel 2>/dev/null || true
192
-
193
- if [ -n "$PREVIOUS_TARGET" ]; then
194
- if sudo -n ln -sfn "$PREVIOUS_TARGET" "$CURRENT_LINK"; then
195
- rm -rf "$NEW_VERSION_DIR"
196
- update_status "failed" "failed" "Database migration failed — reverted to previous version"
197
- else
198
- # Symlink revert failed — keep new version so system stays bootable
199
- update_status "failed" "failed" "Database migration failed — symlink revert also failed"
200
- fi
201
- else
202
- # No previous version — keep current in place so system stays bootable
203
- update_status "failed" "failed" "Database migration failed — no previous version to revert to"
204
- fi
1072
+ if [ ! -f "${NEW_VERSION_DIR}/dist/dataSource.js" ]; then
1073
+ hold_attempt "migrating" "Target data source is missing; update is held for recovery"
1074
+ update_status "failed" "failed" "Target data source is missing; update is held for recovery"
1075
+ FINALIZED="true"
1076
+ exit 1
1077
+ fi
205
1078
 
206
- sudo -n systemctl start smart-panel 2>/dev/null || true
207
- exit 1
1079
+ MIGRATION_ENTERED="true"
1080
+ if ! write_attempt "active" "migrating" "" "false" "true"; then
1081
+ hold_attempt "migrating" "Could not persist migration-entry checkpoint"
1082
+ update_status "failed" "failed" "Could not persist migration-entry checkpoint; update is held for recovery"
1083
+ FINALIZED="true"
1084
+ exit 1
1085
+ fi
1086
+
1087
+ MIGRATION_LOG="${ATTEMPT_DIR}/${ATTEMPT_ID}.migration.log"
1088
+ : > "$MIGRATION_LOG"
1089
+ chmod 600 "$MIGRATION_LOG"
1090
+ write_attempt "active" "migrating" "" "false" "true"
1091
+
1092
+ set +e
1093
+ (
1094
+ set -a
1095
+ # shellcheck source=/dev/null
1096
+ [ -f "$ENV_FILE" ] && . "$ENV_FILE"
1097
+ set +a
1098
+ cd "$NEW_VERSION_DIR"
1099
+ node node_modules/typeorm/cli.js migration:run -d dist/dataSource.js
1100
+ ) 2>&1 | awk -v output="$MIGRATION_LOG" '
1101
+ BEGIN { limit = 65536; bytes = 0 }
1102
+ {
1103
+ if (bytes < limit) {
1104
+ line = $0 ORS;
1105
+ remaining = limit - bytes;
1106
+ if (length(line) > remaining) line = substr(line, 1, remaining);
1107
+ printf "%s", line >> output;
1108
+ bytes += length(line);
1109
+ }
208
1110
  }
1111
+ '
1112
+ pipeline_status=("${PIPESTATUS[@]}")
1113
+ migration_exit_code="${pipeline_status[0]}"
1114
+ capture_exit_code="${pipeline_status[1]}"
1115
+ set -e
1116
+
1117
+ if [ "$capture_exit_code" -ne 0 ]; then
1118
+ hold_attempt "migration_capture_failed" "Could not retain bounded migration diagnostics"
1119
+ update_status "failed" "failed" "Migration diagnostics could not be retained; update is held for recovery"
1120
+ FINALIZED="true"
1121
+ exit 1
1122
+ fi
1123
+
1124
+ if [ "$migration_exit_code" -eq 0 ]; then
1125
+ write_attempt "active" "migrated" "" "false" "true"
1126
+ else
1127
+ hold_attempt "migration_failed" "Database migration failed with exit code ${migration_exit_code}"
1128
+ update_status "failed" "failed" "Database migration failed; update is held for recovery"
1129
+ FINALIZED="true"
1130
+ exit "$migration_exit_code"
209
1131
  fi
210
1132
 
211
1133
  # ── Start service ──
212
1134
  update_status "starting" "starting"
213
- sudo -n systemctl start smart-panel 2>&1 || {
214
- # Try to revert if start fails
215
- if [ -n "$PREVIOUS_TARGET" ]; then
216
- sudo -n ln -sfn "$PREVIOUS_TARGET" "$CURRENT_LINK"
217
- sudo -n systemctl start smart-panel 2>/dev/null || true
218
- fi
219
- update_status "failed" "failed" "Failed to start service after update"
1135
+ write_attempt "active" "starting" "" "false" "true"
1136
+ if ! sudo -n systemctl start "$SERVICE_NAME" 2>&1 || ! wait_for_started_service; then
1137
+ hold_attempt "start_failed" "Target service did not reach verified health after migration"
1138
+ update_status "failed" "failed" "Target service failed to start or become healthy; update is held for recovery"
1139
+ FINALIZED="true"
220
1140
  exit 1
221
- }
1141
+ fi
222
1142
 
223
1143
  # ── Cleanup old versions (keep max 2 previous) ──
224
1144
  cd "$IMAGE_BASE_DIR"
@@ -239,7 +1159,9 @@ if [ "$INSTALL_TYPE" = "image" ]; then
239
1159
  fi
240
1160
 
241
1161
  # ── Mark complete ──
1162
+ release_attempt
242
1163
  update_status "complete" "complete"
1164
+ FINALIZED="true"
243
1165
  trap - EXIT
244
1166
  exit 0
245
1167
  fi