@velor/remote-mouse 6.4.8

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 (316) hide show
  1. package/.artifacts/screenshots/remote-mobile-config.png +0 -0
  2. package/.artifacts/screenshots/remote-mobile-home.png +0 -0
  3. package/.artifacts/screenshots/remote-mobile-preferences.png +0 -0
  4. package/.artifacts/screenshots/remote-mobile-server-info.png +0 -0
  5. package/.artifacts/screenshots/remote-mobile-vlc.png +0 -0
  6. package/.env.example +93 -0
  7. package/README.md +370 -0
  8. package/bin/remote-mouse.js +19 -0
  9. package/client/core/dom.js +148 -0
  10. package/client/core/init-socket-io.js +117 -0
  11. package/client/core/socket-emit.js +6 -0
  12. package/client/core/window-events.js +23 -0
  13. package/client/i18n/apply-page-translations.js +18 -0
  14. package/client/i18n/constants.js +3 -0
  15. package/client/i18n/core.js +40 -0
  16. package/client/i18n/locale-registry.js +77 -0
  17. package/client/i18n/locales/de.js +227 -0
  18. package/client/i18n/locales/en.js +389 -0
  19. package/client/i18n/locales/es.js +228 -0
  20. package/client/i18n/locales/fr.js +389 -0
  21. package/client/i18n/locales/it.js +227 -0
  22. package/client/i18n/locales/ja.js +227 -0
  23. package/client/i18n/locales/pt.js +227 -0
  24. package/client/i18n/locales/ru.js +240 -0
  25. package/client/i18n/locales/zh.js +227 -0
  26. package/client/preferences/constants.js +15 -0
  27. package/client/preferences/effects.js +24 -0
  28. package/client/preferences/load.js +19 -0
  29. package/client/preferences/render.js +26 -0
  30. package/client/preferences/rows.js +78 -0
  31. package/client/preferences/state.js +29 -0
  32. package/client/preview/create-preview-frame-receiver.js +13 -0
  33. package/client/preview/draw.js +77 -0
  34. package/client/preview/parse-preview-frame.js +39 -0
  35. package/client/preview/preview-stream.js +152 -0
  36. package/client/services/app-state/appStateKeys.js +30 -0
  37. package/client/services/app-state/createAppStateService.js +216 -0
  38. package/client/services/app-state/createPersistStoreService.js +122 -0
  39. package/client/services/app-state/createStateStoreService.js +64 -0
  40. package/client/services/app-state/localStorageBinding.js +26 -0
  41. package/client/services/app-state/normalizers.js +74 -0
  42. package/client/services/app-state/resolveEffectiveStateValue.js +58 -0
  43. package/client/services/app-state/stateDefinitions.js +107 -0
  44. package/client/services/backend/createBackendService.js +55 -0
  45. package/client/services/config/ConfigView.js +45 -0
  46. package/client/services/config/createClientConfigService.js +122 -0
  47. package/client/services/config/defaultClientConfig.js +22 -0
  48. package/client/services/createServicesContainer.js +48 -0
  49. package/client/services/createServicesRegistry.js +26 -0
  50. package/client/services/i18n/createI18nService.js +86 -0
  51. package/client/services/notifications/createNotificationService.js +56 -0
  52. package/client/services/pubsub/createPubSubService.js +20 -0
  53. package/client/services/remotes/createRemotesService.js +20 -0
  54. package/client/services/transport/createSocketIoTransportService.js +50 -0
  55. package/client/touch/bindTouchPassthrough.js +94 -0
  56. package/client/touch/createTouchList.js +18 -0
  57. package/client/touch/dispatchSyntheticTouch.js +20 -0
  58. package/client/touch/gesture-zone.js +28 -0
  59. package/client/touch/throttle.js +46 -0
  60. package/client/touch/touch-handlers.js +193 -0
  61. package/client/touch/touchpad.js +231 -0
  62. package/client/ui/config/page.js +840 -0
  63. package/client/ui/main/applyRemoteVisibilityState.js +60 -0
  64. package/client/ui/main/bind-client-notifications.js +3 -0
  65. package/client/ui/main/bindings/bindAccordion.js +121 -0
  66. package/client/ui/main/bindings/bindActionButtons.js +13 -0
  67. package/client/ui/main/bindings/bindAdminDrawer.js +137 -0
  68. package/client/ui/main/bindings/bindAdminRemoteButtons.js +77 -0
  69. package/client/ui/main/bindings/bindAdminVersion.js +27 -0
  70. package/client/ui/main/bindings/bindBrowserRemoteButtons.js +119 -0
  71. package/client/ui/main/bindings/bindRemoteAccordion.js +23 -0
  72. package/client/ui/main/bindings/bindSamsungRemoteButtons.js +320 -0
  73. package/client/ui/main/bindings/bindSystemRemoteButtons.js +41 -0
  74. package/client/ui/main/bindings/bindVlcRemoteButtons.js +64 -0
  75. package/client/ui/main/canvas-ui.js +33 -0
  76. package/client/ui/main/connection-overlay.js +57 -0
  77. package/client/ui/main/init-ui.js +54 -0
  78. package/client/ui/main/keyboard-panel.js +271 -0
  79. package/client/ui/main/mouse-buttons.js +94 -0
  80. package/client/ui/main/page.js +21 -0
  81. package/client/ui/notifications/show-toast.js +48 -0
  82. package/client/ui/preferences/bindPreferenceSwitchers.js +49 -0
  83. package/client/ui/preferences/page.js +61 -0
  84. package/client/ui/preferences/switchers.js +200 -0
  85. package/index.js +9 -0
  86. package/package.json +41 -0
  87. package/public/admin-config.css +447 -0
  88. package/public/admin-config.html +63 -0
  89. package/public/apple-touch-icon.png +0 -0
  90. package/public/brave-icon.svg +13 -0
  91. package/public/favicon.svg +30 -0
  92. package/public/icon-192.png +0 -0
  93. package/public/icon-512.png +0 -0
  94. package/public/index.html +246 -0
  95. package/public/manifest.webmanifest +45 -0
  96. package/public/preferences.html +55 -0
  97. package/public/server-info.html +574 -0
  98. package/public/styles.css +1042 -0
  99. package/public/sw.js +103 -0
  100. package/scripts/codex-commit.sh +112 -0
  101. package/scripts/generate-mobile-screenshots.mjs +244 -0
  102. package/scripts/install-linux.sh +633 -0
  103. package/scripts/install-windows.ps1 +369 -0
  104. package/server/application/createApplicationLifecycle.js +15 -0
  105. package/server/application/logger.js +216 -0
  106. package/server/application/shutdown.js +81 -0
  107. package/server/application/start.js +95 -0
  108. package/server/application/state.js +19 -0
  109. package/server/connection/api/admin-actions.router.js +65 -0
  110. package/server/connection/api/admin-configs.router.js +165 -0
  111. package/server/connection/api/admin-remotes.router.js +61 -0
  112. package/server/connection/api/admin-subs.router.js +49 -0
  113. package/server/connection/api/client.router.js +15 -0
  114. package/server/connection/api/configs.js +96 -0
  115. package/server/connection/api/qr-page.handler.js +17 -0
  116. package/server/connection/api/remotes.router.js +30 -0
  117. package/server/connection/api/server-info.router.js +149 -0
  118. package/server/connection/api/session.middleware.js +93 -0
  119. package/server/connection/api/unauthorized-response.js +73 -0
  120. package/server/connection/socket/createConnectionRegistrar.js +23 -0
  121. package/server/connection/socket/createSocketSessionAuthMiddleware.js +49 -0
  122. package/server/connection/socket/socket-action-responder.js +12 -0
  123. package/server/connection/socket/socket.timestamp-middleware.js +51 -0
  124. package/server/index.js +12 -0
  125. package/server/init/bootstrapApi.js +87 -0
  126. package/server/init/bootstrapSocket.js +79 -0
  127. package/server/init/createAdminApiRouter.js +22 -0
  128. package/server/init/createAdminUiRouter.js +21 -0
  129. package/server/init/createSocketActionRegistrars.js +52 -0
  130. package/server/init/observers/startConfigObserver.js +51 -0
  131. package/server/init/observers/startDisplaySizeObserver.js +55 -0
  132. package/server/init/observers/startNotificationObserver.js +49 -0
  133. package/server/init/observers/startQrOverlayHoverObserver.js +89 -0
  134. package/server/init/observers/startQrOverlayRefreshObserver.js +53 -0
  135. package/server/os/darwin/app.js +45 -0
  136. package/server/os/darwin/applescript.js +51 -0
  137. package/server/os/darwin/cliSocket.js +53 -0
  138. package/server/os/darwin/url.js +9 -0
  139. package/server/os/darwin/window.js +10 -0
  140. package/server/os/linux/app.js +120 -0
  141. package/server/os/linux/cliSocket.js +53 -0
  142. package/server/os/linux/process.js +69 -0
  143. package/server/os/linux/screen.js +58 -0
  144. package/server/os/linux/systemd.js +139 -0
  145. package/server/os/linux/url.js +17 -0
  146. package/server/os/linux/windows.js +144 -0
  147. package/server/os/win32/app.js +66 -0
  148. package/server/os/win32/cliSocket.js +15 -0
  149. package/server/os/win32/powershell.js +70 -0
  150. package/server/os/win32/process.js +20 -0
  151. package/server/os/win32/service.js +95 -0
  152. package/server/os/win32/url.js +9 -0
  153. package/server/os/win32/window.js +10 -0
  154. package/server/remotes/admin/createAdminEventGuardMiddleware.js +29 -0
  155. package/server/remotes/admin/createForceUpdateCheckAction.js +39 -0
  156. package/server/remotes/admin/createInstallUpdateAction.js +64 -0
  157. package/server/remotes/admin/createOpenQrBrowserAction.js +63 -0
  158. package/server/remotes/admin/createOpenServerInfoBrowserAction.js +58 -0
  159. package/server/remotes/admin/createRestartServiceAction.js +20 -0
  160. package/server/remotes/admin/createRotateEntryTokenAction.js +43 -0
  161. package/server/remotes/admin/createToggleQrOverlayAction.js +38 -0
  162. package/server/remotes/admin/index.js +32 -0
  163. package/server/remotes/admin/notifyIfRestarted.js +102 -0
  164. package/server/remotes/admin/registrar.js +94 -0
  165. package/server/remotes/browser/actions/createFocusOrLaunchBrowserAction.js +22 -0
  166. package/server/remotes/browser/actions/openUrlOnHost.js +3 -0
  167. package/server/remotes/browser/browserCatalog.js +70 -0
  168. package/server/remotes/browser/index.js +15 -0
  169. package/server/remotes/browser/listBrowsersOnHost.js +23 -0
  170. package/server/remotes/browser/registrar.js +29 -0
  171. package/server/remotes/browser/spec.js +18 -0
  172. package/server/remotes/browser/state.js +5 -0
  173. package/server/remotes/input/registrar.js +39 -0
  174. package/server/remotes/preview/bgraToRgbaBuffer.js +20 -0
  175. package/server/remotes/preview/captureAroundCursor.js +19 -0
  176. package/server/remotes/preview/createPreviewStreamer.js +85 -0
  177. package/server/remotes/preview/registrar.js +34 -0
  178. package/server/remotes/samsung/createSamsungCommandService.js +203 -0
  179. package/server/remotes/samsung/createSamsungRemote.js +26 -0
  180. package/server/remotes/samsung/createSamsungRemoteGetter.js +46 -0
  181. package/server/remotes/samsung/createSamsungTvGetter.js +102 -0
  182. package/server/remotes/samsung/device-config.js +122 -0
  183. package/server/remotes/samsung/registrar.js +59 -0
  184. package/server/remotes/samsung/utils.js +56 -0
  185. package/server/remotes/vlc/index.js +39 -0
  186. package/server/remotes/vlc/registrar.js +94 -0
  187. package/server/remotes/vlc/spec.js +15 -0
  188. package/server/remotes/vlc/state.js +5 -0
  189. package/server/remotes/window/index.js +6 -0
  190. package/server/remotes/window/registrar.js +28 -0
  191. package/server/services/application/createApplicationDaemonService.js +144 -0
  192. package/server/services/application/getDaemonServiceAdapter.js +56 -0
  193. package/server/services/application/utils.js +29 -0
  194. package/server/services/config/bootstrapConfig.js +38 -0
  195. package/server/services/config/configPaths.js +42 -0
  196. package/server/services/config/configSchema.js +297 -0
  197. package/server/services/config/configService.js +89 -0
  198. package/server/services/config/defaultConfig.js +137 -0
  199. package/server/services/config/envConfig.js +49 -0
  200. package/server/services/config/index.js +9 -0
  201. package/server/services/config/logConfig.js +54 -0
  202. package/server/services/config/system-config.js +44 -0
  203. package/server/services/createServicesContainer.js +94 -0
  204. package/server/services/createServicesRegistry.js +48 -0
  205. package/server/services/input/createInputController.js +17 -0
  206. package/server/services/input/createKeyboardController.js +146 -0
  207. package/server/services/input/createMouseController.js +182 -0
  208. package/server/services/notifier/createClientNotifier.js +23 -0
  209. package/server/services/notifier/createHostNotifier.js +41 -0
  210. package/server/services/notifier/createNotifier.js +13 -0
  211. package/server/services/notifier/createNotifierComposite.js +122 -0
  212. package/server/services/notifier/handleOpenQrBrowserEvent.js +40 -0
  213. package/server/services/notifier/handleOpenServerInfoBrowserEvent.js +40 -0
  214. package/server/services/notifier/handleRestartServiceEvent.js +37 -0
  215. package/server/services/notifier/handleRotateEntryTokenEvent.js +42 -0
  216. package/server/services/notifier/handleSessionEvent.js +24 -0
  217. package/server/services/notifier/handleToggleQrOverlayEvent.js +21 -0
  218. package/server/services/notifier/host-notifier/createLinuxHostNotifier.js +23 -0
  219. package/server/services/notifier/host-notifier/createNodeNotifierHostNotifier.js +24 -0
  220. package/server/services/notifier/host-notifier/index.js +11 -0
  221. package/server/services/notifier/mapper/handleForceUpdateCheckEvent.js +24 -0
  222. package/server/services/notifier/mapper/handleInstallUpdateEvent.js +53 -0
  223. package/server/services/notifier/mapper/handleSocketEvent.js +40 -0
  224. package/server/services/notifier/mapper/handleUpdateManagerEvent.js +24 -0
  225. package/server/services/notifier/mapper/notify.js +7 -0
  226. package/server/services/os/index.js +23 -0
  227. package/server/services/os/platforms/createFallbackAdapter.js +31 -0
  228. package/server/services/os/platforms/darwin/app.js +6 -0
  229. package/server/services/os/platforms/darwin/index.js +21 -0
  230. package/server/services/os/platforms/darwin/scripts.js +4 -0
  231. package/server/services/os/platforms/darwin/url.js +1 -0
  232. package/server/services/os/platforms/darwin/window.js +4 -0
  233. package/server/services/os/platforms/linux/app.js +6 -0
  234. package/server/services/os/platforms/linux/index.js +33 -0
  235. package/server/services/os/platforms/linux/url.js +1 -0
  236. package/server/services/os/platforms/linux/windows.js +11 -0
  237. package/server/services/os/platforms/win32/app.js +6 -0
  238. package/server/services/os/platforms/win32/index.js +21 -0
  239. package/server/services/os/platforms/win32/overlay.js +6 -0
  240. package/server/services/os/platforms/win32/powershell.js +4 -0
  241. package/server/services/os/platforms/win32/url.js +1 -0
  242. package/server/services/os/platforms/win32/window.js +4 -0
  243. package/server/services/os/shared.js +2 -0
  244. package/server/services/overlay/createNoopOverlay.js +13 -0
  245. package/server/services/overlay/createQrOverlay.js +27 -0
  246. package/server/services/overlay/createQrOverlayWin32.js +158 -0
  247. package/server/services/overlay/createQrOverlayYad.js +178 -0
  248. package/server/services/overlay/renderQrPage.js +82 -0
  249. package/server/services/persistence/createConfigDao.js +106 -0
  250. package/server/services/persistence/createDatabaseProvider.js +23 -0
  251. package/server/services/persistence/createEntryTokenDao.js +140 -0
  252. package/server/services/persistence/createRestartLogDao.js +210 -0
  253. package/server/services/persistence/index.js +19 -0
  254. package/server/services/pubsub/createEventStore.js +54 -0
  255. package/server/services/pubsub/createPubSub.js +89 -0
  256. package/server/services/pubsub/createServiceEvents.js +29 -0
  257. package/server/services/pubsub/serviceEventConstants.js +39 -0
  258. package/server/services/remotes/createRemotes.js +25 -0
  259. package/server/services/server/createHttpsServer.js +12 -0
  260. package/server/services/server/createServer.js +66 -0
  261. package/server/services/sse/createSseService.js +96 -0
  262. package/server/services/sse/utils.js +7 -0
  263. package/server/services/system/createSystemService.js +170 -0
  264. package/server/services/system/getNetworkInfo.js +18 -0
  265. package/server/services/system/getScreenInfo.js +32 -0
  266. package/server/services/system/listNetworkInterfaces.js +27 -0
  267. package/server/services/task-manager/createTaskManager.js +68 -0
  268. package/server/services/task-runner/createTaskRunner.js +114 -0
  269. package/server/services/token-manager/createTokenManager.js +244 -0
  270. package/server/services/token-manager/token-format.js +7 -0
  271. package/server/services/token-manager/token-store-utils.js +8 -0
  272. package/server/services/update-manager/CommandUpdateSource.js +70 -0
  273. package/server/services/update-manager/NpmUpdateSource.js +34 -0
  274. package/server/services/update-manager/buildNpmGlobalUpdateCommand.js +15 -0
  275. package/server/services/update-manager/chooseUpdateCheckSource.js +20 -0
  276. package/server/services/update-manager/chooseUpdateInstallSource.js +5 -0
  277. package/server/services/update-manager/createUpdateManager.js +109 -0
  278. package/server/services/update-manager/resolveInstallUpdateCommand.js +42 -0
  279. package/server/term/cli/executeLocalServiceCommand.js +42 -0
  280. package/server/term/cli/getCliSocketPath.js +5 -0
  281. package/server/term/cli/parseCliArgs.js +110 -0
  282. package/server/term/cli/printCliResult.js +18 -0
  283. package/server/term/cli/runCliCmd.js +50 -0
  284. package/server/term/cli/sendCliCommand.js +76 -0
  285. package/server/term/cliSocket.js +29 -0
  286. package/server/term/srv/commands/configCommand.js +84 -0
  287. package/server/term/srv/commands/helpCommand.js +6 -0
  288. package/server/term/srv/commands/infoCommand.js +7 -0
  289. package/server/term/srv/commands/openQrCommand.js +7 -0
  290. package/server/term/srv/commands/samsungDetectCommand.js +52 -0
  291. package/server/term/srv/commands/serviceCommand.js +25 -0
  292. package/server/term/srv/commands/systemConfigCommand.js +7 -0
  293. package/server/term/srv/commands/tasksCommand.js +24 -0
  294. package/server/term/srv/commands/tokensCommand.js +15 -0
  295. package/server/term/srv/executeCliCommand.js +44 -0
  296. package/server/term/srv/executeCliRequest.js +38 -0
  297. package/server/term/srv/startCliServer.js +140 -0
  298. package/server/utils/createRandomToken.js +11 -0
  299. package/server/utils/env.js +155 -0
  300. package/server/utils/git.js +13 -0
  301. package/server/utils/http.js +7 -0
  302. package/server/utils/network.js +45 -0
  303. package/server/utils/paths.js +10 -0
  304. package/server/utils/process.js +57 -0
  305. package/server/utils/robot.js +11 -0
  306. package/server/utils/semver.js +33 -0
  307. package/server/utils/truncateText.js +7 -0
  308. package/utils/createLazy.js +13 -0
  309. package/utils/functional.js +48 -0
  310. package/utils/math.js +26 -0
  311. package/utils/notificationSettings.js +61 -0
  312. package/utils/object.utils.js +79 -0
  313. package/utils/predicates.js +17 -0
  314. package/utils/remoteCommands.js +40 -0
  315. package/utils/sync.js +3 -0
  316. package/velor-remote-mouse-6.4.8.tgz +0 -0
@@ -0,0 +1,840 @@
1
+ import {applyPageTranslations} from '../../i18n/apply-page-translations.js';
2
+ import {
3
+ NOTIFICATION_EVENT_DEFINITIONS,
4
+ NOTIFICATION_TARGET_CLIENT,
5
+ NOTIFICATION_TARGET_HOST,
6
+ getNotificationTargetPath,
7
+ } from '../../../utils/notificationSettings.js';
8
+ import {createServicesRegistry} from '../../services/createServicesRegistry.js';
9
+ import {initializeCoreServices} from '../../services/createServicesContainer.js';
10
+
11
+ const services = createServicesRegistry();
12
+ await initializeCoreServices(services);
13
+
14
+ const form = document.getElementById('config-form');
15
+ const statusNode = document.getElementById('config-status');
16
+ const reloadButton = document.getElementById('reload-config');
17
+ const restartButton = document.getElementById('restart-service');
18
+ const sectionTemplate = document.getElementById('section-template');
19
+ const fieldTemplate = document.getElementById('field-template');
20
+ applyPageTranslations(document, t);
21
+
22
+ function t(key, params) {
23
+ return services.getI18n().t(key, params);
24
+ }
25
+
26
+ function translateOr(key, fallback = '') {
27
+ const value = t(key);
28
+ return value === key ? fallback : value;
29
+ }
30
+
31
+ let currentSchema = {};
32
+ let currentDefaults = {};
33
+ let currentConfig = {};
34
+ const syncTimers = new Map();
35
+ let configStream = null;
36
+ let configSubscriptionId = '';
37
+ let configReloadInFlight = false;
38
+ let configReloadPending = false;
39
+ let configMutationDepth = 0;
40
+ let pendingConfigEventPayload = null;
41
+ const collapsedSections = new Set();
42
+ const fieldContexts = new Map();
43
+
44
+ function getValueAtPath(source, dottedPath) {
45
+ return String(dottedPath || '')
46
+ .split('.')
47
+ .filter(Boolean)
48
+ .reduce((cursor, segment) => (cursor == null ? undefined : cursor[segment]), source);
49
+ }
50
+
51
+ function setStatus(message, tone = '') {
52
+ statusNode.textContent = message;
53
+ statusNode.className = tone ? `status-${tone}` : '';
54
+ }
55
+
56
+ function updateBooleanLabel(input) {
57
+ const copy = input?.closest('.toggle-row')?.querySelector('.toggle-copy');
58
+ if (copy) {
59
+ copy.textContent = input.checked ? t('adminConfig.toggleActive') : t('adminConfig.toggleInactive');
60
+ }
61
+ }
62
+
63
+ function valuesMatch(left, right) {
64
+ return left === right;
65
+ }
66
+
67
+ function normalizeFieldValue(field, rawValue) {
68
+ if (field.type === 'boolean') {
69
+ return Boolean(rawValue);
70
+ }
71
+
72
+ if (field.type === 'integer') {
73
+ const parsed = Number.parseInt(String(rawValue), 10);
74
+ return Number.isFinite(parsed) ? parsed : rawValue;
75
+ }
76
+
77
+ if (field.type === 'number') {
78
+ const parsed = Number.parseFloat(String(rawValue));
79
+ return Number.isFinite(parsed) ? parsed : rawValue;
80
+ }
81
+
82
+ return String(rawValue ?? '').trim();
83
+ }
84
+
85
+ function getInputControl(fieldControlNode, field) {
86
+ if (field.type === 'boolean') {
87
+ return fieldControlNode.querySelector('input');
88
+ }
89
+
90
+ return fieldControlNode.querySelector('input, select');
91
+ }
92
+
93
+ function getCurrentInputValue(input, field) {
94
+ if (!input) {
95
+ return undefined;
96
+ }
97
+
98
+ return normalizeFieldValue(field, field.type === 'boolean' ? input.checked : input.value);
99
+ }
100
+
101
+ function isDeferredSyncField(field) {
102
+ return field?.type !== 'boolean';
103
+ }
104
+
105
+ function isFieldBeingEdited(input, field) {
106
+ if (!input || !isDeferredSyncField(field)) {
107
+ return false;
108
+ }
109
+
110
+ return document.activeElement === input;
111
+ }
112
+
113
+ function setInputValue(input, field, value) {
114
+ if (!input) {
115
+ return;
116
+ }
117
+
118
+ if (field.type === 'boolean') {
119
+ input.checked = Boolean(value);
120
+ updateBooleanLabel(input);
121
+ return;
122
+ }
123
+
124
+ input.value = value ?? '';
125
+ }
126
+
127
+ function syncResetButtonVisibility({input, field, defaultValue, resetButton}) {
128
+ if (!resetButton) {
129
+ return;
130
+ }
131
+ resetButton.hidden = valuesMatch(getCurrentInputValue(input, field), defaultValue);
132
+ }
133
+
134
+ function setActionsDisabled(disabled) {
135
+ reloadButton.disabled = disabled;
136
+ restartButton.disabled = disabled;
137
+ }
138
+
139
+ function isSamsungAutoResolveEnabled() {
140
+ return Boolean(getValueAtPath(currentConfig, 'samsungTv.alwaysAutoResolve'));
141
+ }
142
+
143
+ function syncSamsungFieldAvailability(pathKey, input, resetButton) {
144
+ const shouldDisable = isSamsungAutoResolveEnabled()
145
+ && (pathKey === 'samsungTv.host' || pathKey === 'samsungTv.mac');
146
+ if (!input) {
147
+ return;
148
+ }
149
+ input.disabled = shouldDisable;
150
+ resetButton.disabled = shouldDisable;
151
+ }
152
+
153
+ function syncSamsungFieldsAvailability() {
154
+ for (const [pathKey, context] of fieldContexts.entries()) {
155
+ syncSamsungFieldAvailability(pathKey, context.input, context.resetButton);
156
+ }
157
+ }
158
+
159
+ function syncSectionCollapseState(sectionNode, sectionKey, toggleButton) {
160
+ const collapsed = collapsedSections.has(sectionKey);
161
+ sectionNode.classList.toggle('is-collapsed', collapsed);
162
+ toggleButton.setAttribute('aria-expanded', String(!collapsed));
163
+ const label = collapsed ? t('adminConfig.showSectionLabel') : t('adminConfig.hideSectionLabel');
164
+ toggleButton.setAttribute('aria-label', label);
165
+ toggleButton.setAttribute('title', label);
166
+ toggleButton.textContent = collapsed ? t('adminConfig.showSection') : t('adminConfig.hideSection');
167
+ }
168
+
169
+ function beginConfigMutation() {
170
+ configMutationDepth += 1;
171
+ }
172
+
173
+ function endConfigMutation() {
174
+ configMutationDepth = Math.max(0, configMutationDepth - 1);
175
+ if (configMutationDepth === 0 && pendingConfigEventPayload) {
176
+ queueMicrotask(() => {
177
+ applyConfigEventPayload(pendingConfigEventPayload);
178
+ });
179
+ } else if (configMutationDepth === 0 && configReloadPending) {
180
+ queueMicrotask(() => {
181
+ refreshConfigFromServer({silent: true});
182
+ });
183
+ }
184
+ }
185
+
186
+ function setConfigValue(pathKey, value) {
187
+ const segments = String(pathKey || '').split('.').filter(Boolean);
188
+ if (!segments.length) {
189
+ return;
190
+ }
191
+
192
+ let cursor = currentConfig;
193
+ for (let index = 0; index < segments.length - 1; index += 1) {
194
+ const segment = segments[index];
195
+ if (!cursor[segment] || typeof cursor[segment] !== 'object' || Array.isArray(cursor[segment])) {
196
+ cursor[segment] = {};
197
+ }
198
+ cursor = cursor[segment];
199
+ }
200
+
201
+ cursor[segments[segments.length - 1]] = value;
202
+ }
203
+
204
+ async function patchField(pathKey, value) {
205
+ const response = await fetch(`/api/admin/configs/${encodeURIComponent(pathKey)}`, {
206
+ method: 'PATCH',
207
+ headers: {
208
+ 'Content-Type': 'application/json',
209
+ },
210
+ body: JSON.stringify({value}),
211
+ });
212
+ const payload = await response.json();
213
+ if (!response.ok || !payload.ok) {
214
+ throw new Error(payload.message || `Erreur de synchronisation pour ${pathKey}.`);
215
+ }
216
+
217
+ applyConfigEntry(pathKey, payload.config?.value);
218
+ return payload;
219
+ }
220
+
221
+ async function saveConfigEntries(entries) {
222
+ beginConfigMutation();
223
+ setActionsDisabled(true);
224
+
225
+ try {
226
+ for (const [pathKey, value] of entries) {
227
+ await patchField(pathKey, value);
228
+ }
229
+ } finally {
230
+ setActionsDisabled(false);
231
+ endConfigMutation();
232
+ }
233
+ }
234
+
235
+ function applyConfigEntry(pathKey, value) {
236
+ setConfigValue(pathKey, value);
237
+ const context = fieldContexts.get(pathKey);
238
+ if (!context) {
239
+ return;
240
+ }
241
+
242
+ const existingTimer = syncTimers.get(pathKey);
243
+ if (existingTimer) {
244
+ clearTimeout(existingTimer);
245
+ syncTimers.delete(pathKey);
246
+ }
247
+
248
+ if (!isFieldBeingEdited(context.input, context.field)) {
249
+ setInputValue(context.input, context.field, value);
250
+ }
251
+ syncResetButtonVisibility(context);
252
+ syncSamsungFieldAvailability(pathKey, context.input, context.resetButton);
253
+ }
254
+
255
+ function applyConfigEventPayload(payload) {
256
+ if (configMutationDepth > 0) {
257
+ pendingConfigEventPayload = payload;
258
+ return;
259
+ }
260
+
261
+ pendingConfigEventPayload = null;
262
+ const entries = Array.isArray(payload?.entries) ? payload.entries : [];
263
+ if (!entries.length) {
264
+ refreshConfigFromServer({silent: true});
265
+ return;
266
+ }
267
+
268
+ for (const entry of entries) {
269
+ if (!entry?.path) {
270
+ continue;
271
+ }
272
+ applyConfigEntry(entry.path, entry.value);
273
+ }
274
+
275
+ syncSamsungFieldsAvailability();
276
+ }
277
+
278
+ function formatSamsungDeviceLabel(device, index) {
279
+ const parts = [
280
+ `${index + 1}.`,
281
+ device.name || t('adminConfig.unnamedSamsung'),
282
+ device.model ? `(${device.model})` : '',
283
+ device.host ? `IP ${device.host}` : '',
284
+ device.mac ? `MAC ${device.mac}` : '',
285
+ ].filter(Boolean);
286
+ return parts.join(' ');
287
+ }
288
+
289
+ async function discoverSamsungDevice() {
290
+ setStatus(t('adminConfig.statusSamsungDiscovering'), 'pending');
291
+
292
+ const response = await fetch('/api/admin/configs/samsung/discover', {
293
+ method: 'POST',
294
+ });
295
+ const payload = await response.json();
296
+ if (!response.ok || !payload.ok) {
297
+ throw new Error(payload.message || t('adminConfig.statusSamsungDiscoverError'));
298
+ }
299
+
300
+ const devices = Array.isArray(payload.devices) ? payload.devices : [];
301
+ if (!devices.length) {
302
+ throw new Error(t('adminConfig.statusSamsungNone'));
303
+ }
304
+
305
+ let selectedIndex = devices.findIndex((device) => device.isSelected);
306
+ if (devices.length === 1) {
307
+ const accepted = window.confirm(
308
+ t('adminConfig.samsungSinglePrompt', {
309
+ label: formatSamsungDeviceLabel(devices[0], 0),
310
+ }),
311
+ );
312
+ if (!accepted) {
313
+ setStatus(t('adminConfig.statusSamsungCancelled'));
314
+ return;
315
+ }
316
+ selectedIndex = 0;
317
+ } else {
318
+ const suggestedIndex = selectedIndex >= 0 ? selectedIndex + 1 : 1;
319
+ const choice = window.prompt(
320
+ t('adminConfig.samsungMultiPrompt', {
321
+ choices: devices.map(formatSamsungDeviceLabel).join('\n'),
322
+ }),
323
+ String(suggestedIndex),
324
+ );
325
+ if (choice == null) {
326
+ setStatus(t('adminConfig.statusSamsungCancelled'));
327
+ return;
328
+ }
329
+
330
+ selectedIndex = Number.parseInt(String(choice).trim(), 10) - 1;
331
+ if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= devices.length) {
332
+ throw new Error(t('adminConfig.statusSamsungInvalidSelection'));
333
+ }
334
+ }
335
+
336
+ const selectedDevice = devices[selectedIndex];
337
+ await saveConfigEntries([
338
+ ['samsungTv.host', selectedDevice.host || ''],
339
+ ['samsungTv.mac', selectedDevice.mac || ''],
340
+ ]);
341
+ setStatus(t('adminConfig.statusSamsungApplied', {
342
+ name: selectedDevice.name || selectedDevice.host || t('common.unknown'),
343
+ }), 'success');
344
+ }
345
+
346
+ async function syncField({pathKey, field, input, defaultValue, resetButton}) {
347
+ const nextValue = getCurrentInputValue(input, field);
348
+ const currentValue = getValueAtPath(currentConfig, pathKey);
349
+
350
+ syncResetButtonVisibility({input, field, defaultValue, resetButton});
351
+ if (valuesMatch(nextValue, currentValue)) {
352
+ return;
353
+ }
354
+
355
+ beginConfigMutation();
356
+ setActionsDisabled(true);
357
+ setStatus(t('adminConfig.statusSyncing', {path: pathKey}), 'pending');
358
+
359
+ try {
360
+ const payload = await patchField(pathKey, valuesMatch(nextValue, defaultValue) ? null : nextValue);
361
+ syncResetButtonVisibility({input, field, defaultValue, resetButton});
362
+ setStatus(payload.message || `${pathKey} synchronized.`, 'success');
363
+ } catch (error) {
364
+ setStatus(error.message || t('adminConfig.statusSyncError'), 'error');
365
+ } finally {
366
+ setActionsDisabled(false);
367
+ endConfigMutation();
368
+ }
369
+ }
370
+
371
+ function scheduleFieldSync(context, {immediate = false} = {}) {
372
+ const existingTimer = syncTimers.get(context.pathKey);
373
+ if (existingTimer) {
374
+ clearTimeout(existingTimer);
375
+ }
376
+
377
+ if (immediate) {
378
+ syncTimers.delete(context.pathKey);
379
+ syncField(context);
380
+ return;
381
+ }
382
+
383
+ const timer = window.setTimeout(() => {
384
+ syncTimers.delete(context.pathKey);
385
+ syncField(context);
386
+ }, 350);
387
+
388
+ syncTimers.set(context.pathKey, timer);
389
+ }
390
+
391
+ function createInput(pathKey, field, value) {
392
+ if (field.type === 'boolean') {
393
+ const wrapper = document.createElement('span');
394
+ wrapper.className = 'toggle-row';
395
+
396
+ const copy = document.createElement('span');
397
+ copy.className = 'toggle-copy';
398
+ copy.textContent = value ? t('adminConfig.toggleActive') : t('adminConfig.toggleInactive');
399
+
400
+ const input = document.createElement('input');
401
+ input.type = 'checkbox';
402
+ input.name = pathKey;
403
+ input.checked = Boolean(value);
404
+ input.addEventListener('change', () => {
405
+ copy.textContent = input.checked ? t('adminConfig.toggleActive') : t('adminConfig.toggleInactive');
406
+ });
407
+
408
+ wrapper.append(copy, input);
409
+ return wrapper;
410
+ }
411
+
412
+ if (Array.isArray(field.options) && field.options.length > 0) {
413
+ const select = document.createElement('select');
414
+ select.name = pathKey;
415
+
416
+ for (const optionValue of field.options) {
417
+ const option = document.createElement('option');
418
+ option.value = optionValue;
419
+ option.textContent = optionValue;
420
+ option.selected = String(value ?? '') === optionValue;
421
+ select.append(option);
422
+ }
423
+
424
+ return select;
425
+ }
426
+
427
+ const input = document.createElement('input');
428
+ input.name = pathKey;
429
+ input.placeholder = field.placeholder || '';
430
+ input.value = value ?? '';
431
+
432
+ if (field.type === 'number' || field.type === 'integer') {
433
+ input.type = 'number';
434
+ if (field.min !== undefined) {
435
+ input.min = String(field.min);
436
+ }
437
+ if (field.max !== undefined) {
438
+ input.max = String(field.max);
439
+ }
440
+ if (field.step !== undefined) {
441
+ input.step = String(field.step);
442
+ }
443
+ } else {
444
+ input.type = 'text';
445
+ }
446
+
447
+ return input;
448
+ }
449
+
450
+ function createNotificationMatrixCheckbox(pathKey, value) {
451
+ const input = document.createElement('input');
452
+ input.type = 'checkbox';
453
+ input.name = pathKey;
454
+ input.checked = Boolean(value);
455
+ return input;
456
+ }
457
+
458
+ function renderNotificationsMatrix(fieldsNode) {
459
+ const matrix = document.createElement('div');
460
+ matrix.className = 'notifications-matrix';
461
+
462
+ const table = document.createElement('table');
463
+ table.className = 'notifications-table';
464
+
465
+ const thead = document.createElement('thead');
466
+ thead.innerHTML = `
467
+ <tr>
468
+ <th>${t('adminConfig.notificationsTable.event')}</th>
469
+ <th>${t('adminConfig.notificationsTable.host')}</th>
470
+ <th>${t('adminConfig.notificationsTable.client')}</th>
471
+ </tr>
472
+ `;
473
+ table.append(thead);
474
+
475
+ const tbody = document.createElement('tbody');
476
+
477
+ for (const definition of NOTIFICATION_EVENT_DEFINITIONS) {
478
+ const row = document.createElement('tr');
479
+ const hostPathKey = getNotificationTargetPath(definition.id, NOTIFICATION_TARGET_HOST);
480
+ const clientPathKey = getNotificationTargetPath(definition.id, NOTIFICATION_TARGET_CLIENT);
481
+ const hostDefaultValue = getValueAtPath(currentDefaults, hostPathKey);
482
+ const clientDefaultValue = getValueAtPath(currentDefaults, clientPathKey);
483
+ const hostInput = createNotificationMatrixCheckbox(hostPathKey, getValueAtPath(currentConfig, hostPathKey));
484
+ const clientInput = createNotificationMatrixCheckbox(clientPathKey, getValueAtPath(currentConfig, clientPathKey));
485
+
486
+ const labelCell = document.createElement('td');
487
+ labelCell.className = 'notifications-table-label';
488
+ labelCell.textContent = t(definition.labelKey);
489
+
490
+ const hostCell = document.createElement('td');
491
+ hostCell.className = 'notifications-table-toggle';
492
+ hostCell.append(hostInput);
493
+
494
+ const clientCell = document.createElement('td');
495
+ clientCell.className = 'notifications-table-toggle';
496
+ clientCell.append(clientInput);
497
+
498
+ row.append(labelCell, hostCell, clientCell);
499
+ tbody.append(row);
500
+
501
+ for (const [pathKey, input, defaultValue] of [
502
+ [hostPathKey, hostInput, hostDefaultValue],
503
+ [clientPathKey, clientInput, clientDefaultValue],
504
+ ]) {
505
+ const fieldContext = {
506
+ pathKey,
507
+ field: { type: 'boolean' },
508
+ input,
509
+ defaultValue,
510
+ resetButton: null,
511
+ };
512
+ fieldContexts.set(pathKey, fieldContext);
513
+ input.addEventListener('change', () => {
514
+ scheduleFieldSync(fieldContext, { immediate: true });
515
+ });
516
+ }
517
+ }
518
+
519
+ table.append(tbody);
520
+ matrix.append(table);
521
+ fieldsNode.append(matrix);
522
+ }
523
+
524
+ function renderConfigForm(config, schema) {
525
+ currentConfig = config || {};
526
+ fieldContexts.clear();
527
+ form.replaceChildren();
528
+
529
+ for (const [sectionKey, section] of Object.entries(schema)) {
530
+ if (!collapsedSections.has(sectionKey)) {
531
+ collapsedSections.add(sectionKey);
532
+ }
533
+
534
+ const sectionNode = sectionTemplate.content.firstElementChild.cloneNode(true);
535
+ sectionNode.querySelector('.section-key').textContent = sectionKey;
536
+ sectionNode.querySelector('h2').textContent = translateOr(`adminConfig.section.${sectionKey}.title`, section.title);
537
+ sectionNode.querySelector('.section-description').textContent = translateOr(`adminConfig.section.${sectionKey}.description`, section.description || '');
538
+ const sectionToggleButton = sectionNode.querySelector('.section-toggle');
539
+
540
+ const fieldsNode = sectionNode.querySelector('.section-fields');
541
+
542
+ if (sectionKey === 'samsungTv') {
543
+ const sectionActionsNode = document.createElement('div');
544
+ sectionActionsNode.className = 'section-actions';
545
+ const discoverButton = document.createElement('button');
546
+ discoverButton.type = 'button';
547
+ discoverButton.className = 'section-action-button';
548
+ discoverButton.textContent = t('adminConfig.detectSamsung');
549
+ discoverButton.addEventListener('click', () => {
550
+ discoverSamsungDevice().catch((error) => {
551
+ setStatus(error.message || t('adminConfig.statusSamsungDiscoverError'), 'error');
552
+ });
553
+ });
554
+ sectionActionsNode.append(discoverButton);
555
+ fieldsNode.append(sectionActionsNode);
556
+ }
557
+
558
+ if (sectionKey === 'notifications') {
559
+ renderNotificationsMatrix(fieldsNode);
560
+ }
561
+
562
+ for (const [fieldKey, field] of Object.entries(section.fields)) {
563
+ const pathKey = `${sectionKey}.${fieldKey}`;
564
+ const currentValue = getValueAtPath(currentConfig, pathKey);
565
+ const defaultValue = getValueAtPath(currentDefaults, pathKey);
566
+ const fieldNode = fieldTemplate.content.firstElementChild.cloneNode(true);
567
+ fieldNode.querySelector('.field-label').textContent = translateOr(`adminConfig.field.${pathKey}`, field.label);
568
+ fieldNode.querySelector('.field-path').textContent = pathKey;
569
+ const fieldControlNode = fieldNode.querySelector('.field-control');
570
+ fieldControlNode.append(createInput(pathKey, field, currentValue));
571
+
572
+ const resetButton = fieldNode.querySelector('.field-reset');
573
+ const input = getInputControl(fieldControlNode, field);
574
+ const syncVisibility = () => syncResetButtonVisibility({
575
+ input,
576
+ field,
577
+ defaultValue,
578
+ resetButton,
579
+ });
580
+ const fieldContext = {
581
+ pathKey,
582
+ field,
583
+ input,
584
+ defaultValue,
585
+ resetButton,
586
+ };
587
+ fieldContexts.set(pathKey, fieldContext);
588
+
589
+ input?.addEventListener('input', () => {
590
+ syncVisibility();
591
+ });
592
+ input?.addEventListener('change', () => {
593
+ syncVisibility();
594
+ scheduleFieldSync(fieldContext, {immediate: true});
595
+ });
596
+ input?.addEventListener('keydown', (event) => {
597
+ if (event.key !== 'Enter' || !isDeferredSyncField(field)) {
598
+ return;
599
+ }
600
+ syncVisibility();
601
+ scheduleFieldSync(fieldContext, {immediate: true});
602
+ });
603
+ syncVisibility();
604
+ syncSamsungFieldAvailability(pathKey, input, resetButton);
605
+
606
+ resetButton.addEventListener('click', () => {
607
+ resetField({
608
+ pathKey,
609
+ field,
610
+ input,
611
+ defaultValue,
612
+ resetButton,
613
+ });
614
+ });
615
+
616
+ fieldsNode.append(fieldNode);
617
+ }
618
+
619
+ sectionToggleButton.addEventListener('click', () => {
620
+ if (collapsedSections.has(sectionKey)) {
621
+ collapsedSections.delete(sectionKey);
622
+ } else {
623
+ collapsedSections.add(sectionKey);
624
+ }
625
+ syncSectionCollapseState(sectionNode, sectionKey, sectionToggleButton);
626
+ });
627
+ syncSectionCollapseState(sectionNode, sectionKey, sectionToggleButton);
628
+
629
+ form.append(sectionNode);
630
+ }
631
+ }
632
+
633
+ function refreshLocalizedTexts() {
634
+ applyPageTranslations(document, t);
635
+ renderConfigForm(currentConfig, currentSchema);
636
+ }
637
+
638
+ function collectFormValues(schema) {
639
+ const values = {};
640
+
641
+ for (const [sectionKey, section] of Object.entries(schema)) {
642
+ for (const [fieldKey, field] of Object.entries(section.fields)) {
643
+ const pathKey = `${sectionKey}.${fieldKey}`;
644
+ const input = form.elements.namedItem(pathKey);
645
+ if (!input) {
646
+ continue;
647
+ }
648
+
649
+ values[pathKey] = normalizeFieldValue(field, field.type === 'boolean' ? input.checked : input.value);
650
+ }
651
+ }
652
+
653
+ return values;
654
+ }
655
+
656
+ function getConfigObjectFromEntries(entries) {
657
+ const config = {};
658
+
659
+ for (const entry of entries || []) {
660
+ if (!entry || !entry.id) {
661
+ continue;
662
+ }
663
+
664
+ const segments = String(entry.id).split('.').filter(Boolean);
665
+ let cursor = config;
666
+ for (let index = 0; index < segments.length - 1; index += 1) {
667
+ const segment = segments[index];
668
+ if (!cursor[segment] || typeof cursor[segment] !== 'object' || Array.isArray(cursor[segment])) {
669
+ cursor[segment] = {};
670
+ }
671
+ cursor = cursor[segment];
672
+ }
673
+ cursor[segments[segments.length - 1]] = entry.value;
674
+ }
675
+
676
+ return config;
677
+ }
678
+
679
+ async function loadConfig({silent = false} = {}) {
680
+ if (!silent) {
681
+ setStatus(t('adminConfig.statusLoading'), 'pending');
682
+ }
683
+
684
+ const response = await fetch('/api/admin/configs', {cache: 'no-store'});
685
+ if (!response.ok) {
686
+ throw new Error(t('adminConfig.statusLoadError'));
687
+ }
688
+
689
+ const payload = await response.json();
690
+ currentSchema = payload.schema || {};
691
+ currentDefaults = payload.defaults || {};
692
+ renderConfigForm(getConfigObjectFromEntries(payload.configs || []), currentSchema);
693
+ if (!silent) {
694
+ setStatus(t('adminConfig.statusLoaded'));
695
+ }
696
+ }
697
+
698
+ async function refreshConfigFromServer({silent = false} = {}) {
699
+ if (configReloadInFlight || configMutationDepth > 0) {
700
+ configReloadPending = true;
701
+ return;
702
+ }
703
+
704
+ configReloadInFlight = true;
705
+ configReloadPending = false;
706
+
707
+ try {
708
+ await loadConfig({silent});
709
+ } catch (error) {
710
+ setStatus(error.message || t('adminConfig.statusLoadError'), 'error');
711
+ } finally {
712
+ configReloadInFlight = false;
713
+ if (configReloadPending && configMutationDepth === 0) {
714
+ queueMicrotask(() => {
715
+ refreshConfigFromServer({silent: true});
716
+ });
717
+ }
718
+ }
719
+ }
720
+
721
+ async function subscribeToConfigEvents() {
722
+ if (typeof window.EventSource !== 'function') {
723
+ return;
724
+ }
725
+
726
+ const response = await fetch('/api/admin/subs/configs', {
727
+ method: 'POST',
728
+ headers: {
729
+ 'Content-Type': 'application/json',
730
+ },
731
+ body: JSON.stringify({scope: 'config'}),
732
+ });
733
+ if (!response.ok) {
734
+ throw new Error(t('adminConfig.statusSseCreateError'));
735
+ }
736
+
737
+ const payload = await response.json();
738
+ configSubscriptionId = String(payload.id || '');
739
+ if (!configSubscriptionId || !payload.eventsUrl) {
740
+ throw new Error(t('adminConfig.statusSseInvalid'));
741
+ }
742
+
743
+ configStream?.close();
744
+ configStream = new EventSource(payload.eventsUrl);
745
+ configStream.addEventListener('config.changed', (event) => {
746
+ try {
747
+ applyConfigEventPayload(JSON.parse(event.data || '{}'));
748
+ } catch (_error) {
749
+ refreshConfigFromServer({silent: true});
750
+ }
751
+ });
752
+ }
753
+
754
+ function unsubscribeFromConfigEvents() {
755
+ if (!configSubscriptionId) {
756
+ return;
757
+ }
758
+
759
+ fetch(`/api/admin/subs/${encodeURIComponent(configSubscriptionId)}`, {
760
+ method: 'DELETE',
761
+ keepalive: true,
762
+ }).catch(() => {});
763
+ configSubscriptionId = '';
764
+ }
765
+
766
+ async function resetField({pathKey, field, input, defaultValue, resetButton}) {
767
+ const existingTimer = syncTimers.get(pathKey);
768
+ if (existingTimer) {
769
+ clearTimeout(existingTimer);
770
+ syncTimers.delete(pathKey);
771
+ }
772
+
773
+ beginConfigMutation();
774
+ setActionsDisabled(true);
775
+ setStatus(t('adminConfig.statusResetting', {path: pathKey}), 'pending');
776
+
777
+ try {
778
+ const response = await fetch(`/api/admin/configs/${encodeURIComponent(pathKey)}`, {
779
+ method: 'DELETE',
780
+ });
781
+ const payload = await response.json();
782
+ if (!response.ok || !payload.ok) {
783
+ throw new Error(payload.message || t('adminConfig.statusResetError'));
784
+ }
785
+
786
+ setConfigValue(pathKey, payload.config?.value);
787
+ setInputValue(input, field, payload.config?.value);
788
+ syncResetButtonVisibility({input, field, defaultValue, resetButton});
789
+ setStatus(payload.message || `${pathKey} reset.`, 'success');
790
+ } catch (error) {
791
+ setStatus(error.message || t('adminConfig.statusResetError'), 'error');
792
+ } finally {
793
+ setActionsDisabled(false);
794
+ endConfigMutation();
795
+ }
796
+ }
797
+
798
+ async function restartService() {
799
+ setActionsDisabled(true);
800
+ setStatus(t('adminConfig.statusRestarting'), 'pending');
801
+
802
+ try {
803
+ const response = await fetch('/api/admin/restart-service', {
804
+ method: 'POST',
805
+ });
806
+ const payload = await response.json();
807
+ if (!response.ok || !payload.ok) {
808
+ throw new Error(payload.message || t('adminConfig.statusRestartError'));
809
+ }
810
+
811
+ setStatus(payload.message || t('adminConfig.statusRestartRequested'), 'success');
812
+ } catch (error) {
813
+ setStatus(error.message || t('adminConfig.statusRestartError'), 'error');
814
+ } finally {
815
+ setActionsDisabled(false);
816
+ }
817
+ }
818
+
819
+ reloadButton.addEventListener('click', () => {
820
+ refreshConfigFromServer().catch((error) => {
821
+ setStatus(error.message || t('adminConfig.statusLoadError'), 'error');
822
+ });
823
+ });
824
+
825
+ restartButton.addEventListener('click', () => {
826
+ restartService();
827
+ });
828
+
829
+ refreshConfigFromServer().catch((error) => {
830
+ setStatus(error.message || t('adminConfig.statusLoadError'), 'error');
831
+ });
832
+ subscribeToConfigEvents().catch(() => {});
833
+ window.addEventListener('beforeunload', () => {
834
+ configStream?.close();
835
+ unsubscribeFromConfigEvents();
836
+ });
837
+
838
+ services.getI18n().onChange(() => {
839
+ refreshLocalizedTexts();
840
+ });